动态

详情 返回 返回

C# 中的怎麼做 - 动态 详情

怎麼獲得命令行參數

傳遞給可執行文件的命令行參數可以在頂級語句中訪問,也可以通過 Main 函數的可選參數來獲取。這些參數以字符串數組的形式提供。數組中的每個元素代表一個參數。參數之間的空格會被去除。例如,考慮以下對一個虛構可執行文件的命令行調用:

命令行輸入 傳遞給主程序的字符串數組
executable.exe a b c "a", "b", "c"
executable.exe 1 2 1, 2
executable.exe "1 2" 3 "1, 2", "3"

注意:當您在 Visual Studio 中運行應用程序時,可以在“調試頁面”或“項目設計器”中指定命令行參數。

示例

此示例展示了傳遞給命令行應用程序的命令行參數。所顯示的輸出是上述表格中第一項的對應內容。

// “Length“ 屬性用於獲取數組元素的數量
Console . WriteLine ( $"參數數量 = {args . Length}" );

for ( int i = 0 ; i < args . Length ; i++ )
    {
    Console . WriteLine ( $"參數 [ {i} ] = [ {args [ i ]} ]" );
    }

/* 輸出:( 假設有 3 個命令行參數 ):
    參數數量 = 3
    參數 [ 0 ] = [ a ]
    參數 [ 1 ] = [ b ]
    參數 [ 2 ] = [ c ]
*/

探索麪向對象編程中的 class 與對象的使用方法

在本教程中,您將構建一個控制枱應用程序,並瞭解 C# 語言所包含的基本面向對象特性。

先決條件

  • 最新的 .NET SDK
  • Visual Studio Code 編輯器
  • C# 開發工具包

安裝説明

在 Windows 系統中,此 WinGet 配置文件用於安裝所有必需組件。如果您已經安裝了某些內容,那麼 WinGet 將跳過此步驟。

  1. 下載該文件並雙擊運行它。
  2. 閲讀許可協議,輸入 “y”,然後在提示您接受時選擇 “進入”。
  3. 如果您在任務欄中看到閃爍的用户賬户控制(UAC)提示,請允許安裝繼續進行。

在其他平台上,您需要分別安裝這些組件。

  1. 從 .NET SDK 下載頁面下載推薦的安裝程序,並雙擊運行它。該下載頁面會檢測您的平台,併為您推薦相應的最新安裝程序。
  2. 從 Visual Studio Code 主頁下載最新安裝程序,並雙擊運行它。該頁面也會檢測您的平台,鏈接對於您的系統應該是正確的。
  3. 在 C# DevKit 擴展頁面上點擊 “安裝” 按鈕。這將打開 Visual Studio Code,並詢問您是否要安裝或啓用該擴展。選擇 “安裝”。

創建您的應用程序

在終端窗口中,創建一個名為 “Classes” 的目錄。您將在該目錄中構建您的應用程序。切換到該目錄,並在控制枱窗口中輸入 “dotnet new console”。此命令將創建您的應用程序。打開 “Program.cs”。它應該如下所示:

// See https://aka.ms/new-console-template for more information
Console . WriteLine ( "Hello, World!" );

在本教程中,您將創建表示銀行賬户的新類型。通常,開發人員會將每個類定義在不同的文本文件中。這樣在程序規模擴大時就更易於管理了。在 “Classes” 目錄中創建一個名為 “BankAccount.cs” 的新文件。

此文件將包含銀行賬户的定義。面向對象編程通過創建類的形式來組織代碼。這些類包含代表特定實體的代碼。BankAccount 類代表銀行賬户。該代碼通過方法和屬性來實現特定的操作。在本教程中,銀行賬户支持以下行為:

  1. 它有一個 18 位字符串,用於唯一標識銀行賬户(生成方式同身份證號碼)。
  2. 它有一個字符串來存儲所有者的姓名。
  3. 可以獲取餘額。
  4. 它接受存款。
  5. 它接受取款。
  6. 初始餘額必須為正數。
  7. 取款不能導致餘額為負數。

定義銀行賬户類型

您可以先創建一個定義該行為的基本類。使用 “文件:新建” 命令創建一個新文件。將其命名為 BankAccount.cs。在您的 BankAccount.cs 文件中添加以下代碼:

public class LEI銀行賬户
    {
        public string 標識號碼 { get; }
        public string 所有人 { get; set; }
        public decimal 餘額 { get; }

        public void FF取款 ( decimal 金額 , DateTime 日期 , string 説明 )
            {

            }

        public void FF存款 ( decimal 金額 , DateTime 日期 , string 説明 )
            {

            }
    }

在繼續之前,讓我們先看看您所構建的內容。命名空間聲明提供了一種邏輯組織代碼的方式。本教程相對較小,因此您將把所有代碼放在一個命名空間中。

public class LEI銀行賬户 定義了您正在創建的 class 或類型。緊跟在類聲明後面的 { 和 } 內的所有內容定義了該類的狀態和行為。LEI銀行賬户 類有五個成員。前三個是屬性(Property)。屬性是數據元素,並且可以有代碼來強制執行驗證或其他規則。最後兩個是方法(Method)。方法是一段執行單一功能的代碼塊。閲讀每個成員的名稱應該能為您提供足夠的信息,讓您或其他開發人員瞭解該類的作用。

開設新賬户

要實現的第一個功能是開設銀行賬户。當客户開設賬户時,他們必須提供初始餘額以及該賬户所有者的相關信息。

創建一個 LEI銀行賬户 類型的新對象意味着定義一個構造函數來分配這些值。構造函數是與類同名的成員。它用於初始化該類類型的對象。向 LEI銀行賬户 類型添加以下構造函數。將以下代碼置於 FF取款 聲明的上方:

public LEI銀行賬户 ( string 用户 , decimal 初始金額 )
    {
        this . 所有人 = 用户;
        this . 餘額 = 初始金額;
    }

前面的代碼通過包含 this 限定符來標識正在構造的對象的屬性。該限定符通常是可選的,可以省略。您也可以這樣寫:

public LEI銀行賬户 ( string 用户 , decimal 初始金額 )
    {
        所有人 = 用户;
        餘額 = 初始金額;
    }

只有當局部變量或參數與字段或屬性同名時,才需要使用 this 限定符。在本文的其餘部分,除非必要,否則將省略 this 限定符。

當使用 new 創建對象時,會調用構造函數。將 Program.cs 中的 Console . WriteLine ( "Hello World!" ); 這一行替換為以下代碼(將 “羅金寶” 替換為您的名字):

var ZHH = new LEI銀行賬户 ( "羅金寶" , 10000 );
Console . WriteLine ( $"賬户 “{ZHH . 標識號碼}” 已為 {ZHH . 所有人} 創建,初始餘額為 {ZHH . 餘額}。" );

讓我們運行一下你目前所構建的內容。如果你使用的是 Visual Studio,請從 “調試” 菜單中選擇 “啓動(不進行調試)”。如果你使用的是命令行,請在創建項目所在的目錄中輸入 “dotnet run”。

