接上一篇《C#基礎之類型和成員基礎以及常量、字段、屬性》html
C#中的方法分爲兩類,一種是屬於對象(類型的實例)的,稱之爲實例方法,另外一種是屬於類型的,稱之爲靜態方法(用static關鍵字定義)。你們都是作開發的,這兩個也沒啥好說的。編程
惟一的建議就是:你的靜態方法最好是線程安全的(這點是提及容易作起難啊……)。數組
構造器是一種特殊的方法,CLR中的構造器分爲兩種:一種是實例構造器;另外一種是類型構造器。和其餘方法不一樣,構造器不能被繼承,因此在構造器前應用virtual/new/override/sealed和abstract是沒有意義的,同時構造器也不能有返回值。 安全
實例構造器用來初始化類型的實例(也就是對象)的初始狀態。 網絡
對於引用類型,若是咱們沒有顯式定義實例構造器,C#編譯器默認會生成一個無參實例構造器,這個構造器什麼也不作,只是簡單調用一下父類的無參實例構造器。這裏應該意識到,若是咱們定義的類的基類沒有定義無參構造器,那麼咱們的派生類就必須顯式調用一個基類構造器。ide
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關鍵字(也能夠應用於類、結構和接口),能夠將一個方法定義到多個文件中。
一般有這麼一種場情:咱們每每利用代碼生成工具生成一些模板化的代碼,但又須要對某些細節進行定製,雖然能夠經過虛方法重寫來實現,但這樣作存在兩點問題:
這時候,就能夠利用分部方法來實現。讓代碼生成器生成一個分部類(注意這個類能夠是密封的),把實現細節抽象成一個方法定義。像下面這樣:
//工具生成部分 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) { /*提供實現細節的代碼*/ } }
咱們再來看看提供分部方法的實現代碼後,編譯器生成了什麼:
關於分部方法有幾點要小注意一下:
這兩個方法涉及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智能提示的良好支持(咱們能夠像使用對象實例方法同樣,點出擴展方法)。
定義一個擴展方法,有如下幾點要求:
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(); //擴展方法
關於擴展方法,還有如下幾點要注意:
擴展方法延伸閱讀:鶴沖天 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);
在定義方法參數時,還有幾點要小注意一下:
若是咱們要設計一個方法,來計算全部輸入數字的總和。按以往咱們會這麼實現(不要關注方法內部實現是否合理):
//計算任意個數字和 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);
可變數量的參數,有幾點要小注意一下:
默認狀況下,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一下吧~本絲又敲了兩天鍵盤,眼睛好累啊~
又一個週末,終於敲完了這篇讀書筆記性質的總結。給本身列的提綱中「操做符重載方法、轉換操做符方法」這一部分因爲本身未作過多瞭解,故未寫進來(待往後有機會再補進來吧)。
各位園友同行,本絲也是學習中的菜鳥一枚,若是某些知識點我理解有誤,請你們指出!感謝!