實例方法、靜態方法
C#中的方法分為兩類,一種是屬於對象(類型的實例)的,稱之為實例方法,另一種是屬於類型的,稱之為靜態方法(用static關鍵字定義)。大家都是做開發的,這兩個也沒啥好說的。
唯一的建議就是:你的靜態方法最好是線程安全的(這點是說起容易做起難啊……)。
構造器(構造函數)
構造器是一種特殊的方法,CLR中的構造器分為兩種:一種是實例構造器;另一種是類型構造器。和其他方法不同,構造器不能被繼承,所以在構造器前應用virtual/new/override/sealed和abstract是沒有意義的,同時構造器也不能有返回值。
實例構造器用來初始化類型的實例(也就是對象)的初始狀態。
對於引用類型,如果我們沒有顯式定義實例構造器,C#編譯器默認會生成一個無參實例構造器,這個構造器什么也不做,只是簡單調用一下父類的無參實例構造器。這里應該意識到,如果我們定義的類的基類沒有定義無參構造器,那么我們的派生類就必須顯式調用一個基類構造器。
class MyBase { public MyBase(string name) { } } class MyClass : MyBase { }
上面的代碼會報“MyBase不包含采用0個參數的構造函數”的錯誤,必須顯式調用一個基類的構造器:
class MyBase { public MyBase(string name) { } } class MyClass : MyBase { public MyClass(string name) : base(name) { } }
一個類型可以定義多個實例構造器,只要這些構造器有不同的方法簽名即可。如下MyBase類,我定義了三個構造器:
class MyBase { public MyBase() //無參構造器 : this(string.Empty) { } public MyBase(string name) //一個參數的構造器 : this(name, 0) { } public MyBase(string name, int age) //兩個參數的構造器 { } }
除了實例構造器,C#語言還提供了一種初始化字段的簡便語法,稱為“內聯初始化”:

從編譯后生成的IL代碼可以看出,內聯初始化本質是在所有實例構造器中,生成一段字段初始化代碼的方式來實現的。注意這里一個潛在的代碼膨脹問題,如果我們定義了多個實例構造器,那么在每個實例構造器開頭處,都會生成這樣的初始化代碼。在有多個實例構造器的類型定義中,應盡量減少這種內聯初始化,可以通過創建一個構造器來初始化這些字段,然后讓其他構造器通過this關鍵字來調用這個構造器。
對於值類型,C#不會對值類型生成默認的無參構造器,但CLR總是允許值類型的實例化。即對於以下的值類型定義,雖然我們沒有定義任何構造器,C#也沒有為我們生成默認無參構造器,但它總是可以通過new實例化的(值類型的字段被初始化為0或null)。
struct MyStruct { public int x, y; } MyStruct ms = new MyStruct(); //總是可以實例化
我們可以為值類型定義有參構造器(C#不允許值類型定義無參構造器),但在內部必須初始化值類型的所有字段。
struct MyStruct { public int x, y; public string z; public MyStruct(int a) //異常:在控制返回到調用之前,字段y、z必須完全賦值。 { x = a; } }
像上面這樣為值類型定義一個有參構造器時,編譯器會報“在控制返回到調用之前,字段y、z必須完全賦值”的錯誤。為了修正這個問題,可以采用下面的語法為值類型字段初始化。
struct MyStruct { public int x, y; public string z; public MyStruct(int a) //在控制返回到調用之前,字段y、z必須完全賦值。 { this = new MyStruct(); //this代表值類型實例本身,用new初始化值類型所有字段為0或null。 //this = default(MyStruct); //這種方式書上沒提,但我認為這樣也可以。 x = a; } }
類型構造器(靜態構造器)用來初始化類型的初始狀態,並且有且只能定義一個,且沒有參數。類型構造器總是私有的,C#會自動把它標記為private,事實上C#禁止開發人員對類型構造器應用任何訪問修飾符。
CLR在第一次使用一類型時,如果該類型定義了類型構造器,CLR便會以線程安全的方式調用它。這里應該意識到對類型構造器的調用,由於CLR要做大量檢查與判斷和線程同步,所以性能上會有所損失。
類型構造器的通常用來初始化類型中的靜態字段,C#同樣提供了一種內聯初始化的語法:

從編譯器生成的IL可知,靜態字段的內聯初始化實際上是在類型構造器生成初始化代碼完成的,而且首先生成的是內聯初始化代碼,然后才是類型構造器方法內部顯式包含的代碼。
注意:雖然值類型能定義類型構造器,但永遠都不要那么做。因為CLR有時不會調用值類型的類型構造器。
抽象方法、虛方法
這兩個概念都是針對於類型的繼承層次結構中來說的,如果沒有了繼承,它們是毫無意義的。這也意味着它們的可訪問性至少是protected,即對派生類是可見的。
抽象方法是只定義了方法名稱、簽名和返回值類型,而沒有定義任何方法實現的一種方法。C#中用abstract定義,抽象方法所在的類肯定是抽象類。由於抽象方法沒有定義方法實現,所以它是沒有意義的,必須在派生類中提供方法的實現(如果派生類沒有提供,那么它必須仍然定義成抽象類)。
abstract class MyBase { //靜態方法 public static void Test0() { /*方法實現*/ } //實例方法 public void Test1() { /*方法實現*/ } //抽象方法 public abstract void Test2(); //虛方法 protected virtual void Test3() { /*方法實現*/} }
在C#中用virtual定義的方法是虛方法,它看上去只是比定義一個普通實例方法多了一個virtual關鍵字。虛方法總是允許在派生類中重寫,但不強求,這正在它和抽象方法的區別。也可以邏輯上把虛方法想象成提供了默認實現的抽象方法,因為提供了默認實現,所以不強求派生類中重寫。
抽象方法編譯后被標記為abstract virtual instance(抽象虛實例方法),虛方法編譯后被標記為virtual instance(虛實例方法)。

抽象方法和虛方法共同的特點都是可以在派生類中重寫,在C#中用override關鍵字來重寫一個方法。在VS中,如果我們在類中輸入override關鍵字加空格,便會顯示出所有基類中的虛成員(方法、屬性、事件等)。因為抽象方法編譯后是抽象和虛的,所以也會顯示在列表中。

重寫后的方法仍然是virtual的(但不再是抽象的)

virtual方法是可以被派生類重寫的,如果不希望重寫后的方法被接下來的派生類(即派生自MyClass的類)重寫,可以在override前應用sealed關鍵字,將方法標記為封閉的。
如下圖中,我將MyClass中的Test3標記為sealed后,MyClass的派生類中,VS列出的可重寫的成員中便沒有Test3了。

當然,還可以對類應用sealed關鍵字,這樣整個類都不能被繼承了!類都不能被繼承了,類里包含的所有虛方法更不談重寫了。
分部方法(partial關鍵字)
主要是partial關鍵字(也可以應用於類、結構和接口),可以將一個方法定義到多個文件中。
通常有這么一種場情:我們往往利用代碼生成工具生成一些模板化的代碼,但又需要對某些細節進行定制,雖然可以通過虛方法重寫來實現,但這樣做存在兩點問題:
- 工具生成的類必須是非密封的,因為要繼承重寫虛方法。
- 調用虛方法存在潛在的效率問題。
這時候,就可以利用分部方法來實現。讓代碼生成器生成一個分部類(注意這個類可以是密封的),把實現細節抽象成一個方法定義。像下面這樣:
//工具生成部分 sealed partial class XXOO { //聲明一個分部方法 partial void PrepareSomething(string boy, string girl); public void DoSomething() { //調用分部方法(如果沒有提供實現,編譯后這句會被優化掉) PrepareSomething("", ""); /*其他邏輯*/ } }
如果我們沒有提供分部方法的實現,那么編譯后,整個方法的定義和所有對此方法的調用都會被優化(刪除)掉,這樣可以讓代碼更少更快!也正因為這一點(編譯后分部方法可能不存在),所以分部方法不能定義任何修改符,也不能定義返回值!

當然用分部方法主要還是為了提供實現細節,我們甚至可以在不同的文件中來定義這個類(在VS中輸入partial加空格,便會列出當前分部類中的還未提供實現的分部方法):
//自定義的部分 sealed partial class XXOO { //提供具體的實現細節 partial void PrepareSomething(string boy, string girl) { /*提供實現細節的代碼*/ } }
我們再來看看提供分部方法的實現代碼后,編譯器生成了什么:

關於分部方法有幾點要小注意一下:
- 只能在分部類或結構中聲明。
- 返回類型始終為void,並且參數不能有out修改符,因為編譯后這個方法可能不存在。
- 分部方法總被視為private的,C#編譯器禁止手工指定任何修改符。
Finalize方法與Dispose方法
這兩個方法涉及CLR的垃圾回收部分,這里只是從方法層面上談談這兩個方法。我們知道,C#是托管語言,我們寫的程序最終托管給CLR,CLR有強大的自動垃圾回收機制來幫助我們回收內存資源。但注意CLR自動回收的僅是內存資源,有些類除了要利用內存資源外,還需要利用一些其他的系統資源(比如文件、網絡連接、套接字、互斥體等),所以CLR提供了一種機制來釋放這些資源,這便是Finalize方法(終結器)。
這里的Finalilze方法並不是指直接在類中定義一個Finalize方法(雖然可以定義,但永遠不要這么做!),而是指用析構語法來定義的一種方法,即“~類名()”的方式定義的方法,該方法編譯后,會生成名為Finalize的方法。CLR會在決定回收包含Finalize方法的對象之前用一個特殊的線程調用Finalize方法來釋放一些資源(這個具體的過程待日后寫到CLR垃圾回收部分慢慢聊)。
下圖簡單演示了一下如何定義一個終結器,我們用定義析構函數的語法來定義了一個方法(注意這個方法沒有參數和任何修飾符),編譯后,編譯器為我們生成一個名為Finalize的protected virtual方法。且在方法內部生成一個try塊包裝原方法內的代碼,生成一個finally塊來調用基類的Finalize方法。

雖然定義Finalize方法的語法和C++的析構函數語法一樣,但CLR書上說兩者原理還是完全不同,所以不能稱為析構器(我的理解C++中的析構函數應該是釋放對象所用的資源包括內存資源,調用后對象便被清理干凈了;而C#中的Finalize方法只是釋放對象所用的系統資源,調用后對象仍然存活,直到CLR將其回收,不知道這么理解對不對啊,請指點!)。
雖然Finalize方法很有用,能確釋放一些資源。但有一點要注意,就是它的調用是由CLR決定的,所以調用時間我們無法保證。所以我們需要一種機制來顯式地釋放資源,這便是Dispose模式。.Net里提供了IDisposable接口(包含唯一一個Dispose方法),我們只要實現該接口即代表我們的類實現了Dispose模式。在Dispose方法內部,我們關閉對象所用到的系統資源。這樣我們在代碼中,就可以顯式調用Dispose方法來釋放資源,而不是被動地交給CLR去釋放,《CLR Via C#》書中建議所有實現終結器的類都同時實現Dispose模式。如下面的類,實現終結器的同時還實現Dispose模式(先不管實現細節是否合理):
class MyResource: IDisposable { private Mutex mutex; //構造器 public MyFinalization() { mutex = new Mutex(); } //終結器 ~MyFinalization() { mutex = null; } //實現IDisposable接口 public void Dispose() { mutex = null; } }
這樣在我們使用完MyResource對象后,就可以通過調用Dispose方法釋放資源。
MyResource resource = new MyResource(); // //…使用資源… // resource.Dispose(); //調用Disopse釋放對象所用的資源
對於實現Dispose模式的類型,C#還提供了using語句來簡化我們的編碼。
using (MyResource resource = new MyResource()) { // //…使用資源… // }
上面的代碼等價於
MyResource resource = new MyResource(); try { // //…使用資源… // } finally { if (resource != null) (resource as IDisposable).Dispose(); }
擴展方法
擴展方法使我們能夠向現有類型“添加”方法,而無需創建新的派生類型、重新編譯或以其他方式修改原始類型。 擴展方法是一種特殊的靜態方法,但可以像被擴展類型上的實例方法一樣進行調用,同時它可以得到VS智能提示的良好支持(我們可以像使用對象實例方法一樣,點出擴展方法)。

定義一個擴展方法,有以下幾點要求:
- 必須定義在非泛型靜態類中(類名無所謂)。並且這個類必須頂級類,即不能嵌套在其他類中。
- 擴展方法必須是靜態方法。
- 第一個參數必須指明被擴展的類型,並且用this關鍵字標識。
static class ExtensionMethods //靜態類名 無所謂 { public static bool IsNullOrEmpty(this string s) //擴展方法 { return string.IsNullOrWhiteSpace(s); } }
上面示例為string類型對象定義了一個名為IsNullOrEmpty的方法,只要我們的代碼中引入擴展方法所有靜態類ExtensionMethods 的命名空間,就可以直接在代碼中,像使用string類型原生方法一樣使用它了。
string name = "heku"; name.IsNormalized(); //Sytem.String類型原生方法 name.IsNullOrEmpty(); //擴展方法
關於擴展方法,還有以下幾點要注意:
- C#只支持擴展方法,不支持擴展屬性、擴展事件、擴展操作符等(雖然它們本質都是方法)。
- 使用擴展方法之前,必須要先引入擴展方法所在類的命名空間。
- 多個靜態類可以定義相同的擴展方法。如果發生沖突時,只能使用調用靜態方法的語法來調用擴展方法。
- 擴展方法是可以被繼承的,如果我們擴展了一個類型,那么它的所有派生類型都能調用此擴展方法。
- 潛在版本問題,如果未來微軟在類型中加入了和你擴展方法同名的方法,那么所有對擴展方法的調用都將變成調用微軟的方法。
擴展方法延伸閱讀:鶴沖天 http://www.cnblogs.com/ldp615/archive/2009/08/07/1541404.html
值類型參數和引用類型參數
傳遞參數就是賦值操作,我們可以把方法參數看成方法定義的一些變量,傳參就是對這些變量進行賦值的過程。賦值過程就是拷貝線程棧內容的過程,值類型的棧內容保存的就是值實例本身,而引用類型棧內容保存的是引用實例在堆上的地址。所以這里的區別主要是值類型與引用類型內存分配上的區別,具體可參考《C#基礎之基本類型》。所以在傳參后,方法的值類型參數擁有原始值的復制(一個副本),對其的更改不影響原始值,因為它們根本就不是一塊內存!方法的引用類型參數擁有與原始值相同的地址,它們指向同一塊堆內存,所以對引用類型參數的更改會影響原始值。如下示例,分別定義了一個值類型val和引用類型refObj,在調用Work方法后,值類型val未被修改,引用類型refObj被修改了。
class Program { static void Main(string[] args) { DoWork dw = new DoWork(); int val = 555; RefType refObj = new RefType { Id = 1, Name = "Heku" }; Console.WriteLine(val); Console.WriteLine("Id={0},Name={1}\n", refObj.Id, refObj.Name); //********輸出******** //555 //Id=1,Name=Heku dw.Work(val, refObj); Console.WriteLine(val); Console.WriteLine("Id={0},Name={1}\n", refObj.Id, refObj.Name); //********輸出******** //555 //Id=2,Name=Heku修改后 Console.ReadKey(); } } //一個引用類型 class RefType { public int Id { get; set; } public string Name { get; set; } } class DoWork { //修改值類型a 和 引用類型 b public void Work(int a, RefType b) { a++; b.Id++; b.Name = b.Name + "修改后"; } }
可選參數、命名參數
我們定義一個有很多參數的方法后,那么所有調用處都要准備好這些參數才能調用此方法。但往往有些時候,我們調用時只關心其中的部分參數,通常我們是通過重載來定義幾個參數比較少的方法,內部補全其他參數再調用參數最多的那個方法。但這是純體力活,而且也不能重載出所有可能的參數組合情況。因此C#提供一種機制,可以在定義方法的同時,給參數指定默認值,這樣在方法調用處,如果沒有給參數提供值,就會采用默認值,擁有默認值的參數就稱為可選參數。
//參數isToUpper因為有了默認值true, //參數other因為有了默認值0, //所以參數isToUpper和other在調用時可以不提供,故稱為 可選參數 public string ToUpperOrLower(string message, bool isToUpper = true, int other = 0) { if (isToUpper) return message.ToUpper(); else return message.ToLower(); }
參數isToUpper和other因為提供了默認值,所以我們可以僅提供message的值,來調用方法:
//調用 沒有傳可選參數 string result = dw.ToUpperOrLower("HeKu");
如果我們想為第二個可選參數other顯式提供一個值,那么按參數只能一對一按順序匹配的規則,我們不得不指定isToUpper的值,這很不爽,所以命名參數的登場了!我們可以在調用時,用“參數名:參數值”的語法給參數提供值,這種語法的作用是要求參數的匹配方式不要按參數順序,而是根據提供的名稱。像下面這樣(沒有用命名參數語法的參數還是按參數順序匹配,如第一個參數”Heku”):
//為第二個可選參數 顯示提供一個值 string result = dw.ToUpperOrLower("HeKu", other: 25);
在定義方法參數時,還有幾點要小注意一下:
- 可以為方法、構造器、有參屬性的參數指定默認值,還可以為委托定義一部分的參數指定默認值。
- 有默認值的參數必須放在沒有默認值參數的后面。
- 默認值必須是編譯時能確定的常量值。
- 不要重命名參數變量。如上面ToUpperOrLower方法中,如果重命名了第三個參數,那么在調用處就因找不到other參數而報錯。
- 如果參數要用ref或out關鍵字標識,就不能設置默認值。
可變數量的參數(params關鍵字)
如果我們要設計一個方法,來計算所有輸入數字的總和。按以往我們會這么實現(不要關注方法內部實現是否合理):
//計算任意個數字和 public int sum(int[] numbers) { int sum = 0; foreach (int item in numbers) { sum += item; } return sum; }
因為輸入的數字個數是未知的,這里用一個數組來接收這些數字。在調用時,我們不得不先初始化一個數組,然后再調用方法。為了簡化這種編程方式,可以在numbers參數定義前,應用params關鍵字。
//計算任意個數字和 public int sum(params int[] numbers) { int sum = 0; foreach (int item in numbers) { sum += item; } return sum; }
現在就可以直接用這種直觀的方式調用sum了:
sum(1, 2, 3);
當然也可以用傳統的方式來調用:
int[] numbers = new int[] { 1, 2, 3 }; sum(numbers);
可變數量的參數,有幾點要小注意一下:
- Params關鍵字只能應用於方法簽名中的最后一個參數。
- 這個參數必須且只能是一維數組!
- 調用帶params關鍵字的方法時,會有額外的性能損失(除非顯式傳null)。因為數組對象必須在堆上分配,數組元素必須初始化,數組內存最終也必須垃圾回收。為了降低性能損失,通常可定義幾個沒有作用params關鍵字的重載版本。如System.String類型中Format方法的定義:

以傳引用的方式傳參數(ref和out關鍵字)
默認情況下,CLR中的所有方法參數都是傳值(線程棧的內容)的,但可以通過在參數應用ref或out關鍵字來改變這一默認方式。這兩個關鍵字唯一的區別就是,使用ref標識的參數要在傳遞之前初始化,而使用out標識的參數不需要。
應用了ref或out關鍵字的參數在傳遞時,傳遞的是線程棧內容的引用(地址),注意這里不是堆的地址(以引用方式傳參並不是說將參數轉換成引用類型來傳遞)。下面看一個例子:
public void Update(ref int a,ref object b) { a++; b = null; }
上面定義了一個方法,接收一個值類型參數,一個引用類型參數。並要求參數以引用的方式傳遞(加了ref關鍵字)。下面開始調用:
int a = 100; object o = new object(); object c = o;
Update(ref a,ref c);
Console.WriteLine(a); Console.WriteLine(o == null); Console.WriteLine(c == null);
會輸出什么?你想到了嗎?答案是:
101 False True
上面我們講過,以引用的方式傳參傳遞的是棧的地址。值類型本身的值就是分配在棧上,所以以引用方式傳參的值類型就像以傳值方式傳遞的引用類型(比較繞,好好想一下),最終的效果就是Update的第一個參數指向了變量a的棧,所以在方法內部的更改也直接影響到了變量a。對第二個參數,我特別事先定義了兩個變量o和c,讓它們都指向堆上同一塊內存空間,然后把變量c的棧地址傳給了方法的第二個參數,在方法內部將第二個參數設為null,實際上就是把c的棧內容設為了null(這點我是根據現象推出來的,到底是不是這樣?請大牛指點!),但這絲毫沒有影響到堆上的對象和變量o!所以最終的結果就是a被修改成101,o沒變,c被修改為null。如果把第二個參數的ref去掉,結果會是什么樣呢?這個請大家自己think一下吧~本絲又敲了兩天鍵盤,眼睛好累啊~
結束的話
又一個周末,終於敲完了這篇讀書筆記性質的總結。給自己列的提綱中“操作符重載方法、轉換操作符方法”這一部分由於自己未做過多了解,故未寫進來(待日后有機會再補進來吧)。
各位園友同行,本絲也是學習中的菜鳥一枚,如果某些知識點我理解有誤,請大家指出!感謝!
