Contentshtml
第1章CLR的執行模型... 4git
1.1將源代碼編譯成託管代碼模塊... 4程序員
1.2 將託管模塊合併成程序集... 6正則表達式
1.3加載公共語言運行時... 7redis
1.4執行程序集的代碼... 8算法
1.5本地代碼生成器:NGen.exe. 11數據庫
1.6 Framework類庫... 11編程
1.7通用數據類型... 12c#
1.8公共語言規範... 12windows
第2章 生成、打包、部署和管理應用程序及類型... 13
2.1 .Net Framework部署目標... 13
2.2將類型生成到模塊中... 13
2.3元數據概述... 13
2.4將模塊合併成程序集... 14
第4章 基礎類型... 14
4.1 全部類型都從System.Object派生... 14
4.2類型轉換... 18
4.3命名空間和程序集... 20
4.4運行時的相互關係... 21
第5章 基元類型、引用類型和值類型... 21
5.1編程語言的基元類型... 21
5.2引用類型和值類型... 22
5.3 值類型的裝箱和拆箱... 26
5.4對象哈希碼(之後再看)... 29
5.5 dynamic基元類型... 30
第6章 類型和成員基礎... 31
6.1類型的各類成員... 31
6.2類型的可見性... 33
6.3成員的可訪問性accessibility. 34
6.4靜態類... 35
6.5 分部類、結構和接口... 36
6.6組件、多態和版本控制... 37
第7章 常量和字段... 37
7.1常量... 37
7.2字段... 38
第8章 方法... 40
8.1實例構造器和類(引用類型)... 40
8.2實例構造器和結構(值類型)... 43
8.3類型構造器... 43
8.4操做符重載方法... 45
8.5轉換操做符方法... 47
8.6擴展方法... 47
8.7分部方法... 49
第9章 參數... 50
9.1可選參數和命名參數... 50
9.2 隱式類型的局部變量... 53
9.3以傳引用的方式向方法傳遞參數... 54
9.4向方法傳遞可變數量的參數... 54
9.5參數和返回類型的指導原則... 55
9.6 常量性... 55
第10章 屬性... 55
10.1無參屬性... 55
10.1.1自動實現的屬性... 57
10.1.2合理定義屬性... 57
10.1.3對象和集合初始化器... 57
10.1.4匿名類型... 59
10.1.5System.Tuple類型... 59
10.2有參屬性... 59
第11章事件... 59
11.1設計要公開事件的類型... 60
11.1.1第一步:定義類型來容納全部須要發送給事件通知接收者的附加信息... 60
11.1.2第二步:定義事件成員... 61
11.1.3 第三步:定義負責引起事件的方法來通知事件的登記對象... 61
第12章 泛型... 61
12.1 Framework類庫中的泛型... 63
12.2Wintellect的Power Collections庫... 63
12.3泛型基礎結構... 63
12.3.1開放類型和封閉成員... 63
第13章 接口... 63
13.1類和接口繼承... 64
13.2定義接口... 64
13.3繼承接口... 65
第14章 字符、字符串和文本處理... 66
14.1字符... 66
14.2 System.String類型... 67
14.2.1構造字符串... 67
14.2.2字符串是不可變的... 68
14.2.3比較字符串... 68
14.2.4字符串留用... 69
14.2.5字符串池... 70
14.2.6檢查字符串的字符和文本元素... 70
14.2.7其餘字符串操做... 70
14.3高效率構造字符串... 70
第17章委託... 71
17.1初識委託... 73
17.2用委託回調靜態方法... 75
17.3用委託回調實例方法... 75
17.4委託揭祕... 75
本書分爲五個部分:
v 第一部分,CLR基礎(CLR Basics),介紹CLR的執行模型,程序集概念,以及建立、打包、部署、管理程序集等。
v 第二部分,設計類型(Designing Types),包括CLR類型基礎,基礎類型,方法,特性(Property),事件,泛型,接口等內容。
v 第三部分,基本類型(Essential Types),包括字符、字符串及文本的處理,枚舉類型,數組,委託(Delegate),自定義屬性(Attribute),可控制類型等。
v 第四部分,核心設施(Core Facilities),包括異常與狀態管理,自動內存管理(垃圾收集),CLR託管與應用程序域(AppDomain),程序集加載與反射,運行時序列化等。
v 第五部分,線程(Threading),這是第三版新增長的內容,包括線程基礎,計算密集的異步操做,I/O密集的異步操做,基本的線程同步構造,混合的線程同步構造等。
本章內容:
v 將源代碼編譯成託管代碼模塊
v 將託管模塊合併成程序集
v 加載公共語言運行時
v 執行程序集的代碼
v 本地代碼生成器:NGen.exe
v Framework類庫入門
v 通用類型系統
v 公共語言規範(CLS)
v 與非託管代碼的互操做性
公共語言運行時(Common Language Runtime, CLR)是一個供多種編程語言使用的運行時。可用任何編程語言來開發代碼,只要編譯器是面向CLR的就行。
CLR也能夠看做一個在執行時管理代碼的代理,管理代碼是CLR的基本原則。可以被管理的代碼稱爲託管(managed)代碼,反之稱爲非託管代碼。
CLR的核心功能中的內存管理、程序集加載、安全性、異常處理和線程同步等可被面向CLR的全部語言使用。(不懂)
用支持CLR的任何一種語言建立源代碼文件。用一個對應的編譯器來檢查語法和分析源代碼。經編譯器編譯後生成託管模塊(managed module),它是一個可移植執行體文件,它多是32位(PE32)文件,也多是64位(PE32+)文件。包括中間語言和元數據,須要CLR才能執行。
託管模塊包含以下幾個部分組成:
v PE32/PE32+ 頭:標準的 Windows PE文件頭。若是文件頭使用PE32格式,則此文件只能在Windows 的32位或64位版本上運行;若是文件頭使用PE32+格式,則此文件只能在Windows 的64位版本上運行。編譯器在編譯時,可經過編譯平臺/platform開關來指定該程序集包含一個PE32頭或PE32+頭。
v IL代碼: 也是中間語言。編譯器編譯源代碼時生成的中間代碼,在執行環境中,這些IL代碼將被CLR的JIT編輯器翻譯成CPU能識別的指令,供CPU執行。
v 元數據
中間語言IL(Intermediate Language)代碼:編譯器編譯源代碼後生成的代碼(.exe或.dll文件),但此時編譯出來的程序代碼並非CPU能直接執行的機器代碼。在運行時,CLR將IL代碼編譯成本地CPU指令。
CPU:中央處理器(Central Processing Unit),是一臺計算機的運算核心和控制核心。它的功能主要是解釋計算機指令以及處理計算機軟件中的數據。
DLL (Dynamic Link Library) 文件爲動態連接庫文件,是一種做爲共享函數庫的可執行文件。動態連接提供了一種方法,使進程能夠調用不屬於其可執行代碼的函數。可執行代碼就是將編譯器處理源代碼後所生成的代碼 鏈接後造成的可執行代碼,它通常由機器代碼或接近於機器語言的代碼組成。
在Windows中,許多應用程序並非一個完整的可執行文件,它們被分割成一些相對獨立的動態連接庫,即DLL文件,放置於系統中。當咱們執行某一個程序時,相應的DLL文件就會被調用。多個應用程序也能夠同時訪問內存中單個 DLL 副本的內容。DLL 有助於共享數據和資源。
動態連接與靜態連接的不一樣之處在於:動態連接容許可執行模塊(.dll 文件或 .exe 文件)僅包含 在運行時定位 DLL 函數的可執行代碼所需的信息。在靜態連接中,連接器從靜態連接庫獲取全部被引用的函數,並將庫同代碼一塊兒放到可執行文件中。
使用中間語言的優勢(跨平臺,跨語言):
v 能夠實現平臺無關性,即與特定CPU無關。
v 只需把.NET框架中的某種語言編譯成IL代碼,就實現.NET框架中語言之間的交互操做。
本地代碼編譯器(native code compiler)生成的是面向特定CPU架構(好比x86,x64)的代碼。相反,每一個面向CLR的編譯器生成的都是IL代碼。
除了生成IL,面向CLR的每一個編譯器還要在每一個託管模塊中生成完整的元數據(Metadata)。元數據是描述數據(類型信息) 的數據,一般被解釋爲data about data,是由一組數據表構成的一個二進制數據塊。元數據被CLR編譯器編譯後保存在Windows可移植執行體(PE)文件中,即和它描述的IL嵌入在EXE/DLL文件中,使IL和元數據永遠同步。
PE (Portable Execute) 文件是微軟Windows操做系統上的程序文件,EXE、DLL、OCX、SYS、COM都是PE文件。
元數據主要的類型表:
v 定義表 描述當前程序集中定義的類型和成員信息。
v 引用表 描述任何一個被內部類型引用的外部的類型和成員信息。
v 清單表包含了組成程序集所須要的全部信息,同時包含了對其餘程序集的引用信息。
元數據的用途:
v 編譯時,元數據消除了對本地C/C++頭和庫文件的需求,由於在負責實現類型/成員的IL代碼文件中,已包含和 引用的類型/成員有關的所有信息。編譯器能夠直接從託管模塊讀取元數據。
v CLR的代碼驗證過程當中使用元數據確保代碼只執行「類型安全」的操做。
v VS使用元數據幫助您寫代碼。它的「智能感知」(IntelliSense)技術能夠解析元數據,指出一個類型提供了哪些方法、屬性、事件和字段
v 元數據容許將一個對象的字段序列化到一個內存中,將其發送給另外一臺機器。而後反序列化,在遠程機器上重建對象的狀態。(內存:與CPU進行溝通的橋樑,計算機中全部程序的運行都是在內存中進行的,所以內存的性能對計算機的影響很是大)。
v 元數據容許垃圾回收器跟蹤對象的生存期,垃圾回收器能判斷任何對象的類型,並從元數據知道那個對象中哪些字段引用了其餘對象。
程序集(assembly)是一個或多個託管模塊,以及一些資源文件的邏輯組合。是重用、安全性以及版本控制的最小單元。
CLR不和託管模塊一塊兒工做,是和程序集一塊兒工做的。CLR是經過程序集與託管模塊進行溝通的。
C#編譯器將生成的多個託管模塊和資源文件合併成程序集。
在程序集內有一個清單,其描述了程序集內的文件列表,如託管模塊、jpeg文件、XML文件等。
對於一個可重用的、可保護的、可版本控制的組件,程序集把它的邏輯表示(代碼)和物理表示(資源)區分開。
你生成的每一個程序集既能夠是可執行的應用程序(.EXE),也能夠是DLL。最終都由CLR管理這些程序集中代碼的執行。
Microsoft建立了重分發包(redistribution package),容許將.NET Framework免費分發並安裝到你的用戶的計算機上。
檢查機器上是否安裝好.Net Framework,只需在C:\Windows\System32下檢查是否有MSCorEE.dll文件(Microsoft .NET Runtime Execution Engine)。mscoree.dll必定是惟一的,且老是處於系統目錄的system32下。MSCorEE.dll負責選擇.NET版本、調用和初始化CLR等工做。
Windows鍵+R \ regedit \ KEY_LOCAL_MACHINE \ SOFTWARE \ MICROSOFT NET Framework瞭解安裝了哪些版本的.NET Framework。
若是程序集文件只包含類型安全的託管代碼,那麼不管在32位仍是64位版本的Windows上,所寫的代碼都應該能正常運行。
X86指的是一種CPU的架構,是硬件。由於intel的8086,286,386~586而得名,amd開發的大部分CPU也是基於x86架構的。x86架構的特色是CPU的寄存器是32位的,所以也叫32位CPU。基於32位CPU開發的操做系統就叫32位操做系統。
C#編譯器生成的程序集要麼包含一個PE32頭,要麼包含一個PE32+的頭。
加載CLR的過程:
v 當雙擊一個.exe文件時,Windows會檢查EXE文件的頭(PE32頭或PE32+頭),判斷應用程序須要的是32位地址空間,仍是64位地址空間。
v 會在進程的地址空間中加載MSCorEE.dll的x86,x64版本。
v 進程的主程序調用MSCorEE.dll中定義的_CorExeMain方法,這個方法初始化CLR,加載EXE程序集,而後調用其入口方法(Main)。
v 託管的應用程序將啓動並運行。
初始化CLR包括:
v 分配一塊內存空間,創建託管堆及其它必要的堆,由GC監控整個託管堆。
v 建立線程池。
v 建立應用程序域 (AppDomain):利用sos.dll能夠查看CLR建立了哪些AppDomain。
託管程序集包含元數據和IL。IL是和CPU無關的機器語言,比大多數CPU機器語言都要高級,可將IL視爲面向對象的機器語言。
IL能訪問和操做對象類型,並提供了指令來建立和初始化對象、調用對象上的虛方法以及直接操做數組元素等。
就在Main方法執行以前,CLR會檢測出Main的代碼中引用的全部類型。這致使CLR爲類型建立一個內部數據結構,它用於管理對所引用的類型的訪問。
如上圖中,Main方法引用了一個Console類型,這致使CLR分配一個內部數據結構。在這個結構中,Console類型定義的每一個方法都有一個對應的記錄項。每一個記錄項都容納了一個地址,根據此地址便可找到方法的實現。對這個結構進行初始化時,CLR將每一個記錄項都指向包含在CLR內部的一個未文檔化的函數(C#中沒有函數的概念,一概稱爲方法)。這個函數稱爲JITCompiler。
Main方法首先調用WriteLine方法時,JITCompiler函數會被調用。JITCompiler函數負責將一個方法的IL代碼即時編譯成本地CPU指令。
JITCompiler函數被調用時,它知道要調用的是哪一個方法,以及具體是什麼類型定義了該方法。而後,JITCompiler會在定義該類型的程序集的元數據中查找被調用的方法的IL。接着,JITCompiler驗證IL代碼,並將IL代碼即時編譯成本地CPU指令。本地CPU指令被保存到一個動態分配的內存塊中。而後,JITCompiler返回CLR爲類型建立的內部數據結構,找到與被調用的方法對應的那一條記錄,修改最初對JITCompiler的引用,讓它如今指向內存塊的地址。最後,JITCompiler函數跳轉到內存塊中的代碼。這些代碼正是WriteLine方法的具體實現。這些代碼執行完畢並返還時,會返還到Main中的代碼,並嚮往常同樣繼續執行。
如今,Main要第二次調用WriteLine。這一次因爲已對WriteLine的代碼進行了驗證和編譯,因此會直接執行內存塊中的代碼,徹底跳過JITCompiler函數。
一個方法只有在首次調用時纔會形成一些性能損失。之後對該方法的全部調用都以本地代碼的形式全速進行,無需從新驗證IL並把它編譯成本地代碼。
JIT編譯器將本地CPU指令存儲到動態內存中,一旦應用程序終止,編譯好的代碼也會被丟棄。全部,若是未來再次運行應用程序,JIT編譯器必須再次將IL編譯成本地指令。
在Visual Studio中新建一個C#項目時,項目的debug配置指定的是/optimize-和/debug: full開關。Release 配置指定的是/optimize-和/debug: pdbonly開關。
只有在指定/debug(+/full/pdbonly)開關的前提下,編譯器纔會生成一個Program Debug Database (PDB)文件。PDB文件幫助調試器查找局部變量並將IL指令映射到源代碼。
1.4.1 IL和驗證
IL是基於棧的。這意味着它的全部指令都要將操做數壓入(push)一個執行棧,並從棧彈出(pop)結果。
操做數是操做符做用於的實體,是表達式中的一個組成部分,它規定了指令中進行數字運算的量 。操做數就是你直接處理的數據,操做數地址就是操做數存放在內存的物理地址。
表達式是操做數與操做符的組合。一般一條指令均包含操做符和操做數。
堆棧是兩種數據結構。堆棧都是一種數據項按序排列的數據結構,都在進程的虛擬內存中,(在32位處理器上每一個進程的虛擬內存爲4GB),只能在一端(稱爲棧頂(top))對數據項進行插入和刪除。要點:堆(heap),隊列優先,先進先出(FIFO—first in first out)。棧(stack),先進後出(FILO—First-In/Last-Out)。
IL的最大優點並不在它對底層CPU的抽象,而在於應用程序的健壯性和安全性。將IL編譯成本地代碼CPU指令時,CLR會執行一個名爲驗證(verification)的過程。這個過程會檢查高級IL代碼,肯定代碼所作的一切都是安全的。列如,驗證會覈實調用的每一個方法都有正確數量的參數,傳給每一個方法的每一個參數都具備正確的類型,每一個方法的返回值都獲得了正確的使用,每一個方法都有 一個返回語句,等等。
在windows中,每一個進程都有它本身的虛擬地址空間,這是由於不能簡單的信任一個應用程序代碼。經過驗證託管代碼,可確保代碼不會不正確地訪問內存,不會干擾到另外一個應用程序的代碼。這樣一來就能夠放心的將多個託管應用程序放到一個Windows虛擬地址空間中運行。
事實上,CLR確實提供了在一個操做系統進程中執行多個託管應用程序的能力。每一個託管的應用程序都在一個APPDomain中執行。默認狀況下,每一個託管的EXE文件都在它本身的獨立地址空間中運行,這個空間地址只有一個APPDomain。然而,CLR的宿主進程(好比IIS,SQL Server)可決定在單個操做系統進程中運行多個APPDomain。
.Net Framework中包含了Framework類庫(Framework Class Library, FCL)。FCL是一組DLL程序集的統稱,其中包含了數千個類型定義,每一個類型都公開了一些功能。
能夠利用這些程序集來建立其中一部分應用程序
因爲FCL包含數量極多的類型,因此有必要將相關的一組類型放到一個單獨的命名空間中。System命名空間包含Object基類型,其餘全部類型最終都是從這個基類型派生來的。
System命名空間包含用於整數、字符、字符串、異常處理以及控制檯I/O的類型。
爲了使用Framework的任何一個功能,必須知道這個功能是由什麼類型提供的,以及該類型包含在哪一個命名空間中。
CLR是徹底圍繞類型展開的。因爲類型是CLR的根本,因此Microsoft制定了一個正式的規範,即「通用數據類型」(Common Type System),它描述了類型的定義和行爲。
CTS規範規定一個類型能夠零個或多個成員:
CTS指定了類型可視性規則以及類型成員的訪問規則:
CTS還爲類型繼承、虛方法、對象生存期等定義了相應的規則。
全部類型最終必須從預約義的System.Object類型繼承。Object是System命名空間中定義的一個類型的名稱。Object是其餘全部類型的根。
System.Object類型容許作下面事情:
CLR集成了全部語言,容許在一種語言中使用由另外一種語言建立的對象,由於CLR使用了標準類型集、元數據以及公共執行環境。
Microsoft定義了一個公共語言規範(Common Language Specification),它詳細定義了一個最小功能集。
本節討論如何將包含多個類型的源代碼文件生成爲一個可部署的文件。
System. Console是Microsoft已經實現好的一個類型,用於實現這個類型的各個方法的IL代碼存儲在MSCorLib.dll文件中。
Program.exe中到底包含什麼內容呢?
一個託管PE文件有4個部分組成:PE32(+)頭、CLR頭、元數據以及IL。
PE32(+)頭是Windows要求的標準信息,
元數據是一個二進制數據塊,由定義表(definition table)、引用表(reference table)和清單表(manifest table)構成。
對於這個如此小的Program.exe應用程序,PE頭和元數據佔據了文件至關大的一部分。固然隨着應用程序規模的增大,它會重用它的大部分類型以及對其餘類型
程序集的引用,形成元數據和頭信息在整個文件中所佔的比例逐漸減少。
「運行時」要求每一個類型最終都從System.Object 類型派生,因此能夠保證每一個類型的每一個對象都有一組最基本的方法。
重寫(override):繼承時發生,在子類中從新定義父類中的方法,子類中的方法和父類的方法是同樣的(即方法名,參數,返回值都相同),由 override 聲明重寫的方法稱爲重寫基方法。
例如:基類方法中聲明爲virtual,派生類中使用override申明此方法的重寫。
重寫override通常用於接口實現和繼承類的方法改寫,要注意:
v 覆蓋的方法的標誌必需要和被覆蓋的方法的名字和參數徹底匹配,才能達到覆蓋的效果;
v 覆蓋的方法的返回值必須和被覆蓋的方法的返回一致;
v 覆蓋的方法所拋出的異常必須和被覆蓋方法的所拋出的異常一致,或者是其子類;
v 被覆蓋的方法不能爲private,不然在其子類中只是新定義了一個方法,並無對其進行覆蓋。
v 不能重寫非虛方法或靜態方法。重寫的基方法必須是 virtual、abstract 或 override 的。
namespace 方法重寫
{
class TestOverride
{
public class Employee
{
public string name;
// Basepay is defined as protected, so that it may be accessed only by this class and derived classes.
protected decimal basepay;
// Constructor to set the name and basepay values.
public Employee(string name, decimal basepay)
{
this.name = name;
this.basepay = basepay;
}
// Declared virtual so it can be overridden.
public virtual decimal CalculatePay()
{
return basepay;
}
}
// Derive a new class from Employee.
public class SalesEmployee : Employee
{
// New field that will affect the base pay.
private decimal salesbonus;
// The constructor calls the base-class version, and initializes the salesbonus field.
public SalesEmployee(string name, decimal basepay,
decimal salesbonus)
: base(name, basepay)
{
this.salesbonus = salesbonus;
}
// Override the CalculatePay method to take bonus into account.
public override decimal CalculatePay()
{
return basepay + salesbonus;
}
}
static void Main()
{
// Create some new employees.
SalesEmployee employee1 = new SalesEmployee("Alice",
1000, 500);
Employee employee2 = new Employee("Bob", 1200);
Console.WriteLine("Employee4 " + employee1.name +
" earned: " + employee1.CalculatePay());
Console.WriteLine("Employee4 " + employee2.name +
" earned: " + employee2.CalculatePay());
}
}
/*
Output:
Employee4 Alice earned: 1500
Employee4 Bob earned: 1200
*/
}
System.Object的公共方法:
System.Object的受保護的方法:
內存格局一般分爲四個區:
線程堆棧(Thread Stack)和託管堆(Managed Heap)
每一個正在運行的程序都對應着一個進程(process),在一個進程內部,能夠有一個或多個線程(thread),每一個線程都擁有一塊「自留地」,稱爲「線程堆棧」,大小爲1M,用於保存自身的一些數據,好比函數中定義的局部變量、函數調用時傳送的參數值等,這部份內存區域的分配與回收不須要程序員干涉,主要由操做系統管理。全部值類型的變量都是在線程堆棧中分配的。
另外一塊內存區域稱爲「堆(heap)」,在.NET 這種託管環境下,堆由CLR 進行管理,因此又稱爲「託管堆(managed heap)。
託管堆是CLR中自動內存管理的基礎。初始化新進程時,運行時會爲進程保留一個連續的地址空間區域。這個保留的地址空間被稱爲託管堆。託管堆維護着一個指針,用它指向將在堆中分配的下一個對象的地址。最初,該指針設置爲指向託管堆的基址。
CLR要求全部類型對象都用new操做符來建立:Employee emp = new Employee (「ConstructorParam1」);
用new 關鍵字建立類的對象時,分配給對象的內存單元就位於託管堆中。在程序中咱們能夠隨意地使用new 關鍵字建立多個對象,所以,託管堆中的內存資源是能夠動態申請並使用的,固然用完了必須歸還。
聲明一個Employee的引用emp,在線程堆棧上給這個引用分配存儲空間,這僅僅只是一個引用,不是實際的Employee對象。假定emp佔4個字節的空間,包含了存儲Employee的引用地址。接着分配託管堆上的內存來存儲Employee對象的實例,假定Employee對象的實例是32字節,爲了在託管堆上找到一個存儲Employee對象的存儲位置,.Net運行庫在託管堆中搜索第一個從未使用的,32字節的連續塊來存儲Employee對象的實例,而後把分配給Employee對象實例的地址賦給emp變量。new執行了以上全部這些操做以後,會返回指向新建對象的一個引用。在前面的示例代碼中,這個引用會保存到變量emp中。
如下是new操做符所作的事情:
class SampleClass
{
public string name;
public int id;
public SampleClass() { }
public SampleClass(int id, string name)
{
this.id = id;
this.name = name;
}
}
class ProgramClass
{
static void Main()
{
SampleClass Employee2 = new SampleClass(1234, "Cristina Potra");
}
}
沒有和new操做符對應的一個delete操做符。換言之,沒有辦法顯示釋放 爲一個對象分配的內存。CLR採用了垃圾回收機制,能自動檢測到一個對象再也不被使用或訪問,並自動釋放對象的內存。
CLR最重要的特性之一就是類型安全性。在運行時,CLR老是知道一個對象是什麼類型。調用GetType方法,老是知道一個對象確切的類型是什麼。
CLR容許將一個對象轉換爲它的實際類型或者它的任何基類型。
C#不要求特殊語法便可將一個對象轉換爲它的任何基類型,由於向基類型轉換被認爲是安全的隱式轉換。然而,將對象轉換爲它的某個派生類型時,C#要求開發人員只能進行顯式轉換。
隱式轉換不須要在代碼中指定轉換類型,例如:int intNumber = 10; double doubleNumber = intNumber; intNumber會被隱式轉換成double類型。
顯式轉換則相反,須要指定轉換類型,例如:double doubleNumber = 10.1; int intNumber = (int)doubleNumber;
對於表示數值的基本數據類型來講,數值範圍小的數據類型轉換成數值範圍大的數據類型能夠進行隱式轉換,而反過來則必須進行顯示轉換。
就像上面的兩個例子同樣。 對於類類型來講,子類轉換成父類能夠進行隱式轉換,而反過來則必須進行顯式轉換,例如:string str1 = "abc";object obj = str1; //子類轉換成父類,隱式轉。 string str2 = (string)obj; //父類轉換成子類,顯式轉換 若是兩個類之間沒有繼承關係,則不能進行隱式轉換或顯式轉換,此時必須在被轉換的類中定義一個隱式轉換方法或顯式轉換方法。
在Main方法中,會構造一個Manager對象,並將其傳給PromoteEmployee。這些代碼能成功編譯並運行,由於Manager最終從Object派生的,而PromoteEmployee期待的正是Object。在PromoteEmployee內部,CLR覈實o引用的是一個Employee對象,或者是從Employee派生的一個類型的對象。因爲Manager是從Employee派生的,因此CLR執行類型轉換,運行PromoteEmployee繼續執行。
PromoteEmployee返回以後,Main繼續構造一個DateTime對象,並將其傳給PromoteEmployee。一樣的,DateTime是從Object派生的,因此編譯器會順利編譯調用
PromoteEmployee的代碼。但在PromoteEmployee內部,CLR會檢查類型轉換,發現o引用的是一個DateTime對象,它既不是一個Employee,也不是從Employee派生的任何類型。因此CLR會禁止轉型,並拋出一個System.InvalidCastException異常。
聲明PromoteEmployee方法的正確方式是將參數類型指定Employee,而不是Object。
v 使用C#的is和as操做符來轉型
is檢查一個對象是否兼容於指定類型,並返還一個Boolean值:true或false。is操做符永遠不會拋出異常。
若是對象引用爲null,is操做符總會返還false,由於沒有可檢查其類型的對象。
as操做符的工做方式與強制類型轉換同樣,只是它永遠不會拋出一個異常。
檢查最終生成的引用是否爲null。
命名空間用於對相關的類型進行邏輯性分組,開發人員使用命名空間來方便地定位一個類型。
密封類的修飾符,用了這個修飾符的類就不能被其餘類繼承了。
應該有一種簡單的方式來直接引用FileStream和StringBuilder類型。C#編譯器經過using指令來提供這種機制。
C#的using指令指示編譯器嘗試爲一個類型附加不一樣的前綴,直到找到一個匹配項。
using指令容許爲一個類型或命名空間建立別名。
在C#中namespace指令的做用:只是告訴編譯器爲源代碼中出現的每一個類型名稱附加命名空間名稱前綴,減小程序員的打字量。
命名空間和程序集不必定是相關的。
同一個命名空間的各個類型可能在不一樣的程序集中實現。例如:System.IO.FileStream類型是在MSCorLib.dll程序集中實現,而System.IO.FileSystemWatcher類型是在System.dll程序集中實現的。
參考http://www.cnblogs.com/MeteorSeed/archive/2012/01/24/2325575.html
本節將解釋類型、對象、線程堆棧和託管堆在運行時的相互關係,以及調用靜態方法、實例方法和虛方法的區別。
進程是指在系統中正在運行的一個應用程序;線程是系統分配處理器時間資源的基本單元,或者說進程以內獨立執行的一個單元。
當系統加載一個CLR的進程,進程裏面可能有多個線程,這時候系統會給這個進程建立一個大小爲1M的線程堆棧。這個線程堆棧用來存放方法調用的實參,和方法內部定義的局部變量。
編譯器(Compiler)直接支持的數據類型稱爲基元類型(primitive type)。
我但願編譯器根本不要提供基元類型名稱,強制開發人員使用FCL(Framework類庫)類型名稱:
許多開發人員都困惑於到底應該使用string仍是String。因爲C#的string直接映射到System.String,因此二者是沒有區別的。int始終映射到System.Int32,因此無論在什麼操做系統上運行,表明的都是32位整數。
雖然FCL中大多數類型是引用類型,但程序員用的最多的仍是值類型。
值類型:原類型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚舉(enum)、結構(struct)。
爲提高簡單的經常使用的類型的性能,CLR提供了「值類型」的輕量級類型。值類型的實例通常在線程堆棧上分配。
引用類型老是從託管堆上分配的,C#的new操做符會返回對象的內存地址——也就是指向對象數據的內存地址。
引用類型共有四種:類類型、接口類型、數組類型和委託類型。全部引用類型變量所引用的對象,其內存都是在託管堆中分配的。
使用引用類型時必須考慮性能問題,首先考慮如下事實:
在表明值類型實例的一個變量中,並不包含一個指向實例的指針。相反,變量包含了實例自己的字段。因爲變量已經包含了實例的字段,因此爲了操做實例中的字段,再也不須要提領一個指針,值類型的實例不受垃圾回收器的控制,超出了做用範圍,系統就會自動釋放。所以值類型的使用緩解了託管堆中的壓力,並減小了一個應用程序在其生存期內須要進行的垃圾回收次數。
在.Net Framework SDK文檔中,任何稱爲「類」的類型都是引用類型。例如:System.Exception類、System.IO.FileStream類以及System.Random類都是引用類型。
相反,文檔中將全部值類型稱爲結構或枚舉。例如:System.Int32結構、System.TimeSpan結構、System.DayofWeek枚舉。全部結構都是抽象類型System.ValueType的直接派生類。全部枚舉都從System.Enum 抽象類型派生。System.Enum又是從System.ValueType派生。
全部值類型都是隱式密封的(sealed),目的是防止將一個值類型用做其餘任何引用類型或值類型的基類型。
在代碼中使用類型時,必須注意該類型是引用類型仍是值類型。
SomeVal v1 = new SomeVal ();
上面一行代碼彷佛要在託管堆上分配一個SomeVal實例。然而,C#編譯器知道SomeVal是一個值類型,因此會生成相應的IL代碼,在線程堆棧上分配一個SomeVal實例。C#還會確保值類型中全部字段都初始化爲零。
如下條件都知足時,才應該將一個類型聲明爲值類型:
聲明爲值類型除了知足上面3個條件外,還必須知足一下任何一個條件:
值類型和引用類型的區別:
private static void Main()
{
int i;
MyClass mc;
i = 5;
mc = new MyClass();
}
當一個局部變量聲明以後,就會在棧的內存中分配一塊內存給這個變量,至於這塊內存多大,裏面存放什麼東西,就要看這個變量是值類型仍是引用類型了。
若是是值類型,爲變量分配這塊內存的大小就是值類型定義的大小,存放值類型自身的值(內容)。好比,對於上面的整型變量 i,這塊內存的大小就是 4個字節(一個 int型定義的大小),若是執行 i = 5;這行代碼,則這塊內存的內容就是 5(如圖 -1)。
對於任何值類型,不管是讀取仍是寫入操做,能夠一步到位,由於值類型變量自己所佔的內存就存放着值。
若是是引用類型,爲變量分配的這塊內存的大小,就是一個內存指針(實例引用、對象引用)的大小(在 32位系統上爲 4字節,在 64位系統上爲 8字節)。由於全部引用類型的實例(對象、值)都是建立在託管堆上的,而這個爲變量分配的內存就存放變量對應在堆上的實例(對象、值)的內存首地址(內存指針),也叫實例(對象)的引用。
由圖 -2可知,變量 mc中存放的是 MyClass實例(對象)的對象引用,若是須要訪問 mc實例,系統須要首先從 mc變量中獲得實例的引用(在堆中的地址),而後用這個引用(地址)找到堆中的實例,再進行訪問。須要至少 2步操做才能夠完成實例訪問。
值類型是比引用類型更「輕型」的一種類型,由於它不做爲對象在託管堆中分配,不會被垃圾回收,也不經過指針來引用。但在許多狀況下,都須要獲取對值類型的一個實例的引用,即將值類型轉換成引用類型。
如上面代碼,建立一個ArrayList對象(System.Collections命名空間中定義的一個類型)來容納一組Point結構。
每一次循環迭代都會初始化值類型字段(x和y)。而後這個Point會存儲到ArrayList中。但ArrayList中究竟存儲的是什麼?是Point結構,仍是其餘什麼東西。咱們必須研究ArrayList的Add方法,瞭解它的參數被定義成什麼類型。
Add須要獲取一個Object參數。換言之,Add須要獲取對託管堆上的一個對象的引用(指針)來做爲參數。但在以前的代碼中,傳遞的是p,也就是一個Point,是一個值類型。爲了將一個值類型轉換成引用類型,要使用一個名爲裝箱(boxing)的機制。
對值類型的一個實例進行裝箱操做時在內部發生的事情:
C#編譯器會自動生成對一個值類型的實例進行裝箱所需的IL代碼。
在上述代碼中,C#編譯器檢測到是向一個須要引用類型的方法傳遞一個值類型,因此會自動生成代碼對對象進行裝箱。在運行時,當前存在於Point值類型實例p中字段會複製到新分配的Point對象中。已裝箱的Point對象(如今是一個引用類型)的地址會返回給Add方法。Point對象會一直存在於堆中,直到被垃圾回收。Point值類型變量p能夠重用,由於ArrayList根本不知道關於它的任何事情。在這種狀況下,已裝箱的值類型的生存期超過了未裝箱的值類型的生存期。
在知道裝箱如何進行以後,接着談談拆箱。
假定須要使用如下代碼獲取ArrayList的第一個元素:
如今是要獲取ArrayList的元素0中包含的引用(或指針),並試圖將其放到一個Point值類型的實例p中。包含在已裝箱Point對象中的全部字段都必須複製到值類型變量p中,後者在線程棧上。
CLR分兩步完成這個複製操做(拆箱/複製)。
第一步,獲取已裝箱的Point對象中的各個Point字段的地址。這個過程稱爲拆箱(unboxing)
第二步,將這些字段包含的值從堆中複製到基於棧的值類型實例中
拆箱不是直接將裝箱過程倒過來。拆箱的代價比裝箱低的多。拆箱其實就是獲取一個指針的過程,該指針指向包含在一個對象中的原始值類型(數據字段)。因此,和裝箱不一樣,拆箱不要求在內存中複製任何字節,每每會緊接着拆箱操做後發生一次字段的複製操做。
一個已裝箱的值類型實例在拆箱時,內部會發生下面這些事情:
以上代碼從邏輯上說,徹底能夠獲取o所引用一個已裝箱的Int32,而後將其強制轉換爲一個Int16。然而,在對一個對象進行拆箱的時候,只能將其轉型爲原先未裝箱時的值類型---本例即爲Int32
下面的代碼是正確的寫法:
overload:重載指的是同一個類中有兩個或多個名字相同可是參數不一樣(參數個數和參數類型)的方法。
因爲未裝箱的值類型沒有同步塊索引,因此不能使用System.Threading.Monitor類型的各類方法,讓多個線程同步對這個實例的訪問。
5.3.2對象相等性和同一性(之後重看)
有時須要將對象放到一個集合中,並編寫代碼對集合中的對象進行排序、搜索或比較。
對於Object的Equals方法的默認實現來講,它實現的實際是同一性(identity),而非相等性(equality)。
哈希:經過將哈希算法應用到任意數量的數據所獲得的固定大小的結果。若是輸入數據中有變化,則哈希也會發生變化。哈希可用於許多操做,包括身份驗證和數字簽名。也稱爲「消息摘要」。
哈希表:根據設定的哈希函數和處理衝突方法將一組關鍵字映象到一個有限的地址區間上,並以關鍵字在地址區間中的象做爲記錄在表中的存儲位置,這種表稱爲哈希表或散列,所得存儲位置稱爲哈希地址或散列地址。做爲線性數據結構與表格和隊列等相比,哈希表無疑是查找速度比較快的一種。
在.NetFramework中,HashTable是System.Collections命名空間提供的容器,用來處理和表現相似keyvalue的鍵\值對。其中key區分大小寫,一般用來快速查找。value用來儲存對應於key的值。Hashtable中keyvalue鍵\值對均爲object類型,因此Hashtable能夠支持任何類型的keyvalue鍵\值對。
Hashtable是非泛型的集合,因此在檢索和存儲值類型時一般會發生裝箱與拆箱的操做。
在哈希表中添加一個keyvalue鍵\值對:HashtableObject.Add(key,value);
在哈希表中去除某個keyvalue鍵\值對:HashtableObject.Remove(key);
從哈希表中移除全部元素:HashtableObject.Clear();
判斷哈希表是否包含特定鍵key:HashtableObject.Contains(key);
哈希算法:將任意長度的二進制值映射爲固定長度的較小二進制值,這個小的二進制值稱爲哈希值。哈希值是一段數據惟一且極其緊湊的數值表示形式。
hashcode標識對象的地址,用於區別不一樣的對象。
普通的查找慢是由於要一個一個比, Hash就是讓把比較的次數下降 而下降的辦法就是靠計算。
哈希算法會根據你要存入的數據,先經過該算法計算出一個地址值,這個地址值就是你須要存入到集合當中的數據的位置,而不會像數組那樣一個個的進行挨個存儲,挨個遍歷一遍後面有空位就存這種狀況了。而你查找的時候也是根據這個哈希算法來的。將你的要查找的數據進行計算,得出一個地址,這個地址會映射到集合當中的位置,這樣就可以直接到這個位置上去找了,而不須要像數組那樣,一個個遍歷,一個個對比去尋找,這樣天然增長了速度,提升了效率了。
若是能將任何對象的任何實例放到一個哈希表集合中,會帶來不少好處。爲此,System.Object提供了虛方法GetHashCode,它能獲取任意對象的Int32哈希碼。
若是你定義的一個類型重寫了Equals方法,那麼還應重寫GetHashCode方法,確保相等性算法和對象哈希碼算法是一致的。是由於在System.Collections.Hashtable類型、System.Collections.Generic.Dictionary類型以及其餘一些集合的實現中,要求兩個對象爲了相等,必須具備相同的哈希碼。
簡單地說,在一個集合中添加一個鍵\值對時,首先會獲取鍵對象的一個哈希碼。這個哈希碼指出鍵\值對應該儲存到哪個哈希桶(bucket)中。集合須要查找一個鍵時,會獲取指定的鍵對象的哈希碼。這個哈希碼標識瞭如今要搜索的目標哈希桶,要在其中查找與指定鍵對象相等的一個鍵對象。採用這種算法來儲存和查找鍵,意味中一旦修改了集合中的一個鍵對象,集合就再也找不到對象。因此,須要修改一個哈希表中的鍵對象時,正確的作法是移除原來的鍵\值對,修改鍵對象,再將新的鍵\值對添加回哈希表。
C#是一種類型安全的編程語言。這意味着全部表達式都解析成某個類型的一個實例,在編譯器生成的代碼中,只會執行對這個類型來講有效的操做。
從面向對象的角度來看, 對象的實例表示的是 個體, 而static的屬性和方法則表示 全體所共有的方法和屬性 , 如「會員張三」、「會員李四」是「會員」的兩個個體, 暱稱、等級是他們各自不一樣的屬性,而 會員總數、註冊新會員 則是 全體會員所共享的屬性和方法。 雙好比 「圓」這個class, 半徑、面積、周長是 個體的屬性,而圓周率PI則是共性。
從應用的角度來看,本質就是爲了節省內存,在內存中只有一個引用。
靜態類的主要功能以下:
在許多時候,程序仍需處理一些運行時纔會知曉的消息。若是你寫的是一個純C#應用程序,那麼只有在使用反射的時候,纔會在運行時才能肯定的信息打交道。然而,許多開發者在使用C#時,都要和一些不是和C#實現的組件進行通訊。有的組件是.Net動態語言,好比Python或Ruby,有的是HTML文檔對象模型(DOM)對象。
C#編譯器容許將一個表達式的類型標記爲dynamic。還能夠將一個表達式的結果放到一個變量中,並將變量的類型標記爲dynamic。而後能夠用這個dynamic表達式/變量調用一個成員,好比字段、屬性/索引器、方法、委託以及一元/二元/轉換操做符。
代碼使用dynamic表達式/變量調用一個成員時,編譯器會生成一個特殊的IL代碼來描述所需的操做。這種特殊的代碼稱爲payload(有效載荷)。在運行是payload代碼會根據當前由dynamic表達式/變量引用的對象的實際類型來決定具體執行的操做。
這些Payload代碼使用了一個稱爲運行時綁定器(runtime binder)的類。C#的運行時綁定器的代碼在Microsoft.CSharp.dll程序集中,構建使用dynamic關鍵字的項目時,必須引用該程序集。
Plus方法將參數的類型申明爲dynamic,在方法內部,實參做爲二元+操做符的兩個操做數使用。因爲arg是dynamic,因此C#編譯器會生成payload代碼,以便在運行時檢查arg的實際類型,並決定+操做符實際要作的事情。
第一次調用Plus時,傳遞的是5(一個Int32),因此Plus向它的調用者返回值10。結果放到result變量(一個dynamic類型)中。而後調用M方法,將result傳給它。針對對M的調用,編譯器會生成payload代碼,以便在運行時檢查傳給M的值的實際類型,以及應該調用M方法的重載版本。
第二次調用Plus時,同第一次的原理同樣。
在字段類型、方法參數或方法返回類型被指定爲dynamic的前提下,編譯器會將這個類型轉換爲System.Object,並在元數據中向字段、參數或方法類型應用System.Runtime.ComplierServices.DynamicAttribute的一個實例。若是是一個局部變量被指定爲dynamic,變量類型也會變成Object,但不會向局部變量應用DynamicAttribute,由於它的使用限制在方法以內。
在運行時,Microsoft.CSharp.dll程序集必須加載到AppDomain中,這會損壞應用程序的性能,並增大內存耗用。雖然能用動態功能簡化語法,但也要看是否值得。
在本章及本部分後續的章節,我將解釋如何在一個類型中定義不一樣種類的成員,從而設計出符合本身須要的類型。
在一個類型中能夠定義0個或多個如下種類的成員:
不管使用什麼編程語言,它的編譯器都必須能處理你的源代碼,爲上述列表中的每一種成員生成元數據和IL代碼。不管使用的編程語言是什麼,元數據的格式都是徹底一致的。正是由於這個特色,才使CLR成爲公共語言運行時。
CLR使用公共元數據格式決定常量、字段、構造器、方法、屬性和事件在運行時的行爲。元數據是整個.Net Framework開發平臺的關鍵,它實現了編程語言、類型和對象的無縫集成。
如下C#代碼展現了一個類型定義,其中包含了全部可能的成員。
在文件範圍內定義類型時,能夠將類型的可見性指定爲public和internal。
public類型不只對它的定義程序集中的全部代碼可見,還對其餘程序集中的代碼可見。
internal類型僅對定義程序集中的全部代碼可見,對其餘程序集中的代碼不可見。
定義類型時,若是不顯式指定類型的可見性,C#編譯器默認將類型的可見性設爲internal(二者中較有限的那一個)。
友元程序集
友元程序集功能用於訪問內部成員;私有類型和私有成員仍然不可訪問。
若要使程序集(程序集 B)可以訪問另外一個程序集(程序集 A)的內部類型和成員,應使用程序集 A 中的 InternalsVisibleToAttribute 屬性
在代碼中引用一個成員時,成員的可訪問性指出這種引用是否合法。
固然,任何成員想要被別人訪問到,都必須在一個可見的類型內定義。例如,若是程序集AssemblyA定義了一個internal類型,該類型有一個public方法,那麼程序集AssemblyB中的代碼不能調用AssemblyA中的public方法,由於internal類型對於AssemblyB是不可見的。
在C#中,若是沒有顯式聲明成員的可訪問性,編譯器一般默認選擇private(限制最大的那個)。CLR要求接口類型的全部成員都具備public可訪問性。
靜態類是不能實例化的,例如Console,Math,Environment和ThreadPool類。這些類只有static成員。咱們直接使用它的屬性與方法,靜態類最大的特色就是共享,做用是將一組相關的成員組合到一塊兒。例如Math類中定義了一組執行數學運算的方法。Math 類:爲三角函數、對數函數和其餘通用數學函數提供常數和靜態方法。
static關鍵字只能應用於類,不能應用於結構(值類型)。這是由於CLR老是容許值類型實例化。
靜態類的主要特色以下:
C#編譯器對靜態類進行了以下限制:
在類或結構內部定義的類型稱爲嵌套類型。例如:
class Container
{
class Nested
{
Nested() { }
}
}
經過static關鍵字修飾,是屬於類,實例成員屬於對象,在這個類第一次加載的時候,這個類下面的全部靜態成員會被加載。
實例構造函數:使用 new 表達式建立某個類的對象時,會使用實例構造函數建立和初始化全部實例成員變量。
只要建立基於 CoOrds 類的對象,就會調用此實例構造函數。 諸如此類不帶參數的構造函數稱爲「默認構造函數」。 然而,提供其餘構造函數一般十分有用。 例如,能夠向 CoOrds 類添加構造函數,以即可覺得數據成員指定初始值:
class CoOrds
{
public int x, y;
// constructor
public CoOrds()
{
x = 0;
y = 0;
}
public CoOrds(int x, int y)
{
this.x = x;
this.y = y;
}
}
class MainClass
{
static void Main()
{
CoOrds p1 = new CoOrds();
CoOrds p2 = new CoOrds(5, 3);
}
}
partial這個關鍵字告訴C#編譯器,一個類、結構或者接口的定義源代碼可能要分散到一個或多個源代碼文件中。
當使用大項目或自動生成的代碼(如由 Windows 窗體設計器提供的代碼)時,將一個類、結構或接口類型拆分到多個文件中的作法就頗有用。
局部類型適用於如下狀況:
局部類型的注意點:
組件軟件編程(Component Software Programming)
下面列舉了組件的一些特色:
在.Net中,版本號爲1.2.3.4的程序集,其主版本號1,次版本號2,內部版本號3,修訂號爲4。
6.6.1 CLR如何調用虛方法、屬性和事件
方法表明在類型或者類型的實例執行某些操做的代碼。
在類型上執行操做稱爲靜態方法。在類型的實例上執行操做稱爲非靜態方法。
任何方法都有一個名稱、一個簽名和一個返回值(能夠是void)。
常量(constant)是一個特殊的符號,它有一個從不變化的值。常量只能在聲明中初始化。定義常量符號時,它的值必須在編譯時肯定。
常數表達式是在編譯時可被徹底計算的表達式。所以不能從一個變量中提取的值來初始化常量。
若是 const int a = b+1;b是一個變量,顯然不能再編譯時就計算出結果,因此常量是不能夠用變量來初始化的。
肯定以後,編譯器將常量的值保存到程序集的元數據中。這意味着只能爲編譯器認定的基元類型定義常量。然而C#也容許定義一個非基元類型的常量變量(constant variable),前提是把它的值設爲null。
class Calendar1
{
public const int months = 12;
}
代碼引用一個常量符號時,編譯器會在定義常量的程序集的元數據中查找該符號,提取 常量的值,並將值嵌入生成的IL代碼中。因爲常量的值直接嵌入代碼,因此在運行時不須要爲常量分配任何內存。
常量是在編譯時已知並在程序的生存期內不發生改變的不可變值。常量使用const修飾符進行聲明。
只有C#內置類型能夠聲明爲const。用戶定義的類型(類,結構和數組)不能爲const。請用readonly修飾符建立在運行時初始化一次即不可更改的類、結構和數組。
能夠使用枚舉類型爲整數內置類型(列如int、long等)定義命名常量。
當編譯器遇到 C# 源代碼中的常量修飾符時,將直接把文本值替換到它生成的中間語言 (IL) 代碼中。由於在運行時沒有與常量關聯的變量地址,因此 const 字段不能經過引用傳遞。
常量可標記爲 public、private、protected、internal 或 protectedinternal。
未包含在定義常量的類中的表達式必須使用類名、一個句點和常量名來訪問該常量。例如:
int birthstones = Calendar.months;
因爲常量的值從不變化,常量老是被視爲靜態成員,而不是實例成員。
常量沒有很好的跨程序性版本控制特性。開發人員更改了常量的值後,應用程序要獲取新的常量值,必須從新編譯。若是在運行時,從一個程序集提取另外一個程序集的值,那麼不該該使用常量,而應該使用readonly字段。
字段(field)是一種數據成員,其中容納了一個值類型的實例或者對一個引用類型的引用。
「字段」是直接在類或結構中聲明的任何類型的變量。
字段修飾符:
v static:是類型狀態的一部分,而不是對象狀態的一部分。這使得調用方法在任什麼時候候都可以使用字段,即便類沒有任何實例。無論包含該靜態字段的類生成多少個對象或根本無對象,該字段都只有一個實例,靜態字段不能被撤銷。必須採用以下方法引用靜態字段:類名.靜態字段名。
v readonly:只讀字段只能在初始化期間(字段聲明中)或在定義類的構造函數中賦值(這種構造器方法只能調用一次,也就是對象首次建立時),在其它任何地方都不能改變只讀字段的值。注意:但可利用反射來修改readonly字段。
CLR支持類型(靜態)字段和實例(非靜態)字段。對於類型字段,用於容納字段數據的動態內存是在類型對象中分配的,而類型對象是在類型加載到一個AppDomain時建立的。對於實例字段,用於容納字段數據的動態內存則是在構造類型的實例時分配的。
AppDomain簡單的說就是應用程序的邊界。能夠經過它對相同進程中進行再次隔離。一個程序在運行的時候,它和它所引用、反射加載的全部模塊的集合構成了一個程序域。普通桌面程序,一個程序就是一個AppDomain。CLR容許在一個進程中託管多個程序(好比IIS一類程序),一個IIS是能夠運行不少網站的,若是這些網站都放在一個AppDomain裏,一個網站崩潰了,其餘網站也不能訪問了。若是每一個網站都做爲獨立的程序,對機器的性能要求又過高,並且無法共享一些資源。因此.net就有AppDomain的概念,一個IIS進程裏,給每一個網站一個AppDomain,這個每一個網站都相互獨立。
因爲字段存儲在動態內存中,因此他們的值在運行時才能獲取。字段能夠是任何數據類型,沒必要像常量那樣僅僅是編譯器內置的基元類型。
構造器(constructor)是容許將類型的實例初始化爲良好狀態的一種特殊方法。
編譯後,構造器方法在「方法定義元數據表」中始終叫.ctor。
建立一個引用類型的實例,首先爲實例的數據字段分配內存,而後初始化對象的附加字段(類型對象指針和同步塊索引),最後調用類型的實例構造器來設置對象實例的初始狀態。
實例構造器永遠不能被繼承,派生類會自動調用基類的構造函數,也就是說類只有類本身定義的實例構造器。不能將如下修飾符應用於實例構造器:virtual, new, override, sealed和abstract。(實例構造器永遠不能被繼承,由於若是帶參數的構造函數寫了不少個,那用哪個呢?)
若是你定義的類沒有顯示定義任何構造器,C#編譯器將自動隱式生成一個默認(無參)構造器,同時將字段初始化爲它們的默認值。看以下代碼:
public class A
{
}
//能夠理解爲它已經存在一個以下的構造函數
public class A
{
public A()
{
}
}
派生類構造函數自動調用基類的不帶參數的構造函數,看如下代碼:
public class B : A
{
public B()
{
}
}
//至關於
public class B : A
{
public B()
: base()
{
}
}
基類中帶參數的構造函數必須顯式調用,以下:
public class A
{
public A()
{
}
public A(string str)
{
}
}
public class B : A
{
public B()
: base("aaa")
{
}
}
base關鍵字用於從派生類中訪問基類的成員:https://msdn.microsoft.com/zh-cn/library/hfw7t1ce.aspx
v 調用基類上已被其餘方法重寫的方法。
v 指定建立派生類實例時應調用基類的構造方法。
什麼是抽象類:
抽象類提供多個派生類共享基類的公共定義,它既能夠提供抽象方法,也能夠提供非抽象方法。
抽象類不能實例化,必須經過繼承由派生類實現其抽象方法,所以對抽象類不能使用new關鍵字,也不能被密封。
若是派生類沒有實現全部的抽象方法,則該派生類也必須聲明爲抽象類。另外,實現抽象方法由overriding方法來實現。
抽象類具備如下特性:
v 抽象方法是隱式的虛方法。
v 抽象類不能實例化。
v 不能用 sealed修飾符修飾抽象類,由於這兩個修飾符的含義是相反的。
採用 sealed 修飾符的類沒法繼承,而 abstract 修飾符要求對類進行繼承。
v 從抽象類派生的非抽象類必須包括繼承的全部抽象方法和抽象訪問器的實際實現。
v 抽象類能夠包含抽象方法和抽象訪問器。
v 只允許在抽象類中使用抽象方法聲明。
v 由於抽象方法聲明不提供實際的實現,因此沒有方法體。
方法聲明只是以一個分號結束,而且在簽名後沒有大括號「{}」。
v 在派生類中,經過包括使用override修飾符的屬性聲明,能夠重寫抽象的繼承屬性。
abstract class ShapesClass
{
abstract public int Area();
}
class Square : ShapesClass
{
int side = 0;
public Square(int n)
{
side = n;
}
// Area method is required to avoid
// a compile-time error.
public override int Area()
{
return side * side;
}
static void Main()
{
Square sq = new Square(12);
Console.WriteLine("Area of the square = {0}", sq.Area());
}
}
抽象方法和虛方法最重要的區別:
v 抽象方法不能實例化,要子類必須強制性的覆蓋它的方法 。
而虛方法則是提供了選擇,能夠覆蓋能夠不覆蓋,繼承基類中的虛方法。
虛擬方法必須有一個實現部分,併爲派生類提供了覆蓋該方法的選項。
相反,抽象方法沒有提供實現部分,強制派生類覆蓋方法(不然 派生類不能成爲具體類)。
v abstract方法只能在抽象類中聲明,虛方法則不是。
v abstract方法必須在派生類中重寫,而virtual則沒必要。
v abstract方法不能聲明方法實體,虛方法則能夠。
若是類的修飾符(modifier/modify declarations)爲abstract,那麼編譯器生成的默認構造器的可訪問性就爲protected。
一個類型能夠定義多個實例構造器。爲了使代碼可驗證,類的實例構造器在訪問從基類繼承的任何字段前,必須先調用基類的構造器。
若是派生類的構造器沒有顯式調用基類的構造器,那麼C#編譯器會自動生成對默認的基類構造器的調用。
在極少數的狀況下,能夠在不調用實例構造器的前提下建立一個類型的實例。一個典型的例子是Object的MemberwiseClone方法。
該MemberwiseClone方法的做用是分配內存,初始化對象的附加字段(類型對象指針和同步塊索引),而後將源對象的字節數據複製到新對象中。
C#語言提供了一個簡單的語法,容許在構造引用類型的一個實例時,對類型中定義的字段進行初始化。換句話說,容許之內聯(inline)方法初始化實例字段。
值類型構造器的工做方式與引用類型的構造器大相徑庭。
CLR老是容許建立值類型的實例,而且沒有辦法阻止值類型的實例化。全部,值類型其實並不須要定義構造器,C#編譯器根本不會爲值類型生成默認的無參構造器。
類型構造器也稱爲靜態構造器、類構造器或者類型初始化器。
類型構造器的做用是設置類型的初始狀態。實例構造器的做用是設置類型的實例的初始狀態。
默認狀況下,類型沒有定義類型構造器。類型構造器永遠沒有參數。
以下,C#爲引用類型和值類型定義一個類型構造器:
internal sealed class SomeRefType
{
static SomeRefType() { }
}
internal struct SomeValType
{
static SomeValType() { }
}
類型構造器的特色是:無參,static標記,並且可訪問性都是private,可是不能顯示指定爲private。
定義類型構造器相似於定義無參實例構造器,區別在於必須將它們標記爲static。但C#會自動把類型構造器標記爲private。事實上若是在源代碼中顯示將類型構造器標記爲private,C#編譯器會顯示如下錯誤消息「靜態構造函數中不容許出現訪問修飾符」。必須是私有的緣由是,爲了阻止任何由開發人員寫的代碼調用它,對它的調用老是由CLR完成的。
類型構造器的調用比較麻煩。當JIT編譯器編譯一個方法時,它會檢查代碼裏面是否引入了其餘類型。若是引入了其餘類型的類型構造器,則JIT編譯器會檢測是否已經在AppDomain裏面執行過。若是沒有執行,則發起對類型構造器的調用,不然不調用。
多個線程同時調用某個類型的靜態構造器時,如何確保構造器僅僅被執行一次:
在編譯以後,線程會開始執行並最終獲取調用構造函數的代碼。實際上有多是多個線程執行同一個方法,CLR想要確保一個類型構造器在一個AppDomain裏面只執行一次。當一個類型構造器被調用時,調用的線程會獲取一個互斥的線程同步鎖,這時若是有其餘的線程在調用,則會阻塞。第一個線程會執行靜態構造器中的代碼。當第一個線程執行完後離開,其餘的線程被喚醒並發現構造器的代碼執行過了,因此不會繼續去執行了,從構造器方法返回。CLR經過這種方式來確保構造器僅僅被執行一次。
因爲CLR會確保類型構造器在每個AppDomain裏面只會執行一次,是線程安全的。因此若是要初始化任何單例對象(singleton object),放在類型構造器裏面是再合適不過了。
類型構造器裏面的代碼只能訪問類型的靜態字段,它的常規用途是初始化這些字段。C#提供了簡單的語法來初始化類型的靜態字段:
internal sealed class SomeType
{
private static Int32 s_x = 5;
}
上面的代碼生成時,編譯器自動回SomeType建立一個類型構造器以下:
internal sealed class SomeType
{
private static Int32 s_x;
static SomeType()
{
s_x = 5;
}
}
可是,C#不容許值類型使用內聯字段初始化語法來實例化字段,因此下面這種方式就是錯的:
internal sealed struct SomeType
{
private Int32 s_x = 5; //這樣會報錯,須要加static關鍵字
}
若是顯式的定義了類型構造器,以下:
internal sealed class SomeType
{
private static Int32 s_x = 5;
static SomeType()
{
s_x = 10;
}
}
最終s_x的結果是10。這裏,C#編譯器首先會生成一個類型構造器方法,這個構造器首先初始化s_x爲5,而後初始化爲10。換句話說,在類型構造器裏面的顯示定義的代碼會在 使用內聯字段初始化語法來實例化靜態字段以後執行。
只有當AppDomain卸載時,類型纔會卸載。
類型構造器的性能(不懂)
有的編程語言容許一個類型定義操做符應該如何操做類型的實例。好比System.String重載了相等(==)和不等(!=)操做符。CLR對操做符重載一無所知,它甚至不知道什麼是操做符。是編程語言定義了每一個操做符的含義,以及當這些操做符出現時,應該生成什麼樣的代碼。
例如在C#中,向基元數字應用+符合,編譯器會生成將兩個數加到一塊兒的代碼。將+操做符應用於String對象,C#編譯器會生成將兩個字符串鏈接到一塊兒的代碼。
編譯 源代碼時,編譯器會生成一個標識操做符行爲的方法。CLR規範要求操做符重載方法必須是public和static方法。
如下C#代碼中展現了一個類中定義的操做符重載方法:
namespace HelloCSharp
{
class OperatorTest
{
public int Value { get; set; }
public static void Main()
{
OperatorTest o1 = new OperatorTest();
o1.Value = 11;
OperatorTest o2 = new OperatorTest();
o2.Value = 22;
OperatorTest o3 = o1 + o2;
Console.WriteLine(o3.Value);
Console.ReadKey();
}
public static OperatorTest operator +(OperatorTest o1, OperatorTest o2)
{
OperatorTest o = new OperatorTest();
o.Value = o1.Value + o2.Value;
return o;
}
}
}
C# 容許用戶定義的類型經過使用 operator 關鍵字定義靜態成員函數來重載運算符。注意必須用public修飾,必須是類的靜態的方法。同時,重載相等運算符(==)時,還必須重載不相等運算(!=)。< 和 > 運算符以及 <= 和 >= 運算符也必須成對重載。
之後重看
因爲StringBuilder是可變的(mutable),因此它是處理字符串方法的首選。如今假定你想親自定義一些缺失的方法,以方便操做一個StringBuilder。列如,StringBuilder中沒有自定義的IndexOf方法,你也許想本身定義一個IndexOf方法。
C#擴展方法所作的事情是它容許你定義一個靜態方法,並用實例方法的語法來調用它。爲了將Indexof方法轉變成擴展方法,只需在第一個參數前添加this關鍵字:
public static class StringBuilderExtensions
{
public static Int32 IndexOf(this StringBuilder sb, Char value)
{
for (Int32 index = 0; index < sb.Length; index++)
{
if (sb[index] == value)
return index;
}
return -1;
}
}
當C#編譯器看到如下代碼:
public class TestProgram
{
public static void Main()
{
StringBuilder sb = new StringBuilder("Hello. My name is Chris.");
Int32 index = sb.IndexOf('!');
//Int32 index1 = StringBuilderExtensions.IndexOf(sb, '!');
}
}
StringBuilderExtensions.IndexOf(sb, '!')影響了咱們對代碼行爲的理解,StringBuilderExtensions的使用顯得「小題大作」,形成程序員沒法專一於當前要執行的操做:IndexOf。
編譯器首先檢查StringBuilder類或者它的任何基類是否提供了獲取單個Char參數、名爲IndexOf的一個實例方法。若是存在這樣的一個實例方法,編譯器會生成IL代碼來調用它。若是沒有發現匹配的實例方法,則繼續檢查是否存在任何靜態類定義了一個名爲IndexOf的靜態方法。靜態方法中的第一個參數的類型是和當前用於調用方法的那個表達式的類型匹配的一個類型,而且這個類型必須使用this關鍵字來標識。在本例中,表達式是sb,它是StringBuilder類型。編譯器會查找一個靜態IndexOf方法,它有兩個參數:一個StringBuilder(用this關鍵字進行標記),以及一個Char。編譯器發現了這個IndexOf方法,並生成IL代碼來調用這個靜態方法。
String(引用類型)的不變性(immutable):
v String最爲顯著的一個特色就是它具備恆定不變性:一旦建立了一個String對象,在managed heap 上爲他分配了一塊連續的內存空間,咱們將不能以任何方式對這個String進行修改使之變長、變短、改變格式(不能修改String對象的值)。全部對這個String進行各項操做(好比調用ToUpper得到大寫格式的String)而返回的String,其實是另外一個從新建立的String,其自己並不會產生任何變化。每次使用 String 類中的方法之一或進行運算時(如賦值、拼接等)時,都要在內存中建立一個新的字符串對象,這就須要爲該新對象分配新的空間。
v StringBuilder此類表示值爲可變字符序列的相似字符串的對象。之因此說值是可變的,是由於在經過追加、移除、替換或插入字符而建立它後能夠對它進行修改。大多數修改此類的實例的方法都返回對同一實例的引用。實例的 int Capacity 屬性,它表示內存中爲存儲字符串而物理分配的字符串總數。該數字爲當前實例的容量。容量可經過 Capacity 屬性或 EnsureCapacity 方法來增長或減小,但它不能小於 Length 屬性的值。
注: .NET Framework中可變集合類如ArrayList 的Capacity 屬性也相似這種自動分配機制。
8.6.1規則和原則
v C#只支持擴展方法,不支持擴展屬性、擴展事件、擴展操做符等。
v 擴展方法(第一個參數前面有this的方法)必須在非泛型的靜態類中聲明。類名沒有限制。擴展方法至少要有一個參數,並且只有第一個參數能用this關鍵字標記。
v C#編譯器查找這些靜態類中定義的擴展方法時,要求這些靜態類自己必須具備文件做用域。換言之,此靜態類不能嵌套在另外一個類中。
v 擴展方法有潛在的版本控制問題。若是Microsoft將來爲StringBuilder添加了IndexOf實例方法,那麼在從新編譯咱們的代碼時,編譯器會從新綁定Microsoft的IndexOf的實例方法,而不是咱們的靜態IndexOf方法。
8.6.2用擴展方法擴展各類類型
8.6.3 ExtensionAttribute類
分部方法partial method在分部類型的一個部分中定義它的簽名,並在該類型的另一個部分中定義它的實現。
//工具生成的代碼,存儲在某個源代碼文件中
internal sealed partial class Base
{
private String m_name;
//分佈方法的聲明
partial void OnNameChanging(String value);
public String Name
{
get { return m_name; }
set
{
OnNameChanging(value.ToUpper());
m_name = value;
}
}
}
//開發人員生成的代碼,存儲在另外一個源代碼文件中
internal sealed partial class Base
{
//分部方法的實現
partial void OnNameChanging(String value)
{
//Calling the base class OnNameChanging method:
//base.OnNameChanging(value);
if (String.IsNullOrEmpty(value))
{
throw new ArgumentNullException(value);
}
}
}
分部方法規則和原則:
v 它們只能在分部類中聲明。
v 分部方法的返回類型始終是void,任何參數都不能用out修飾符標記(out和ref的區別就是傳入的參數是否已經初始化了)。
緣由:若是不是返回null,同時沒有提供實現,那麼調用一個未實現的方法,返回什麼才合理呢?爲了不對返回值進行任何無故的猜想,c#的設計者決定只容許方法返回void。
v 分部方法的聲明和實現必須具備徹底一致的方法簽名。
v 若是沒有對應的實現部分,便不會在代碼中建立一個委託來引用這個分部方法。
v 分部方法老是被視爲隱式的private方法。可是C#編譯器禁止你在分部方法聲明以前添加訪問修飾符關鍵字。
v 工具生成的代碼,分佈方法的聲明要用partial關鍵字標記,無主體,沒有方法實現。
v 開發者本身的代碼中,分佈方法的聲明也要用partial關鍵字標記,有主體,有方法實現。
分部方法容許一個方法而不須要實現。若是沒有實現分部方法,編譯器會自動移除方法簽名,不會生成任何表明分部方法的元數據。編譯器也不會生成任何調用分部方法的IL指令。並且編譯器也不會生成對本該傳給分部方法的實參進行求值的IL的指令。在這個例子中,編譯器不會生成調用ToUpper方法的指令。結果就是更少的元數據/IL,運行時的性能也獲得大幅提高。
設計一個方法的參數時,可爲部分或所有參數分配默認值。而後,調用這些方法的代碼能夠選擇不指定部分實參,接受其默認值。
除此以外,調用方法時,還可經過指定參數名稱的方式爲其傳遞實參。
如下代碼演示了可選參數和命名參數的用法:
public static class Program
{
private static Int32 s_n = 0;
private static void M(Int32 x = 9, String s = "A", DateTime dt = default (DateTime), Guid guid = new Guid())
{
Console.WriteLine("x={0},s={1},dt={2},guid={3}", x, s, dt, guid);
}
public static void Main() {
//若是調用時省略了一個實參,C#編譯器會自動嵌入參數的默認值
M();
M(8, "x");
//爲x顯式傳遞5,指出我想爲guid和dt的參數傳遞一個實參
M(5, guid: Guid.NewGuid(), dt: DateTime.Now);
M(s_n++, s_n++.ToString());
//使用已命名的參數傳遞實參
M(s: (s_n++).ToString(), x: s_n++);
}
}
Guid(Globally Unique Identifier 全球惟一標識符)一個經過特定算法產生的二進制長度爲128位的數字標識符(16 字節),用於指示產品的惟一性。
向方法傳遞實參時,編譯器按從左到右的順序對實參進行求值。
C#中值傳遞與引用傳遞的區別:
把實參當作實參來傳遞時,就產生了一個新的拷貝。
class Test
{
static void Main(String[] args)
{
int x = 8;
Fo(x);
Console.WriteLine("x={0}", x);
}
static void Fo(int p)
{
p = p + 1;
Console.WriteLine("p={0}", p);
}
}
以上程序運行結果爲p=9,x=8; 即X的值不會受P影響,給P賦一個新值並不會改變X的內容,由於P和X存在於內存中不一樣的位置。
同理,用傳值的方式傳遞一個引用類型對象時,只是複製這個對象自己,即複製其地址值,而不是它指代的對象。下面代碼中Fo中看到的StringBuilder對象,就是在Main方法中實例化的那一個,只是有不一樣的引用指向它而已。
class Test
{
static void Fo(StringBuilder foSB)
{
foSB.Append("test");
foSB = null;
}
static void Main()
{
StringBuilder sb = new StringBuilder();
Fo(sb);
Console.WriteLine(sb.ToString());
}
}
運行結果:test.
換句話說,sb和foSB是指向同一對象的不一樣引用變量。由於FoSB是引用的拷貝,把它置爲null並無把sb置爲 null。
值傳遞:傳的是對象的值拷貝。
引用傳遞:傳的是棧中對象的地址。
ref和out:
ref和out關鍵字都致使參數經過引用傳遞。
傳遞到 ref 形參的實參必須先通過初始化,而後才能傳遞。
out 形參不一樣,在傳遞以前,不須要顯式初始化該形參的實參,out形參必須在Method方法中初始化。
關鍵字類似。
class RefExample
{
static void Method(ref int i)
{
// Rest the mouse pointer over i to verify that it is an int.
// The following statement would cause a compiler error if i
// were boxed as an object.
i = i + 44;
}
static void Main()
{
int val = 1;
Method(ref val);
Console.WriteLine(val);
// Output: 45
}
}
class OutExample
{
static void Method(out int i)
{
i = 44;
}
static void Main()
{
int value;
Method(out value);
// value is now 44
}
}
經過引用傳遞的效果是,把變量做爲參數傳遞給方法,在方法中修改該參數,會改變這個變量的值。
不要混淆經過引用傳遞的概念與引用類型的概念。不管方法參數是值類型仍是引用類型,都可由 ref 修改。
當經過引用傳遞時,不會對值類型裝箱。
若要使用 ref 參數,方法定義和調用方法均必須顯式使用 ref 關鍵字。
儘管 ref 和 out 關鍵字會致使不一樣的運行時行爲,它們並不被視爲編譯時方法簽名的一部分。
9.1.1規則和原則
在你定義的方法中,若是爲部分參數指定了默認值,請注意下述這些額外的規則和原則:
v 能夠爲方法,構造器方法和有參屬性的參數指定默認值。
v 沒有默認值的參數必須放在有默認值的參數以前。例如,若是刪除s的默認值(「A「),就會出現編譯錯誤。
v 默認值必須是編譯時能肯定的常量值。能夠設置默認值的參數的類型是:C#認定的基元類型,枚舉類型,以及能設爲null的任何引用類型。
v 能夠用default關鍵字或new關鍵字,將值類型的參數的默認值設爲值類型的一個實例。
v 不要重命名參數變量(s, x, dt, guid)。不然,任何調用者以傳參數名的方式傳遞實參,都必須修改它們的代碼。
v 若是參數用ref或out關鍵字進行了標識,就不能設置默認值。由於沒有辦法爲這些參數傳遞有意義的默認值。
v 實參可按任意順序傳遞,但命名實參只能出如今實參列表的尾部。
v C#不容許省略逗號之間的實參,好比M(5, , dt: DateTime.Now);
9.1.2 DefaultParameterValueAttribute和OptionalAttribute
在C#中,一旦爲某個參數分配了一個默認值,編譯器就會在內部向該參數應用一個定製attribute,即System.Runtime.InteropServices.OptionalAttribute。這個attribute會在最終生成的文件的元數據中持久性的存儲下來。
針對一個方法中的局部變量,C#容許根據初始化表達式的類型來推斷它的類型。
默認狀況下,CLR假定全部方法參數都是傳值的。傳遞引用類型的對象時,對一個對象的引用會傳給方法。注意這個引用自己是以傳值方式傳給方法的。
在方法中,必須知道傳遞的每一個參數是引用類型仍是值類型,由於用於操縱不一樣類型的代碼可能有顯著的差別。
CLR容許以傳引用而非傳值的方式傳遞參數。在C#中,這是用關鍵字out或ref來作到的。這兩個關鍵字都告訴C#編譯器生成元數據來指明該參數是傳引用的。編譯器將生成代碼來傳遞參數的地址,而不是傳遞參數自己。
若是方法的參數用out來標記,代表不期望調用者在調用方法以前初始化好對象。被調用的方法不能讀取參數的值,並且在返回前必須向這個值寫入。
public sealed class Program
{
public static void Main()
{
Int32 x;
GetVal(out x);//x在調用GetVal前沒必要初始化
Console.WriteLine(x);
}
private static void GetVal(out Int32 v)
{
v = 10;//返回前必須初始化v
}
}
在前面的代碼中,x是存儲在Main的棧幀中聲明的,而後x的地址傳遞給GetVal。GetVal的v是一個指針,它指向Main棧中的Int32值。
棧幀:在執行線程的過程當中進行的每一個方法調用都會在調用棧中建立並壓入一個StackFrame。
相反,若是方法的參數用ref來標記,調用者必須在調用方法前初始化參數的值,被調用的方法能夠讀取值以及或者向值寫入。
有時候開發人員想定義一個方法來獲取可變數量的參數。
public static class Program
{
static Int32 Add(params Int32[] values)
{
Int32 sum = 0;
if (values != null)
{
for (Int32 x = 0; x < values.Length; x++)
{
sum += values[x];
}
}
return sum;
}
public static void Main()
{
//Console.WriteLine(Add(new Int32[] { 1, 2, 3 }));
Console.WriteLine(Add(1, 2, 3));
Console.ReadKey();
}
}
除了params外,以上方法的一切對你來講都應該是很是熟悉的。
很明顯數組能用任意數量的一組元素來初始化,再傳給Add方法進行處理。
params只能用於方法簽名中的最後一個參數。即在方法聲明中的 params 關鍵字以後不容許任何其餘參數,而且在方法聲明中只容許一個 params 關鍵字。
params關鍵字告訴編譯器向參數應用System.ParamArrayAttribute的一個實例。
C#編譯器檢測到一個方法調用時,會先檢查全部具備指定名稱、同時參數沒有應用ParamArrayAttribute的方法。若是找到一個匹配的方法,編譯器就生成調用它所需的代碼。若是編譯器沒有找到一個以上匹配的方法,會接着檢查應用了ParamArrayAttribute的方法。若是找到一個應用了ParamArrayAttribute的方法,編譯器會先生成代碼來構造一個數組,填充它的元素,在生成代碼來調用選定的方法。
在前一個例子中,沒有定義可獲取3個Int32兼容實參的Add方法。可是編譯器發如今一個Add方法調用中傳遞了一組Int32值,並且有一個Add方法的Int32數組參數應用了ParamArrayAttribute。所以,編譯器會認爲這是一個匹配,因此會生成代碼,將實參保存到一組Int32數組中,再調用Add方法,並傳遞該實參。
最終的結果就是你能夠直接向Add方法傳遞一組實參,編譯器會生成代碼,像上面例子中註釋的代碼同樣,幫你構造和初始化一個數組來容納實參。
聲明方法的參數類型時,應儘可能指定最弱的類型,最好是接口而不是基類。
(重看)
CLR沒有提供對常量對象/實參的支持。
屬性容許源代碼用一個簡化的語法來調用一個方法。
CLR支持兩種屬性:無參屬性 (parameterless property) ,有參屬性(parameterful property)
C#中將有參屬性稱爲索引器(indexer)
通常用類型的字段成員來實現獲取或改變類型的狀態信息。
面向對象設計和編程的重要原則之一就是數據封裝(data encapsulation),它意味着類型的字段永遠不該該公開。強烈建議將全部的字段都設爲private。
要容許獲取類型狀態信息,就公開一個針對該用途的方法。
封裝了字段訪問的方法一般稱爲訪問器(accessor)方法(以下面的GetName,SetName)。訪問器方法能夠對數據的合理性進行檢查,確保對象的狀態不被破壞。
public sealed class Employee
{
private String m_Nmae;
private Int32 m_Age;
public String GetName()
{
return m_Nmae;
}
public void SetName(String value)
{
m_Nmae = value;
}
public Int32 GetAge()
{
return m_Age;
}
public void SetAge(Int32 value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("value", value.ToString(), "The value must be grater than or equal to 0");
}
m_Age = value;
}
public static void Main()
{
Employee e = new Employee();
e.SetName("Jeffery Richter");
String EmployeeName = e.GetName();
e.SetAge(41);
e.SetAge(-5);
Int32 EmployeeAge = e.GetAge();
}
}
將SetXxx方法標記爲protected,就能夠實現只容許派生類型修改值。
以上代碼中,類型的用戶必須調用方法,而不能直接引用一個字段名。
編程語言和CLR還提供了一種稱爲屬性(property)的機制,以下:
public sealed class Employee
{
private String m_Nmae;
private Int32 m_Age;
public String Name
{
get { return (m_Nmae); }
set { m_Nmae = value; }
}
public Int32 Age
{
get { return (m_Age); }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("value", value.ToString(), "The value must be greater than or equal to 0");
}
m_Age = value;
}
}
public static void Main()
{
Employee e = new Employee();
e.Name = "Jeffery Richter";
String EmployeeName = e.Name;
e.Age = 41;
e.Age = -5;
Int32 EmployeeAge = e.Age;
}
}
可將屬性想象成智能字段(smart field),即背後有額外邏輯的字段。
每一個屬性都有一個名稱(Name,Age)和一個類型(String,Int32不能爲void)。屬性不能重載。定義屬性時,能夠省略set方法來定義一個只讀屬性,或者省略get方法來定義一個只寫屬性。
經過屬性的get和set方法來操做類型內私有的字段,是一種很常見的作法。
之前面的Employee類型爲例。編譯器編譯這個類型時,會發現其中的Name和Age屬性。因爲兩個屬性都有get和set訪問器方法,因此編譯器在Employee類型中生成4個方法定義。
若是隻是爲了封裝一個支持字段而建立一個屬性,C#還提供了更簡單的語法,稱爲自動實現的屬性(automatically Implemented Property)。
我我的不喜歡屬性
System.Collections命名空間包含可以使用的集合類和相關的接口,提供了集合的基本功能。
IEnumerable 接口
System.Collections
該枚舉數支持在非泛型集合上進行簡單迭代
全部繼承了IEnumerable的類,要使用foreach迭代器時,就須要使用該方法。所以也只有實現了該接口的類才能夠使用foreach。
名稱 |
說明 |
GetEnumerator() |
返回循環訪問集合的枚舉數。 |
IList 接口
System.Collections
IList 是 ICollection 接口的子代,而且是全部(非???)泛型列表的基接口
IList繼承自ICollection
名稱 |
說明 |
Add(Object) |
將某項添加到 IList 中。 |
Clear() |
從 IList 中移除全部項。 |
Contains(Object) |
肯定 IList 是否包含特定值。 |
CopyTo(Array, Int32) |
從特定的 Array 索引處開始,將 ICollection 的元素複製到一個 Array 中。(從 ICollection 繼承。) |
GetEnumerator() |
返回循環訪問集合的枚舉數。(從 IEnumerable 繼承。) |
IndexOf(Object) |
肯定 IList 中特定項的索引。 |
Insert(Int32, Object) |
將一個項插入指定索引處的 IList。 |
Remove(Object) |
從 IList 中移除特定對象的第一個匹配項。 |
RemoveAt(Int32) |
移除指定索引處的 IList 項。 |
ICollection<T> 接口
System.Collections.Generic
定義操做泛型集合的方法。
ICollection繼承自IEnumerable
名稱 |
說明 |
Add(T) |
將某項添加到 ICollection<T> 中。 |
Clear() |
從 ICollection<T> 中移除全部項。 |
Contains(T) |
肯定 ICollection<T> 是否包含特定值。 |
CopyTo(T[], Int32) |
從特定的 Array 索引開始,將 ICollection<T> 的元素複製到一個 Array 中。 |
GetEnumerator() |
返回一個循環訪問集合的枚舉器。(從 IEnumerable<T> 繼承。) |
Remove(T) |
從 ICollection<T> 中移除特定對象的第一個匹配項。 |
ICollection主要針對靜態集合;IList主要針對動態集合。
若是一個方法的返回值是IEnumerable<T> ,必須在方法後面使用.ToList()方法才能獲得一個集合數據。
集合的初始化被認爲是相加(Additive)操做,而非替換的操做。編譯器發現Student屬性的類型是List<String>,並且這個類型實現了IEnumerable<String>接口。以下:
public sealed class Classroom
{
private List<String> m_students = new List<String>();
public List<String> Students { get { return m_students; } }
public Classroom() { }
public static void Main()
{
Classroom classroom = new Classroom
{
Students = { "Chris","Jeff" }
};
//Classroom classroom = new Classroom();
//classroom.Students.Add("Chris");
//classroom.Students.Add("Jeff");
foreach (var student in classroom.Students)
Console.WriteLine(student);
}
}
重看
若是類型定義了事件成員,那麼類型就能夠通知其餘對象發生了特定的事情。
例如,Button類提供了一個名爲Click的事件。應用程序中的一個或多個對象可能想接收關於這個事件的通知,以便在Button被單擊後採起某些操做。事件是實現這種交互的類型成員。
具體的說,若是定義一個事件成員,意味着類型要提供如下能力:
類型之因此能提供事件通知功能,是由於類型維護了一個已登記方法的列表。事件發生後,類型將通知列表中全部已登記的方法。
CLR的事件模型創建在委託基礎上的。委託是調用回調方法的一種類型安全的方式。對象憑藉回調方法接收它們訂閱的通知。
爲了幫助你徹底理解事件在CLR中的工做機制,先來描述一個事件頗有用的場景。假定如今要設計一個電子郵件應用程序。電子郵件到達時,用戶可能但願將該郵件轉發給傳真機。建構這個應用程序時,假定先設計了一個名爲MailManager的類型,它負責接收傳入的電子郵件。MailManager類型公開了一個名爲NewMail的事件。其餘類型(如Fax和Pager)的對象登記它們對這個事件的關注。MailManager收到一封新電子郵件時,會引起該事件。形成郵件分發給每個已登記的對象。每一個對象都用它們本身的方式處理該郵件。
應用程序初始化時,讓咱們只實例化一個MailManager實例。而後,應用程序可實例化任意數量的Fax和Pager對象。
MailManager示例應用程序展現了MailManager類型,Fax類型和Pager類型的全部源代碼。
事件引起時,引起事件的對象可能但願向接收事件通知的對象傳遞一些附加的信息。這些附加的信息須要封裝到它本身的類中,該類一般包含一組私有字段,以及一些用於公開這些字段的只讀公共屬性。根據約定,這種類應該從EventArgs類派生,而且類名必須以EventArgs結束。
定義一個沒有附加信息須要傳遞的事件時,可直接使用EventArgs.Empty,不用構造一個新的EventArg對象。
//第一步:定義一個類型來容納全部應該發給事件通知接收者的附加信息
internal class NewMailEventArgs : EventArgs
{
private readonly String m_from, m_to, m_subject;
public NewMailEventArgs(String from, String to, String subject)
{
m_from = from; m_to = to; m_subject = subject;
}
public String From { get { return m_from; } }
public String To { get { return m_to; } }
public String Subject { get { return m_subject; } }
}
//後續的將在MailManager類中進行
internal class MailManager { }
internal class MailManager
{
public event EventHandler<NewMailEventArgs> NewMail;
void MethodName(object sender, NewMailEventArgs e);
}
事件成員使用C#關鍵字event來定義。每一個事件成員都要指定如下內容:
事件成員的類型是EventHandler<NewMailEventArgs>,意味着「事件通知」的全部接收者都必須提供一個原型和EventHandler<NewMailEventArgs>委託類型匹配的回調方法。
因爲泛型EventHandler委託類型的定義以下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
因此方法原型必須具備如下形式:
void MethodName(object sender, NewMailEventArgs e);
。。。
泛型(generic)是CLR和編程語言提供的一種特殊機制,它支持另外一種形式的代碼重用,即算法重用。
簡單的說,開發人員先定義好一個算法,好比排序、搜索、交換、比較或者轉換等。可是,定義算法的開發人員並不設定該算法要操做什麼數據類型。該算法能夠普遍地應用於不一樣類型的對象。而後,另外一個開發人員,只有指定了算法要操做的具體數據類型,就能夠開始使用這個現成的算法了。例如,能夠用一個排序算法來操做Int32和String等類型對象。
大多數算法都封裝在一個類型中,CLR容許建立泛型引用類型和泛型值類型,但不容許建立泛型枚舉類型。CLR還容許建立泛型接口和泛型委託。
先來看一個簡單的例子,Framework類庫中定義了一個泛型列表算法,它知道如何管理一個對象集合。泛型算法沒有設定這些對象的數據類型。
封裝了泛型列表算法的FCL類稱爲List<T>。泛型List類的設計者緊接着在這個類名後添加一個<T>,代表它操做的是一個未指定的數據類型。
定義泛型類型時,它爲類型指定的任何變量(好比T)都稱爲類型參數(type parameter)。T是一個變量名,在源代碼中可以使用一個數據類型的任何位置,都能使用T。
列如:在List類定義中,
T被用做方法參數,Add方法接收一個T類型的參數public void Add(T item);
T被用做返回值,ToArray方法返回一組T類型的一維數組public T[] ToArray();
根據Microsoft的設計原則,泛型參數變量要麼稱爲T,要麼至少以大寫T開頭(如TKey和TValue)。
使用泛型類型或方法時,指定的具體數據類型稱爲類型實參(type argument)。
例如:開發人員可指定一個DateTime類型實參來使用List算法。
public class Program{
private static void SomeMethod()
{
//構造一個List來操做DateTime對象
List<DateTime> dtList = new List<DateTime>();
//向列表添加DateTime對象,不進行裝箱
dtList.Add(DateTime.Now);
dtList.Add(DateTime.MinValue);
//嘗試向列表中添加一個String對象,編譯時報錯,Invalid arguments
dtList.Add("1/1/2004");
//從列表提取一個DateTime對象
DateTime dt = dtList[0];
}
}
從以上代碼能夠看出,泛型爲開發人員提供瞭如下優點:
泛型最明顯的應用就是集合類。
FCL定義的幾個泛型集合類,大多數都在System.Collections.Generic和System.Collections.ObjectModel命名空間中。
泛型是在CLR2.0版本中加入的,爲了在CLR中加入泛型,Microsoft作了一下工做:
多繼承(multiple inheritance)是指一個類從兩個或多個基類派生的能力。
CLR不支持多繼承,CLR只是經過接口提供了「縮水版」的多繼承。
實現接口的類或結構必須實現接口定義中指定的接口成員。
interface IEquatable<T>
{
bool Equals(T obj);
}
實現IEquatable<T>接口的任何類或結構都必須包含與該接口指定的簽名匹配的Equals方法的定義。
public class Car : IEquatable<Car>
{
public string Make { get; set; }
public string Model { get; set; }
public string Year { get; set; }
public bool Equals(Car car)
{
if (this.Make == car.Make && this.Model == car.Model && this.Year == car.Year)
{
return true;
}
else
{
return false;
}
}
}
IEquatable<T>的定義不爲Equals提供實現,該接口僅定義簽名。
類或結構能夠實現多個接口,可是類只能繼承單個類(抽象或不抽象)。
接口能夠包含方法、屬性、事件、索引器或這四種成員類型的任意組合。
接口成員會自動成爲公共成員,不能包含任何訪問修飾符。成員也不能是靜態成員。
若要實現接口成員,實現類的對應成員必須是公共、非靜態,而且具備與接口成員相同的名稱和簽名。
接口具備如下屬性:
從Object派生任何類實際都繼承瞭如下內容:
接口對一組方法簽名進行了統一命名。接口還能定義事件,無參屬性和索引器。全部這些本質上都是方法。但接口不能定義構造器方法。接口也不能定義任何實例字段。
C#禁止接口定義任何一種這樣的靜態成員。
在C#中是用interface關鍵字定義接口的。要爲接口指定一個名稱和一組實例方法簽名。
對CLR而言,接口定義就像是一個類型定義。也就是說,CLR會爲接口類型對象定義一個內部數據結構,同時可用反射機制來查詢接口類型的功能。
和類型同樣,接口可用在文件範圍內定義,也可嵌套在另外一個類型中定義。定義接口類型時,可指定你但願的任何可視性/可訪問性(public, protect, internal等)。
接口成員會自動成爲公共成員,不能包含任何訪問修飾符。成員也不能是靜態成員。
根據約定,接口類型名稱要以大寫I開頭,目的是方便在源代碼中辨認接口類型。
CLR支持泛型接口和在接口中的泛型方法。
如下代碼展現瞭如何定義一個實現該接口的類型:
public interface IComparable<in T>
{
//接口成員不能包含任何訪問修飾符,不能是靜態成員,它會自動成爲公共成員
int CompareTo(T other);
}
public sealed class Point : IComparable<Point>
{
private Int32 m_x, m_y;
//接口不能定義構造器方法,實現接口的類能夠定義構造器方法
public Point(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
//和接口對應的成員必須是公共非靜態的,和接口成員相同的名稱和簽名
public Int32 CompareTo(Point other)
{
return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)
- Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
}
public override String ToString()
{
return String.Format("{0},{1}", m_x, m_y);
}
}
public static class Program
{
public static void Main()
{
Point[] points = new Point[] { new Point(3, 3), new Point(1, 2) };
if (points[0].CompareTo(points[1]) > 0)
{
Point tempPoint = points[0];
points[0] = points[1];
points[1] = tempPoint;
}
Console.WriteLine("Points from closest to (0,0) to farthest:");
foreach (Point p in points)
Console.WriteLine(p);
}
}
C#編譯器要求將用於實現一個接口的方法標記爲public。
編譯器會將實現接口的方法標記爲virtual和sealed。
在.Net Framework中,字符老是表示成16位Unicode代碼值,這簡化了國際化應用程序的開發。
每一個字符都表示成System.Char結構的一個實例。
針對Char的一個實例,能夠調用靜態GetUnicodeCategory方法,這個方法返回的是System.Globalization.UnicodeCategory枚舉類型的一個值。
Char類型提供了幾個靜態方法,好比IsDigit,IsUpper等。注意,全部這些方法要麼獲取單個字符做爲參數,要麼獲取一個String以及目標字符在這個String中的索引做爲參數。
ToLower和ToUpper之因此須要語言文化信息,是由於字母的大小寫轉換是一種依賴於語言文化的操做。語言文化信息是這兩個方法在內部查詢System.Threading.Thread類的靜態CurrentCulture屬性獲取的。
除了這些靜態方法,Char類型還提供了幾個實例方法。好比:Equals方法會在兩個Char實例表明同一個16位Unicode碼位的前提下返回true。CompareTo方法返回兩個Char實例忽略語言文化的比較結果。GetNumericValue方法,它返回字符的數值形式,如下代碼演示了這個方法。
public static class Program {
public static void Main() {
Double d;
d = Char.GetNumericValue('3');//3
Console.WriteLine(d.ToString());
d = Char.GetNumericValue('A');//-1
Console.WriteLine(d.ToString());
}
}
能夠使用三種技術實現各個數值類型與Char實例的相互轉換。
如下代碼演示瞭如何使用者三種技術:
public static class Program
{
public static void Main()
{
Char c;
Int32 n;
//使用C#強制類型轉換
c = (Char)65;
n = (Int32)c;
//使用Convert類型
c = Convert.ToChar(65);
try
{
//700000000000對於Char的16位來講過大
c = Convert.ToChar(700000000000);
}
catch (OverflowException)
{
Console.WriteLine("Cannot convert 700000000000 to a Char");
}
//使用IConvertible接口
c = ((IConvertible)65).ToChar(null);
}
}
一個String表明一個不可變(immutable)的順序字符集。String類型直接派生自Object,因此它是一個 引用類型。所以,String對象永遠存在於堆上,永遠不會跑到線程棧。
C#將String視爲一個基元類型-也就是說,編譯器容許在源代碼中直接表示文本常量字符串。編譯器將這些文本常量字符串放到模塊的元數據中,並在運行時加載和引用它們。
在C#中,不能使用new操做符從一個文本常量字符串構造一個String對象,必須使用簡化過的語法。
public static class Program
{
//錯誤
String s = new String("Hi");
//正確
String s1 = "Hi";
}
對於換行符、回車符和退格符這樣的特殊字符,C#採用轉義機制。
\r return 回車
\n newline 換行
//包含回車換行符的字符串
String s = "Hi\r\nthere";
//如下是定義上述字符串的正確方式
String s1 = "Hi" + Environment.NewLine + "there";
能夠使用C#的+操做符將幾個字符串鏈接成一個。String s2 = "Hi" + "" + "there";
在上述代碼中,因爲全部字符串都是文本常量字符串,因此C#編譯器會在編譯時鏈接它們,最終只會將一個字符串(即"Hi there")放到模塊的元數據中。對非文本常量字符串使用+操做符,鏈接則會在運行時進行。若要在運行時將幾個字符串鏈接到一塊兒,請避免使用+操做符,由於它會在堆上建立多個字符串對象,而堆是須要垃圾回收的,從而影響到性能。相反,應儘可能使用String.Text.StringBuilder類型。
C#還提供了「逐字字符串(verbatim strings)」聲明方式,一般用於指定文件或目錄的路徑,或者與正則表達式配合使用。
//不使用逐字字符串字符@來聲明字符串
String file = "C:\\Windows\\System32\\Notepad.exe";
//使用逐字字符串字符@來聲明字符串
String file = @"C:\Windows\System32\Notepad.exe";
在字符串以前添加@符號,是編譯器知道字符串是一個逐字字符串。事實上,這告訴編譯器將反斜槓字符視爲文本常量,而不是轉義字符,使文件路徑在源代碼中更易讀。
String對象最重要的一個事實就是,它使不可變的。也就是字符串一經建立便不能更改,不能變長變短或修改其中的任何字符。
通常會出於兩方面的緣由來比較字符串:
進行排序時,應該老是執行區分大小寫的比較。
Compare方法中ignoreCase設爲true,不區分大小寫。
判斷字符串相等性或對字符串進行排序時,強烈建議調用下面的方法之一:
public bool Equals(string value, StringComparison comparisonType);
public static bool Equals(string a, string b, StringComparison comparisonType);
public static int Compare(string strA, string strB, StringComparison comparisonType);
public static int Compare(string strA, string strB, bool ignoreCase, CultureInfo culture);
public static int Compare(string strA, string strB, CultureInfo culture, CompareOptions options);
public static int Compare(string strA, int indexA, string strB, int indexB, int length, StringComparison comparisonType);
public static int Compare(string strA, int indexA, string strB, int indexB, int length, CultureInfo culture, CompareOptions options);
public static int Compare(string strA, int indexA, string strB, int indexB, int length, bool ignoreCase, CultureInfo culture);
public bool StartsWith(string value, StringComparison comparisonType);
public bool StartsWith(string value, bool ignoreCase, CultureInfo culture);
public bool EndsWith(string value, StringComparison comparisonType);
public bool EndsWith(string value, bool ignoreCase, CultureInfo culture);
許多程序都將字符串用於內部編程目的,好比路徑名、文件名、URL、註冊表項/值、環境變量、反射、XML等。出於編程目的而比較字符串時,應該老是使用StringComparison.Ordinal,這是執行字符串比較時最快的一種方式,由於在執行比較時,不須要考慮語言文化信息。
從如今起咱們將討論如何執行在語言文化上正確的比較。.Net Framework使用System.Globalization.CultureInfo表示一個「語言/國家」。
如下代碼演示了序號比較和依賴語言文化比較的區別:
static void Main()
{
String s1 = "Strasse";
String s2 = "Straße";
Boolean eq;
//Compare返回非零值,若是傳遞Ordinal標誌,Compare方法會忽略指定的語言文化
eq = String.Compare(s1, s2, StringComparison.Ordinal) == 0;
Console.WriteLine("Ordinal comparison:'{0}'{2}'{1}'", s1, s2, eq ? "==" : "!=");
//面向在德國說德語的人羣
CultureInfo ci = new CultureInfo("de-DE");
//Compare返回零值
eq = String.Compare(s1, s2, true, ci) == 0;
Console.WriteLine("Cultural comparison:'{0}'{2}'{1}'", s1, s2, eq ? "==" : "!=");
}
如上一節所述,檢查字符串的相等性是許多應用程序的常見操做 - 這個任務可能嚴重損害性能。
執行序號ordinal相等性檢查時,CLR快速檢查兩個字符串是否具備數量相同的字符。若是答案是確定的,字符串有可能相等。而後CLR必須比較每一個單獨的字符才能肯定。
除此以外,若是在內存中複製同一個字符串的多個實例,會形成內存的浪費,由於字符串是不可變的。若是隻在內存中保留字符串的一個實例,那麼將顯著提升內存的利用率。須要引用字符串的全部變量只需指向單獨一個字符串對象。
若是應用程序常常對字符串進行區分大小寫的、序號式的比較,或者事先知道許多字符串對象都有相同的值,就可利用CLR的字符串留用(string interning)機制來顯著提升性能。
CLR初始化時會建立一個內部哈希表,在這個表中,鍵(key)是字符串,而值(value)是對託管堆中的String對象的引用。
String類提供了兩個方法,便於你訪問這個內部哈希表:
public static string Intern(string str);
public static string IsInterned(string str);
Equals和ReferenceEquals的區別:
如下代碼演示了字符串留用:
static void Main()
{
String s1 = "Hello";
String s2 = "Hello";
Boolean a = Object.ReferenceEquals(s1, s2);//true
s1 = String.Intern(s1);
s2 = String.Intern(s2);
Boolean b = Object.ReferenceEquals(s1, s2);//true
}
在對ReferenceEquals方法的第一個調用中,在CLR低版本中,s1引用堆中「Hello」字符串對象,而s2引用堆中另外一個「Hello」字符串對象。在CLR的4.0版本上運行時,CLR選擇忽視C#編譯器生成的attribute/flag。但程序集加載到AppDomain中時,CLR會對文本常量字符串「Hello」進行默認留用。結果就爲True。
在對ReferenceEquals方法的第二個調用以前,「Hello」字符串被顯示留用,s1如今引用一個已留用的「Hello」。而後,經過再次調用Intern,s2被設置成s1引用的同一個「Hello」字符串。如今,當第二次調用ReferenceEquals時,就能保證得到一個True的結果,無論程序集在編譯時是否設置了attribute/flag。
編譯器有將單個字符串的多個實例合併成一個實例的能力。
還能夠利用String類型提供的一些方法來複制一個字符串或者一部分。
public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count);
ToString:返回對同一個對象的引用
上述方法中將字符串的部分字符複製到字符數組中。
使用全部這些方法時都請牢記一點,它們返回的都是一個新的字符串對象。
因爲String類型表明的是一個不可變字符串,因此FCL提供了另外一個名爲System.Text.StringBuilder的類型。可利用它高效地對字符串進行動態處理,最後基於處理結果建立一個String。
從邏輯上說,StringBuilder對象包含一個字段,該字段引用了由Char結構構成的一個數組。可利用StringBuilder的成員來操縱這個字符數組,高效地縮短字符串或更改字符串中的字符。
使用StringBuilder的方法時要記住,大多數方法返回的都是對同一個StringBuilder對象的引用。因此能夠方便的將幾個操做連接到一塊兒完成:
static void Main()
{
StringBuilder sb = new StringBuilder();
String s = sb.AppendFormat("{0} {1}", "Jeffrey", "Richter").Replace(' ', '-').Remove(4, 3).ToString();
}
拼接字符串示例:
String[] value = { "1", "2", "3" };
String a = "";
StringBuilder str = new StringBuilder();
foreach (String text in value)
{
str.AppendFormat(",{0}", text);//將value數組中的值拼接成一個字符串,以逗號分隔
}
if (str != null && str.Length > 0)
{
str.Remove(0, 1);//移除第一個逗號
}
a = str.ToString();//要將StringBuilder轉換成字符串
String和StringBuilder類提供的方法並非徹底匹配的。例如:String提供了ToLower,ToUpper,EndsWith,Trim等方法,但StringBuilder類沒有提供任何與之對應的方法。另外一方面,StringBuilder類提供了一個功能更全面的Replace方法,它容許替換做爲一個字符串的一部分字符。而String類中Replace方法是public string Replace(char oldChar, char newChar);
因爲這兩個類中的方法不徹底對應,因此有時須要在String和StringBuilder轉換來完成特定的任務。
StringBuilder sb = new StringBuilder();
String s = sb.AppendFormat("{0}, {1}", "Jeffrey", "Richter").ToString();
s.ToUpper();
sb.Length = 0;
sb.Append(s).Insert(8, "Marc-");
s = sb.ToString(1, 2);
咱們常常都要獲取一個對象的字符串表示。能夠調用ToString方法來獲取任何對象的字符串表示。
無參ToStirng方法有兩個問題。
String的Format方法。。。
解析字符串來獲取一個對象,偶爾會用到。
Int32 x = Int32.Parse("1A", NumberStyles.HexNumber);//26
能解析一個字符串的任何類型都提供了Parse的一些public static方法。
先來看看如何將一個字符串解析成數值類型:
public static int Parse(string s, NumberStyles style, IFormatProvider provider);
s是字符串參數,NumberStyles是字符串參數s中運行的樣式
Int32 x = Int32.Parse(" 123", NumberStyles.None); //要解析的字符串包含一個前導空白字符,會報FormatExpection異常
應該設成NumberStyles.AllowLeadingWhite
Microsoft在FCL中添增了一個更安全的字符串類System.Security.SecureString
Enumeration提供了一些很是炫酷的功能,相信大多數開發人員都不熟悉。這些新功能極大的簡化了應用程序開發。
枚舉類型(enumerated types)定義了一組「符號名稱/值」配對。
如下Color類型定義了一組符號,每一個符號都標識一種顏色:
internal enum Color
{
White,//賦值0
Red, //賦值1
Greed,//賦值2
Blue, //賦值3
Orange//賦值4
}
固然,也能夠寫個程序用0表明白色,1表明紅色,以此類推。但不該該將這些數字硬編碼到代碼中,而應換用枚舉類型,由於:
每一個枚舉類型都直接從System.Enum派生,後者從System.ValueType派生。而System.ValueType又從System.Object派生。因此,枚舉類型是值類型,可表示成未裝箱和已裝箱形式。有別於其餘值類型,枚舉類型不能定義任何方法、屬性和事件。
編譯枚舉類型時,C#編譯器會把每一個符號轉換成類型的一個常量字段。例如,編譯器會把前面的Color枚舉類型當作如下代碼:
C#編譯器實際上並不編譯這段代碼,由於它禁止定義從System.Enum這一特殊類型派生的類型。
枚舉類型定義的符號是常量值,因此當編譯器一旦發現代碼引用了一個枚舉類型的符號,就會在編譯時用數值替代符號,代碼將再也不引用定義了符號的枚舉類型。
簡單地說,枚舉類型只是一個結構,其中定義了一組常量字段和一個實例字段。常量字段會嵌入程序集的元數據中,並可經過反射來訪問。這意味着在運行時得到與一個枚舉類型關聯的全部符號及其值。還意味着能夠將一個字符串符號轉換成對應的數值。這些操做是經過System.Enum基類型來提供的。下面討論其中的一些操做:
例如,System.Enum類型有一個名爲GetUnderlyingType的靜態方法,而System.Type類型有一個GetEnumUnderlyingType的實例方法。
public static Type GetUnderlyingType(Type enumType);
public virtual Type GetEnumUnderlyingType();
這些方法返回用於容納一個枚舉類型的值的基礎類型。每一個枚舉類型都有一個基礎類型,能夠是byte,short,int(最經常使用,也是C#默認選擇的),long。C#要求只能指定基元類型名稱,若是使用FCL類型名稱(好比Int32),會報錯。
咱們定義的枚舉類型應該與須要調用它的那個類型同級。
如下代碼演示瞭如何聲明一個基礎類型爲byte的枚舉類型:
internal enum Color : byte
{
White,
Red,
Greed,
Blue,
Orange
}
static void Main()
{
Console.WriteLine(Enum.GetUnderlyingType(typeof(Color))); //System.Byte
}
C# typeof() 和 GetType()區是什麼?
C#編譯器將枚舉類型視爲基元類型,因此,能夠用許多熟悉的操做符(==,!=,<,>,<=,>=,+,-,^,&,|,++,--)來操縱枚舉類型的實例。
全部這些操做符實際做用於每一個枚舉類型實例內部的value_實例字段。
給定一個枚舉類型的實例,可調用從System.Enum繼承的ToString方法:
public static class Program
{
static void Main()
{
//Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));
Color c = Color.Blue;
Console.WriteLine(c.ToString());//"Blue" 常規格式
Console.WriteLine(c.ToString("G"));//"Blue" 常規格式
Console.WriteLine(c.ToString("D"));//"3" 十進制格式
Console.WriteLine(c.ToString("X"));//"03" 十六進制格式
}
}
internal enum Color : byte
{
White,
Red,
Greed,
Blue,
Orange
}
Format:可調用它格式化一個枚舉類型的值:
public static string Format(Type enumType, object value, string format);
Console.WriteLine(Enum.Format(typeof(Color), 3, "G"));//顯示"Blue"
GetValues:獲取枚舉類型中定義的全部符號以及對應的值。
public static Array GetValues(Type enumType);
Color[] colors = (Color[])Enum.GetValues(typeof(Color));
Console.WriteLine("Number of symbols defined:" + colors.Length);
Console.WriteLine("Value\tSymbol\n-----\t------");
foreach (Color c in colors)
{
Console.WriteLine("{0,5:D}\t{0:G}", c);
}
GetName:返回數值的字符串表示。
Enum.GetName(typeof(Color), 3);//"Blue"
GetNames:返回一個String數組,每一個符號都表明一個String。
Enum.GetNames(typeof(Color));
// {string[5]}
//[0]: "White"
//[1]: "Red"
//[2]: "Greed"
//[3]: "Blue"
//[4]: "Orange"
Parse, TryParse:將一個符號轉換成枚舉類型的實例。
public static object Parse(Type enumType, string value, bool ignoreCase);
Color c = (Color)Enum.Parse(typeof(Color), "orange", true); //Orange
Enum.Parse(typeof(Color), "0", true);//White
bool a=Enum.TryParse<Color>("Brown", false, out c);//false, 枚舉中沒有定義Brown
IsDefine:判斷一個值對於一個枚舉類型是否合法。
Enum.IsDefined(typeof(Color), "white");//false, 執行的是區分大小寫的檢查
Enum.IsDefined(typeof(Color), 5);//false, Color枚舉類型沒有與5對應的符號
咱們能夠將位標誌當作一種特殊的枚舉類型。
FileAttributes類型是基本類型爲Int32的枚舉類型,其中每一位都反映文件的一項屬性。
[Flags] //指示能夠將枚舉做爲位域(即一組標誌)處理。
public enum FileAttributes
{
ReadOnly = 1,
Hidden = 2,
System = 4,
Directory = 16,
Archive = 32,
Device = 64,
Normal = 128,
Temporary = 256,
SparseFile = 512,
ReparsePoint = 1024,
Compressed = 2048,
Offline = 4096,
NotContentIndexed = 8192,
Encrypted = 16384,
IntegrityStream = 32768,
NoScrubData = 131072
}
以上FileAttributes類型中,1的二進制爲1,2的二進制爲10,4的二進制爲100。也就是說能夠用每一個二進制位來確認惟一性,這就是位標誌的原理。
public static void Main()
{
//獲得可執行文件(.exe文件)的相對路徑(如:"...\bin\Debug\ConsoleApplication1.exe")
String file = Assembly.GetEntryAssembly().Location;
//調用System.IO.File類型的GetAttributes方法,會返回FileAttributes類型的一個實例
FileAttributes attributes = File.GetAttributes(file);
//由於二進制1&1才爲1,因此只要存在最後的數值必定不爲1,判斷文件是否隱藏
Console.WriteLine("IS {0} hidden?{1}", file, (attributes & FileAttributes.Hidden) != 0);
//判斷文件是否隱藏,換種寫法。Enum有一個HasFlag方法,肯定當前實例attributes中是否設置了一個或多個位域
Console.WriteLine("IS {0} hidden?{1}", file, attributes.HasFlag(FileAttributes.Hidden));
//將一個文件的屬性改成只讀和隱藏
File.SetAttributes(file, FileAttributes.ReadOnly | FileAttributes.Hidden);
}
數組是容許將多個數據項看成一個集合來處理的機制。CLR支持一維數組、多維數組和交錯數組(即由數組構成的數組)。全部數組類型都隱式地從System.Array抽象類派生,意味着數組始終爲引用類型,是在託管堆上進行內存分配的。在你的應用程序的變量或字段中,包含的是對數組的引用,而不是包含數組自己的元素。
Int32[] myIntegers;//聲明一個數組引用
myIntegers = new Int32[100];//建立含有100個Int32的數組
在第一行代碼中,myIntegers變量能指向一個一維數組。myIntegers剛開始被設爲null,由於當時尚未分配數組。第二行代碼中分配了含有100個Int32值的一個數組,全部Int32都被初始化爲0。因爲數組是引用類型,因此會在託管堆上分配容納100個未裝箱Int32所需的內存塊。除了數組元素,數組對象佔據的內存塊還包含一個類型對象指針、一個同步塊索引和一些額外的成員。該數組的內存塊地址被返回並保存到myIntegers變量中。
還能夠建立引用類型的數組:
本章要討論回調函數。回調函數是一種很是有用的編程機制,它的存在已經有不少年了。
Microsoft .Net Framework經過委託(delegate)來提供了一種回調函數機制。
列如:委託確保回調方法是類型安全的。委託還容許順序調用多個方法,並支持調用靜態方法和實例方法。
C#中委託是在程序運行時能夠使用它們來調用不一樣的函數。
舉個簡單的例子,你是編程的,你如今正在寫一個ASP.NET網頁,而JS是你不熟悉的,因而你委託你的一位同事來幫助你完成JS部分。這就是委託,把你所不能作的事情交給其餘人去作。
1.簡單的委託http://www.cnblogs.com/birdshover/archive/2008/01/07/1029471.html
那麼委託須要承載哪些信息呢?首先它存儲了方法名,還有參數列表(方法簽名),以及返回類型,好比:
delegate String/*返回類型*/ ProcessDelegate(Int32 i);
藍色部分是聲明委託的關鍵字,紅色是返回類型,黑色部分是委託的類型名,()裏的就是參數部分。你要使用這個委託來作事情,必須知足一下條件:
例如:
輸出的結果是:Text1Tex2
public delegate String ProcessDelegate(String s1, String s2);
class Program
{
static void Main()
{
//使用委託ProcessDelegate來調用Process方法
ProcessDelegate pd = new ProcessDelegate(new Test().Process);
Console.WriteLine(pd("Text1", "Text2"));
}
}
public class Test
{
public String Process(String s1, String s2)
{
return s1 + s2;
}
}
2.回調函數
回調函數就是把一個方法傳給另外一個方法去執行。它與委託不一樣在於,它的方法參數,返回值均可以和調用者的參數,返回值能夠不同。
輸出結果:
Text1Text2
Text1
Text2
Text2Text1
public delegate String ProcessDelegate(String s1, String s2);
class Program
{
static void Main()
{
Test t = new Test();
//Process方法(調用者)調用了一個回調函數Process1,固然這裏只執行了回調函數。
//能夠看出,能夠把任意一個符合這個委託的方法傳遞進去,意思就是說這部分代碼是可變的。
//將Process1 2 3方法傳遞給Process方法去執行
string r1 = t.Process("Text1", "Text2", new ProcessDelegate(t.Process1));
string r2 = t.Process("Text1", "Text2", new ProcessDelegate(t.Process2));
string r3 = t.Process("Text1", "Text2", new ProcessDelegate(t.Process3));
Console.WriteLine(r1);
Console.WriteLine(r2);
Console.WriteLine(r3);
}
}
public class Test
{
public String Process(String s1, String s2, ProcessDelegate process)
{
return process(s1, s2);
}
public String Process1(String s1, String s2)
{
return s1 + s2;
}
public String Process2(String s1, String s2)
{
return s1 + Environment.NewLine + s2;
}
public String Process3(String s1, String s2)
{
return s2 + s1;
}
}
如下代碼演示瞭如何聲明、建立和使用委託:
using System;
using System.Windows.Forms;
using System.IO;
namespace WindowsFormsApplication1
{
//聲明一個委託類型,它的實例引用一個方法
//指定一個回調函數的簽名,該方法獲取一個Int32參數,返回void
internal delegate void Feedback(Int32 value);
public sealed class Program
{
public static void Main()
{
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program());
}
public static void StaticDelegateDemo()
{
Console.WriteLine("----Static Delegate Demo----");
Counter(1, 3, null);
//前綴Program可選
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
Counter(1, 3, new Feedback(FeedbackToMsgBox));
Console.WriteLine();
}
private static void InstanceDelegateDemo()
{
Console.WriteLine("----Instance Delegate Demo----");
Program p = new Program();
Counter(1, 3, new Feedback(p.FeedbackToFile));
Console.WriteLine();
}
private static void ChainDelegateDemo1(Program p)
{
Console.WriteLine("----Chain Delegate Demo 1----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(Program p)
{
Console.WriteLine("----Chain Delegate Demo 2----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
//若是指定了任何回調函數,就調用它們
if (fb != null)
fb(val);
}
}
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item=" + value);
}
private static void FeedbackToMsgBox(Int32 value)
{
MessageBox.Show("Item=" + value);
}
private void FeedbackToFile(Int32 value)
{
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Item=" + value);
sw.Close();
}
}
}
在StaticDelegateDemo方法中,第一次調用Counter方法時,爲第三個參數傳遞的是null。因爲Counter的fb參數收到的是null,因此每一個數據項在處理時,都不會調用回調函數。
接着StaticDelegateDemo方法再次調用Counter方法,爲第三個參數傳遞一個新構造的Feedback委託對象。委託對象(new操做符新建的Feedback對象)是方法的一個包裝器(wrapper),使方法能經過包裝器來間接回調。
在本例中,靜態方法的完整名稱Program.FeedbackToConsole被傳給Feedback委託類型的構造器。代表FeedbackToConsole就是要包裝的方法。new操做符返回的引用做爲Counter的第三個參數來傳遞。
在一個類型中,能夠經過委託來調用另外一個類型的私有成員時,只要委託對象是有具備足夠安全性和可訪問性的代碼建立時,便不會有問題。
這個例子中的全部操做都是類型安全的。例如,在構造Feedback委託對象時,編譯器確保Program的FeedbackToConsole方法的簽名,兼容於Feedback委託定義的簽名。具體的說,FeedbackToConsole必須獲取一個參數,並且二者都必須有相同的返回類型(void)。
將一個方法綁定到委託時,C#和CLR都容許引用類型的協變形和逆變性。
InstanceDelegateDemo中構造了一個名爲p的Program對象。這個Program對象沒有定義任何實例字段和屬性。向Counter委託類型的構造函數傳遞的是p.FeedbackToFile,這致使委託包裝對FeedbackToFile方法的一個引用,這個方法是實例方法,而不是靜態方法。當Counter調用由其fb實參標識的回調函數時,會調用FeedbackToFile實例方法。
從表面看,委託彷佛很容易使用:用C#的delegate關鍵字,用熟悉的new操做符構造委託實例。
CLR和編譯器作了大量的工做來隱藏委託的複雜性。
首先讓咱們從新審視這一行代碼:
爲何會有可空類型:
當咱們設計一個數據庫時,可將一個數據庫字段的數據類型定義成一個32位整數,並映射到FCL的Int32數據類型對象。在數據庫中的一個列可能容許值爲空,但在C# 語言中是不能爲null的。用.Net Framework處理數據庫數據可能變得至關困難,由於在CLR中,沒有辦法將Int32值表示爲null。爲了解決這個問題,Microsoft在CLR中引入了可空值類型(nullable value type)的概念。
可空類型也是值類型,只是它是包含null的一個值類型。C#用問號表示法來聲明並初始化變量。
這個」?」修飾符只是C#提供的一個語法糖 (所謂語法糖,就是C#提供的一種方便的形式,其實確定沒有Int32? 這個類型。這個Int32?編譯器認爲的就是Nullable< Int32>類型,便可空值類型)
public struct Nullable<T> where T : struct
C#容許在可空類型上執行轉換和轉型:
public static void ConversionsAndCasting()
{
Int32? a = 5; //從非可空的Int32轉換爲Nullable<Int32>, 等同與Nullable<Int32> a = 5;
Int32? b = null; //從null隱式轉換爲Nullable<Int32>
Int32 c = (Int32)a; //從Nullable<Int32>顯式轉換爲非可空Int32
//在可空基元類型之間轉換
Double? d = 5;//Int32轉型爲Double
Double? e = b;//Int32?轉型爲Double
}
C#容許向可空類型實例應用操做符:
public static void Operators()
{
Int32? a = 5;
Int32? b = null;
a++;
b = -b;
a = a + 3;
b = b * 3;
if (a == null) { } else { }
if (b == null) { } else { }
if (a != b) { } else { }
if (a < b) { } else { }
}
C#提供了空接合操做符,即??操做符,它要獲取兩個操做數。
假如左邊的操做數不爲null,就返回左邊的這個操做數的值。若是左邊的操做數爲null,就返回右邊的操做數的值。
利用空接合操做符,能夠方便地設置變量的默認值,避免在代碼中寫if / else語句,簡化代碼數量,從而有利於閱讀。
public static void NullCoalescingOperator()
{
Int32? b = null;
Int32 x = b ?? 123;//等價於x = (b.HasValue) ? b.Value : 123;
}
其實可空類型的裝箱和拆箱操做你們能夠就理解爲非可空值類型的裝箱和拆箱的過程,只是對於非可空類型由於包含null值,因此CLR會提早對它進行檢查下它是否爲空,爲null就不不任何處理,若是不爲null,就按照非可空值類型的裝箱和拆箱的過程來裝箱和拆箱。
錯誤處理要分幾個部分。首先,咱們要定義什麼是錯誤。而後,咱們要討論如何判斷代碼正在經歷一個錯誤,以及如何從這個錯誤中恢復。這個時候,狀態就成爲一個要考慮的問題,由於錯誤經常在不恰當的時機發生。代碼可能在狀態改變的中途發生錯誤。在這種狀況下,就可能須要將一些狀態還原成改變以前的狀態。固然,咱們還要討論代碼如何通知它的調用者檢測到了一個錯誤。
本章要討論針對未處理的異常、約束執行區域(constraind execution region, CER)、代碼契約、運行時包裝的異常以及未捕捉的異常。
設計類型時,首先要想好類型的各類使用狀況。類型名稱一般是一個名詞,例如FileStream或者StringBuilder。而後,要爲類型定義屬性、方法、事件等。這些成員(屬性的數據類型、方法的參數、返回值等)的定義方式就是類型的編程接口。這些成員表明類自己或者類型實例能夠執行的行動。行動成員一般用動詞表示,例如Read,Write,Flush,Append,Insert和Remove等。當行動成員不能完成任務時,就應拋出異常。
面向對象的編程大大提升了開發人員的效率,由於咱們能夠這樣寫代碼:
public bool TestFunc(string input)
{
return input.Substring(1, 1).ToUpper().EndsWith("E");
}
咱們沒有作任何的參數檢查,而直接調用了一長串的方法。當input參數爲null或空時,上面的代碼就會拋出異常。即便方法爲沒有返回值的void型也應該報告錯誤,.Net Framework提供的這種機制就叫作異常處理(excepton handling)。
本節將介紹異常處理(exception handling)的機制,以及進行異常處理所需的C#構造(construct)。
下面的C#代碼展現了異常處理機制的標準用法,經過它能夠對異常處理及用途有一個初步認識,後續將對try,catch和finally塊作進一步講解。
private void SomeMethod()
{
try
{
//須要執行的代碼放在這裏
}
catch (InvalidOperationException) { }
catch (IOException)
{
//從IOException恢復的代碼放在這裏
}
catch
{
//從除上面的異常外的其餘異常恢復的代碼放在這裏
throw; //從新拋出捕捉到的任何東西
}
finally
{
//這裏的代碼老是執行,對始於try塊的任何操做進行清理
}
// 若是try塊沒有異常,或異常被捕獲後沒有拋出,就執行這裏的代碼
}
try塊包含的代碼一般須要執行一些通用的資源清理操做,或者可能拋出異常須要從異常中恢復。清理代碼應放在一個finally塊中。try塊還可包含也許會拋出異常的代碼。異常恢復代碼應該放在一個或多個catch塊中。針對應用程序能從中安全恢復的每一種異常,都應該建立一個catch塊。一個try塊至少要有一個關聯的catch塊或finally塊。
catch塊包含的是響應一個異常須要執行的代碼。若是try塊中的代碼沒有形成異常的拋出,CLR永遠不會執行它的任何catch塊中的代碼。線程將跳過全部catch塊,直接執行finally中的代碼。finally塊中的代碼執行完畢後,從finally塊後面的代碼繼續執行。catch關鍵字後面的圓括號中的表達式稱爲捕捉類型(catch type)。在C#中必須將捕捉類型指定爲System.Exception或者它的一個派生類型。
用VS調試catch塊時,可經過在監視窗口中添加特殊的變量名稱$exception來查看當前拋出的異常對象。
CLR自上而下搜索一個匹配的catch塊,因此應該將較具體的異常放在頂部。也就是說,首先出現的是派生程度最大的異常類型,接着是它們的基類型,最後是System.Exception。
若是在try塊中的代碼拋出一個異常,CLR將搜索捕捉類型與拋出的異常相同的(或者是它的基類)catch塊。沒有捕捉類型的catch塊將捕捉剩餘的全部異常。
catch塊中的代碼一般執行一些對異常進行處理的操做。C#容許在捕捉異常後指定一個變量。捕捉到一個異常時,該變量將引用拋出的這個System.Exception派生對象。catch塊中的代碼,能夠經過引用該變量來訪問異常的具體信息。
finally塊包含的代碼是保證會執行的代碼。一般finally塊中的代碼執行的是try塊中行動所要求的資源清理操做。
private void ReadData(String pathname)
{
FileStream fs = null;
try
{
fs = new FileStream(pathname, FileMode.Open);
//處理文件中的數據...
}
catch (IOException)
{
//在此添加從IOException恢復的代碼
}
finally
{
//確保文件被關閉
if (fs != null)
fs.Close();
}
}
上述代碼中,將關閉文件的語句放在finally塊以後是不正確的,由於倘若異物拋出但未被捕捉到,該語句就執行不到,形成文件打開狀態,直到下一次垃圾回收。
try塊並不是必定要關聯一個finally塊。有時候try中的代碼並不須要任何清理工做。可是,若是有finally塊,它必須出如今全部catch塊以後。記住,finally塊中的代碼是清理代碼,這些代碼只需負責對try塊中發起的操做進行清理。
微軟定義了一個System.Exception類型,並規定全部公共語言規範(CLS)相容的編程語言都必須能拋出和捕捉派生自該類型的異常。C#只容許拋出CLS相容的異常。派生自System.Exception的異常類型被認爲是CLS相容的。
最經常使用的Exception的屬性是Message,StackTrace和InnerException。分別表示異常的文字消息,異常的方法堆棧信息,以及內部異常。
這裏有必要講一下System.Exception類型提供的只讀屬性StackTrace。catch塊可讀取該屬性來獲取一個堆棧跟蹤(stack trace),它描述了異常發生以前調用的全部方法和簽名,該屬性對於調試很是有用。訪問該屬性時,實際要調用CLR中的代碼,該屬性並非簡單地返回一個字符串。構造Exception派生類型的一個新對象時,StackTrace屬性被初始化爲null。若是此時讀取該屬性,獲得的不是堆棧追蹤,而是一個null。
一個異常拋出時,CLR會在內部記錄throw指令的位置。一個catch塊捕捉到該異常時,CLR又會記錄異常的捕捉位置。在catch塊內訪問被拋出的異常對象的StackTrace屬性時,負責實現該屬性的代碼會調用CLR內部的代碼,後者建立一個字符串來指出從異常拋出位置到異常捕捉位置的全部方法。
字符串--at ConsoleApplication2.Program.Main(String[] args) in d:\CLR練習\ConsoleApplication2\ConsoleApplication2\Program.cs:line 20
若是CLR能找到你的程序集的調試符號,那麼在System.Exception屬性返回的字符串中,將包含源代碼文件路徑和代碼行號。
相反,若是僅僅使用throw關鍵字自己(刪除後面的e)來從新拋出一個異常對象,CLR就不會重置堆棧的起點。
實現本身的方法時,若是方法沒法完成方法名所指明的任務,就應拋出一個異常。
拋出異常時,須要注意兩個問題:
設計本身的異常不只繁瑣,還容易出錯。主要緣由是從Exception派生的全部類型都應該是可序列化的,使它們能穿越AppDomain邊界邊界或者寫入日誌/數據庫。
下面是建立一個自定義異常類型的幾個原則:
1,聲明序列化,這樣能夠跨AppDomain訪問。
2,添加默認構造函數。
3,添加只有一個message參數的構造函數。
4,添加包含message,內部異常參數的構造函數。
5,添加序列化信息的構造函數,訪問級別設爲private或protected。
定義自定義異常類型:
[Serializable]
public sealed class DiskFullException : Exception
{
public DiskFullException()
: base()
{ }
public DiskFullException(string message)
: base(message)
{ }
public DiskFullException(string message, Exception innerException)
: base(message, innerException)
{ }
public DiskFullException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
使用例:
try
{
throw new DiskFullException("disk is full");
}
catch (DiskFullException ex)
{
Console.WriteLine(ex.Message);
}
面向對象編程,編譯器功能,CLR功能以及龐大的類庫——使.Net Framework成爲一個頗具吸引力的開發平臺。但全部的這些東西,都會在你的代碼中引入你沒有什麼控制權的「錯誤點」,若是 OutOfMemoryExcepton等。程序開發不可能對這些異常進行一一捕捉,讓應用程序變得絕對健壯。意料意外的異常每每形成程序狀態的破壞,爲 了緩解對狀態的破壞,能夠作下面幾件事:
●執行catch或finally塊時,CLR不容許終止線程,因此能夠向下面這樣寫是Transfer方法變得健壯:
private void Transfer(Account from, Account to, decimal amount)
{
try {/* 這裏什麼也沒作*/ }
finally
{
from.Money -= amount;
//如今,這裏不可能發生線程終止(因爲Thread.Abort/AppDomain.Unload)
to.Money += amount;
}
}
可是,毫不建議將全部代碼都放到finally塊中!這個技術只適合於修改及其敏感的數據。
●能夠用System.Diagnostics.Contracts.Constract類向方法應用代碼契約。
●能夠使用約束執行區域(Constrained Excecution Region,CER),它提供了消除CLR不肯定性的一種方式。
●可利用事務(transaction)來確保狀態要麼修改,要麼都不修改。如TransactionScope類。
●將本身的方法設計的更明確。以下面的Monitor類實現線程同步:
public static class SomeType
{
private static readonly object s_lockObject = new object();
public static void SomeMethod()
{
Monitor.Enter(s_lockObject);//若是拋出異常,是否獲取了鎖?
//若是已經獲取了鎖,它就得不到釋放
try
{
//在這裏執行線程安全的操做
}
finally
{
Monitor.Exit(s_lockObject);
}
}
}
因爲存在上面展現的問題,這個重載的Monitor的Enter方法已經再也不鼓勵使用,建議像下面這樣寫:
public static class SomeType
{
private static readonly object s_lockObject = new object();
public static void SomeMethod()
{
bool lockTaken = false;//假定沒有獲取鎖
try
{
Monitor.Enter(s_lockObject,ref lockTaken);//不管是否拋出異常,如下代碼都能正常工做
//在這裏執行線程安全的操做
}
finally
{
//若是以獲取就釋放它。
if(lockTaken == true) Monitor.Exit(s_lockObject);
}
}
}
雖然以上代碼變得更明確,但在線程同步鎖的狀況下,如今的建議是根本不要隨同異常處理使用它們。
●若是肯定狀態以損壞到沒法修改的程度,就應銷燬全部損壞的狀態,防止它形成更多的傷害。而後重啓應用程序,將應用程序恢復到一個良好的狀態。因爲託管代碼不能泄露到一個AppDomain的外部,你能夠調用AppDomain的Unload方法來卸載整個AppDomain。若是以爲狀態過於糟糕,以致於須要終止這個進程,你能夠調用Environment的FailFast方法。這個方法中能夠指定異常消息,調用這個方法時,不會運行任何活動的try/finally塊或者Finalize方法。而後它會將消息發送個Windows Application的日誌。
咱們認爲finally塊很是強悍!無論線程拋出什麼樣的異常,finally塊中的代碼都保證會執行。應該用finally塊清理那些已成功啓動的操 做,而後再返回調用者或執行finally塊以後的代碼。Finally塊還常常用於顯示釋放對象以免資源泄漏。以下例:
public static void SomeMethod()
{
FileStream fs = new FileStream(@"c:\test.txt", FileMode.Open);
try
{
//顯示用100除以文件第一個字節的結果
Console.WriteLine(100 / fs.ReadByte());
}
finally
{
//清理資源,即便發生異常,文件都能關閉
fs.Close();
}
}
確保清理代碼的執行時如此重要,以致於許多編程語言都提供了一些構造來簡化清理代碼的編寫。例如:只要使用了lock,using和foreach語 句,C#編譯器就會自動生成try/finally塊。另外,重寫類的析構器(Finalize)時,C#編譯器也會自動生成try/catch塊。使用 這些構造時,編譯器將你寫的代碼放到try塊內,並自動將清理代碼放在finally塊內,具體以下:
●使用lock語句,鎖會在finally塊中釋放。
●使用using語句,會在finally塊中調用對象的Dispose方法。
●使用foreach語句,會在finally塊中調用IEnumerator對象的Dispose方法。
●定義析構方法時,會在finally塊調用基類的Finalize方法。
例如,用using語句代替上面的代碼,代碼量更少,但編譯後的結果是同樣的。
public static void SomeMethod()
{
using (FileStream fs = new FileStream(@"c:\test.txt", FileMode.Open))
{
Console.WriteLine(100 / fs.ReadByte());
}
}
異常拋出時,CLR會在調用棧中向上查找與拋出的異常類型匹配的catch塊。
異常拋出時,CLR會在調用棧中向上查找與拋出異常類型匹配的catch塊。若是沒有找到一個匹配的catch塊,就發生一個未處理異常。CLR檢測到進程中的任何線程有一個未處理的異常,就會終止進程。Microsoft的每種應用程序都有本身的與未處理異常打交道的方式。
●對於任何應用程序,查閱System.Domain的UnhandledException事件。
●對於WinForm應用程序,查閱System.Windows.Forms.NativeWindow的 OnThreadException虛方法,System.Windows.Forms.Application的OnThreadException虛 方法,System.Windows.Forms.Application的ThreadException事件。
●對於WPF應用程序,查閱System.Windows.Application的 DispatcherUnhandledException事件和System.Windows.Threading.Dispatcher的 UnhandledException和UnhandledExceptionFilter事件。
●對於Silverlight,查閱System.Windows.Forms.Application的ThreadException事件。
●對於ASP.NET應用程序,查閱System.Web.UI.TemplateControl的Error事件。 TemplateControl類是System.Web.UI.Page類和System.Web.UI.UserControl類的基類。另外還要查 詢System.Web.HttpApplication的Error事件。
約束執行區是必須對錯誤有適應能力的一個代碼塊,說白點,就是這個代碼塊要保證可靠性很是高,儘可能不出異常。看看下面這段代碼:
public static void Demo1()
{
try {
Console.WriteLine("In Try");
}
finally
{//Type1的靜態構造器在這裏隱式調用
Type1.M();
}
}
private sealed class Type1
{
static Type1()
{
//若是這裏拋出異常,M就得不到調用
Console.WriteLine("Type1's static ctor called.");
}
public static void M() { }
}
運行上述代碼,獲得如下的結果:
In Try
Type1's static ctor called.
咱們但願的目的是,除非保證finally塊中的代碼獲得執行,不然try塊中的代碼根本就不要開始執行。爲了達到這個目的,能夠像下面這樣修改代碼:
public static void Demo1()
{
//強迫finally的代碼塊提早準備好
RuntimeHelpers.PrepareConstrainedRegions();
try {
Console.WriteLine("In Try");
}
finally
{//Type1的靜態構造器在這裏隱式調用
Type1.M();
}
}
private sealed class Type1
{
static Type1()
{
//若是這裏拋出異常,M就得不到調用
Console.WriteLine("Type1's static ctor called.");
}
//應用p了1System.Runtime.ConstrainedExecution命?名?空o間的IReliabilityContract特A性á
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static void M() { }
}
獲得的結果以下:
Type1's static ctor called.
In Try
PrepareConstrainedRegions是個很是特別的方法,JIT編譯器遇到這個方法,就會提早編譯與try關聯的catch和 finally塊中的代碼。JIT編譯器會加載任何程序集,建立任何類型,調用任何靜態構造器,並對方法進行JIT編譯,若是其中的任何操做發生異常,這 個異常會在try塊錢拋出。
須要JIT提早準備的方法必需要應用ReliabilityContract特性,而且向這個特性傳遞的參數必須是 Consistency.WillNotCorruptState或Consistency.MayCorruptInstance。這是因爲假如方法會 損壞AppDomain或進程的狀態,CLR便沒法對狀態的一致性作出任何保證。請確保finally塊中只有剛剛描述的應用了 ReliabilityContract特性的方法。向ReliabilityContract傳遞的另外一個參數Cer.Success,表示保證該方法 不會失敗,不然用Cer.MayFail。Cer.None這個值代表方法不進行CER保證。換言之,方法沒有CER的概念。對於沒有應用 ReliabilityContract特性的方法等價於下面這樣
[ReliabilityContract(Consistency.MayCorruptProcess, Cer.None)]
迫使JIT編譯器預先準備的還有幾個靜態方法,它們都定義在RuntimeHelper中:
public static void PrepareMethod(RuntimeMethodHandle method);
public static void PrepareMethod(RuntimeMethodHandle method, RuntimeTypeHandle[] instantiation);
public static void PrepareDelegate(Delegate d);
public static void PrepareContractedDelegate(Delegate d);
還應關注下RuntimeHelpers 的ExecuteCodeWithGuaranteedCleanup這個方法,它是在資源保證獲得清理的前提下執行代碼的另外一種方式:
public static void ExecuteCodeWithGuaranteedCleanup(RuntimeHelpers.TryCode code, RuntimeHelpers.CleanupCode backoutCode, object userData);
調用這個方法要將try和finally塊的主體做爲回調方法傳遞,他們的原型要分別匹配如下的兩個委託:
public delegate void TryCode(object userData);
public delegate void CleanupCode(object userData, bool exceptionThrown);
最後,另外一種保證代碼得以執行的方式是使用CriticalFinalizerObject類。
代碼契約(code contract)提供了直接在代碼中申明代碼設計決策的一種方式。
●前條件 通常用於參數的驗證。
●後條件 方法由於一次普通的返回或者由於拋出一個異常而終止時,對狀態進行驗證。
●對象不變性(object Invariant) 用於對象的整個生命期內,保持對象字段的良好性狀態。
代碼契約有利於代碼的使用、理解、進化、測試、文檔和初期錯誤檢查。可將前條件、後條件和對象不變性想象爲方法簽名的一部分。因此,代碼新版本的契約能夠變得更寬鬆,可是,除非破壞向後兼容性,不然代碼新版本的契約不能變得更嚴格。
代碼契約的核心是靜態類System.Diagnostics.Contracts.Contract。因爲該技術較新,實際中運用機會很少,故再也不投入大量精力去研究。具體用時能夠查閱MSDN相關文檔。
本章將討論託管應用程序如何構造新對象,託管堆如何控制這些對象的生存期,以及如何回收這些對象的內存。簡單的說,本章要解釋CLR中的垃圾回收器是如何工做的,還要解釋與它有關的性能問題。
在.NET Framework中,內存中的資源(即全部二進制信息的集合)分爲「託管資源」和「非託管資源」。託管資源必須接受.NET Framework的CLR的管理 (如內存類型安全性檢查) 。而非託管資源則沒必要接受.NET Framework的CLR管理, 須要手動清理垃圾(顯式釋放)。注意,「垃圾回收」機制是.NET Framework的特性,而不是C#的。
每一個程序都要使用這樣或那樣的資源,好比文件、內存緩衝區、屏幕空間、網絡鏈接、數據庫資源等。事實上,在面向對象的環境中,每一個類型都表明可供程序使用的一種資源。要使用這些資源,必須爲表明資源的類型分配內存。
如下是訪問一個資源所需的具體步驟
垃圾回收(garbage collection)自動發現和回收再也不使用的內存,不須要程序員的協助。使開發人員獲得瞭解放,如今沒必要跟蹤內存的使用,也沒必要知道在何時釋放內存。可是,垃圾回收器不能夠管理內存中的全部資源,對內存中的類型所表明的資源也是一無所知的。這意味着垃圾回收器不知道怎麼執行「摧毀資源的狀態以進行清理」。這部分資源就須要開發人員本身寫代碼實現回收。在.Net framework中,開發人員一般會把清理這類資源的代碼寫到Dispose,Finalize和Close方法中。
在.net中提供三種模式來回收內存資源:dispose模式,finalize方法,close方法:
然而,值類型、集合類型、String、Attribute、Delegate和Exception所表明的資源無需執行特殊的清理操做。列如,只需銷燬對象的內存中維護的字符數組,一個String資源就會被徹底清理。
值類型(包括引用和對象實例)和引用類型的引用實際上是不須要什麼「垃圾回收器」來釋放內存的,由於當它們出了做用域後會自動釋放所佔內存(由於它們都保存在「堆棧」中,學過數據結構可知這是一種先進後出的結構)。只有引用類型的引用所指向的對象實例才保存在「堆」中,而堆由於是一個自由存儲空間,因此它並無像「堆棧」那樣有生存期 (「堆棧」的元素彈出後就代 表生存期結束,也就表明釋放了內存)。而且很是要注意的是,「垃圾回收器」只對「堆」這塊區域起做用。
從託管堆分配資源
.Net clr把全部的引用對象都分配到託管堆上,這一點很像c-runtime堆。初始化新進程時,運行時會爲進程保留一個連續的地址空間區域。這個保留的地址空間被稱爲託管堆,而且這個地址空間最初並無對應的物理存儲空間。除值類型外,CLR要求全部資源都從託管堆分配。
託管堆還維護着一個指針,我把它稱爲NextObjPtr。它指向下一個對象在堆中的分配位置。
IL指令newobj用於建立一個對象。C#提供了new操做符,它致使編譯器在方法IL代碼中生成一個newobj指令。newobj指令將致使CLR執行如下步驟(如何爲類型分配內存?):
下圖展現了3個對象(A,B和C)的一個託管堆。若是要分配新對象,它將放在NextObjPtr指針指向的位置(緊接着對象C後)。
應用程序調用new操做符建立對象時,可能沒有足夠的地址空間來分配該對象。託管堆將對象須要的字節數加到NextObjPtr指針中的地址上來檢測這個狀況。若是結果值超過了地址空間的末尾,代表託管堆已滿,必須執行一次垃圾回收。
垃圾回收器檢查託管堆中是否有應用程序再也不使用的對象。若是有,它們使用的內存就能夠被回收。那麼,垃圾回收器是怎麼知道一個對象再也不被使用呢?
CPU寄存器(CPU Register)是CPU本身的「臨時存儲器」,比內存的存取還快。按與CPU遠近來分,離得最近的是寄存器,而後緩存 (計算機1、2、三級緩存),最後內存。
每一個應用程序都包含一組根(Roots)。每一個根都是一個存儲位置,他們可能指向託管堆上的某個地址,也多是null。
類中定義的任何靜態字段,方法的參數,局部變量(僅限引用類型變量)等都是根,另外cpu寄存器中的對象指針也是根。
例如,全部的全局和靜態對象指針是應用程序的根,另外在線程棧上的局部變量/參數也是應用程序的根。只有引用類型的變量才被認爲是根,值類型的變量永遠不被認爲是跟。
若是一個根引用了堆中的一個對象,則該對象爲「可達」,不然便是「不可達」。被根引用的堆中的對象不被視爲垃圾。
當垃圾回收器開始運行,它會假設託管堆上的全部對象都是垃圾。換句話說,它假設線程棧中沒有引用堆中對象的變量,沒有CPU寄存器引用堆中的對象,也沒有靜態字段引用堆中的對象。
垃圾回收分爲2個階段:
垃圾回收器的第一階段是所謂的標記(marking)階段。
垃圾回收器沿着線程棧上行以檢查全部的根。若是發現一個根引用了一個對象,就在對象的「同步塊索引字段」上開啓一位(將這個bit設爲1)---對象就是這樣被標記的。當全部的根都檢查完畢後,堆中將包含可達(已標記)與不可達(未標記)對象。
以下圖,展現了一個堆,其中包含幾個已分配的對象。應用程序的根直接引用對象ACDF,全部這些對象都被標記。標記好根和它的字段引用對象以後,垃圾回收器檢查下一個根,並繼續標記對象。若是垃圾回收器試圖標記以前已經被標記過的對象,就會換一個路徑繼續遍歷。這樣作有兩個目的:首先,垃圾回收器不會屢次遍歷一組對象,提升性能。其次,若是存在對象的循環鏈表,能夠避免無限循環。
垃圾回收器的第二個階段是壓縮(compact)階段。
在這個階段中,垃圾回收器線性地遍歷堆,以尋找未標記的連續內存塊。若是發現大的可用的連續內存塊,垃圾回收器會把非垃圾(標記/可達)的對象移動到這裏來進行壓縮堆。堆內存壓縮後,託管堆的NextObjPtr指針將指向緊接在最後一個非垃圾對象以後的位置。這時候new操做符就能夠繼續成功的建立對象了。這個過程有點相似於磁盤空間的碎片整理。以此,對堆進行壓縮,不會形成進程虛擬地址空間的碎片化。
如上圖所示,綠色框表示可達對象,黃色框爲不可達對象。不可達對象清除後,移動可達對象實現內存壓縮(變得更緊湊)。
壓縮以後,「指向這些對象的指針」的變量和CPU寄存器如今都會失效,垃圾回收器必須從新訪問全部根,並修改它們來指向對象的新內存位置。這會形成顯著的性能損失。這個損失也是託管堆的主要缺點。
基於以上特色,垃圾回收引起的回收算法也是一項研究課題。由於若是真等到託管堆滿纔開始執行垃圾回收,那就真的太「慢」了。
垃圾回收器的好處:
垃圾回收算法 --- 分代(Generation)算法
代是CLR垃圾回收器採用的一種機制,它惟一的目的就是提高應用程序的性能。分代回收,速度顯然快於回收整個堆。
CLR託管堆支持3代:第0代,第1代,第2代。第0代的空間約爲256KB,第1代約爲2M,第2代約爲10M。新構造的對象會被分配到第0代。
如上圖所示,當第0代的空間滿時,垃圾回收器啓動回收,不可達對象(上圖C、E)會被回收,存活的對象被歸爲第1代。
當第0代空間已滿,第1代也開始有不少不可達對象以致空間將滿時,這時兩代垃圾都將被回收。存活下來的對象(可達對象),第0代升爲第1代,第1代升爲第2代。
實際CLR的代回收機制更加「智能」,若是新建立的對象生存週期很短,第0代垃圾也會馬上被垃圾回收器回收(不用等空間分配滿)。另外,若是回收了第0代,發現還有不少對象「可達」,
並無釋放多少內存,就會增大第0代的預算至512KB,回收效果就會轉變爲:垃圾回收的次數將減小,但每次都會回收大量的內存。若是尚未釋放多少內存,垃圾回收器將執行徹底回收(3代),若是仍是不夠,則會拋出「內存溢出」異常。
也就是說,垃圾回收器會根據回收內存的大小,動態的調整每一代的分配空間預算!達到自動優化!
.NET垃圾回收器的基本工做原理是:經過最基本的標記清除原理,清除不可達對象;再像磁盤碎片整理同樣壓縮、整理可用內存;最後經過分代算法實現性能最優化。
終結(Finalization)是CLR提供的一種機制,容許對象在垃圾回收器回收其內存以前執行一些得體的清理工做。任何包裝了本地資源(例如文件)的類型都必須支持終結操做。簡單的說,類型實現了一個命名爲Finalize的方法。當垃圾回收器判斷一個對象是垃圾時,會調用對象的Finalize方法。
C#團隊認爲,Finalize方法是編程語言中須要特殊語法的一種方法。在C#中,必須在類名前加一個~符號來定義Finalize方法。
internal sealed class SomeType
{
~SomeType()
{
//這裏的代碼會進入Finalize方法
}
}
編譯上述代碼,會發現C#編譯器實際是在模塊的元數據中生成一個名爲Finalize的protected override方法。方法主體被放到try塊中,finally塊放入了一個對base.Finalize的調用。
實現Finalize方法時,通常都會調用Win32 CloseHandle函數,並向該函數傳遞本地資源的句柄。
若是包裝了本地資源的類型沒有定義Finalize方法,本地資源就得不到關閉,致使資源泄露,直至進程終止。進程終止時,這些本地資源纔會被操做系統回收。
不要對託管資源進行終結操做,終結操做幾乎專供釋放本地資源。
Finalize方法在垃圾回收結束時調用,有如下5種事件會致使開始垃圾回收:
終結操做表面看起來簡單:建立一個對象,當它被回收時,它的Finalize方法會獲得調用。但深究下去,遠沒有這麼簡單。
應用程序建立一個新對象時,new操做符會從堆中分配內存。若是對象的類型定義了Finalize方法,那麼在該類型的實例構造器調用以前,會將一個指向該對象的指針放到一個終結列表 (finalization list) 中。終結列表是由垃圾回收器控制的一個內部數據結構。列表中的每一項都指向一個對象,在回收該對象以前,會先調用對象的Finalize方法。
下圖展現了包含幾個對象的一個託管堆。有的對象從應用程序的根可達,有的不可達(垃圾)。對象C,E,F,I,J被建立時,系統檢測到這些對象的類型定義來了Finalize方法,全部指向這些對象的指針要添加到終結列表中。
垃圾回收開始時,對象B,E,G,H,I和J被斷定爲垃圾。垃圾回收器掃描終結列表以查找指向這些對象的指針。找到一個指針後,該指針會從終結列 表中移除,並追加到freachable隊列中。freachable隊列(發音是「F-reachable」)是垃圾回收器的內部數據結構。 Freachable隊列中的每一個指針都表明其Finalize方法已準備好調用的一個對象。
下圖展現了回收完畢後託管堆的狀況。從圖中咱們能夠看出B,E和H已經從託管堆中回收了,由於它們沒有Finalize方法,而E,I,J則暫時沒有被回收,由於它們的Finalize方法還未調用。
一個特殊的高優先級的CLR線程負責調用Finalize方法。使用專用的線程可避免潛在的線程同步問題。freachable隊列爲空時,該線程將睡眠。當隊列中有記錄項時,該線程就會被喚醒,將每一項從freachable隊列中移除,並調用每一項的 Finalize方法。
若是一個對象在freachable隊列中,那麼意味這該對象是可達的,不是垃圾。
本來,當對象不可達時,垃圾回收器將把該對象當成垃圾回收了,但是當對象進入freachable隊列時,有奇蹟般的」復活」了。而後,垃圾回收 器壓縮(內存脆片整理)可回收的內存,特殊的CLR線程將清空freachable隊列,並調用其中每一個對象的Finalize方法。
垃圾回收器下一次回收時,發現已終結的對象成爲真正的垃圾,由於應用程序的根再也不指向它,freachhable隊列也再也不指向它。因此,這些對象的內存會直接回收。
整個過程當中,可終結對象須要執行兩次垃圾回收器才能釋放它們佔用的內存。可在實際開發中,因爲對象可能被提高到較老的一代,因此可能要求不止兩次進行垃圾回收。下圖展現了第二次垃圾回收後託管堆中的狀況。
Finalize方法很是有用,由於它確保了當託管對象的內存被釋放時,本地資源不會泄漏。可是,Finalize方法的問題在於,他的調用時間不能保證。另外,因爲他不是公共方法,因此類的用戶不能顯式調用它。
類型爲了提供顯式進行資源清理的能力,提供了Dispose模式。全部定義了Finalize方法的類型都應該同時實現Dispose模式,使類型的用戶對資源的生存期有更多的控制。
類型經過實現System.IDisposable接口的方式來實現Dispose模式:
public interface IDisposable
{
void Dispose();
}
任何類型只有實現了該接口,將至關於聲稱本身遵循Dispose模式。無參Dispose和Close方法都應該是公共和非虛的。
FileStream類實現了System.IDisposable接口。
FileStream fs = new FileStream();
//顯示關閉文件 Dispose/Close
fs.Dispose();
fs.Close();
fs.Write();
顯示調用一個類型的Dispose或Close方法只是爲了能在一個肯定的時間強迫對象執行清理。這兩個方法並不能控制託管堆中的對象所佔用的內存的生存期。這意味着即便一個對象已完成了清理,仍然可在它上面調用方法,但會拋出ObjectDisposedException異常。
若是決定顯式地調用Dispose和Close這兩個方法之一,強烈建議把它們放到一個異常處理finally塊中。這樣能夠保證清理代碼獲得執行。
C#提供了一個using語句,這是一種簡化的語法來得到上述效果。
using(FileStream fs = new FileStream()){
fs.Write();
}
在using語句中,咱們初始化一個對象,並將它的引用保存到一個變量中。而後在using語句的大括號內訪問該變量。編譯這段代碼時,編譯器自動生成一個try塊和一個finally塊。在finally塊中,編譯器會生成代碼將變量轉型成一個IDispisable並調用Dispose方法。顯然,using語句只能用於哪些實現了IDisposable接口的類型。
CLR爲每個AppDomain都提供了一個GC句柄表 (GC Handle table) 。該表容許應用程序監視對象的生存期,或手動控制對象的生存期。
在一個AppDomain建立之初,該句柄表是空的。句柄表中的每一個記錄項都包含如下兩種信息:一個指針,它指向託管堆上的一個對象;一個標誌(flag),它指出你想如何監視或控制對象。
爲了在這個表中添加或刪除記錄項,應用程序要使用System.Runtime.InteropServices.GCHandle類型。
前面說過,須要終結的一個對象被認爲死亡時,垃圾回收器會強制該對象重生,使它的Finalize方法得以調用。Finalize方法調用以後,對象才真正的死亡。
須要終結的一個對象會經歷死亡、重生、再死亡的「三部曲」。一個死亡的對象重生的過程稱爲復活(resurrection) 。復活通常不是一件好事,應避免寫代碼來利用CLR這個「功能」。
前面討論的垃圾回收算法有一個很大的前提就是:只在一個線程運行。
在現實開發中,常常會出現多個線程同時訪問託管堆的狀況,或至少會有多個線程同時操做堆中的對象。一個線程引起垃圾回收時,其它線程絕對不能訪問任何線程,由於垃圾回收器可能移動這些對象,更改它們的內存位置。
CLR想要進行垃圾回收時,會當即掛起執行託管代碼中的全部線程,正在執行非託管代碼的線程不會掛起。而後,CLR檢查每一個線程的指令指針,判斷線程指向到哪裏。接着,指令指針與JIT生成的表進行比較,判斷線程正在執行什麼代碼。
若是線程的指令指針剛好在一個表中標記好的偏移位置,就說明該線程抵達了一個安全點。線程可在安全點安全地掛起,直至垃圾回收結束。若是線程指令指針不在表中標記的偏移位置,則代表該線程不在安全點,CLR也就不會開始垃圾回收。在這種狀況下,CLR就會劫持該線程。也就是說,CLR會修改該線程棧,使該線程指向一個CLR內部的一個特殊函數。而後,線程恢復執行。當前的方法執行完後,他就會執行這個特殊函數,這個特殊函數會將該線程安全地掛起。
然而,線程有時長時間執行當前所在方法。因此,當線程恢復執行後,大約有250毫秒的時間嘗試劫持線程。過了這個時間,CLR會再次掛起線程,並檢查該線程的指令指針。若是線程已抵達一個安全點,垃圾回收就能夠開始了。可是,若是線程尚未抵達一個安全點,CLR就檢查是否調用了另外一個方法。若是 是,CLR再一次修改線程棧,以便從最近執行的一個方法返回以後劫持線程。而後,CLR恢復線程,進行下一次劫持嘗試。
全部線程都抵達安全點或被劫持以後,垃圾回收才能使用。垃圾回收完以後,全部線程都會恢復,應用程序繼續運行,被劫持的線程返回最初調用它們的方法。
實際應用中,CLR大多數時候都是經過劫持線程來掛起線程,而不是根據JIT生成的表來判斷線程是否到達了一個安全點。之因此如此,緣由是JIT生成表須要大量內存,會增大工做集,進而嚴重影響性能。
任何85000字節或更大的對象都被自動視爲大對象(large object)。
大對象從一個特殊的大對象堆中分配。這個堆中採起和前面小對象同樣的方式終結和釋放。可是,大對象永遠不壓縮(內存碎片整理),由於在堆中下移850000字節的內存塊會浪費太多CPU時間。
大對象老是被認爲是第2代的一部分,因此只能爲須要長時間存活的資源建立大對象。若是分配短期存活的大對象,將致使第2代被更頻繁地回收,進而會損害性能。
寄宿容許使任務應用程序都能利用CLR的功能。寄宿(hosting)容許使任務應用程序都能利用CLR的功能。另外,寄宿還爲應用程序提供了經過編程來進行自定義和擴展能力。AppDomain容許第三方的,不受信任的代碼在一個現有的進程中運行,而CLR保證數據結構、代碼和安全上下文不會被濫用或破壞。
.Net Framework必須用Windows能夠理解的技術來構建。首先,全部託管模塊和程序集都必須使用Windows PE文件格式。
開發CLR時,Microsoft實際是將它實現成包含在一個DLL中的COM服務器。也就是說,Microsoft爲CLR定義了一個標準的COM接口,並未該接口和COM服務器分配了GUID(全局通用標識符)。安裝.Net Framework時,表明CLR的COM服務器和其餘COM服務器同樣在Windows註冊表中註冊。
任何Windows應用程序均可以寄宿CLR。非託管宿主應該調用MetaHost.h文件中聲明的CLRCreateInstance函數。CLRCreateInstance函數是在MSCorEE.dll文件中實現的,該文件通常是在C:\Windows\System32目錄中。這個DLL被稱爲「墊片」(shim),它的工做是決定建立哪一個版本的CLR,注意墊片DLL自己並不包含CLR COM服務器。
一臺機器可安裝多個版本的CLR,但只有一個版本的MSCorEE.dll文件(墊片)。
CLRCreateInstance函數能夠返回一個ICLRMetaHost接口。宿主應用程序可調用這個接口的GetRuntime函數,指定宿主要建立的CLR的版本。而後,墊片將所需版本的CLR加載到宿主的進程中。
宿主應用程序可調用ICLRRuntimeHost接口定義的方法作下面的事情:
寄宿使任何應用程序都能提供CLR功能和可編程性,如下是寄宿CLR的部分好處:
CLR COM服務器初始化時,會建立一個AppDomain。AppDomain是一組程序集的邏輯容器。CLR初始化時建立的第一個AppDomain稱爲默認AppDomain,這個默認的AppDomain只有在Windows進程終止時纔會被銷燬。
除了默認AppDomain,託管類型方法的一個宿主還可指示CLR建立額外的AppDomain。AppDomain惟一的做用是進行隔離。
下面總結了AppDomain的具體功能:
一個AppDomain中的代碼建立了一個對象後,該對象被該AppDomain「擁有」。換言之,它的生存期不能比建立它的代碼所在的AppDomain還要長。一個AppDomain中的代碼爲了訪問另外一個AppDomain中的對象,只能使用「按引用封送」或者「按值封送」的語義。這就增強了一個清晰的分隔和邊界,由於一個AppDomian中的代碼沒有對另外一個AppDomain中的代碼所建立的對象的直接引用。
CLR不支持從AppDomain中卸載一個程序集的能力。可是,能夠告訴CLR卸載一個AppDomain,從而卸載當前包含在該AppDomain內的全部程序集。
AppDomain在建立以後,會應用一個權限集,它決定了向這個AppDomain中運行的程序集授予的最大權限。
AppDomain在建立以後,會關聯一組配置設置。
下圖演示了一個Windows進程,其中運行着一個CLR COM服務器。該CLR當前管理着兩個AppDomain。每一個AppDomain都有本身的Loader堆,每一個Loader堆都記錄了自AppDomain建立以來已訪問過哪些類型。Loader堆中的每一個類型對象都有一個方法表,方法表中的每一個記錄項都指向JIT編譯的本地代碼(前提是方法至少執行過一次)。
除此以外,每一個AppDomain都加載了一些程序集。AppDomain有三個程序集:MyApp.exe,TypeLib.dll和System.dll。AppDomain#2有兩個程序集:Wintellect.dll和System.dll。
如圖所示,System.dll程序集被加載到兩個AppDomain中。若是這兩個AppDomain都使用了來自System.dll的一個類型,那麼在兩個AppDomain的Loader堆中,都會爲同一個類型分配一個類型對象;類型對象的內存不會由兩個AppDomain共享。
AppDomain的所有目的就是提供隔離性;CLR要求在卸載某個AppDomain並釋放它的全部資源的同時,不會對其餘AppDomain產生負面影響。經過複製CLR的數據結構,就能夠保證這一點。除此以外,還能保證由多個AppDomain使用的一個類型在每一個AppDomain中都有一個靜態字段。
AppDomain是CLR的功能,Windows對此一無所知。
CreateInstanceAndUnwrap方法致使調用線程從當前AppDomain到新的AppDomain。線程將制定程序集加載到新 AppDomain中,並掃描程序集類型定義元數據表,查找指定類型「MarshalByRefType」)。找到類型後,調用它的無參構造函數。而後, 線程又範圍默認AppDomain,對CreateInstanceAndUnwrap返回的MarshalByRefType對象進行操做。
如何將一個對象從一個AppDomain(源AppDomain,這裏指真正建立對象的地方)封送到另外一個AppDomain(目標AppDomain,這裏指調用CreateInstanceAndUnwrap的地方)?
1. Marshal-by-Reference
CLR會在目標AppDomain的Loader堆中定義一個代理類型。這個代理類型是用原始類型的數據定義的。所以,它看起來和原始類型徹底同樣;有徹底同樣的實例成員(屬性、事件和方法)。可是,實例字段不會成爲(代理)類型的一部分。
2. Marshal-by-Value
CLR將對象字段序列化一個字節數組。這個字節數組從源AppDomain複製到目標AppDomain。而後,CLR在目標AppDomain中 反序列化字節數組,這會強制CLR將定義了的「被反序列化的類型」的程序集加載到目標AppDomain中。接着,CLR建立類型的一個實例,並利用字節 數組初始化對象的字段,使之與源對象中的值相同。換言之,CLR在目標AppDomain中準確的複製了源對象。
AppDomain.Unload()中執行操做:
(1)CLR掛起進程中執行中執行的託管代碼的全部線程;
(2)CLR檢查全部線程棧,查看哪些線程正在執行要卸載的那個AppDomain中的代碼,或者哪些線程會在某個時刻返回至要卸載的那個 AppDomain。在任何一個棧上,若是準備卸載的AppDomain,CLR都會強迫對應的線程拋出一個ThreadAbortException異 常(同時恢復線程的執行)。這將致使線程展開(unwind),在展開的過程當中執行遇到的全部finally塊中的內容,以執行資源清理代碼。若是沒有代 碼捕捉ThreadAbortException,它最終會成爲一個未處理的異常,CLR會「吞噬」這個異常,線程會終止,但進程能夠繼續運行。這一點是 很是特別的,由於對於其餘全部未處理的異常,CLR都會終止進程。
重要提示:若是一個線程當前正在finally塊、catch塊、類構造器、臨界執行區(critical execution region)域或非託管代碼中執行,那麼CLR不會當即終止該線程。不然,資源清理代碼、錯誤恢復代碼、類型初始化代碼、關鍵代碼或者其餘任何CLR不 瞭解的代碼都沒法完成,致使應用程序的行爲變得沒法預測,甚至可能形成安全漏洞。線程在終止時,會等待這些代碼塊執行完畢。而後當代碼塊結束時,CLR再 強制線程拋出一個ThreadAbortException。
臨界區是指線程終止或未處理異常的影響可能不限於當前任務的區域。相反,非臨界區中的終止或失敗只對出現錯誤的任務有影響。
(3)當上一步發現的全部線程都離開AppDomain後,CLR遍歷堆,爲引用了「已卸載的AppDomain建立的對象」的每個代理都設置一 個標誌(flag)。這些代理對象如今知道它們引用的真實對象已經不在了。若是任何代碼在一個無效的代理對象上調用一個方法,該方法會拋出一個 AppDomainUnloadedException
(4)CLR強制垃圾回收,對現已卸載AppDomain建立的任何對象佔用的內存進行回收。這些對象的Finalize方法被調用(若是存在Finalize方法),使對象有機會完全清理它們佔用的資源
(5)CLR恢復剩餘全部線程的執行。調用AppDomain.Unload方法的線程將繼續執行,對AppDomain.Unload的調用是同 步進行的在前面的例子中,全部工做都用一個線程來作。所以,任什麼時候候只要調用AppDomain.Unload都不可能有另外一個線程在要卸載的 AppDomain中。所以,CLR沒必要拋出任何ThreadAbortException異常。
程序集加載和反射,實現了在編譯時對一個類型一無所知的狀況下,如何在運行時發現類型的信息,建立類型的實例以及訪問類型的成員。顯現的功能以及效果是十分強大的,好比使用第三方提供的程序集,以及建立動態可擴展應用程序。
JIT編譯器在將方法的IL代碼編譯成本地代碼時,會查看IL代碼中引用了哪些類型。在運行時,JIT編譯器查看元數據表TypeRef和AssemblyRef來肯定哪個程序集定義了所引用的類型。在AssemblyRef元數據表的記錄項中,包含了構成程序集強名稱的各個部分 :名稱(無擴展名和路徑),版本,語言文化和公鑰(PublicKeyToken)。
JIT編譯器利用如下這些信息,鏈接成字符串。例如:( StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5 )。而後嘗試將一個匹配的程序集加載到AppDomain中。若是是弱命名程序集,則只包含程序集的名稱。
常見的程序集加載方式有三種:
(1)Assembly.Load
在內部,CLR使用System.Reflection.Assembly類的靜態方法Load來嘗試加載程序集,經常使用的版本原型:
public class Assembly
{
public static Assembly Load(AssemblyName assemblyRef);
public static Assembly Load(String assemblyString);
}
首先熟悉下這兩個方法的使用,建立個強命名程序集,而後查看強命名信息(SN.exe, Reflector工具, Assembly.GetAssemblyName)
Assembly assemblyLoadString = Assembly.Load("StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5");
Assembly assemblyLoadRef = Assembly.Load(new AssemblyName("StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5"));
注意:
a.對於強命名程序集,在內部,Load致使CLR向程序集應用一個版本綁定重定向策略,並在GAC中查找程序集。若是沒有找到,就接着去應用程序的基目錄,私有路徑子目錄和codebase位置中查找
b.若是Load時傳遞的是一個弱命名程序集,Load就不會向程序集應用一個版本綁定重定向策略,CLR也不會去GAC中查找程序集
c.若是找到指定的程序集,返回的是那個程序集的一個Assembly對象的引用;若是沒有找到指定的程序集,拋出System.IO.FileNotFoundException
d.對於須要加載爲特定CPU架構生成的程序集,在指定程序集標識時,還可包括一個進程架構部分。
StrongNameDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=07f452de4cf765d5 , ProcessorArchitecture=MSIL
CLR容許ProcessorArchitecture取4個值之一:MSIL, x86, IA64, AMD64
(2)Assembly.LoadFrom
使用Load時,它要求你事先掌握構成程序集標識的各個部分。在某些狀況下,咱們也能夠指定一個程序集文件的路徑名(包括文件擴展名),獲取Assembly對象。
Assembly assemblyFromPath = Assembly.LoadFrom(@"E:\StrongNameDLL.dll");
對於使用LoadFrom,傳入路徑名的使用方式,須要瞭解內部的實現機制,避免誤用的狀況:
a.在內部,LoadFrom首先會調用System.Reflection.AssemblyName類的靜態方法GetAssemblyName。
該方法打開指定的文件,提取AssemblyRef記錄項中的程序集標識信息,而後以一個System.Reflection.AssemblyName對象的形式返回這些信息(文件同時關閉)。
b.LoadFrom方法在內部調用Assembly的Load方法,將AssemblyName對象傳給它。
c.CLR會爲應用版本綁定重定向策略,並在各個位置查找匹配的程序集。若是Load找到了匹配的程序集,就會加載它,並返回一個Assembly對象;LoadFrom返回這個值。若是沒有找到匹配項,LoadFrom就會加載路徑名中的程序集。
(3)Assembly.LoadFile
加載指定路徑上的程序集文件。
Assembly assemblyFromPath = Assembly.LoadFile(@"E:\StrongNameDLL.dll");
注意:
a.可將具備相同標識的一個程序集屢次加載到一個AppDomain中
b.CLR不會解析任何依賴性問題
c.必須向AppDomain的AssemblyResolve事件登記,並讓事件回調方法顯式地加載任何依賴的程序集
三者對比
既然已經對Load,LoadFrom,LoadFile有所瞭解,那麼接着來看看這三者之間的區別與對比。
1.Load和LoadFrom
a.根據LoadFrom的內部實現機制,LoadFrom返回的實際是Load找到的匹配程序集的一個Assembly對象(在找到匹配的程序集的狀況下)
b.LoadFrom存在屢次打開文件的現象,而且內部仍是要走一套Load的邏輯操做,還存在對比狀況,因此LoadFrom比Load效率低
c.LoadFrom 要求指定路徑中包含 FileIOPermissionAccess.Read 和 FileIOPermissionAccess.PathDiscovery 或 WebPermission
d. LoadFrom, and the probing path includes an assembly with the same identity but a different location, an InvalidCastException, MissingMethodException, or other unexpected behavior can occur. "> 若是用 LoadFrom 加載一個程序集,而且probing路徑包括具備相同標識但位置不一樣的程序集,則發生 InvalidCastException 、 MissingMethodException 或其餘意外行爲。
2.LoadFrom和LoadFile
a.根據MSDN的解釋,能夠看出功能實現機制存在區別。
LoadFrom:已知程序集的文件名或路徑,加載程序集。
LoadFile:加載指定路徑上的程序集文件的內容。
b.LoadFrom會加載程序集的依賴項,可是LoadFile不會。
例如,上面的StrongName.dll存在引用程序集ReferenceDemo.dll。使用LoadFrom,StrongName.dll和 ReferenceDemo.dll都會被載入,可是使用LoadFile,ReferenceDemo.dll不會被載入。
c.能夠使用LoadFile加載並檢查具備相同標識但位於不一樣路徑的程序集,可是LoadFrom不行, LoadFrom returns the loaded assembly even if a different path was specified. ">若是已加載一個具備相同標識的程序集,則即便指定了不一樣的路徑,仍返回已加載的程序集。 。
例如,StrongName.dll存在於兩個不一樣路徑下,LoadFile能夠正確載入兩個程序集;可是LoadFrom若是已經加載了一次 StrongName.dll,再一次加載不一樣路徑下的Assembly時,會先檢查前面是否已經載入過相同名字的 Assembly,最終返回的仍是第一次加載的Assembly對象的引用。
總結:
因爲LoadFrom具備很多缺點,通常優先考慮使用Load方法加載程序集。而LoadFile則只在有限的狀況下使用。
微軟始終OS內核時,決定在一個進程(progress)中運行應用程序的每一個實例。進程是應用程序的一個實例要使用的資源的一個集合。每一個進程都被賦予了一個虛擬地址空間,確保一個進程使用的代碼和數據沒法由另外一個進程訪問。除此以外,OS的內核代碼和數據是進程訪問不到的。因此,應用程序代碼破壞不了其餘應用程序和OS自身。
上面聽起來不錯,但CPU自己呢?若是一個應用程序進入無限循環,若是機器只有一個CPU,它會執行無限循環,不會執行其餘東西。微軟要修復這個問題,提出了線程。做爲一個Windows機率,線程(thread)的職責是對CPU進行虛擬化,可將線程理解爲一個邏輯CPU。Windows爲每一個進程都提供了該進程專用的線程。若是引用程序的代碼進入無限循環,與那個代碼關聯的進程會凍結,但其餘進程不會凍結,由於它們有本身的線程。