.NET C#基礎(2):方法修飾符 - 給方法疊buff

来源:https://www.cnblogs.com/HiroMuraki/archive/2022/06/04/16342044.html
-Advertisement-
Play Games

0. 文章目的 本文面向有一定.NET C#基礎知識的學習者,介紹C#中的方法修飾符的含義和使用以及註意事項。 1. 閱讀基礎 理解C#基本語法(如方法聲明) 理解OOP基本概念(如多態) 2. 概念:什麼是方法修飾符 在C#中,一個方法通常按如下形式聲明 [訪問修飾符] [方法修飾符] [返回類型 ...


0. 文章目的

  本文面向有一定.NET C#基礎知識的學習者,介紹C#中的方法修飾符的含義和使用以及註意事項。

 

1. 閱讀基礎

  理解C#基本語法(如方法聲明)

  理解OOP基本概念(如多態)

 

2. 概念:什麼是方法修飾符

  在C#中,一個方法通常按如下形式聲明

[訪問修飾符] [方法修飾符] [返回類型] 方法名(參數列表)

  例如,一個方法的聲明如下:

public virtual async Task HelloAsync();

  其中的virtualasync就是方法修飾符,方法修飾符為編譯器指示方法的的特性,從而讓編譯器對方法進行特別處理。例如,這裡的方法修飾符指示該方法是一個可被子類重寫的虛方法(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修飾符同樣用於標記一個方法可被子類重寫,但是其使用有嚴格的限制:

  1. abstract只能修飾抽象方法,並且抽象方法必須用abstract修飾
  2. 子類必須重寫被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修飾的參數)
  • 沒有以下任何修飾符:virtualoverridesealednewextern

  實際上,在編譯時編譯器會刪除滿足上述條件且沒有實現的分部方法的調用。

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編譯器選項


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2 Sentinel 限流熔斷降級 Sentinel 可以簡單的分為 Sentinel 核心庫和 Dashboard。核心庫不依賴 Dashboard,但是結合 Dashboard 可以取得最好的效果。我們先來學習Sentinel 核心庫的使用,後面再學習Dashboard使用。 在我們項目中,用戶 ...
  • 題目描述 密碼要求: 1.長度超過8位 2.包括大小寫字母.數字.其它符號,以上四種至少三種 3.不能有長度大於2的包含公共元素的子串重覆 (註:其他符號不含空格或換行) 數據範圍:輸入的字元串長度滿足 1≤n≤100 輸入描述 一組字元串 輸入描述 如果符合要求輸出:OK,否則輸出NG 代碼和解題 ...
  • 前言 以下僅做相關知識的簡述,更深入的瞭解和學習,請自行查閱資料或留言。 一、Python簡介 Python請查看官網自行瞭解。 Python是一種編程語言,可以讓您更快地工作,並更有效地集成您的系統。 Python is a programming language that lets you w ...
  • Java 8是Java的一個重大版本,是目前企業中使用最廣泛的一個版本。 它支持函數式編程,新的Stream API 、新的日期 API等一系列新特性。 掌握Java8的新特性已經是java程式員的標配,掌握了它,就可以看懂公司里的代碼、高效率地處理大量集合數據以及消滅“嵌套地獄”等等。 ...
  • 0. 文章目的: 介紹變體的概念,並介紹其對C#的意義 1. 閱讀基礎 瞭解C#進階語言功能的使用(尤其是泛型、委托、介面) 2. 從示例入手,理解變體 變體這一概念用於描述存在繼承關係的類型間的轉化,這一概念並非只適用於C#,在許多其他的OOP語言中也都有變體概念。變體一共有三種:協變、逆變與不變 ...
  • 0. 文章目的 本文面向有一定.NET C#基礎知識的學習者,介紹C#中結構體定義、使用以及特點。 1. 閱讀基礎 瞭解C#基本語法 瞭解.NET中的棧與托管堆 2. 值類型 2.1 .NET的兩大類型 在.NET中,所有類型都是object類型的子類,而在object繁多的子類中,又可以將它們歸結 ...
  • 最近通過WPF開發項目,為了對WPF知識點進行總結,所以利用業餘時間,開發一個學生信息管理系統【Student Information Management System】。前四篇文章進行了框架搭建和模塊劃分,後臺WebApi介面編寫,以及課程管理模塊,班級管理模塊,學生管理模塊的開發,本文在前四篇... ...
  • 0. 文章目的 本文面向有一定.NET C#基礎知識的學習者,介紹.NET中事件的相關概念、基本知識及其使用方法 1. 閱讀基礎 理解C#基本語法(方法的聲明、方法的調用、類的定義) 2. 從委托說起,到底什麼是事件 2.1 方法與委托 (1)從一個案例開始說起 在討論本節主題之前,我們先來看一個實 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...