0. 文章目的 本文面向有一定.NET C#基礎知識的學習者,介紹C#中的方法修飾符的含義和使用以及註意事項。 1. 閱讀基礎 理解C#基本語法(如方法聲明) 理解OOP基本概念(如多態) 2. 概念:什麼是方法修飾符 在C#中,一個方法通常按如下形式聲明 [訪問修飾符] [方法修飾符] [返回類型 ...
0. 文章目的
本文面向有一定.NET C#基礎知識的學習者,介紹C#中的方法修飾符的含義和使用以及註意事項。
1. 閱讀基礎
理解C#基本語法(如方法聲明)
理解OOP基本概念(如多態)
2. 概念:什麼是方法修飾符
在C#中,一個方法通常按如下形式聲明
[訪問修飾符] [方法修飾符] [返回類型] 方法名(參數列表)
例如,一個方法的聲明如下:
public virtual async Task HelloAsync();
其中的virtual
與async
就是方法修飾符,方法修飾符為編譯器指示方法的的特性,從而讓編譯器對方法進行特別處理。例如,這裡的方法修飾符指示該方法是一個可被子類重寫的虛方法(virtual)
,並且是一個非同步方法(async
)。
本文重點介紹[方法修飾符],在C#中,有如下方法修飾符:
abstract
virtual
override
sealed
new
async
static
readonly
extern
partial
unsafe
大多數方法修飾符之間存在互斥性,即一個修飾符使用後則無法使用另一個修飾符。不需要刻意記憶互斥關係,只需要理解各個修飾符的含義即可。
3. 從示例出發:如何使用方法修飾符
按照不同的歸類方式,可以把上述方法修飾符歸為幾組,這裡我們按照修飾符的性質進行分類:
實現多態 | 用於封裝 | 改變性質 | 特性指示 |
virtual | sealed | static | async |
override | new | readonly | |
abstract | extern | ||
partial | |||
unsafe |
下麵按以上組織逐一介紹各個修飾符
3.1.實現多態
3.1.1 virtual與override
(1)使用
virtual修飾符主要用於標記一個方法可被子類重寫,其主要使用方法如下所示:
class Base
{
public virtual void Hello()
{
Console.WriteLine("Hello, I am Base");
}
}
要重寫被virtual修飾的方法,需要在子類中聲明相同函數簽名的方法,並使用override修飾符:
class Dervied : Base
{
public override void Hello()
{
Console.WriteLine("Hello, I am Dervied");
}
}
下麵是調用示例:
Base b = new Base();
Dervied d = new Dervied();
b.Hello(); // Hello, I am Base
d.Hello(); // Hello, I am Dervied
b = d;
b.Hello(); // Hello, I am Dervied
上述代碼中第三次的輸出將會輸出"Hello, I am Dervied"。這是由於雖然變數b是一個Base類型的引用,但是其實際指向的是一個Dervied類型的對象,由於Dervied重寫了其基類Base的Hello方法,故通過變數b調用Hello方法時,根據多態性,此時實際調用的是Dervied中定義的Hello方法。
(2)特別說明
virtual與override用於修飾方法,但由於在C#中屬性的本質也是方法,因此也可以將其應用到修飾屬性上,實現屬性的多態性,如下:
class Base
{
public virtual int Value
{
get
{
return 0;
}
}
}
class Dervied : Base
{
public override int Value
{
get
{
return 1;
}
}
}
Base b = new Dervied();
Console.WriteLine(b.Value); // 輸出是1,因為此時實際調用的是Dervied中定義的Value屬性
3.1.2 abstract
(1)使用
abstract修飾符同樣用於標記一個方法可被子類重寫,但是其使用有嚴格的限制:
- abstract只能修飾抽象方法,並且抽象方法必須用abstract修飾
- 子類必須重寫被abstract修飾的方法
所謂抽象方法就是只有方法聲明而沒有方法體,且被abstract修飾的方法,這種方法只能定義在抽象類(abstract class)中,如下:
abstract class Base
{
public abstract void Hello();
}
Hello是一個抽象方法,只定義了方法簽名而沒有定義方法體,因此它的實際表現由繼承類決定,因此繼承類必須重寫父類中的抽象方法(除非繼承類依然為抽象類則可以不用重寫),重寫abstract方法和重寫virtual方法一致:
class Dervied : Base
{
public override void Hello()
{
// do some thing
}
}
(2)特別說明
顯然abstract修飾符的使用相當固定,雖然看起來C#完全可以將沒有方法體的方法預設為抽象方法從而避免使用abstract修飾符,之所以保留此設計可能是為了增強語義。
基本上所有使用abstract修飾方法的地方都可以使用virtual替代,abstract最主要的特性其實在於其會強制要求子類重寫被其修飾的方法,是一種編碼規範的協定。從某種意義上來說,abstract其實更傾向於用來模擬介面方法聲明。如下:
abstract class IFlayable
{
public abstract void Fly();
}
上述聲明其實就類似於下麵的介面聲明:
interface IFlyable
{
void Fly();
}
3.2.用於封裝
3.2.1 sealed
(1)使用
用於修飾方法時,sealed的含義是:被修飾的方法無法被子類重寫。由於只有父類中被聲明為virtual或者abstract的方法才可被其子類重寫,而顯然你不能將sealed修飾符配合virtual或abstract使用,因此,只有在其子類中被重寫的方法才有繼續被子類的子類重寫的可能。如下述代碼:
class A
{
public virtual void Hello()
{
Console.WriteLine("I am A");
}
}
class B : A
{
public override void Hello()
{
Console.WriteLine("I am B");
}
}
class C : B
{
// 此Hello方法將再次重寫基類的Hello方法
public override void Hello()
{
Console.WriteLine("I am C");
}
}
基類A中定義了virtual方法Hello,儘管類型B已經重寫了基類A中的Hello方法,而類型C繼承自類型B,但是你依然可以在類型C中再次重寫與基類A中定義的Hello方法。有時候基於一些封裝的需求,你可能希望避免上述情況發生,就需要用到sealed修飾符。如下:
class A
{
public virtual void Hello()
{
Console.WriteLine("I am A");
}
}
class B : A
{
public sealed override void Hello()
{
Console.WriteLine("I am B");
}
}
class C : B
{
// 此方法無法通過編譯,因為類型B中將Hello方法設為了sealed方法
public override void Hello()
{
Console.WriteLine("I am C");
}
}
簡而言之,sealed修飾符就是讓“可被重寫”的方法在子類中回歸到“不可重寫”的狀態
3.2.2 new
(1)使用
有時候可能會在子類中定義與基類方法簽名相同的方法,但基類沒有將該方法用virtual或abstract標記為“可重寫”,如以下:
class Base
{
public void Hello()
{
Console.WriteLine("Hello, I am Base");
}
}
class Dervied : Base
{
public void Hello()
{
Console.WriteLine("Hello, I am Dervied");
}
}
上述代碼可以通過編譯,Dervied中定義的Hello方法將會隱藏Base中定義的Hello方法,但是會收到編譯器的警告。這個時候就可以使用new關鍵字來強制讓子類覆蓋基類中簽名相同的方法,並避免編譯器警告。
class Base
{
public void Hello()
{
Console.WriteLine("Hello, I am Base");
}
}
class Dervied : Base
{
// 通過new修飾後,將不會有編譯器警告
public new void Hello()
{
Console.WriteLine("Hello, I am Dervied");
}
}
然而,這一顯式覆蓋行為同樣不會提供多態性,這意味著會有下麵的代碼執行結果:
Base b = new Base();
Dervied d = new Dervied();
b.Hello(); // Hello, I am Base
d.Hello(); // Hello, I am Dervied
b = d;
b.Hello(); // Hello, I am Base
第三次調用Hello時,雖然此時b已經指向了一個Dervied對象,然而在調用Hello方法時,調用的依然是Base中定義的Hello方法。換言之,Hello方法不具有多態性。實際上,new修飾符的含義是:被修飾的方法與基類的相似簽名的成員無任何關係。
(2)特別說明
除非有不得已而為之的理由,否則當子類方法簽名與父類衝突時,應當優先考慮修改方法名避免衝突,而不是使用new修飾符。
除了用於方法外,new亦可以用於屬性、欄位甚至事件:
class Base
{
public event Action? Action;
public int Field;
public int Property { get; set; }
}
class Dervied : Base
{
public new event Action? Action;
public new int Field;
public new int Property { get; set; }
}
請記住,new修飾符的實際含義是:被修飾的方法與基類中相似簽名的成員無任何關係。
3.3.改變性質
3.3.1 static
(1) 使用
預設情況下,在類中聲明的方法是實例方法,其調用需要通過類的實例進行調用,如下:
class Printer
{
public void Hello()
{
Console.WriteLine("Hello");
}
}
Printer p = new Printer();
p.Hello(); // 通過Base類的實例來調用Hello方法
大多數情況下,這一行為是合理的,因為類的方法往往涉及到對其實例欄位的訪問和修改。然而有時候一個方法可能不需要訪問任何實例欄位,例如,定義一個有Add方法進行加法運算的Math類:
class Math
{
public int Add(int a, int b)
{
return a + b;
}
}
要使用這個Math類的Add方法,需要按下述步驟調用:
Math math = new Math();
int n = math.Add(1, 2);
然而這一過程稍顯繁瑣,Add方法本身不依賴Math類中的任何實例欄位屬性,完全可以獨立運行,並且創建對象需要消耗額外的空間和時間。為了避免這一無意義的行為,可以考慮繞過實例化來直接調用Add方法。此時便可以使用static修飾符。static修飾符指示一個方法不會訪問類中的實例欄位,並且不需要實例化可直接使用類自身來調用,如下:
class Math
{
public static int Add(int a, int b)
{
return a + b;
}
}
要使用這個Math類的Add方法,只需要像下麵這樣調用:
int n = Math.Add(1, 2);
(2)特別說明
可以將static方法視為由類管理的函數。
同樣的,static修飾符也可以用於修飾欄位、屬性與事件:
class Foo
{
public static Action? StaticEvent;
public static int StaticField;
public static int StaticProperty { get; set; }
}
// 直接通過類名調用
Foo.StaticEvent;
Foo.StaticField;
Foo.StaticProperty;
對於靜態類(static class),所有成員都需要使用static修飾。
3.4. 標記特性
3.4.1 async
(1) 使用
async修飾符用於指示一個方法為非同步方法,需要配合方法體內的await關鍵字使用。關於非同步方法的概念這裡礙於篇幅不進行闡述,僅在此說明其使用。示例如下:
class Printer
{
public async void HelloAsync()
{
await Task.Delay(1000);
Console.WriteLine("Hello");
}
}
需要註意的是async的作用僅僅是標記方法為非同步方法,並非指示該方法要非同步調用,也就是說,在下麵的實例中,儘管Wait1被aysnc標記,但Wait1和Wait2的實際表現是一樣,都是同步方法,都會將調用線程阻塞1秒:
class Foo
{
public async void Wait()
{
Thread.Sleep(1000);
}
public void Wait()
{
Thread.Sleep(1000);
}
}
Foo foo = new Foo();
foo.Hello1(); // 阻塞1秒
foo.Hello2(); // 阻塞1秒
要真正發揮async的作用,需要配合TAP(Task-based Asynchronous Pattern)非同步設計模式
(2)特別說明
作為編碼規範,被async修飾的方法名應當以Async結尾,如:
async void HelloAsync();
3.4.2 extern
(1)使用
extern指示方法由外部實現,通常配合P/Invoke使用來調用由其他語言寫成的API。例如,下述方法中聲明表示該方法實際需要調用C語言編寫的math庫中的Add方法:
[DllImport("math.dll")]
private static extern int Add(int a, int b);
註意上述方法除了被extern修飾外,還需要被static修飾。這是可以理解的,因為從外部庫中調用的方法顯然不會是用於本類的實例方法(從設計邏輯與實現邏輯上都說不通)。
(2)註意事項
被調用的由其他語言寫成的API需要遵循一定的編碼規範,因此並非所有函數都可以像上述那樣被簡單調用。考慮到篇幅和文章重點,這裡不做贅述。
3.4.3 partial
(1)使用
在開始介紹partial方法前,需要先介紹分部類(partial class),因為這partial方法需要配合分部類使用。簡單來說,partial class就是指一個類可以在多個地方定義類成員,編譯時由編譯器進行合併。例如有如下分部類聲明:
partial class Printer
{
public void Hello()
{
Console.WriteLine("Hello");
}
}
partial class Printer
{
public void World()
{
Console.WriteLine("World");
}
}
在編譯時,編譯器將各個部分相同的類型實現進行合併,因此實際效果等同於以下聲明:
class Printer
{
public void Hello()
{
Console.WriteLine("Hello");
}
public void World()
{
Console.WriteLine("World");
}
}
儘管看起來分部類似乎讓事情變得麻煩,但實際上分部類有許多實際作用,例如用於合併用戶代碼與生成代碼,一個應用場景即合併WPF或WinForm視窗設計器自動生成的代碼與用戶編寫的代碼。此外,分部類也可方便於類的協作開發。有關分佈類的詳細信息,請參考官方文檔:分部類和方法
上面是分部類基本使用知識。下麵來介紹和分部類配合使用的由partial修飾的方法,這稱之為分部方法。例如對於以下聲明:
class Printer
{
public void Hello()
{
Console.WriteLine("Hello");
}
}
可以使用分部類和分部方法修改為以下聲明形式:
partial class Printer
{
public partial void Hello();
}
partial class Printer
{
public partial void Hello()
{
Console.WriteLine("Hello");
}
}
兩者在效果和實質上都是相同的。你可能會好奇這一行為有何意義,畢竟這似乎沒有帶來什麼便捷,還會多書寫一次方法聲明。實際上,有時候方法的實現可能是有代碼生成器生成,這時候就需要分部方法來幫助合併由用戶定義的方法聲明與由代碼生成器完成的方法實現。
(2)註意事項
如果分部方法滿足以下條件,則可以不用提供代碼實現。
- 沒有任何訪問修飾符(包括預設的private)
- 返回值為void
- 沒有任何輸出參數(即被out修飾的參數)
- 沒有以下任何修飾符:
virtual
、override
、sealed
、new
或extern
實際上,在編譯時編譯器會刪除滿足上述條件且沒有實現的分部方法的調用。
3.4.4 readonly
(1)使用
不同於其他修飾符,readonly只能用於修飾結構體的方法聲明,其含義為:方法體不會修改結構體的實例欄位。示例聲明如下:
struct Point
{
public float X;
public float Y;
public readonly void Print()
{
Console.WriteLine(X + "," + Y);
}
}
上述的readonly修飾符指示Print方法不會修改實例欄位X和Y,方法中只存在訪問行為。如果嘗試在readonly方法中修改實例欄位,將導致編譯錯誤。
(2)特別說明
實際上配合ref,readonly也可以用於修飾類方法,然而此時的readonly有完全不同的語義,例如:
class Grid
{
private Point _origin = new Point();
public ref readonly Point GetOrigin()
{
return ref _origin;
}
}
上述聲明中的readonly實際的含義是:返回的ref引用為不可修改的只讀引用。也就是說下麵的代碼無法通過編譯:
Grid grid = new Grid();
ref Point p = ref grid.GetPoint(); // 錯誤
p.X = 1;
可通過為ref變數添加readonly聲明來保證不會修改只讀引用的返回值:
ref readonly Point p = ref grid.GetPoint();
3.4.5 unsafe
(1)使用
unsafe實際就是指示方法可以運行不安全代碼,它是unsafe關鍵字的方法級聲明。一個簡單的unsafe方法如下:
class Math
{
public static unsafe void Increase(int* value)
{
*value += 1;
}
}
Math類的Increase方法接受一個int指針,並將指向的值+1。該方法涉及到指針操作,並且需要接受一個指針類型的參數,因此需要使用unsafe對方法進行標記。unsafe的方法調用和一般方法調用相似:
int n = 10;
unsafe
{
Math.Increase(&n);
}
Console.WriteLine(n); // 11
請註意unsafe不必是static方法,這裡只是為了方便調用將方法聲明為了static。此外,這裡需要使用unsafe塊並不是因為Increase是unsafe方法,而是因為需要使用取址符&
獲取變數n的地址傳遞給該方法。
(2)特別說明
編譯unsafe代碼需要指定AllowUnsafeBlocks
編譯器選項