0. 文章目的
??本文面向有一定.NET C#基礎知識的學習者,介紹C#中的方法修飾符的含義和使用以及注意事項,
1. 閱讀基礎
??理解C#基本語法(如方法宣告)
??理解OOP基本概念(如多型)
2. 概念:什么是方法修飾符
??在C#中,一個方法通常按如下形式宣告
[訪問修飾符] [方法修飾符] [回傳型別] 方法名(引數串列)
??例如,一個方法的宣告如下:
public virtual async Task HelloAsync();
??其中的virtual與async就是方法修飾符,方法修飾符為編譯器指示方法的的特性,從而讓編譯器對方法進行特別處理,例如,這里的方法修飾符指示該方法是一個可被子類重寫的虛方法(virtual),并且是一個異步方法(async),
??本文重點介紹[方法修飾符],在C#中,有如下方法修飾符:
abstractvirtualoverridesealednewasyncstaticreadonlyexternpartialunsafe
??大多數方法修飾符之間存在互斥性,即一個修飾符使用后則無法使用另一個修飾符,不需要刻意記憶互斥關系,只需要理解各個修飾符的含義即可,
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編譯器選項
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/487790.html
標籤:C#
