類(Class)
最常見的一種引用類型
class YourClassName { }
class前面可以加上一些關鍵字,比如public、private、abstract、static、readonly
class后面是這個類的名稱,類型名稱后面也可以跟一些東西,比如泛型類、繼承的父類等
字段(Fields)
是Class或Struct的成員,它是一個變量
class Octopus { string name; public int Age = 10; }
readonly修飾符
- readonly修飾符防止字段在構造之后被改變
- readonly字段只能在聲明的時候被賦值,或在構造函數里被賦值
字段的初始化
- 字段可以可選的初始化
- 未初始化的字段有一個默認值
- 字段的初始化在構造函數之前運行
同時聲明多個字段
static readonly int legs = 8, eyes = 2;
方法
- 一個方法通常有一些語句組成,執行某個動作
- 參數
- 返回類型
- void
- ref/out
方法的簽名
類型類方法的簽名必須唯一
簽名:方法名、參數類型(含順序,但與參數名稱和返回類型無關)
Expression-bodied methods (C# 6)
int Foo (int x)
{
return x * 2;
}
int Foo (int x) => x * 2;
void Foo (int x) => Console.WriteLine (x);
第一個是一個常規的寫法,第二個就是第一個的Expression-bodied方法的形式。當然沒有返回類型的也可以這樣寫,這種寫法只適用於一些簡單的方法,單表達式的方法。
方法的重載(Overloading methods)
類型里的方法可以進行重載(允許多個同名的方法同時存在),只要這些方法的簽名不同就行
void Foo (int x) {...} void Foo (double x) {...} void Foo (int x, float y) {...} void Foo (float x, int y) {...}
在看一個特殊點的方法的例子
void Foo (int x) {...} float Foo (int x) {...} // 編譯時報錯,簽名相同,因為簽名不包含方法的返回類型 void Goo (int[] x) {...} void Goo (params int[] x) {...} // 編譯時錯誤,簽名相同,因為簽名不包含參數類型的修飾符(params去掉就是一樣的)
按值傳遞 和 按引用傳遞
參數是按值傳遞的還是按引用傳遞的,也是方法簽名的一部分
void Foo (int x) {...} void Foo (ref int x) {...} // 是可以的
因為參數的傳遞方式是不同的,一個是值類型,一個是引用類型,所以簽名不一致,是可以的,下面這樣就不行了:
void Foo (int x) {...} void Foo (ref int x) {...} // 是可以的 void Foo (out int x) {...} // 編譯時錯誤
后兩個方法的參數類型和方法名都是一樣的,而且都是按引用傳遞,所以后兩個的簽名是一樣的
本地方法(C# 7)
一個方法在另一個方法的里面,就叫本地方法(Local methods)
void WriteCubes() { Console.WriteLine (Cube (3)); Console.WriteLine (Cube (4)); Console.WriteLine (Cube (5)); int Cube (int value) => value * value * value; }
- 本地方法僅對封閉方法可見,Cube僅在WriteCubes方法內可見
- 本地方法可以出現在其他函數類型中,例如屬性訪問器、構造函數等
- 本地方法也可以是迭代器或異步方法
- static修飾符對於本地方法是無效的
構造函數
- 運行class或struct的初始化代碼
- 和方法差不多,方法名和類型一致,返回類型也和類型一致,但不寫了
看一個構造的例子:
public class Panda { string name; // 定義字段 public Panda (string n) // 定義構造函數 { name = n; // 初始化代碼 } } …… Panda p = new Panda ("Petey"); // 調用構造函數
構造函數允許以下修飾符
- 訪問修飾符: public internal private protected
- 非托管代碼修飾符: unsafe extern
從C#7開始,允許單語句的構造函數寫成expression-bodied成員的形式,上面的Panda類的構造函數可以寫成如下形式:
public Panda (string n) => name = n;
構造函數的重載
- class和struct可以重載構造函數
- 調用重載構造函數時使用this
看下例子:
public class Wine { public decimal Price; public int Year; public Wine (decimal price) { Price = price; } public Wine (decimal price, int year) : this (price) { Year = year; } }
- 當同一個類型下的構造函數A調用構造函數B的時候,B先執行
看下下面的例子:
public class Wine { public decimal Price; public int Year; public Wine (decimal price) { Price = price; Console.WriteLine("1"); } public Wine (decimal price, int year) : this (price) { Year = year; Console.WriteLine("2"); } }
調用構造函數var wine = new Wine(25m, 1988);
結果是先輸出1,再輸出2
- 可以把表達式傳遞給另一個構造函數,但表達式本身不能使用this引用,因為這時候對象還沒有被初始化,所以任何方法的調用都會失敗,但是可以使用static方法。
無參構造函數
- 對於class,如果你沒有定義任何構造函數,那么C#編譯器會自動生成一個無參的public構造函數
- 但是如果你定義了構造函數,那么這個無參的構造函數就不會被編譯器生成了
構造函數和字段的初始化順序
- 字段的初始化發生在構造函數執行之前
- 字段按照聲明的先后順序進行初始化
非public的構造函數
構造函數可以不是public的,看個例子:
public class Class1 { Class1() {} // 私有的構造函數 public static Class1 Create (...) { // 在這里執行自定義的邏輯來返回Class1的實例 ... } }
那么獲取類的實例則無法在通過new關鍵字獲取了,而是通過類內部的創建實例的靜態方法獲取:var instance = Class1.Create();
單例模式則可以這么寫
Deconstructors (C# 7)
C#7引入了deconstructor模式,作用基本和構造函數相反,它會把字段反賦給一堆變量
有一些要求,比如方法名必須是Deconstruct,有一個或多個out參數
class Rectangle { public readonly float Width, Height; public Rectangle (float width, float height) { Width = width; Height = height; } public void Deconstruct (out float width, out float height) { width = Width; height = Height; } }
調用析構函數,我們可以下面的方式
var rect = new Rectangle (3, 4); (float width, float height) = rect; // Deconstruction Console.WriteLine (width + " " + height); // 3 4
第二行就是調用析構函數,當然,它也可以這么寫,似乎更好理解一點
float width, height; rect.Deconstruct (out width, out height);
或者以下的方法都是可以的:
rect.Deconstruct (out var width, out var height); //或者這樣 (var width, var height) = rect; //或這樣 var (width, height) = rect; //或這樣 float width, height; (width, height) = rect;
析構函數,可以重載,也可以是擴展方法,擴展方法這里就不展開說了
對象初始化器(Object Initializers)
對象任何可訪問的字段/屬性在構造之后,可通過對象初始化器直接為其進行設定值
下面是一個Bunny類,包含一個無參的構造函數和一個有參的構造函數
public class Bunny { public string Name; public bool LikesCarrots; public bool LikesHumans; public Bunny () {} public Bunny (string n) { Name = n; } }
使用對象初始化器實例化Bunny對象
// 無參的構造函數可以省略掉小括號 Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false }; Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };
上面的這個對象初始化器的代碼,就相當於下面的這段代碼:
Bunny temp1 = new Bunny(); // temp1 是編譯器生成的名稱 temp1.Name = "Bo"; temp1.LikesCarrots = true; temp1.LikesHumans = false; Bunny b1 = temp1; Bunny temp2 = new Bunny ("Bo"); temp2.LikesCarrots = true; temp2.LikesHumans = false; Bunny b2 = temp2;
臨時變量是為了確保如果在初始化過程中發生了異常,那么不會以一個初始化了一半的對象而結束
對象初始化器與可選參數
如果不使用初始化器,上面例子中的構造函數也可以使用可選參數:
public Bunny (string name, bool likesCarrots = false, bool likesHumans = false) { Name = name; LikesCarrots = likesCarrots; LikesHumans = likesHumans; }
那么在調用的時候可以這么調用,name是必須的,后面兩個參數可傳可不傳:
Bunny b1 = new Bunny (name: "Bo", likesCarrots: true);
可選參數方式有個優點,可以做到讓Bunny類的字段/屬性只讀,也有個缺點,每個可選參數的值都被嵌入到了調用棧(calling site),C#會把構造函數的調用翻譯成下面的形式:
Bunny b1 = new Bunny ("Bo", true, false);
其實我們使用了兩個參數,最后一個值我們沒有用,但是它把這個值嵌入進去(默認值)
這個可選參數的缺點,有時候可能會引起一些問題,比如,我們在另一個程序集里實例化這個類,然后為這個Bunny再添加了一個參數,變成4個參數了,這個時候就可能有問題,因為如果另一個程序集沒有重新編譯,那么調用的就還是原來的三個參數的構造函數,然后一運行運行時就會報錯了;還有一種情況是,如果改變了其中一個可選參數的值,那么其他程序集的調用者調用的還是會使用原來舊的參數值,直到那些程序集被重新編譯。
this 引用
this引用指的是實例的本身
public class Panda { public Panda Mate; public void Marry (Panda partner) { Mate = partner; partner.Mate = this; } }
this引用可以讓你把字段與本地變量或參數區分開
public class Test { string name; public Test (string name) { this.name = name; } }
只有class/struct的非靜態成員才可以使用this
屬性(Properties)
從外部來看,屬性和字段很像,但從內部看,屬性含有邏輯,就像方法一樣
Stock msft = new Stock(); msft.CurrentPrice = 30; msft.CurrentPrice -= 3; Console.WriteLine (msft.CurrentPrice);
我們從外面看不出來CurrentPrice是屬性還是字段
屬性的聲明
屬性的聲明和字段的聲明很像,但是多了一個get、set塊
public class Stock { decimal currentPrice; // The private "backing" field public decimal CurrentPrice // The public property { get { return currentPrice; } set { currentPrice = value; } } }
屬性的get set
- get/set代表屬性的訪問器
- get訪問器會在屬性被讀取的時候運行,必須返回一個該屬性類型的值
- set訪問器會在屬性被賦值的時候運行,有一個隱式的該類型的參數value,通常你會把value賦值給一個私有字段
屬性與字段的區別
盡管屬性的訪問方式與字段的訪問方式相同,但不同之處在於,屬性賦予了實現者對獲取和賦值的完全控制權。這種控制允許實現者選擇任意所需的內部表示,不向屬性的使用者公開其內部實現細節。
只讀和計算的屬性
如果屬性只有get訪問器,那么它就是只讀的
如果屬性只有set訪問器,那么它就是只寫的(很少這樣用)
屬性通常擁有一個專用的“幕后”字段(backing field),這個幕后的字段用來存儲數據
decimal currentPrice, sharesOwned; public decimal Worth { get { return currentPrice * sharesOwned; } }
Expression-bodied 屬性
從C#6開始,你可以使用Expression-bodied形式來表示只讀屬性
public decimal Worth => currentPrice * sharesOwned;
C#7,允許set訪問器也可以使用這種形式
public decimal Worth { get => currentPrice * sharesOwned; set => sharesOwned = value / currentPrice; }
自動屬性
屬性最常見的一種實踐是:setter和getter只是對private field進行簡單的直接的讀寫。自動屬性聲明就告訴編譯器來提供這種實現
public class Stock { ... public decimal CurrentPrice { get; set; } }
編譯器會自動生成一個幕后的私有字段,其名稱不可引用
set訪問器也可以是private或protected
屬性初始化器
從C#6開始,你可以為自動屬性添加屬性初始化器
public decimal CurrentPrice { get; set; } = 123;
只讀的自動屬性也可以使用(只讀自動屬性也可以在構造函數里被賦值)
public int Maximum { get; } = 999;
get 和 set 的訪問性
get和set訪問器可以擁有不同的訪問級別,典型的用法:public get,internal/private set
public class Foo { private decimal x; public decimal X { get { return x; } private set { x = Math.Round (value, 2); } } }
注意,通常屬性的訪問級別更寬松一些,訪問器的訪問級別更嚴一些(上面例子中的屬性X的public和set的private)
CLR的屬性實現
C#的屬性訪問器內部會編譯成get_XXX 和 set_XXX
public decimal get_CurrentPrice {...} public void set_CurrentPrice (decimal value) {...}
簡單的非virtual屬性訪問器會被JIT編譯器進行內聯(inline)操作,這會消除訪問屬性與訪問字段之間的性能差異。內聯是一種優化技術,它會把方法調用換成直接使用方法體。
索引器
索引器提供了一種可以訪問封裝了列表值或字典值的class/struct的元素的一種自然的語法
string s = "hello"; Console.WriteLine (s[0]); // 'h' Console.WriteLine (s[3]); // 'l'
語法很像使用數組時用的語法,但是這里的索引參數是可以是任何類型的
索引器和屬性擁有同樣的修飾符
可以按照下列方式使用null條件操作符:
string s = null; Console.WriteLine (s?[0]);
實現索引器
需要定義一個this屬性,並通過中括號指定參數
class Sentence { string[] words = "The quick brown fox".Split(); public string this [int wordNum] // 索引器 { get { return words [wordNum]; } set { words [wordNum] = value; } } }
使用索引器
Sentence s = new Sentence(); Console.WriteLine (s[3]); // fox s[3] = "kangaroo"; Console.WriteLine (s[3]); // kangaroo
多個索引器
一個類型可以聲明多個索引器,它們的參數類型可以不同
一個索引器可以有多個參數
public string this [int arg1, string arg2] { get { ... } set { ... } }
只讀索引器
如果不寫set訪問器,那么這個索引器就是只讀的
在C#6以后,也可以使用Expression-bodied語法
public string this [int wordNum] => words [wordNum];
CLR的索引器實現
索引器在內部會編譯成get_Item和set_Item方法
public string get_Item (int wordNum) {...} public void set_Item (int wordNum, string value) {...}
常量
- 一個值不可以改變的靜態字段,在編譯時值就已經定下來了
- 任何使用常量的地方,編譯器都會把這個常量替換為它的值
- 常量的類型可以是內置的數值類型、bool、char、string或enum
- 使用const關鍵字聲明,聲明的同時必須使用具體的值來對其初始化
一個常量的例子:
public class Test { public const string Message = "Hello World"; }
常量與靜態只讀字段
常量比靜態只讀字段更嚴格:
體現在兩個方面,一個是可使用的類型,第二個是字段初始化的語義上
常量是在編譯時進行值得估算的
public static double Circumference (double radius) { return 2 * System.Math.PI * radius; }
編譯成下面的形式:
public static double Circumference (double radius) { return 6.2831853071795862 * radius; }
有一點需要注意,當值有可能改變,並且需要暴露給其它程序集的時候,靜態只讀字段是相對較好的選擇
public const decimal ProgramVersion = 2.3;
如果Y 程序集引用了X程序集並且使用了這個常量,那么在編譯的時候,2.3這個值就會固化在Y程序集里,這意味着,如果后來X重新編譯了,這個常量變成了2.4,如果Y不重新編譯的話,Y仍將使用2.3這個值,直到Y被重新編譯,它的值才會變成2.4.靜態只讀字段就會避免這個問題的發生。
常量不僅僅可以作為類型的字段,它還可以作為本地常量,在方法里聲明常量,例如:
static void Main() { const double twoPI = 2 * System.Math.PI; ... }
靜態構造函數
- 靜態構造函數,每個類型執行一次
- 非靜態構造函數,每個實例執行一次
- 一個類型只能定義一個靜態構造函數,並且必須無參、方法名與類型一致
class Test { static Test() { Console.WriteLine ("Type Initialized"); } }
- 在類型使用之前的一瞬間,編譯器會自動調用類型的靜態構造函數:
一個是實例化一個類型之前,一個是訪問類型的一個靜態成員之前
- 只允許使用unsafe 和 extern 修飾符
如果靜態構造函數跑出來未處理的異常,那么這個類型在該程序的剩余生命周期內將無法使用了
靜態構造函數和字段初始化順序
- 靜態字段的初始化器在靜態構造函數被調用之前的一瞬間運行
- 如果類型沒有靜態構造函數,那么靜態字段初始化器在類型被使用之前的一瞬間執行,或者更早,在運行時突發奇想的時候就執行了
- 靜態字段的初始化順序與它們的聲明順序一致
class Foo { public static int X = Y; // 0 public static int Y = 3; // 3 }
因為是按照聲明的順序初始化的,所以X的時候Y還沒有,所以是0。
class Program { static void Main() { Console.WriteLine (Foo.X); } // 3 } class Foo { public static Foo Instance = new Foo(); public static int X = 3; Foo() { Console.WriteLine (X); } // 0 }
在類Program里,輸出的是3,因為在使用Foo類之前這個類型就已經初始化完成,所以是3,而在Foo類里面,則按照什么的先后順序分別初始化靜態構造函數,然后是靜態字段,所以靜態構造函數輸出的是0,X還沒有初始化。
靜態類
類也可以是靜態的,但是有個要求,就是其成員也必須全是靜態的,不可以有子類。
例如:System.Console 、System.Math 等
終結器(Finalizers)
Finalizer是class專有的一種方法,在GC回收未引用對象的內存之前運行,其實就是對object的Finalize()方法重寫的一種語法
這個很少用,例子:
class Class1 { ~Class1() { ... } }
編譯生成的是這樣的
protected override void Finalize() { ... base.Finalize(); }
C#7可以這樣寫:
~Class1() => Console.WriteLine ("Finalizing");