您是否注意到賬號編號處是空白的?現在是時候解決這個問題了。賬號編號應在對象創建時進行分配。但不應由調用者來創建它。LEI銀行賬號 類的代碼應該知道如何分配新的賬號編號。一種簡單的方法是從一個 18 位數字開始,每次創建新賬號時對其進行遞增操作。最後,在對象創建時存儲當前的賬號編號。
在 “LEI銀行賬號” 類中添加一個成員聲明。在 “LEI銀行賬號” 類開頭的花括號 “{” 之後,插入以下代碼行:
private static string s_標識 = "123456789012345678";
“s_標識” 是一個數據成員。它是 private 的,這意味着只能通過銀行賬户類內部的代碼來訪問它。這是一種將公共成員(比如擁有賬户號碼)與私有成員(賬户號碼的生成方式)分離開來的方式。它也是 static 的,這意味着它被所有銀行賬户對象共享。非 static 變量的值對於每個銀行賬户對象的實例都是獨一無二的。s_標識 是一個 private 的 static 字段,因此按照 C# 命名約定,它帶有 “s_” 前綴。“s” 表示 static,“_” 表示 private 字段。在構造函數中添加以下代碼來分配賬户號碼。將它們放在 “this . 餘額 = 初始金額” 的那行之後:

string Z本地代碼 = "370303"; // 我老家的身份證前六位
DateTime RQ = DateTime . Now; // 現在的時間
string Nian = RQ . Year . ToString ( "0000" ); // 現在的年份
string Yue = RQ . Month . ToString ( "00" ); // 現在的月份(一位數補上前面的 0)
string Ri = RQ . Day . ToString ( "00" ); // 現在的日期(一位數補上前面的 0)
string Z分鐘數 = RQ . Subtract ( new DateTime ( RQ . Year , RQ . Month , RQ . Day , 8 , 30 , 0 ) ) . Minutes . ToString ( "000" ); // 假設每一個新賬户與前一個新賬户之間的創立時間在一分鐘以上,以此區別每個新賬户(自早上八點半銀行上班為起始,999 分鐘長達 16 小時,銀行下班了……)
string Z17 = Z本地代碼 + Nian + Yue + Ri + Z分鐘數; // 生成類似身份證號碼的每個賬户號碼前十七位
int Z總和 = 0; // 存儲前十七位的加權總和
string Z校驗碼 = ""; // 存儲以前十七位加權後計算出的校驗位(最後一位)

// 按照標準的身份證計算方式生成客户的標識號碼
for ( int i = 0 ; i < 17 ; i++ )
    {
        if ( i == 0 )
            {
                Z總和 += ( int ) Z17 [ i ] * 7;
            }
        else if ( i == 1 )
            {
                Z總和 += ( int ) Z17 [ i ] * 9;
            }
        else if ( i == 2 )
            {
                Z總和 += ( int ) Z17 [ i ] * 10;
            }
        else if ( i == 3 )
            {
                Z總和 += ( int ) Z17 [ i ] * 5;
            }
        else if ( i == 4 )
            {
                Z總和 += ( int ) Z17 [ i ] * 8;
            }
        else if ( i == 5 )
            {
                Z總和 += ( int ) Z17 [ i ] * 4;
            }
        else if ( i == 6 )
            {
                Z總和 += ( int ) Z17 [ i ] * 2;
            }
        else if ( i == 7 )
            {
                Z總和 += ( int ) Z17 [ i ];
            }
        else if ( i == 8 )
            {
                Z總和 += ( int ) Z17 [ i ] * 6;
            }
        else if ( i == 9 )
            {
                Z總和 += ( int ) Z17 [ i ] * 3;
            }
        else if ( i == 10 )
            {
                Z總和 += ( int ) Z17 [ i ] * 7;
            }
        else if ( i == 11 )
            {
                Z總和 += ( int ) Z17 [ i ] * 9;
            }
        else if ( i == 12 )
            {
                Z總和 += ( int ) Z17 [ i ] * 10;
            }
        else if ( i == 13 )
            {
                Z總和 += ( int ) Z17 [ i ] * 5;
            }
        else if ( i == 14 )
            {
                Z總和 += ( int ) Z17 [ i ] * 8;
            }
        else if ( i == 15 )
            {
                Z總和 += ( int ) Z17 [ i ] * 4;
            }
        else if ( i == 16 )
            {
                Z總和 += ( int ) Z17 [ i ] * 2;
            }

// 按照前十七位加權後的總和整除 11 的結果決定校驗碼
int Z11 = Z總和 % 11;
    switch ( Z11 )
        {
            case 0:
                Z校驗碼 = "1";
                break;
            case 1:
                Z校驗碼 = "0";
                break;
            case 2:
                Z校驗碼 = "X";
                break;
            case 3:
                Z校驗碼 = "9";
                break;
            case 4:
                Z校驗碼 = "8";
                break;
            case 5:
                Z校驗碼 = "7";
                break;
            case 6:
                Z校驗碼 = "6";
                break;
            case 7:
                Z校驗碼 = "5";
                break;
            case 8:
                Z校驗碼 = "4";
                break;
            case 9:
                Z校驗碼 = "3";
                break;
            case 10:
                Z校驗碼 = "2";
                break;
        }

    標識號碼 = Z17 + Z校驗碼; // 得到整個十八位標識號碼

創建存款和取款功能

您的銀行賬户類需要能夠進行存款和取款操作,才能正常運行。讓我們通過為賬户創建每筆交易的日記來實現存款和取款功能。與僅在每次交易時更新餘額相比,跟蹤每筆交易有幾個優勢。歷史記錄可用於審計所有交易並管理每日餘額。在需要時根據所有交易的歷史記錄計算餘額,可確保單個交易中的任何錯誤得到修正後,其結果會在下一次計算中正確反映在餘額中。

首先,讓我們創建一個新的類型來表示交易。這個交易是一個簡單的類型,沒有任何職責。它需要一些屬性。創建一個名為 “LEI交易.cs” 的新文件。將以下代碼添加到該文件中:

public class LEI交易
    {
        public decimal 金額 { get; }
        public DateTime 日期 { get; }
        public string 説明 { get; }

        public LEI交易 ( decimal 金額 , DateTime 日期 , string 説明 )
            {
                this . 金額 = 金額;
                this . 日期 = 日期;
                this . 説明 = 説明;
            }
    }

現在,讓我們在 LEI銀行賬號 類中添加一個 LEI交易 對象的 List < T >。在您的 LEI銀行賬號.cs 文件的構造函數之後添加以下聲明:
private List < LEI交易 > _JY全 = new ( );
現在,讓我們正確計算餘額。當前餘額可以通過將所有交易的值相加得出。按照當前代碼,您只能獲取賬户的初始餘額,所以您需要更新 “餘額” 屬性。將 LEI銀行賬户.cs 中的 “public decimal 餘額 { get; }” 這一行替換為以下代碼:

public decimal 餘額
    {
        get
            {
                decimal ye = 0;
                foreach ( LEI交易 jy in _JY全 )
                    {
                        ye += jy . 金額;
                    }
                return ye;
            }
    }

此示例展示了屬性的一個重要方面。現在當其他程序員請求值時,您會計算餘額。您的計算會遍歷所有交易,並將總和作為當前餘額提供。

接下來,實現 FF存款 和 FF取款 這兩個方法。這兩個方法將執行最後兩條規則:初始餘額必須為正數,任何取款操作都不能導致餘額為負數。

這些規則引入了異常的概念。表明方法無法成功完成其工作的標準方式是拋出異常。異常的類型以及與之相關的消息描述了錯誤。在此,如果存款金額不大於 0,FF存款 方法將拋出異常。如果取款金額不大於 0 或應用取款操作會導致餘額為負數,FF取款 方法將拋出異常。在 _jy全 列表的聲明之後添加以下代碼:

public void FF取款 ( decimal 金額 , DateTime 日期 , string 説明 )
    {
        if ( 金額 <= 0 )
            {
                throw new ArgumentOutOfRangeException ( nameof ( 金額 ) , "取款金額不能為零或負數。" );
            }
        if ( 餘額 - 金額 < 0 )
            {
                throw new InvalidOperationException ( "此次取款所需金額不足。" );
            }
        LEI交易 JY = new ( -金額 , 日期 , 説明 );
        _JY全 . Add ( JY );
    }

public void FF存款 ( decimal 金額 , DateTime 日期 , string 説明 )
    {
        if ( 金額 <= 0 )
            {
                throw new ArgumentOutOfRangeException ( nameof ( 金額 ) , "存款金額不能為零或負數。" );
            }
        LEI交易 JY = new ( 金額 , 日期 , 説明 );
        _JY全 . Add ( JY );
    }

throw 語句會拋出一個異常。當前代碼塊的執行會終止,並且控制權會轉移到調用棧中找到的第一個匹配的 catch 代碼塊。稍後您將添加一個 catch 代碼塊來測試這段代碼。

構造函數應該執行一次操作,以便添加一個初始交易,而不是直接更新餘額。由於您已經編寫了 “FF存款” 方法,所以從構造函數中調用它即可。完成後的構造函數應如下所示:

public LEI銀行賬户 ( string 用户 , decimal 初始金額 )
    {
        this . 所有人 = 用户;
        FF存款 ( 初始金額 , DateTime . Now , "初始金額" );
        ……

DateTime . Now 是一個返回當前日期和時間的屬性。通過在主方法中添加一些存款和取款操作來測試這段代碼,具體操作方式如下:在創建新銀行賬户的代碼之後,再添加一些存款和取款操作即可。

var ZHH = new LEI銀行賬户 ( "羅金寶" , 10000 );
Console . WriteLine ( $"賬户“{ZHH . 標識號碼}”已為 {ZHH . 所有人} 創建,初始餘額為 {ZHH . 餘額}。" );

ZHH . FF取款 ( 1000 , DateTime . Now , "買衣服" );
Console . WriteLine ( ZHH . 餘額 ); // 9000

ZHH . FF存款  ( 10000 , DateTime . Now , "工資" );
Console . WriteLine ( ZHH . 餘額 ); // 19000

接下來,測試一下是否能夠檢測到錯誤情況,即嘗試創建一個餘額為負數的賬户。在您剛剛添加的代碼之後添加以下代碼:

try
    {
        LEI銀行賬户 ZHH錯誤 = new ( "張嘎" , -10 );
    }
catch ( ArgumentOutOfRangeException yc )
    {
        Console . WriteLine ( "“創建賬户時出現負餘額錯誤”" );
        Console . WriteLine ( yc . Message ); // 存款金額不能為零或負數。 (Parameter '金額')
        return;
    }

您使用 “try-catch” 語句來標記可能拋出異常的一段代碼,並捕獲您所預期的那些錯誤。您也可以使用同樣的方法來測試會拋出異常的代碼是否會出現負餘額的情況。在您的 “Main” 方法中,在 “ZHH錯誤” 的聲明之前添加以下代碼:

try
    {
        ZHH . FF取款 ( 20000 , DateTime . Now , "清户" );
    }
catch ( InvalidOperationException yc )
    {
        Console . WriteLine ( "“嘗試超額支取時發生錯誤”" );
        Console . WriteLine ( yc . Message );
    }

挑戰 - 記錄所有交易

要完成本教程,您可以編寫 “FF獲取交易記錄” 方法,該方法會生成包含交易歷史的字符串。將此方法添加到 “LEI銀行賬户” 類型中:

public string FF獲取交易記錄 ( )
    {
        StringBuilder BG = new ( $"日期\t\t金額\t餘額\t説明{Environment . NewLine}" );
        decimal y = 0;

        foreach ( LEI交易 jy in _JY全 )
            {
                y += jy . 金額;
                BG . AppendLine ( $"{jy . 日期 . ToShortDateString ( )}  {jy . 日期 . ToShortTimeString ( )}\t{jy . 金額}\t{y}\t{jy . 説明}" );
            }
        return BG . ToString ( );
    }

該示例使用 StringBuilder 類來格式化一個字符串,該字符串中每筆交易都佔一行。在這些教程中,您之前已經見過字符串格式化代碼。還有一個新的字符是 \t 。它用於插入製表符以格式化輸出。

添加以下代碼於 “Main” 測試 ZHH 賬户的後面:
Console . WriteLine ( ZHH . FF獲取交易記錄 ( ) );

面向對象編程(C#)

C# 是一種面向對象的編程語言。面向對象編程的四個基本原則是:

  • 抽象化——將實體的相關屬性和相互關係表示為類,以此來定義系統的抽象表示。
  • 封裝——隱藏對象的內部狀態和功能,並僅通過一組公共函數提供訪問權限。
  • 繼承——基於現有抽象創建新抽象的能力。
  • 多態——實現繼承的屬性或方法在多個抽象中以不同的方式進行。

在前面的教程中,您已經見識了 class 的抽象化和封裝。LEI銀行賬户 為銀行賬户的概念提供了抽象。您可以修改其實現,而不會影響使用 LEI銀行賬户 的任何代碼。LEI銀行賬户 和 LEI交易 都對描述這些概念所需的代碼組件進行了封裝。

在本教程中,您將對上述應用程序進行擴展,以利用繼承和多態性來添加新功能。您還將為 LEI銀行賬户 添加功能,充分利用在上一個教程中所學的抽象和封裝技術。

創建不同類型的銀行賬户

在構建了這個程序之後,就會收到添加功能的請求。在只有一個銀行賬户類型的情況下,它運行得非常好。隨着時間的推移,需求會發生變化,於是就出現了需要添加相關賬户類型的情況:

  • 一種每月末會計算利息的收益賬户。
  • 一種可以出現負餘額的信用額度,但當有餘額時,每月都會產生利息費用。
  • 一種預付費禮品卡賬户,初始存入一筆款項,只能一次性結清。每月開始時可以一次性充值一次。

所有這些不同的賬户與之前教程中定義的 LEI銀行賬户 類似。您可以複製那段代碼,重命名類,並進行修改。這種方法短期內可行,但隨着時間的推移會變得更加繁瑣。任何更改都會在所有受影響的類中被複制。

相反,您可以創建新的銀行賬户類型,這些類型會繼承前一教程中創建的 “LEI銀行賬户” 類中的方法和數據。這些新的類可以對 “LEI銀行賬户” 類進行擴展,以添加每個類型所需的具體行為:

public class LEI付息 : LEI銀行賬户
    {
        public LEI付息 ( string 用户 , decimal 初始金額 ) : base ( 用户 , 初始金額 )
            {
            }
    }

public class LEI信用 ( string 用户 , decimal 初始金額 ) : LEI銀行賬户 ( 用户 , 初始金額 )
    {

    }

public class LEI禮品 ( string 用户 , decimal 初始金額 ) : LEI銀行賬户 ( 用户 , 初始金額 )
    {

    }

這些類各自都從其共同的基類(即 “LEI銀行賬户” 類)繼承了共有的行為。在每個派生類中編寫新的和不同的功能實現。這些派生類已經具備了 “LEI銀行賬户” 類中定義的所有行為。

將每個新類創建在不同的源文件中是一個很好的做法。在 Visual Studio 中,您可以右鍵點擊項目,然後選擇 “添加類” 來在新文件中添加新類。在 Visual Studio Code 中,選擇 “文件” 然後 “新建” 來創建一個新的源文件。在任何工具中,給文件命名時要與類名保持一致:LEI付息.cs、LEI信用.cs 和 LEI禮品.cs。

當您沒有按照前面的示例創建類時,您會發現您的所有派生類都無法編譯,因為它們都缺少構造函數。構造函數負責對對象進行初始化。派生類的構造函數必須對派生類進行初始化,並提供有關如何初始化包含在派生類中的基類對象的説明。正確的初始化通常無需任何額外代碼即可完成。LEI銀行賬户 類聲明瞭一個具有以下簽名的 public 構造函數:
public LEI銀行賬户 ( string 用户 , decimal 初始金額 )
當您自行定義構造函數時,編譯器不會自動生成默認構造函數。也就是説,每個派生類都必須顯式調用這個構造函數。您需要聲明一個構造函數,以便能夠向基類構造函數傳遞參數。上面的派生類展示了構造函數(兩種不同的聲明方式)。

public class LEI付息 : LEI銀行賬户
    {
        public LEI付息 ( string 用户 , decimal 初始金額 ) : base ( 用户 , 初始金額 )
            {

            }
    }

public class LEI信用 ( string 用户 , decimal 初始金額 ) : LEI銀行賬户 ( 用户 , 初始金額 )
    {

    }

此新構造函數的參數與基類構造函數的參數類型和名稱相匹配。您使用 : base ( ) 語法來表示對基類構造函數的調用。有些類定義了多個構造函數,此語法使您能夠選擇要調用的基類構造函數。更新構造函數後,您可以為每個派生類開發代碼。新類的要求可以表述如下:

  • 一個付息賬户:

    • 將在月末餘額基礎上獲得 2% 的信用額度。
  • 信用賬户:

    • 可以有負餘額,但絕對值不能超過信用額度。
    • 如果月末餘額不為 0,則每月會產生利息費用。
    • 每次取款超過信用額度時會產生費用。
  • 禮品卡賬户:

    • 每月最後一天可以充值一次,充值金額需指定。

可以看出這三種賬户類型在每個月末都有一個操作。然而,每種賬户類型執行的任務不同。您使用多態性來實現此代碼。在 LEI銀行賬户 類中創建一個單一的 virtual 方法:
public virtual void FF月末交易 ( ) { }
前面的代碼展示瞭如何使用 virtual 關鍵字在基類中聲明一個方法,派生類可以為其提供不同的實現。虛擬方法是指派生類可以選擇重新實現的方法。派生類使用 override 關鍵字來定義新的實現。通常,這被稱為 “重寫基類的實現”。virtual 關鍵字指明派生類可以重寫行為。您還可以聲明 virtual 方法,派生類必須重寫其行為。基類不為 virtual 方法提供實現。接下來,您需要為新創建的兩個類定義實現。從 LEI付息 開始:

public override void FF月末交易 ( )
    {
        decimal LX = 餘額 * 0.02m;
        FF存款 ( LX , DateTime . Now , "月息" );
    }

在 “LEI信用” 中添加以下代碼。該代碼將餘額取反以計算從賬户中扣除的正利息費用:

public override void FF月末交易 ( )
    {
        {
            if ( 餘額 > -最小余額 ) // 當餘額大於最小余額,支付或者收取 2% 利息
                {
                    decimal lixi = 餘額 * 0.02m;
                    if ( lixi >= 0 )
                        { FF存款 ( lixi , DateTime . Now , "支付月息" ); }
                    else
                        { FF取款 ( -lixi , DateTime . Now , "收取月息" ); }
                }
            else
                {
                    decimal lixi = -餘額 * 0.07m; // 當餘額小於最小余額,支付 7% 利息
                    FF取款 ( lixi , DateTime . Now , "收取月息" );
                }
        }
    }

“LEI禮品” 類需要進行兩項更改才能實現其月末功能。首先,修改構造函數以包含每月可選添加的金額:

public class LEI禮品 : LEI銀行賬户
    {
        private readonly decimal _yc = 0m;

        public LEI禮品 ( string 用户 , decimal 初始金額 , decimal YC = 0m) : base ( 用户 , 初始金額 )
            {
                _yc = YC;
            }
    }

構造函數為 YC(月存)值提供了一個默認值,這樣調用者就可以省略每月存款為 0 的情況。接下來,重寫 FF月末交易 方法,如果在構造函數中將 YC 設置為非零值,則添加每月存款。

該重寫操作會應用在構造函數中設置的每月存款金額。在 Main 方法中添加以下代碼以測試針對 LEI禮品 賬户和 LEI付息 的這些更改:

LEI禮品 LW = new ( "生日快樂" , 100 , 20 );
LW . FF取款 ( 20 , new DateTime ( 2000 , 3 , 31 ) , "喝咖啡" );
LW . FF取款 ( 50 , new DateTime ( 2000 , 3 , 31 ) , "買雜物" );
LW . FF月末交易 ( );
LW . FF存款 ( 300 , new DateTime ( 2000 , 4 , 1 ) , "增加一些金額" );
Console . WriteLine ( LW . FF獲取交易記錄 ( ) );

LEI付息 CK = new ( "個人存款" , 10000 );
CK . FF存款 ( 2000 , new DateTime ( 2025 , 9 , 1 ) , "工資" );
CK . FF存款 ( 750 , new DateTime ( 2025 , 9 , 10 ) , "過節費" );
CK . FF取款 ( 1500 , new DateTime ( 2025 , 9 , 15 ) , "生活費" );
CK . FF月末交易 ( );
Console . WriteLine ( CK . FF獲取交易記錄 ( ) );

驗證結果。現在,為 “LEI信用” 添加一組類似的測試代碼:

LEI信用 XY = new ( "我的信用卡" , -3500 );
XY . FF取款 ( 20 , new DateTime ( 2025 , 9 , 1 ) , "買飯" );
XY . FF存款 ( 4800 , new DateTime ( 2025 , 9 , 15 ) , "預存" );
XY . FF取款 ( 2800 , new DateTime ( 2025 , 10 , 1 ) , "旅遊" );
XY . FF存款 ( 180 , new DateTime ( 2025 , 10 , 15 ) , "還款" );
XY . FF月末交易 ( );
Console . WriteLine ( XY . FF獲取交易記錄 ( ) );

當您添加上述代碼並運行程序後,您將會可能看到類似以下的錯誤信息(例如你的初始金額是 0;或者餘額不足):
Unhandled exception。System.ArgumentOutOfRangeException:存款金額不能為零或負數。 (Parameter '金額')
at OOProgramming.LEI銀行賬户.FF存款(Decimal 金額, DateTime 日期, String 説明) in LEI銀行賬户.cs:line 42
at OOProgramming.LEI銀行賬户..ctor(String 名稱, Decimal 初始金額) in LEI銀行賬户.cs:line 31
at OOProgramming.LEI信用..ctor(String 名稱, Decimal 初始金額) in LEI信用.cs:line 9
at OOProgramming.Program.Main(String[] args) in Program.cs:line 29

注意:實際輸出包含了項目所在文件夾的完整路徑。由於篇幅限制,文件夾名稱未列出。此外,具體行號可能會因您的代碼格式而略有不同。

這段代碼之所以會失敗,是因為 “LEI銀行賬户” 類假定初始餘額必須大於 0。該 “LEI銀行賬户” 類中還包含另一個假設,即餘額不能為負。相反,任何超出賬户餘額限制的取款操作都會被拒絕。這兩個假設都需要改變。信用額度賬户的初始餘額為 0,並且通常會有負餘額。此外,如果客户借入過多資金,就會產生費用。交易會被接受,只是成本會更高。第一條規則可以通過在 “LEI銀行賬户” 構造函數中添加一個可選參數來實現,該參數指定最低餘額。默認值為 0。第二條規則需要一種機制,使派生類能夠修改默認算法。從某種意義上説,基類 “詢問” 派生類型在出現透支情況時應該採取什麼措施。默認行為是通過拋出異常來拒絕交易。

首先,我們添加一個包含可選的 “最小余額” 參數的第二個構造函數。這個新構造函數會執行現有構造函數所完成的所有操作,並且還會設置 “最小余額” 屬性。您可以複製現有構造函數的主體內容,但這意味着將來需要在兩個地方進行修改。相反,您可以使用構造函數鏈來讓一個構造函數調用另一個構造函數。以下代碼展示了這兩個構造函數以及新增的字段:

public LEI銀行賬户 ( string 用户 , decimal 初始金額 ) : this ( 用户 , 初始金額 , 0) { }

private readonly decimal _最小余額;

public LEI銀行賬户 ( string 用户 , decimal 初始金額 , decimal 最小余額 )
    {
        this . 所有人 = 用户;
        標識號碼 = FF卡號 ( );
        _最小余額 = 最小余額;
        if ( 初始金額 > 0 )
            FF存款 ( 初始金額 , DateTime . Now , "初始金額" );
    }

上述代碼展示了兩種新的技術。首先,將 “_最小金額” 字段標記為 “readonly”。這意味着在對象創建後,其值無法再被更改。一旦創建了銀行賬户對象,其 “_最小金額” 值就不能再改變。其次,接受兩個參數的構造函數使用 “this ( 用户 , 初始金額 , 0 ) {}” 作為其實現方式。“: this ( )” 表達式調用了另一個具有三個參數的構造函數。這種技術允許您為初始化對象提供單一的實現,儘管客户端代碼可以選擇多種構造函數中的任何一個。

此實現僅在初始餘額大於 0 的情況下才會調用 “FF存款” 函數。這樣就保持了存款必須為正數的規則,同時又允許信用賬户以 0 的餘額開啓。

既然銀行賬户類已經有一個用於表示最低餘額的只讀字段,那麼最後的改動就是在 “FF取款” 方法中將硬編碼的 0 更改為 “最小余額”:

if ( 餘額 - 金額 < _最小余額 ) // 這裏早先是小於 0
    {
        throw new InvalidOperationException ( "此次取款所需金額不足。" );
    }

在擴展了 LEI銀行賬户 類之後,您可以修改 LEI信用 構造函數以調用新的基類構造函數,如下所示的代碼:

public class LEI信用 ( string 用户 , decimal 初始金額 , decimal 最小余額 ) : LEI銀行賬户 ( 用户 , 初始金額 , -最小余額 )

請注意,LEI信用 構造函數改變了 最小余額 參數的符號,使其與基類的 最小余額 參數的含義相匹配。

不同的透支規則

最後一個要添加的功能使信用額度賬户在超過信用額度時收取費用,而不是拒絕交易。

一種技術是定義一個虛函數,在其中實現所需的行為。LEI銀行賬户 類將 FF取款 方法重構為兩個方法。新方法在取款使餘額低於最低限額時執行指定的操作。修改 FF取款 方法為以下代碼:

public void FF取款 ( decimal 金額 , DateTime 日期 , string 説明 )
    {
        if ( 金額 <= 0 )
            {
                throw new ArgumentOutOfRangeException ( nameof ( 金額 ) , "取款金額不能為零或負數。" );
            }
        LEI交易 JY透支 = FF檢查取款限制 ( 餘額 - 金額 < _最小余額 );
        LEI交易 JY撤銷 = new LEI交易( -金額 , 日期 , 説明 );
        _JY全 . Add ( JY撤銷 );
        if ( JY透支 != null )
            _JY全 . Add ( JY透支 );
    }

protected virtual LEI交易? FF檢查取款限制 ( bool 透支 )
    {
        if ( 透支 )
            {
                throw new InvalidOperationException ( "此次取款所需資金不足。" );
            }
        else { return default; }
    }

新增的方法具有 protected 屬性,這意味着只能從派生類中調用該方法。這一聲明可防止其他客户端調用該方法。該方法也是 virtual 方法,以便派生類能夠更改其行為。返回類型為 Transaction?。? 標註表示該方法可能返回 null。在 LEI信用 中添加以下實現,以在超出取款限額時收取費用:
protected override LEI交易? FF檢查取款限制 ( bool 透支 ) => 透支 ? new LEI交易 ( -20 , DateTime . Now , "收取透支費" ) : default;
當賬户出現透支情況時,超限額操作會返回一筆費用交易。如果取款未超出限額,則該方法會返回一個空交易。這表示沒有費用。通過在的 “Main” 方法中添加以下代碼來測試這些更改:

LEI信用 XY = new ( "我的信用卡" , 0 , 3500 );
XY . FF取款 ( 20 , new DateTime ( 2025 , 9 , 1 ) , "買飯" );
XY . FF存款 ( 200 , new DateTime ( 2025 , 9 , 15 ) , "預存" );
XY . FF取款 ( 2800 , new DateTime ( 2025 , 10 , 1 ) , "旅遊" );
XY . FF存款 ( 180 , new DateTime ( 2025 , 10 , 15 ) , "還款" );
XY . FF取款 ( 1800 , new DateTime ( 2025 , 10 , 16 ) , "喝酒" );
XY . FF月末交易 ( );
Console . WriteLine ( XY . FF獲取交易記錄 ( ) );

總結

如果您遇到困難,可以查看本教程的源代碼,其位於我們的 GitHub 倉庫中。

本教程展示了面向對象編程中所使用的一些技術:

  • 在為每種不同的賬户類型定義類時,您使用了抽象概念。這些類描述了該類型賬户的行為。
  • 在每個類中,您將許多細節設為私有,這是採用了封裝原則。
  • 當您利用銀行賬户類中已有的實現來節省代碼時,使用了繼承。
  • 當您創建 virtual 方法並讓派生類能夠重寫這些方法以為該賬户類型創建特定行為時,使用了多態。

C# 和 .NET 中的繼承

本教程將向您介紹 C# 中的繼承概念。繼承是面向對象編程語言的一項特性,它允許您定義一個基礎類,該類提供特定的功能(數據和行為),然後定義派生類,這些派生類可以繼承或重寫該功能。

安裝説明

在 Windows 系統中,此 WinGet 配置文件用於安裝所有必需組件。如果您已經安裝了某些內容,那麼 WinGet 將跳過此步驟。

  1. 下載該文件並雙擊運行它。
  2. 閲讀許可協議,輸入 “y”,然後在提示您接受時選擇 “進入”。
  3. 如果您在任務欄中看到閃爍的用户賬户控制(UAC)提示,請允許安裝繼續進行。

在其他平台上,您需要分別安裝這些組件。

  1. 從 .NET SDK 下載頁面下載推薦的安裝程序,並雙擊運行它。該下載頁面會檢測您的平台,併為您推薦相應的最新安裝程序。
  2. 從 Visual Studio Code 主頁下載最新安裝程序,並雙擊運行它。該頁面也會檢測您的平台,鏈接對於您的系統應該是正確的。
  3. 在 C# DevKit 擴展頁面上點擊 “安裝” 按鈕。這將打開 Visual Studio Code,並詢問您是否要安裝或啓用該擴展。選擇 “安裝”。

運行示例

要創建並運行本教程中的示例,請在命令行中使用 dotnet 工具。針對每個示例,請按照以下步驟操作:

  1. 創建一個目錄來存放示例文件。
  2. 在命令提示符下輸入 “dotnet new console” 命令,即可創建一個新的 .NET Core 項目。
  3. 將示例中的代碼複製並粘貼到您的代碼編輯器中。
  4. 在命令行中輸入 “dotnet restore” 命令,以加載或恢復項目的依賴項。
    您無需手動運行 “dotnet restore” 命令,因為所有需要執行恢復操作的命令(如 “dotnet new”、“dotnet build”、“dotnet run”、“dotnet test”、“dotnet publish” 和 “dotnet pack”)都會自動執行此操作。若要禁用自動恢復功能,請使用 “--no-restore” 選項。
    “dotnet restore” 命令在某些特定場景中仍具有實用性,比如在 Azure DevOps 服務中的持續集成構建過程中,或者在需要明確控制恢復發生時間的構建系統中。
  5. 輸入 “dotnet run” 命令即可編譯並執行該示例。

背景:什麼是繼承?

繼承是面向對象編程的基本屬性之一。它使您能夠定義一個子類,該子類可以複用(繼承)、擴展或修改父類的行為。那些成員被繼承的類被稱為基類。而繼承了基類成員的類則被稱為派生類。

C# 和 .NET 只支持單一繼承。也就是説,一個類只能從一個類繼承。然而,繼承具有傳遞性,這使得您可以為一組類型定義一個繼承層次結構。換句話説,類型 D 可以從類型 C 繼承,而類型 C 又從類型 B 繼承,而類型 B 又從基礎類類型 A 繼承。由於繼承具有傳遞性,類型 A 的成員對類型 D 也是可用的。

並非基類的所有成員都會被派生類繼承。以下這些成員不會被繼承:

  • static 構造函數,用於初始化類的 static 數據。
  • 實例構造函數,您可通過調用該函數來創建該類的新實例。每個類都必須定義自己的構造函數。
  • 終結器是由運行時的垃圾回收器調用的,用於銷燬某個類的實例。

雖然基類的所有其他成員都會被派生類繼承,但這些成員是否可見則取決於其可訪問性。一個成員的可訪問性會對其在派生類中的可見性產生如下影響:

  • 私有成員僅在嵌套在其基類中的派生類中可見。否則,在派生類中它們是不可見的。在以下示例中,A . B 是從 A 派生的嵌套類,而 C 從 A 派生。私有成員 A . _值 字段在 A . B 中是可見的。但是,如果您從 C . FF獲取值 方法中刪除註釋並嘗試編譯該示例,它會生成編譯錯誤 警告 CS0122:“'A . _值' 的訪問權限因保護級別而受限。”

    internal class Program
      {
          static void Main(string[] args)
              {
                  A . B b = new ( );
                  Console . WriteLine ( b . FF獲取值 ( ) ); // 10
              }
      }
    
    public class A
      {
          private int _值 = 10;
    
          public class B : A
              {
                  public int FF獲取值 ( )
                      {
                          return _值;
                      }
              }
      }
    
    public class C : A
      {
          //public int FF獲取值 ( )
          //    {
          //        return _值; // 警告 CS0122:“A . _值”不可訪問,因為它有一定的保護級別
          //    }
      }
  • protected 成員僅在派生類中可見。
  • internal 成員僅在與基類位於同一程序集中的派生類中可見。在與基類位於不同程序集中的派生類中,這些成員是不可見的。
  • public 成員在派生類中是可見的,並且是派生類的公共接口的一部分。公共繼承的成員可以像在派生類中定義一樣被調用。假設類 A 定義了一個名為 FF1 的 public 方法,類 B 從類 A 繼承而來。B 類的實例可以像調用 B 類中的實例方法那樣調用了 FF1。

派生類還可以通過提供替代實現的方式來重寫繼承的成員。要能夠重寫某個成員,基類中的該成員必須帶有 “virtual” 關鍵字。默認情況下,基類成員不會被標記為 “virtual” 或 “abstract”,因此無法被重寫。例如,如果嘗試重寫一個非 virtual 成員(如示例所示),則會生成編譯器錯誤 警告 CS0506:
繼承成員 “成員” 未被標記為 virtual、abstract 或 override,無法進行重寫。

public class A
    {
        public void FF1 ( )
            {
                Console . WriteLine ( 1 );
            }
    }

public class B : A
    {
        public override void FF1 ( ) // CS0506:繼承成員 “A . FF1 ( )” 未被標記為 virtual、abstract 或 override,無法進行重寫
            {

            }
    }

在某些情況下,派生類必須重寫基類的實現。帶有 “abstruct” 關鍵字標記的基類成員要求派生類對其進行重寫。如果嘗試編譯以下示例,將會生成編譯錯誤 CS0534:“<類>未實現繼承自的抽象成員 <成員>”,這是因為類 B 沒有為 A . FF1 提供實現。

public abstract class A
    {
        public abstract void FF1 ( );
    }

public class B : A // 警告 CD0534:“B”不實現繼承的抽象成員“A . FF1 ( )”
    {
        public void FF3 ( )
            {
                Console . WriteLine ( 3 );
            }
    }

繼承僅適用於 class 和 interface。其他類型類別(struct、delegate 和 enum)不支持繼承。由於這些規則,嘗試編譯如以下示例這樣的代碼會引發編譯錯誤 CS0527:“接口列表中的 ‘ValueType’ 類型不是接口。” 錯誤消息表明,儘管可以定義 struct 所實現的 interface,但繼承功能並不支持。

public struct ValueStructure : ValueType // 警告 CS0527:接口列表中的類型“ValueType”不是接口
    {
    }

隱式繼承

除了通過單一繼承方式可能繼承的任何類型之外,.NET 類型系統中的所有類型都會隱式地繼承自 Object 或其派生類型。Object 的通用功能對任何類型都可用。

為了理解隱式繼承的含義,我們先定義一個新的類 - “LEI簡單”,它只是一個空的類定義:

public class LEI簡單
{ }

然後,您可以使用反射(reflection,它允許您檢查類型的相關元數據以獲取有關該類型的相關信息)來獲取屬於 LEI簡單 類型的成員列表。儘管您在 LEI簡單 類中尚未定義任何成員,但示例輸出表明它實際上有九個成員。其中有一個成員是無參數(即默認)構造函數,該構造函數由 C# 編譯器為 LEI簡單 類型自動提供。其餘的八個成員是 Object 類型的成員,而所有類和接口在 .NET 類型系統中最終都會隱式繼承自該類型。

static void Main(string[] args)
    {
        Type lx = typeof ( LEI簡單 );
        BindingFlags bdbzhs = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
        MemberInfo [ ] cyxx = lx . GetMembers ( bdbzhs );
        Console . WriteLine ( $"類型 {lx . Name} 擁有 {cyxx . Length} 個成員:" );
        foreach ( MemberInfo cy in cyxx )
            {
                string fw = "" , zt = "";
                var ff = cy as MethodBase;
                if ( ff != null )
                    {
                        if ( ff . IsPublic )
                            fw = "Public(公共的)";
                        else if ( ff . IsPrivate )
                            fw = "Private(私有的)";
                        else if ( ff . IsFamily )
                            fw = "Protected(受保護的)";
                        else if ( ff . IsAssembly )
                            fw = "Internal(內部的)";
                        else if ( ff . IsFamilyOrAssembly )
                            fw = "Protected Internal(受保護的內部的)";
                if ( ff . IsStatic )
                    zt = "Static(靜態的)";
            }
        Console . WriteLine ( $"{cy . Name} - ({cy . MemberType}):{fw}{zt},聲明自 {cy . DeclaringType}" );
        }
    }

public class LEI簡單
    { }
// 類型 LEI簡單 擁有 9 個成員:
// GetType - (Method):Public(公共的),聲明自 System.Object
// MemberwiseClone - (Method):Protected(受保護的),聲明自 System.Object
// Finalize - (Method):Protected(受保護的),聲明自 System.Object
// ToString - (Method):Public(公共的),聲明自 System.Object
// Equals - (Method):Public(公共的),聲明自 System.Object
// Equals - (Method):Public(公共的)Static(靜態的),聲明自 System.Object
// ReferenceEquals - (Method):Public(公共的)Static(靜態的),聲明自 System.Object
// GetHashCode - (Method):Public(公共的),聲明自 System.Object
// .ctor - (Constructor):Public(公共的),聲明自 C__和_.NET_中的繼承.Program+LEI簡單

從 Object 類隱式繼承而來的這些方法使它們能夠被 LEI簡單 類所使用:

  • public 的 ToString 方法將一個 LEI簡單 對象轉換為其字符串表示形式,它會返回完全限定的類型名稱。在這種情況下,ToString 方法返回字符串 “LEI簡單”。
  • 有三種方法用於檢驗兩個對象是否相等:public 實例方法 Equals ( Object )、public static 方法 Equals ( Object , Object ) 以及 public static 方法 ReferenceEquals ( Object , Object )。默認情況下,這些方法檢驗的是引用相等性;也就是説,要相等,兩個對象變量必須指向同一個對象。
  • public 的 GetHashCode 方法用於計算一個值,該值能使該類型的實例能夠被用於哈希集合中。
  • public 的 GetType 方法會返回一個表示 “LEI簡單” 類型的 Type 對象。
  • protected 的 “Finalize” 方法旨在在對象的內存被垃圾回收器回收之前釋放未管理資源。
  • protected 的 MemberwiseClone 方法,用於創建當前對象的淺層克隆。

由於存在隱式繼承,您可以從 LEI簡單 對象中調用任何繼承的成員,就好像該成員實際上是 LEI簡單 類中定義的成員一樣。例如,以下示例調用了 LEI簡單 的 ToString 方法,該方法是 LEI簡單 從 Object 類繼承而來的。

static void Main(string[] args)
    {
        LEI簡單 ljd = new ( );
        Console . WriteLine ( ljd . ToString ( ) ); // C__和_.NET_中的繼承 . Program + LEI簡單
    }

public class LEI簡單
    { }

以下表格列出了在 C# 中您可以創建的類型類別以及它們所隱式繼承的類型。每個基類型通過繼承為隱式派生類型提供了不同的成員集。

類型類別 隱式繼承自
class Object
struct ValueType、Object
enum Enum、ValueType、Object
delegate MulticastDelegate(多態委託)、Delegate、Object

繼承與 “是某種類型” 的關係

通常,繼承用於表達基類與一個或多個派生類之間的 “是某種類型” 的關係,其中派生類是基類的特化版本;派生類是基類的一種類型。例如,LEI出版物 類表示任何類型的出版物,而 LEI書 和 LEI雜誌 類則表示特定類型的出版物。

注意:class 或 struct 可以實現一個或多個接口。儘管接口實現通常被視為解決單一繼承問題的一種方法,或者是一種將繼承與 struct 結合使用的方式,但其目的是表達一種不同於繼承的不同的關係(一種 “能夠做” 的關係) - 即接口與其實現類型之間的關係。接口定義了一部分功能(例如能夠進行相等性測試、比較或排序對象、支持文化敏感的解析和格式化等),這些功能由接口提供給其實現類型使用。

請注意,“是某種類型” 這一表述還體現了類型與該類型特定實例之間的關係。在以下示例中,“LEI汽車” 是一個 class,它具有三個獨特的只讀屬性:製造商(Make),即汽車的製造商;車型(Model),即汽車的種類;以及製造年份(Year),即其製造年份。您的 “LEI汽車” 類還有一個構造函數,其參數會被賦值給這些屬性的值,並且它重寫了 Object . ToString 方法,以生成一個能夠唯一標識汽車實例而非 “LEI汽車” 的字符串。

public class LEI汽車
    {
        public LEI汽車 ( string 製造商 , string 型號 , int 出廠年份 )
            {
                if ( 製造商 == null )
                    throw new ArgumentNullException ( nameof ( 製造商 ) , "製造商不能為 null。" );
                else if ( string . IsNullOrWhiteSpace ( 製造商 ) )
                    throw new ArgumentException ( "製造商不能為空字符串或只擁有空格字符。" );
                this . 製造商 = 製造商;

                if ( 型號 == null )
                    throw new ArgumentNullException ( nameof ( 型號 ) , "型號不能為 null。" );
                else if ( string . IsNullOrWhiteSpace ( 型號 ) )
                    throw new ArgumentException ( "型號不能為空或只用有空格字符。" );
                this . 型號 = 型號;

                if ( 出廠年份 < 1857 || 出廠年份 > DateTime . Now . Year + 2 )
                    throw new ArgumentException ( "出廠年份超出範圍。" );
                this . 出廠年份 = 出廠年份;
            }

        public string 製造商 { get; }
        public string 型號 { get; }
        public int 出廠年份 { get; }

        public override string ToString ( ) => $"{出廠年份} {製造商} {型號}";
    }

在這種情況下,您不應依靠繼承來表示具體的汽車品牌和型號。例如,您無需定義一個 “帕卡德” 類型來表示由帕卡德汽車公司生產的汽車。相反,您可以通過創建一個帶有適當值傳遞給其類構造函數的 “LEI汽車” 對象來對其進行表示,就像下面的示例那樣。

static void Main(string[] args)
    {
        LEI汽車 pkd = new ( "帕卡德" , "自由 8" , 1948 );
        Console . WriteLine ( pkd ); // 1948 帕卡德 自由 8
    }

public class LEI汽車
    {
        public LEI汽車 ( string 製造商 , string 型號 , int 出廠年份 )
            {
                if ( 製造商 == null )
                    throw new ArgumentNullException ( nameof ( 製造商 ) , "製造商不能為 null。" );
                else if ( string . IsNullOrWhiteSpace ( 製造商 ) )
                    throw new ArgumentException ( "製造商不能為空字符串或只擁有空格字符。" );
                this . 製造商 = 製造商;

                 if ( 型號 == null )
                    throw new ArgumentNullException ( nameof ( 型號 ) , "型號不能為 null。" );
                else if ( string . IsNullOrWhiteSpace ( 型號 ) )
                    throw new ArgumentException ( "型號不能為空或只用有空格字符。" );
                this . 型號 = 型號;

                if ( 出廠年份 < 1857 || 出廠年份 > DateTime . Now . Year + 2 )
                    throw new ArgumentException ( "出廠年份超出範圍。" );
                this . 出廠年份 = 出廠年份;
            }

        public string 製造商 { get; }

        public string 型號 { get; }

        public int 出廠年份 { get; }

        public override string ToString ( ) => $"{出廠年份} {製造商} {型號}";
    }

基於繼承的 “是某種關係” 這種模式最適合應用於基類以及那些在基類基礎上添加了額外成員或需要基類所不具備的額外功能的派生類。

設計基類和派生類

讓我們來看看設計基類及其派生類的過程。在本節中,您將定義一個基類 “LEI出版物”,它代表任何類型的出版物,例如書籍、雜誌、報紙、期刊、文章等。您還將定義一個 “LEI書” 類,它從基類 “LEI出版物” 派生而來。您可以輕鬆地擴展此示例,定義其他派生類,例如 “LEI期刊”、“LEI雜誌”、“LEI報紙” 和 “LEI文章”。

基類 “LEI出版物”

在設計 “LEI出版物” 類時,您需要做出若干設計決策:

  • 在您的基礎 “LEI出版物” 類中應包含哪些成員,以及這些 “LEI出版物” 成員是提供方法實現,還是 “LEI出版物” 本身是一個抽象基類,用作其派生類的模板。
    在這種情況下,LEI出版物 類將提供方法實現。“設計抽象基類及其派生類” 部分包含了一個示例,該示例使用抽象基類來定義派生類必須重寫的方法。派生類可以自由地提供適合其派生類型的任何實現。
    能夠重複使用代碼(即多個派生類共享基類方法的聲明和實現,並且無需對其進行重寫)是非抽象基類的一個優點。因此,如果某些或大多數特定類型的出版物可能會共享其代碼,那麼您就應該向 “LEI出版物” 類中添加成員。如果未能有效地提供基類的實現,您最終可能會在派生類中提供幾乎完全相同的成員實現,而不是在基類中提供一個單一的實現。在多個位置維護重複的代碼是一個潛在的錯誤來源。
    為了最大限度地提高代碼的複用性,並創建一個邏輯清晰、易於理解的繼承層次結構,您需要確保在 “LEI出版物” 類中只包含所有或大多數出版物共有的數據和功能。然後,派生類將實現那些僅屬於它們所代表的特定類型出版物的成員。
  • 您的類層次結構應擴展到何種程度?您是否希望構建包含三個或更多類的層次結構,而非僅僅是一個基類以及一個或多個派生類?例如,LEI出版物 可以作為 LEI定期出版物 的基類,而 LEI定期出版物 又可以作為 LEI雜誌、LEI期刊 和 LEI報紙 的基類。
    以您的示例為例,您將使用一個小型的 “LEI出版物” 類層次結構以及一個單一的派生類 “LEI書”。您可以輕鬆地擴展這個示例,創建許多從 “LEI出版物” 類派生而來的其他類,例如 “LEI雜誌” 和 “LEI文章”。
  • 是否有必要實例化基類呢?如果不需要,那麼就應該給該類加上 “abstract(抽象)” 關鍵字。否則,您可以通過調用其類的構造函數來實例化 “LEI出版物” 類。如果嘗試通過直接調用帶有 “abstract” 關鍵字的類的類構造函數來實例化該類,C# 編譯器會生成錯誤 CS0144:“無法創建抽象類或接口的實例。” 如果嘗試通過反射來實例化該類,反射方法會拋出 MemberAccessException 異常。
    默認情況下,基類可以通過調用其類構造函數來實例化。您無需顯式定義類構造函數。如果基類的源代碼中沒有定義構造函數,C# 編譯器會自動提供一個默認的(無參數的)構造函數。
    以您的示例為例,您會將 “LEI出版物” 類標記為抽象類,這樣就無法對其進行實例化。一個沒有任何抽象方法的抽象類表明該類代表了多個具體類(如書籍、期刊)所共有的一個抽象概念。
  • 無論是派生類是否必須繼承基類中特定成員的實現,還是它們是否有權覆蓋基類的實現,又或者是它們是否必須提供自己的實現。您使用 “abstract” 關鍵字來強制派生類提供實現。您使用 “virtual(虛擬)” 關鍵字來允許派生類覆蓋基類的方法。默認情況下,基類中定義的方法不可被覆蓋。
    “LEI出版物” 類中沒有任何抽象方法,但該類本身卻是抽象類。
  • 一個派生類是否代表了繼承層次結構中的最終類,並且自身不能作為其他派生類的基類。默認情況下,任何類都可以作為基類。您可以使用 “sealed(封閉)” 關鍵字來表明一個類不能作為任何其他類的基類。如果嘗試從一個 “sealed” 類派生,則會生成編譯錯誤 CS0509,“不能從密封類型 < typeName > 派生”。
    對於您的示例,您需要將派生類標記為 “sealed” 狀態。

以下示例展示了 “LEI出版物” 類的源代碼,以及由 “LEI出版物 . 出版物類型” 屬性返回的 “出版物類型” 枚舉。除了從 “Object” 類繼承的成員之外,該 “LEI出版物” 類還定義了以下獨特的成員和成員重寫:

public abstract class LEI出版物
    {
        // 私有變量
        private bool _已出版 = false;
        private DateTime _出版日期;
        private int _頁數;

        // 公共屬性
        public string 出版商 { get; }
        public string 題目 { get; }
        public MJ出版物類型 類型 { get; }
        public string? 版權名 { get; private set; }
        public int 版權期限 { get; private set; }
        public int 頁數
            {
                get { return _頁數; }
                set
                    {
                        if ( value <= 0 ) throw new ArgumentOutOfRangeException ( nameof ( value ) , "頁數必須大於零。" );
                        _頁數 = value;
                    }
            }

        // 公共方法
        public string FF獲取出版日期 ( )
            {
                if ( !_已出版 )
                    {
                        return "尚未出版(NYP)";
                    }
                else
                    {
                        return _出版日期 . ToString ( "d" );
                    }
            }

        public void FF出版 ( DateTime 出版日期 )
            {
                _已出版 = true;
                _出版日期 = 出版日期;
            }

        public void FF版權 ( string 版權名 , int 版權期限 )
            {
                if ( string . IsNullOrWhiteSpace ( 版權名 ) )
                    {
                        throw new ArgumentException ( "必須註明版權持有者的姓名。" );
                    }
                this . 版權名 = 版權名;

                int Nian當前 = DateTime . Now . Year;
                if ( ( 版權期限 < Nian當前 - 10 ) || ( 版權期限 > Nian當前 + 2 ) )
                    {
                        throw new ArgumentOutOfRangeException ( $"版權年份必須在 {Nian當前 - 10} 到 {Nian當前 + 1} 之間。" );
                    }
                else
                    this . 版權期限 = 版權期限;
            }

        public override string ToString ( )
            {
                return 題目;
            }

        /// <summary>
        /// LEI出版物 的構造函數。
        /// </summary>
        /// <param name="標題">出版物的題目(例如書名)</param>
        /// <param name="出版商">出版物的出版商</param>
        /// <param name="類型">出版物的類型(例如雜誌或書籍)</param>
        /// <exception cref="ArgumentException">標題和出版商不可或缺</exception>
        public LEI出版物 ( string 標題 , string 出版商 , MJ出版物類型 類型 )
            {
                if ( string . IsNullOrWhiteSpace ( 出版商 ) )
                    throw new ArgumentException ( "出版商是必需的。" );
                    this . 出版商 = 出版商;

                if ( string . IsNullOrWhiteSpace ( 標題 ) )
                    throw new ArgumentException ( "題目是必需的。" );
                    題目 = 標題;

                    this . 類型 = 類型;
            }
    }
  • 僅有的構造函數
    因為 “LEI出版物” 類是 abstract(抽象)類,所以不能像下面的示例那樣通過代碼直接對其進行實例化:
    LEI出版物 Shu我的 = new ( "我的人生" , "淄博市圖書發行公司" , MJ出版物類型 . 書籍 ); // 警告 CS0144:無法創建抽象類型或接口“LEI出版物”的實例
    然而,其實例構造函數可以直接在派生類的構造函數中被調用,正如 LEI書 類的源代碼所示。
  • 兩項與出版物相關的特性/屬性
    “題目” 是一個只讀的字符串屬性,其值是通過調用 “LEI出版物” 構造函數來設定的。
    “頁數” 是一個可讀寫的 32 位整數屬性,用於表示該出版物的總頁數。其值存儲在名為 “_頁數” 的私有字段中。該值必須為正數,否則將拋出 “ArgumentOutOfRangeException” 異常。
  • 與出版商相關的成員
    有兩個 readonly 屬性,即 “出版商” 和 “類型”。其值最初是由對 “LEI出版物” 類構造函數的調用所設定的。
  • 與出版相關的人士
    有兩種方法,即 “FF出版” 和 “FF獲取出版日期”,用於設置並返回出版日期。“FF出版” 方法在被調用時將一個 private “_已出版” 標誌設置為 “true”,並將作為參數傳遞給它的日期賦值給 private “_出版日期” 字段。而 “FF獲取出版日期” 方法則在已發佈標誌為 “false” 時返回字符串 “尚未出版”,在標誌為 “true” 時返回 “_出版日期” 字段的值。
  • 與版權相關的成員
    “FF版權” 方法將版權持有者的姓名以及版權的年份作為參數,並將它們分別賦值給 “版權名” 和 “版權期限” 這兩個屬性。
  • 對 “ToString” 方法的重寫
    如果一個類型未重寫 Object 類的 ToString 方法,那麼它將返回該類型的完整限定名稱,而這種形式在區分不同實例方面幾乎毫無用處。而 LEI出版物 類則重寫了 Object 類的 ToString 方法,以返回 題目 屬性的值。

下圖展示了您的基礎 “LEI出版物” 類與其隱式繼承的 “對象” 類之間的關係。

書 類

“LEI書” 類將書籍視為一種特殊的出版物類型。以下示例展示了 “LEI書” 類的源代碼。

public sealed class LEI書 : LEI出版物
    {
        // 公共屬性
        public string ISBN { get; }
        public string 作者 { get; }
        public decimal 價格 { get; private set; }
        public string? 貨幣 { get; private set; }
        
        // 構造函數
        public LEI書 ( string 標題 , string 作者 , string 出版商 ) : this ( 標題 , string . Empty , 作者 , 出版商 ) { }

        public LEI書 ( string 標題 , string ISBN , string 作者 , string 出版商 ) : base ( 標題 , 出版商 , MJ出版物類型 . 書籍 )
            {
                // ISBN 參數必須是一個由 10 個或 13 個字符組成的無“-”字符的數字字符串。
                // 我們還可以通過將 ISBN 的校驗位與計算出的校驗位進行比較來確定其是否有效。
                if ( ! string . IsNullOrEmpty ( ISBN ) )
                    {
                        // 檢查如果 ISBN 不合適……
                        if ( ! ( ISBN . Length == 10 ) | ISBN . Length == 13 )
                            {
                                throw new ArgumentException ( "ISBN(國際標準書號)必須是 10 位或 13 位的數字字符串。" );
                            }
                        if ( ! ulong . TryParse ( ISBN , out _ ) )
                            { throw new ArgumentException ( "ISBN(國際標準書號)只能由數字字符組成。" ); }
                            }
                        this . ISBN = ISBN;
                        this . 作者 = 作者;
                    }

        public decimal FF設置價格 ( decimal 價格 , string 貨幣 )
            {
                if ( 價格 < 0 )
                    throw new ArgumentOutOfRangeException ( nameof ( 價格 ) , "價格不能是負數。" );
                decimal Jiu = this . 價格;
                this . 價格 = 價格;

                if ( 貨幣 . Length != 3 )
                    {
                        throw new ArgumentException ( "國際標準化組織(ISO)的貨幣符號是一個由三個字符組成的字符串。" );
                    }
                this . 貨幣 = 貨幣;

                return Jiu;
            }

        public override bool Equals ( object? obj )
            {
                if ( obj is not LEI書 Shu )
                    return false;
                else
                    return ISBN == Shu . ISBN;
            }

        public override int GetHashCode ( )
            {
                return ISBN . GetHashCode ( );
            }

        public override string ToString ( )
            {
                return $"{( string . IsNullOrEmpty ( 作者 ) ? "" : 作者 + "," )}{題目}";
            }
    }

除了從 “LEI出版物” 類繼承的成員之外,LEI書 類還定義了以下獨特的成員及成員重寫內容:

  • 兩個構造函數
    這兩個 LEI書 函數共有三個共同的參數。其中兩個參數 “標題” 和 “出版商” 與 “LEI出版物” 構造函數的參數相對應。第三個參數是 “作者”,其值被存儲到一個公開的不可變 “作者” 屬性中。其中一個構造函數包含一個 “ISBN” 參數,該參數存儲在 “ISBN” 自動屬性中。
    第一個構造函數使用 “this” 關鍵字來調用另一個構造函數。構造函數鏈是一種常見的構造函數定義模式。參數較少的構造函數在調用參數數量最多的構造函數時會提供默認值。
    第二個構造函數使用 “base” 關鍵字將標題和出版商名稱傳遞給基類的構造函數。如果您在源代碼中沒有明確調用基類的構造函數,那麼 C# 編譯器會自動提供對基類默認構造函數或無參數構造函數的調用。
  • 一個只讀的 ISBN 屬性,該屬性會返回書籍對象的國際標準書號,這是一個由 10 位或 13 位數字組成的唯一編號。ISBN 作為參數被傳遞給其中一個書籍構造函數。該 ISBN 存儲在一個 private 後備字段中,該字段由編譯器自動生成。
  • 一個只讀的 “作者” 屬性。作者姓名作為參數被提供給兩個 “LEI書” 構造函數,並存儲在該屬性中。
  • 有兩個 readonly 的與價格相關的屬性,即 “價格” 和 “貨幣”。它們的值會在調用 “FF設置價格” 方法時作為參數提供。貨幣屬性是三位數的國際標準化組織貨幣符號(例如,USD 表示美元)。國際標準化組織貨幣符號可以從 “ISOCurrencySymbol” 屬性中獲取。這兩個屬性都是外部的只讀屬性,但都可以在 “LEI書” 類中通過代碼進行設置。
  • 一個 “FF設置價格” 方法,用於設置 “價格” 和 “貨幣” 屬性的值。這些值正是由這兩個屬性所返回的。
  • 對 “ToString” 方法(繼承自 “LEI出版物” 類)以及 “Object . Equals ( Object )” 和 “GetHashCode” 方法(繼承自 “Object” 類)的重寫。
    除非被其他方法覆蓋,否則 Object . Equals ( Object ) 方法會進行引用相等性測試。也就是説,如果兩個對象變量指向的是同一個對象,那麼它們就被視為相等的。而在 LEI書 類中,如果兩個 LEI書 對象具有相同的 ISBN,那麼它們就應該被視為相等的。
    當您重寫 Object . Equals ( Object ) 方法時,還必須重寫 GetHashCode 方法。該方法會返回一個值,運行時會利用此值將項存儲在哈希集合中以實現高效檢索。哈希碼應返回與相等性測試相一致的值。由於您已重寫 Object . Equals ( Object ) 方法,使其在兩個 LEI書 對象的 ISBN 屬性相等時返回 true,因此您會調用 ISBN 屬性所返回的字符串的 GetHashCode 方法來計算哈希碼。

下圖展示了 “LEI書” 類與 “LEI出版物”(其基類)之間的關係。

現在您可以創建一個 “LEI書” 對象,調用其獨有的和繼承的成員,並將其作為參數傳遞給一個期望接收 “LEI出版物” 或 “LEI書” 類型參數的方法,如下例所示。

static void Main(string[] args)
    {
        LEI書 S1 = new ( "我的一生" , "1234567890" , "我本人" , "公共出版社" );
        FF查看出版物信息 ( S1 );
        S1 . FF出版 ( new DateTime ( 2024 , 12 , 31 ) );
        FF查看出版物信息 ( S1 );

        LEI書 S2 = new ( "我的一生" , "經典出版社" , "我老婆" );
        Console . Write ( $"《{S1 . 題目}》 和 《{S2 . 題目}》 是相同的出版物:" + $"{( ( LEI出版物 ) S1 ) . Equals ( S2 )}" );
    }

public static void FF查看出版物信息 ( LEI出版物 出版物 )
    {
        string pubDate = 出版物 . FF獲取出版日期 ( );
        Console . WriteLine ( $"《{出版物 . 題目}》,{( pubDate == "尚未出版" ? "尚未出版" : "出版於:" + pubDate ):d},出版商: {出版物 . 出版商}" );
    }

設計抽象基類及其派生類

在前面的示例中,您定義了一個基類,該基類為多個方法提供了實現,以便派生類能夠共享代碼。然而,在許多情況下,基類並不需要提供實現。相反,基類是一個抽象類,它聲明瞭抽象方法;它充當了一個模板,定義了每個派生類必須實現的成員。通常在抽象基類中,每個派生類型的實現對於該類型來説都是獨一無二的。您將該類標記為 “abstract” 關鍵字,是因為實例化 “LEI出版物” 對象沒有實際意義,儘管該類確實提供了與出版物相關的通用功能的實現。

例如,每一個封閉的二維幾何圖形都具有兩個屬性:面積,即圖形內部的範圍;以及周長,即沿着圖形邊緣的長度。然而,這些屬性的計算方式完全取決於具體的圖形類型。例如,計算圓的周長(或圓周)的公式與計算正方形的周長的公式是不同的。LEI形狀 類是一個抽象類,包含抽象方法。這表明派生類具有相同的功能,但這些派生類以不同的方式實現該功能。

以下示例定義了一個名為 “LEI形狀” 的抽象基類,該類定義了兩個屬性:面積和周長。除了用 “abstract” 關鍵字標記該類之外,每個實例成員也用 “abstract” 關鍵字進行了標記。在這種情況下,“LEI形狀” 還重寫了 “Object . ToString” 方法,以返回類型名稱(而非完全限定名稱)。並且它定義了兩個靜態成員,即 “FF獲取面積” 和 “FF獲取周長”,使調用者能夠輕鬆地獲取任何派生類實例的面積和周長。當您將派生類的實例傳遞給這兩個方法中的任何一個時,運行時會調用派生類的方法重寫版本。

internal abstract class LEI形狀
    {
        public abstract double 面積 { get; }
        public abstract double 周長 { get; }
        public override string ToString ( )
            {
                return GetType ( ) . Name;
            }

        public static double FF獲取面積 ( LEI形狀 形狀 ) => 形狀 . 面積;
        public static double FF獲取周長 ( LEI形狀 形狀 ) => 形狀 . 周長;
    }

然後,您可以從 “LEI形狀” 類派生出一些表示特定形狀的類。以下示例定義了三個類:LEI正方形、LEI矩形 和 LEI圓形。每個類都使用針對特定形狀特有的公式來計算面積和周長。一些派生類還定義了一些屬性,例如 “LEI矩形 . 對角線”(矩形的對角線長度)和 “LEI圓 . 直徑”(圓的直徑),這些屬性是它們所表示的形狀所特有的。

正方形

internal class LEI正方形 : LEI形狀
    {
        // 公共屬性
        public double 邊長 { get; }
        public override double 面積 => Math . Pow ( 邊長 , 2 );
        public override double 周長 => 邊長 * 4;
        public double 對角線 => Math . Round ( Math . Sqrt ( 2 ) * 邊長 , 2 );

        // 構造函數
        public LEI正方形 ( double 邊 )
            {
                邊長 = 邊;
            }
    }

矩形

internal class LEI矩形 : LEI形狀
    {
        // 公共屬性
        public double 長邊 { get; }
        public double 寬邊 { get; }
        public override double 面積 => 長邊 * 寬邊;
        public override double 周長 => 長邊 * 2 + 寬邊 * 2;
        public bool SH正方形 ( ) => 長邊 == 寬邊;
        public double 對角線 => Math . Round ( Math . Sqrt ( Math . Pow ( 長邊 , 2 ) + Math . Pow ( 寬邊 , 2 ) ) , 2 );

        // 構造函數
        public LEI矩形 ( double 長 , double 寬 )
            {
                長邊 = 長;
                寬邊 = 寬;
            }
    }

圓形

internal class LEI圓 : LEI形狀
    {
        // 公共屬性
        public double 半徑 { get; }
        public double 直徑 => 半徑 * 2;
        public override double 面積 => Math . Round ( Math . PI * Math . Pow ( 半徑 , 2 ) );
        public override double 周長 => Math . Round ( Math . PI * 半徑 * 2 );
        public double 圓周 => 周長;

        // 構造函數
        public LEI圓 ( double 半徑 )
            {
                this . 半徑 = 半徑;
            }
    }

以下示例使用了源自 “LEI形狀” 類的對象。它創建了一個由 “LEI形狀” 類派生的對象數組,並調用了 “LEI形狀” 類的靜態方法,這些方法會返回 “LEI形狀” 類的屬性值。運行時會從派生類型的重寫屬性中獲取值。該示例還將數組中的每個 “LEI形狀” 對象轉換為其派生類型,並且如果轉換成功,則獲取該特定 “LEI形狀” 子類的屬性。

static void Main(string[] args)
    {
        LEI形狀 [ ] XZs = { new LEI矩形 ( 3.5 , 2.4 ) , new LEI正方形 ( 1.5 ) , new LEI圓 ( 2.3 ) };
        foreach ( LEI形狀 XZ in XZs )
            {
                Console . WriteLine ( $"{XZ}:面積 - {LEI形狀 . FF獲取面積 ( XZ )};周長 - {LEI形狀 . FF獲取周長 ( XZ )}" );
                if ( XZ is LEI正方形 z )
                    {
                        Console . WriteLine ( $"    正方形對角線:{z . 對角線}" );
                        continue;
                    }
                if ( XZ is LEI矩形 j )
                    {
                        Console . WriteLine ( $"    矩形對角線:{j . 對角線};是正方形嗎:{j . SH正方形 ( )}" );
                        continue;
                    }
                if ( XZ is LEI圓 y )
                    {
                        Console . WriteLine ( $"    圓直徑:{y . 直徑}(圓周:{y . 圓周})" );
                        continue;
                    }
            }
    }

轉換類型 - 如何通過使用模式匹配以及 “is” 和 “as” 運算符來實現安全的賦值操作

由於對象具有多態性,所以一個基類類型的變量可以存儲派生類類型的值。要訪問派生類的實例成員,必須將值轉換回派生類類型。然而,這種轉換可能會導致拋出 “InvalidCastException”。C# 提供了模式匹配語句,它僅在轉換成功時才有條件地進行轉換。C# 還提供了 “is” 和 “as” 運算符來測試一個值是否為某種類型。

以下示例展示瞭如何使用模式匹配的 “if” 語句:

static void Main ( string [ ] args )
    {
        LEI狗 g = new ( );
        LEI動物 d = new ( );
        FF飼養哺乳動物 ( g ); // 飼養中……
        FF飼養哺乳動物 ( d ); // LEI動物 不是一個哺乳動物

        LEI超新星 xing = new ( );
        FF哺乳動物測試 ( g ); // 我是一隻動物
        FF哺乳動物測試 ( xing ); // LEI超新星 不是個哺乳動物
    }

static void FF飼養哺乳動物 ( LEI動物 動物 )
    {
        if ( 動物 is LEI哺乳動物 br )
            {
                br . FF飼養 ( );
            }
        else
            {
                // 變量“m”在此處不在作用域內,無法使用
                Console . WriteLine ( $"{動物 . GetType ( ) . Name} 不是一個哺乳動物" );
            }
        }

static void FF哺乳動物測試 ( object 對象 )
    {
        // 您還可以使用“as”運算符,並在引用變量之前先對其進行 null 值檢查
        LEI哺乳動物 br = 對象 as LEI哺乳動物;
        if ( br != null )
            {
                Console . WriteLine ( br . ToString ( ) );
            }
        else
            {
                Console . WriteLine ( $"{對象 . GetType ( ) . Name} 不是個哺乳動物" );
            }
    }

class LEI動物
    {
        public void FF飼養 ( ) { Console . WriteLine ( "飼養中……" ); }
        public override string ToString ( )
            {
                return "我是一隻動物。";
            }
    }

class LEI哺乳動物 : LEI動物 { }
class LEI狗 : LEI哺乳動物 { }
class LEI超新星 { }

上述示例展示了模式匹配語法的一些特性。if ( a is LEI哺乳動物 br ) 這條語句將測試與初始化賦值結合在一起。賦值僅在測試成功時才會發生。變量 br 僅在嵌入的 if 語句中處於作用域內,在該語句中它已被賦值。在同一個方法中之後不能訪問 br。上述示例還展示瞭如何使用 as 運算符將對象轉換為指定類型。

您還可以使用相同的語法來測試一個可為 null 的值類型是否具有值,如以下示例所示:

static void Main ( string [ ] args )
    {
        int z = 5;
        FF模式匹配null ( z ); // 5

        int? n = null;
        FF模式匹配null ( n ); // 值類型 是一個可為 null 的類型並且是 null 值

        double sjd = 9.87654;
        FF模式匹配null ( sjd ); // 不能轉換 9.87654

        FF模式匹配開關 ( z ); // 5 — System . Int32
        FF模式匹配開關 ( n ); //  是一個可為 null 的類型並且是 null 值
        FF模式匹配開關 ( sjd ); // System . Double
    }

static void FF模式匹配開關 ( ValueType? 值類型 )
    {
        switch ( 值類型 )
            {
                case int z:
                    Console . WriteLine ( $"{z} — {z . GetType ( )}" );
                    break;
                case long z:
                    Console . WriteLine ( $"{z} — {z . GetType ( )}" );
                    break;
                case decimal x:
                    Console . WriteLine ( $"{x} — {x . GetType ( )}" );
                    break;
                case float djd:
                    Console . WriteLine ( $"{djd} — {djd . GetType ( )}" );
                    break;
                case double sjd:
                    Console . WriteLine ( $"{sjd} — {sjd . GetType ( )}" );
                    break;
                case null:
                    Console . WriteLine ( $"{值類型} 是一個可為 null 的類型並且是 null 值" );
                    break;
                default:
                    Console . WriteLine ( "Could not convert " + 值類型 . ToString ( ) );
                    break;
        }
    }

static void FF模式匹配null ( ValueType? 值類型 )
    {
        if ( 值類型 is int z ) // 在模式中不允許使用可 null 類型
            {
                Console . WriteLine ( z );
            }
        else if ( 值類型 is null ) // 如果變量 值類型 是一個可能為 null 的類型且其值為 null,則此表達式為真
            {
                Console . WriteLine ( "值類型 是一種可為 null 的類型,其值為 null" );
            }
        else
            {
                Console . WriteLine ( "不能轉換 " + 值類型 . ToString ( ) );
            }
    }

上述示例展示了模式匹配在轉換中的其他應用特性。您可以通過專門檢查空值來測試變量是否符合 null 模式。當變量的運行時值為 null 值時,檢查類型的所有 is 語句總是返回 false。模式匹配語句不允許使用可 null 值類型,例如 int? 或 Nullable < int >,但您可以測試任何其他值類型。前面示例中的 is 模式並不侷限於可 null 值類型。您還可以使用這些模式來測試引用類型的變量是否有值或是否為 null。

上述示例還展示瞭如何在 switch 語句中使用類型模式,其中變量可能屬於多種不同的類型之一。

如果您想要檢驗一個變量是否屬於某種特定類型,但又不想將其賦值給一個新的變量,那麼可以使用引用類型和可 null 值類型的 “is” 和 “as” 運算符來實現。以下代碼展示瞭如何使用在引入模式匹配之前就已存在於 C# 語言中的 “is” 和 “as” 語句來檢驗一個變量是否屬於特定類型:

static void Main ( string [ ] args )
    {
        // 在進行類型轉換之前,使用“is”運算符來驗證類型。
        LEI狗 g = new();
        FF使用is ( g );

        // 使用“as”運算符,並在引用變量之前先對其進行空值檢查。
        FF使用as ( g );

        // 使用模式匹配來在引用變量之前先檢查其是否為 null 狀態。
        FF使用模式匹配is ( g );

        // 使用“as”運算符來檢測不兼容的類型。
        LEI超新星 sn = new ( );
        FF使用as ( sn );

        // 使用“as”運算符與值類型配合使用。
        // 請注意方法體中隱式的轉換為“int?”類型。
        int i = 5;
        FF使用as可null ( i );

        double d = 9.78654;
        FF使用as可null ( d );
    }

static void FF使用is ( LEI動物 a )
    {
        if ( a is LEI哺乳動物 )
            {
                LEI哺乳動物 m = (LEI哺乳動物)a;
                m . FF飼養 ( );
            }
    }

static void FF使用模式匹配is ( LEI動物 a )
    {
        if ( a is LEI哺乳動物 m )
            {
                m . FF飼養 ( );
            }
    }

static void FF使用as ( object o )
    {
        LEI哺乳動物? m = o as LEI哺乳動物;
        if ( m is not null )
            {
                Console . WriteLine ( m . ToString ( ) );
            }
        else
            {
                Console . WriteLine ( $"{o . GetType ( ) . Name} 不是一個哺乳動物" );
            }
    }

static void FF使用as可null ( System . ValueType val )
    {
        int? j = val as int?;
        if ( j is not null )
            {
                Console . WriteLine ( j );
            }
        else
            {
                Console . WriteLine ( "無法轉換 " + val . ToString ( ) );
            }
    }

class LEI動物
    {
        public void FF飼養 ( ) => Console . WriteLine ( "飼養中……" );
        public override string ToString ( ) => "我是一隻動物。";
    }

class LEI哺乳動物 : LEI動物 { }
class LEI狗 : LEI哺乳動物 { }

class LEI超新星 { }

通過將此代碼與模式匹配代碼進行對比,您會發現模式匹配語法通過將測試和賦值整合到同一語句中,提供了更強大的功能。請儘可能使用模式匹配語法。

教程:利用模式匹配構建類型驅動和數據驅動的算法

您可以編寫一些代碼,使其表現得像是您對其他庫中的類型進行了擴展一樣。模式的另一個用途是創建您的應用程序所需的、但並非被擴展類型的基本功能的代碼。

在本教程中,您將學習如何:

  • 識別應使用模式匹配的情況。
  • 使用模式匹配表達式來根據類型和屬性值實現特定行為。
  • 將模式匹配與其他技術相結合,以創建完整的算法。

安裝説明

在 Windows 系統中,此 WinGet 配置文件用於安裝所有必需組件。如果您已經安裝了某些內容,那麼 WinGet 將跳過此步驟。

  1. 下載該文件並雙擊運行它。
  2. 閲讀許可協議,輸入 “y”,然後在提示您接受時選擇 “進入”。
  3. 如果您在任務欄中看到閃爍的用户賬户控制(UAC)提示,請允許安裝繼續進行。

在其他平台上,您需要分別安裝這些組件。

  1. 從 .NET SDK 下載頁面下載推薦的安裝程序,並雙擊運行它。該下載頁面會檢測您的平台,併為您推薦相應的最新安裝程序。
  2. 從 Visual Studio Code 主頁下載最新安裝程序,並雙擊運行它。該頁面也會檢測您的平台,鏈接對於您的系統應該是正確的。
  3. 在 C# DevKit 擴展頁面上點擊“安裝”按鈕。這將打開 Visual Studio Code,並詢問您是否要安裝或啓用該擴展。選擇“安裝”。

本教程假設您熟悉 C# 和 .NET,包括使用 Visual Studio 或 .NET CLI。

模式匹配的場景

現代開發通常需要整合來自多個來源的數據,並在單一的連貫應用程序中呈現這些數據所包含的信息和見解。而您和您的團隊將無法掌控或訪問所有代表輸入數據的類型。

傳統的面向對象設計要求在應用程序中創建相應的類型,以表示來自多個數據源的每種數據類型。然後,您的應用程序將使用這些新類型,構建繼承層次結構,創建虛方法,並實現抽象。這些技術是有效的,有時它們也是最好的工具。而在其他時候,您可以編寫更少的代碼。您可以使用將數據與操作數據的代碼分開的技術來編寫更清晰的代碼。

在本教程中,您將創建並探索一個應用程序,該程序能夠從多個外部來源獲取單個場景下的輸入數據。您將瞭解到模式匹配是如何提供一種高效的方式,以不同於原系統的方式消費和處理這些數據的。

設想一個大型都市區正在採用收費和高峯時段定價的方式來管理交通。你編寫了一個應用程序,能夠根據車輛類型計算出相應的通行費。之後的改進又加入了根據車內乘客數量來計算收費的功能。再後來的改進又增加了根據時間以及一週中的具體日期來計算收費的功能。

根據上述簡要描述,您或許已經迅速構建出一個對象層次結構來對這個系統進行建模。然而,您的數據來自多個來源,比如其他車輛註冊管理系統。這些系統提供了不同的類來對這些數據進行建模,而您並沒有一個可以通用的單一對象模型可供使用。在本教程中,您將使用這些簡化後的類來對來自這些外部系統的車輛數據進行建模,如以下代碼所示:

namespace KJ消費者車輛登記
    {
    public class LEI轎車
        {
            public int 乘客數 { get; set; }
        }
    }

namespace KJ商業註冊
    {
    public class LEI送貨卡車
        {
            public int 總重量 { get; set; }
        }
    }

namespace KJ執照註冊
    {
        public class LEI的士
            {
                public int 乘客數 { get; set; }
            }

        public class LEI公交
            {
                public int 客容 { get; set; }
                public int 乘客數 { get; set; }
            }

您可以從 dotnet/samples(鏈接失效)倉庫的 GitHub 上下載示例代碼。您會發現車輛類來自不同的系統,並且位於不同的命名空間中。除了 System . Object 這個通用基類之外,沒有其他共同的基類可以使用。

模式匹配設計

本教程所採用的場景展示了模式匹配在解決哪些類型問題方面具有顯著優勢:

  • 您需要處理的對象並未處於與您的目標相匹配的對象層次結構中。您可能正在使用屬於不同系統中的類。
  • 您要添加的功能並非這些類的核心抽象的一部分。車輛的通行費會因車輛類型的不同而有所變化,但通行費並非車輛的核心功能。

當數據的特徵以及對該數據的操作未一同描述時,C# 中的模式匹配功能會使處理起來更加容易。

執行基本的通行費計算

最基礎的通行費計算僅依據車輛類型進行:

  • 一輛小汽車的費用是 ¥2.00。
  • 一輛出租車的費用是 ¥3.50。
  • 一輛公交車的費用是 ¥5.00。
  • 一輛送貨卡車的費用是 ¥10.00。

創建一個新的 “LEI收費計算器” 類,並對車輛類型進行模式匹配以獲取通行費金額。以下代碼展示了 “LEI收費計算器” 的初始實現。

internal class LEI計算器
    {
        public decimal FF計算費用 ( object 交通工具 ) => 交通工具 switch
            {
                LEI轎車 j => 2.00m,
                LEI的士 d => 3.50m,
                LEI公交 g => 5.00m,
                LEI送貨卡車 k => 10.00m,
                { } => throw new ArgumentException ( message: "未知的交通工具" , paramName: nameof ( 交通工具 ) ),
                null => throw new ArgumentNullException ( nameof ( 交通工具 ) )
        };
    }

上述代碼使用了一種稱為 “switch 表達式”(與 “switch 語句” 不同)的語法結構,用於測試聲明模式。在上述代碼中,switch 表達式以變量 “交通工具” 開頭,隨後是 “switch” 關鍵字。接下來是大括號內的所有 “switch” 分支。這種 switch 表達式對圍繞 switch 語句的語法進行了其他改進。省略了 “case” 關鍵字,並且每個分支的結果是一個表達式。最後兩個分支展示了一種新的語言特性。{ } case 會匹配任何未與早期分支匹配的非 null 對象。此分支會捕獲傳遞給此方法的任何錯誤類型。{ } case 必須緊跟每個車輛類型的 “cases”。如果順序顛倒,{ } case 將具有優先級。最後,null 常量模式檢測當向此方法傳遞 null 時的情況。null 模式可以放在最後,因為其他模式僅匹配正確類型的非空對象。

您可以在 Program.cs 文件中使用以下代碼來測試這段代碼:

LEI計算器 jsq = new ( );

LEI公交 gj = new ( );
LEI的士 ds = new ( );
LEI轎車 jc = new ( );
LEI送貨卡車 kc = new ( );

Console . WriteLine ( $"卡車的通行費:{jsq . FF計算費用 ( kc )}" );
Console . WriteLine ( $"轎車的通行費:{jsq . FF計算費用 ( jc )}" );
Console . WriteLine ( $"的士的通行費:{jsq . FF計算費用 ( ds )}" );
Console . WriteLine ( $"公交的通行費:{jsq . FF計算費用 ( gj )}" );

try
    {
        jsq . FF計算費用 ( "這將導致失敗" );
    }
catch ( ArgumentException yc )
    {
        Console . WriteLine ( $"由於錯誤的參數類型導致失敗 - {yc . Message}" );
    }

try
    {
        jsq . FF計算費用 ( null! );
    }
catch ( ArgumentNullException yc )
    {
        Console . WriteLine ( $"由於空的參數(null)導致失敗 - {yc . Message}" );
    }

那段代碼包含在啓動項目中,但被註釋掉了。去掉註釋,您就可以測試您所編寫的內容了。

您開始明白模式如何幫助您創建代碼與數據分離的算法了。switch 表達式會測試類型,並根據結果生成不同的值。這僅僅是個開始。

增加按載客量定價

收費機構希望鼓勵車輛滿載出行。他們決定對載客量較少的車輛收取更高的費用,並通過提供較低的收費標準來鼓勵滿載車輛:

  • 無乘客的 LEI轎車 和 LEI的士 需額外支付 ¥0.50。
  • 載有兩名乘客的 LEI轎車 和 LEI的士 可享受 ¥0.50 的折扣。
  • 載有三名或更多乘客的 LEI轎車 和 LEI的士 可享受 ¥1.00 的折扣。
  • 載客率低於 50% 的 LEI公交 需額外支付 ¥2.00。
  • 載客率超過 90% 的 LEI公交 可享受 ¥1.00 的折扣。

這些規則可以通過在同一個 switch 表達式中使用屬性模式來實現。屬性模式將屬性值與常量值進行比較。在確定對象類型後,屬性模式會檢查對象的屬性。對於 Car 類型的單個 case 會擴展為四個不同的 case:

LEI轎車 { 乘客數: 0 } => 2m + 0.5m,
LEI轎車 { 乘客數: 1 } => 2m,
LEI轎車 { 乘客數: 2 } => 2m - 0.5m,
LEI轎車               => 2m - 1m,

前三個案例首先將該類型判定為 “LEI轎車”,然後檢查 “乘客數” 屬性的值。如果兩者都匹配,則對該表達式進行計算並返回結果。

您還會以類似的方式擴大出租車的運營範圍:

LEI的士 { 乘客數: 0 } => 3.5m + 1m,
LEI的士 { 乘客數: 1 } => 3.5m,
LEI的士 { 乘客數: 2 } => 3.5m - 0.5m,
LEI的士               => 3.5m - 1m,

接下來,通過擴大公交車的適用範圍來實施這些佔用規則,如下例所示:

LEI公交 g when ( ( double ) g . 乘客數 / ( double ) g . 客容 ) < 0.5 => 5m + 2m,
LEI公交 g when ( ( double ) g . 乘客數 / ( double ) g . 客容 ) > 0.9 => 5m - 1m,
LEI公交                                                              => 5m,

收費管理部門並不關心運輸卡車上的乘客數量。相反,他們會根據卡車的重量類別來調整收費金額,具體方式如下:

  • 重量超過 150 噸的卡車需額外支付 ¥5.00。
  • 重量在 60 噸以下的輕型卡車可享受 ¥2.00 的折扣。

該規則通過以下代碼實施:

LEI送貨卡車 k when ( k . 總重量 > 150 ) => 10m + 5m,
LEI送貨卡車 k when ( k . 總重量 < 60 ) => 10m - 2m,
LEI送貨卡車                            => 10m,

上述代碼展示了一個開關臂的 “when” 語句部分。您使用 “when” 語句來測試除屬性相等性之外的其他條件。完成操作後,您將得到一個類似於以下代碼的方法:

{
    LEI轎車 { 乘客數: 0 } => 2m + 0.5m,
    LEI轎車 { 乘客數: 1 } => 2m,
    LEI轎車 { 乘客數: 2 } => 2m - 0.5m,
    LEI轎車               => 2m - 1m,

    LEI的士 { 乘客數: 0 } => 2m + 0.5m,
    LEI的士 { 乘客數: 1 } => 2m,
    LEI的士 { 乘客數: 2 } => 2m - 0.5m,
    LEI的士               => 2m - 1m,

    LEI公交 g when ( ( double ) g . 乘客數 / ( double ) g . 客容 ) < 0.5 => 5m + 2m,
    LEI公交 g when ( ( double ) g . 乘客數 / ( double ) g . 客容 ) > 0.9 => 5m - 1m,
    LEI公交                                                              => 5m,

    LEI送貨卡車 k when ( k . 總重量 > 150 ) => 10m + 5m,
    LEI送貨卡車 k when ( k . 總重量 < 60 ) => 10m - 2m,
    LEI送貨卡車                            => 10m,

    { } => throw new ArgumentException ( message: "未知的交通工具" , paramName: nameof ( 交通工具 ) ),
    null => throw new ArgumentNullException ( nameof ( 交通工具 ) )
};

這些開關臂中的許多都屬於遞歸模式的示例。例如,“LEI轎車 { 乘客數: 1}” 就是一個在屬性模式內部呈現的恆定模式的實例。

通過使用嵌套的 switch 語句,您可以使這段代碼的重複性降低。在前面的示例中,LEI轎車 和 LEI的士 都有四個不同的分支。在兩種情況下,您都可以創建一個聲明模式,然後將其與一個常量模式相結合。此技術在以下代碼中有所體現:

LEI轎車 j => j . 乘客數 switch
    {
        0 => 2m + 0.5m,
        1 => 2m,
        2 => 2m - 0.5m,
        _ => 2m - 1m,
    },

LEI的士 d => d . 乘客數 switch
    {
        0 => 3.5m + 0.5m,
        1 => 3.5m,
        2 => 3.5m - 0.5m,
        _ => 3.5m - 1m,
    },

在上述示例中,使用遞歸表達式意味着您無需重複包含子臂(這些子臂用於測試屬性值)的 “LEI轎車” 和 “LEI的士” 臂。這種方法並未應用於 “LEI公交” 和 “LEI送貨卡車” 臂,因為這些臂所測試的是屬性的範圍,而非具體的值。

添加高峯時段收費

對於最後一個功能,收費管理部門希望添加時間敏感型高峯時段收費機制。在早高峯和晚高峯時段,通行費將翻倍。該規則僅影響單向交通:早上從城市進入的車輛,以及晚上高峯時段從城市駛出的車輛。在工作日的其他時段,通行費將增加 50%。深夜和清晨,通行費將減少 25%。在週末,無論時間如何,通行費均為正常費率。您可以使用一系列的 if 和 else 語句來用以下代碼表達這一功能:

public decimal FF時間段收費比例 ( bool 入境 )
    {
        if ( DateTime . Now . DayOfWeek == DayOfWeek . Saturday || DateTime . Now . DayOfWeek == DayOfWeek . Sunday )
            return 1m; // 週末任何時間正常收費

        else // 非週末
            {
                int s = DateTime . Now . Hour;
                if ( s < 6 ) // 凌晨 6 點之前減免 25%
                    return 0.75m;
                else if ( s < 10 ) // 上午 10 點之前,入境加倍,出境正常
                    {
                        if ( 入境 ) return 2m;
                        else return 1m;
                    }
                else if ( s < 16 ) // 下午 4 點之前,加收 50%
                    return 1.5m;
                else if ( s < 20 ) // 晚上 8 點之前,入境正常,出境加倍
                    {
                        if ( 入境 ) return 1m;
                        else return 2m;
                    }
                else return 0.75m; // 入夜,減免 25%
            }
    }

上述代碼確實能正常運行,但可讀性較差。要理解這段代碼,您必須逐個處理所有的輸入情況以及嵌套的 if 語句。相比之下,您將使用模式匹配來實現此功能,但會將其與其他技術相結合。您可以構建一個單一的模式匹配表達式,以涵蓋方向、一週中的日期以及時間的所有組合情況。結果會是一個複雜的表達式。它難以閲讀且難以理解。這使得確保其正確性變得困難。相反,可以將這些方法結合起來,構建一個包含三個離散條件的值元組,以簡潔地描述所有這些狀態。然後使用模式匹配來計算通行費的乘數。該元組包含三個獨立的條件:

  • 這一天要麼是工作日,要麼是週末。
  • 這是收取通行費的時間段。
  • 方向是入境還是出境。

以下表格展示了輸入值的組合及其峯值收費倍數:

出入境 收費
工作日 早高峯 入境 × 2
出境 × 1
日間 出入境 × 1.5
晚高峯 出境 × 1
入境 × 2
夜間 出入境 × 0.75
週末 任意 出入境 × 1

這三個變量共有 16 種不同的組合方式。通過將某些條件結合起來,您將能夠簡化最終的切換表達式。
該用於收取通行費的系統使用了一個 DateTime 結構來記錄通行費收取的時間。構建成員方法來根據前面的表格創建相關變量。以下函數使用模式匹配的 switch 表達式來表示一個 DateTime 是否代表週末或工作日:

private static bool FF是否週末 ( ) => DateTime . Now . DayOfWeek switch
    {
        DayOfWeek . Saturday => true,
        DayOfWeek . Sunday => true,
        _ => false,
    };

接下來,添加一個類似的功能來將時間劃分成不同的時間段:

private enum MJ時間段
    {
        早高峯 = 0,
        日間 = 1,
        晚高峯 = 2,
        夜間 = 4,
    }
private static MJ時間段 FF獲取時間段 ( ) => DateTime . Now . Hour switch
    {
        < 6 or > 19 => MJ時間段 . 夜間,
        < 10 => MJ時間段 . 早高峯,
        < 16 => MJ時間段 . 日間,
        _ => MJ時間段 . 晚高峯,
    };

您添加一個 private enum 來將每個時間範圍轉換為一個離散值。然後,FF獲取時間段 方法使用關係模式和合取 or 模式。關係模式允許您使用 < 、 > 、 <= 或 >= 來測試一個數值。或模式用於測試一個表達式是否與一個或多個模式匹配。您還可以使用 and 模式來確保一個表達式匹配兩個不同的模式,以及 not 模式來測試一個表達式是否不匹配某個模式。

在您創建了這些方法之後,您可以使用帶有元組模式的另一個 switch 表達式來計算價格溢價。您還可以構建一個包含全部 16 個分支的 switch 表達式:

public decimal FF時間段收費比例 ( bool 入境 )
    => (FF是否週末 ( ), FF獲取時間段 ( ), 入境) switch
        {
            (true, _, _ ) => 1m, // 週末任意時間均正常收費
            (false, MJ時間段 . 早高峯, true ) => 2m, // 非週末早高峯入境加倍
            (false, MJ時間段 . 早高峯, false ) => 1m, // 非週末早高峯出境正常
            (false, MJ時間段 . 日間, _ ) => 1.5m, // 非週末日間出入境加收 50%
            (false, MJ時間段 . 晚高峯, true ) => 1m, // 非週末晚高峯入境正常
            (false, MJ時間段 . 晚高峯, false ) => 2m, // 非週末晚高峯出境加倍
            (false, MJ時間段 . 夜間, _ ) => 0.75m, // 非週末夜間減免 25%
            _ => throw new NotImplementedException ( "未提供任何日期時間及出入境信息……" ),
    };

最後,您可以刪除兩個按正常價格計費的高峯時段。一旦刪除了這些時段,您就可以在最終的切換臂中用 “_” 替換掉 “false” 或 “true”。但本例中不替換這些,因為或許可以在未來需要的時候增加規則。

這個例子突顯了模式匹配的一個優點:模式分支是按順序進行評估的。如果你將它們重新排列,使得較早的分支能夠處理較晚出現的情況,那麼編譯器就會提醒你存在無法執行的代碼。這些語言規則使得我們能夠有信心地進行上述簡化操作,同時還能確保代碼不會發生變化。

模式匹配使得某些類型的代碼更具可讀性,並且在無法向類中添加代碼的情況下,為面向對象技術提供了一種替代方案。雲技術使得數據和功能處於分離狀態。數據的形態及其操作並不一定同時被描述。在本教程中,您以完全不同於其原始功能的方式使用了現有的數據。模式匹配使您能夠編寫能夠覆蓋這些類型的功能,儘管您無法對其進行擴展。

下一步行動

您可以從 dotnet/samples 倉庫的 GitHub 上下載已完成的代碼。自行探索各種模式,並將此技術融入您的日常編程工作中。掌握這些技巧能為您提供另一種解決問題和開發新功能的方法。

如何使用 “try……catch” 結構來處理異常情況

try……catch 塊的目的是捕獲並處理由工作代碼產生的異常。有些異常可以在 catch 塊中得到處理,並且問題得以解決,而無需重新拋出該異常;然而,通常情況下,您所能做的唯一事情就是確保拋出適當的異常。

示例

在這個例子中,IndexOutOfRangeException 並非是最合適的異常類型;ArgumentOutOfRangeException 對該方法來説更為恰當,因為錯誤是由調用方傳入的索引參數所導致的。

static int FF獲取值 ( int [ ] 數組 , int 索引 )
    {
        try
            {
                return 數組 [ 索引 ];
            }
        catch ( IndexOutOfRangeException yc)  // CS0168
            {
                Console . WriteLine ( yc . Message );
                // 將 “IndexOutOfRangeException” 設置為新異常的 “InnerException”
            throw new ArgumentOutOfRangeException ( "索引參數超出範圍。" , yc );
            }
    }

評論

導致異常的代碼被封裝在 try 塊中。緊接着在它後面添加了一個 catch 語句,用於處理 IndexOutOfRangeException 錯誤,如果出現該錯誤的話。catch 塊會處理 IndexOutOfRangeException 錯誤,並拋出更合適的 ArgumentOutOfRangeException 代替。為了給調用者提供儘可能多的信息,可以考慮將原始異常指定為新異常的 InnerException 屬性值。由於 InnerException 屬性是隻讀的,您必須在新異常的構造函數中對其進行賦值。

如何使用 “finally” 語句來執行清理代碼

finally 語句的作用在於確保對對象(通常是指那些持有外部資源的對象)進行必要的清理工作能夠立即完成,即便在發生異常的情況下也是如此。這種清理工作的一個例子是,在使用完 FileStream 對象後立即調用其 Close 方法,而不是等待該對象由通用語言運行時(CLR)進行垃圾回收,如下所示:

static void FF寫入並不清理 ( )
    {
        FileStream? wj = null;
        FileInfo wjxx = new FileInfo ( "./file.txt" );

        wj = wjxx . OpenWrite ( );
        wj . WriteByte ( 0xF );

        wj . Close ( );
    }

示例

要將之前的代碼轉換為一個 “try……catch……finally” 語句,需要將清理代碼與工作代碼分開,具體如下。

static void FF寫入並清理 ( )
    {
        FileStream? wj = null;
        FileInfo? wjxx = null;

        try
            {
                wjxx = new FileInfo ( "./file.txt" );

                wj = fileInfo . OpenWrite ( );
                wj . WriteByte ( 0xF );
            }
        catch ( UnauthorizedAccessException yc )
            {
                Console . WriteLine ( yc . Message );
            }
        finally
            {
                wj? . Close ( );
            }
    }

因為在 try 塊中的任何時刻都可能發生異常(在調用 OpenWrite ( ) 之前,或者 OpenWrite ( ) 本身也可能出現失敗情況),所以我們無法保證在嘗試關閉文件時文件確實處於打開狀態。finally 塊添加了一個檢查,以確保在調用 Close 方法之前,FileStream 對象不為 null。如果沒有這個 null 檢查,finally 塊可能會拋出其自身的 NullReferenceException 異常,但在可能的情況下,應避免在 finally 塊中拋出異常。

在 “finally” 代碼塊中關閉數據庫連接也是一個不錯的選擇。因為連接數據庫服務器的次數有時是有限制的,所以您應該儘快關閉數據庫連接。如果在關閉連接之前拋出了異常,那麼使用 “finally” 代碼塊要比等待垃圾回收處理要好。

Add a new 评论

Some HTML is okay.