探索使用類別與物件的物件導向程式設計

在本教學課程中,您會建置一個主控台應用程式,並了解 C# 語言的基本物件導向功能。

必要條件

建立您的應用程式

使用終端機視窗,建立名為 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. 它具有能唯一識別銀行帳戶的 10 位數數字。
  2. 它具有能儲存擁有者一個或多個名稱的字串。
  3. 可擷取餘額。
  4. 它接受存款。
  5. 它接受提款。
  6. 初始餘額必須為正數。
  7. 提款不可使餘額成為負數。

定義銀行帳戶類型

您可以從建立能定義該行為之類別的基礎項目來開始。 使用 File:New 命令建立新的檔案。 命名為 BankAccount.cs。 將下列程式碼新增至 BankAccount.cs 檔案:

namespace Classes;

public class BankAccount
{
    public string Number { get; }
    public string Owner { get; set; }
    public decimal Balance { get; }

    public void MakeDeposit(decimal amount, DateTime date, string note)
    {
    }

    public void MakeWithdrawal(decimal amount, DateTime date, string note)
    {
    }
}

在繼續之前,讓我們先查看您所建置的內容。 namespace 宣告能提供以邏輯方式組織程式碼的方式。 此教學課程的規模相對較小,所以您會將所有程式碼置於一個命名空間。

public class BankAccount 能定義您要建立的類別 (或類型)。 類別宣告後面的 {} 之內的所有內容,皆定義該類別的狀態和行為。 BankAccount 類別有五個成員。 前三個為屬性。 屬性是資料元素,且可以具有強制執行驗證或其他規則的程式碼。 後兩個為方法。 方法是執行單一函式的程式碼區塊。 閱讀每個成員的名稱,應該能提供足夠的資訊,以供您或其他開發人員了解該類別的功能。

開啟新帳戶

第一個要實作的功能是開啟一個銀行帳戶。 當客戶開啟帳戶時,他們必須提供初始餘額,以及該帳戶的一或多個擁有者的相關資訊。

建立一個 BankAccount 類型的新物件,表示定義一個能指派那些值的建構函式建構函式是具有和該類別相同名稱的成員。 用來初始化該類別類型的物件。 將下列建構函式新增到 BankAccount 類型。 在 MakeDeposit 宣告之上放置下列程式碼:

public BankAccount(string name, decimal initialBalance)
{
    this.Owner = name;
    this.Balance = initialBalance;
}

上述程式碼會藉由包含 this 限定詞來識別要建構之物件的屬性。 該限定詞通常是選擇性的,並省略。 您也可以撰寫:

public BankAccount(string name, decimal initialBalance)
{
    Owner = name;
    Balance = initialBalance;
}

只有在區域變數或參數的名稱與該欄位或屬性相同時,才需要 this 限定詞。 除非必要,否則本文其餘部分會省略 this 限定詞。

當您使用 new 建立物件時,系統便會呼叫建構函式。 以下列行取代 program.cs 中的 Console.WriteLine("Hello World!"); 程式碼 (以您的名字取代 <name>):

using Classes;

var account = new BankAccount("<name>", 1000);
Console.WriteLine($"Account {account.Number} was created for {account.Owner} with {account.Balance} initial balance.");

執行到目前為止所建置的內容。 如果您使用 Visual Studio,請從 [偵錯] 功能表選取 [開始但不偵錯]。 如果您使用命令列,請在您已建立專案的目錄中輸入 dotnet run

您是否注意到帳戶號碼是空白的? 讓我們來修正此情況。 帳戶號碼應該要在物件建構時指派。 但帳戶號碼不應該由呼叫者負責建立。 BankAccount 類別程式碼應該要知道如何指派新的帳戶號碼。 最簡單的方法是從 10 位數的數字開始。 建立每個新帳戶時就增加它。 最後,在物件完成建構時儲存目前的帳戶號碼。

請將成員宣告加入 BankAccount 類別。 將下列程式程式碼放在 BankAccount 類別開頭的左大括弧 { 後面:

private static int s_accountNumberSeed = 1234567890;

accountNumberSeed 是一個資料成員。 它是 private,這表示它只能由 BankAccount 類別中的程式碼存取。 這是將公開責任 (例如具有帳戶號碼) 和私用實作 (帳戶號碼產生的方式) 區隔開來的方法。 它也是 static,這表示它是由所有 BankAccount 物件共用的。 非靜態變數的值對於每個 BankAccount 物件的執行個體而言都是唯一的。 accountNumberSeedprivate static 欄位,因此根據 C# 命名慣例,其具有 s_ 前置詞。 s 表示 static,而 _ 表示 private 欄位。 將下列兩行新增到建構函式來指派帳戶號碼。 將它們放在顯示 this.Balance = initialBalance 的行後面:

Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;

輸入 dotnet run 來查看結果。

建立存款和提款

您的銀行帳戶必須能接受存款及提款,才算能正常運作。 讓我們透過建立帳戶每一筆交易的日誌,來實作存款和提款。 相較於單純地在每次交易時更新餘額,追蹤每一項交易能提供數個好處。 該記錄可用來對所有交易進行稽核,以及管理每日餘額。 在必要時計算所有交易記錄的餘額,確保在任何單一交易中已修正的錯誤都會正確地在下一次計算中反映出來。

讓我們從建立代表交易的新類型開始。 交易是一個不具任何責任的簡單類型。 它需要幾個屬性。 建立名為 Transaction.cs 的新檔案。 將下列程式碼加入該檔案:

namespace Classes;

public class Transaction
{
    public decimal Amount { get; }
    public DateTime Date { get; }
    public string Notes { get; }

    public Transaction(decimal amount, DateTime date, string note)
    {
        Amount = amount;
        Date = date;
        Notes = note;
    }
}

現在,讓我們將 Transaction 物件的 List<T> 新增到 BankAccount 類別。 在 BankAccount.cs 檔案中的建構函式後面新增下列宣告:

private List<Transaction> _allTransactions = new List<Transaction>();

現在,讓我們正確地計算 Balance。 目前的餘額可以透過針對所有交易的值進行加總來取得。 程式碼目前只能取得帳戶的初始餘額,因此您必須更新 Balance 屬性。 以下列程式碼取代 BankAccount.cs 中的行 public decimal Balance { get; }

public decimal Balance
{
    get
    {
        decimal balance = 0;
        foreach (var item in _allTransactions)
        {
            balance += item.Amount;
        }

        return balance;
    }
}

此範例顯示出屬性的一個重要層面。 現在您會在另一個程式要求餘額時計算該值。 您的計算會列舉所有交易,並提供總和作為目前的餘額。

接下來,請實作 MakeDepositMakeWithdrawal 方法。 這些方法會強制執行最後兩個規則:初始餘額必須為正數,且所有提款都不能產生負數的餘額。

這些規則引進 例外狀況 的概念。 這是指出方法若無法完成其工作便應擲回例外狀況的標準方法。 例外狀況的類型和與它相關的訊息會描述該錯誤。 在這裡,如果存款的金額是不大於 0,MakeDeposit 方法便會擲回例外狀況。 如果提款金額是不大於 0,或如果套用提款金額會造成負數的餘額,則 MakeWithdrawal 方法會擲回例外狀況。 將下列程式碼新增至宣告後面的 _allTransactions 清單:

public void MakeDeposit(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of deposit must be positive");
    }
    var deposit = new Transaction(amount, date, note);
    _allTransactions.Add(deposit);
}

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    if (Balance - amount < 0)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

throw 陳述式擲回例外狀況。 目前區塊的執行會結束,而且控制權會移轉給呼叫堆疊中找到最初相符的 catch 區塊。 您會在稍後新增 catch 區塊來測試此程式碼。

應該對建構函式進行一項變更來使它會新增初始交易,而不是直接更新餘額。 由於您已撰寫 MakeDeposit 方法,請從建構函式呼叫它。 完成的建構函式應該看起來如下:

public BankAccount(string name, decimal initialBalance)
{
    Number = s_accountNumberSeed.ToString();
    s_accountNumberSeed++;

    Owner = name;
    MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

DateTime.Now 是會傳回目前日期和時間的屬性。 在 Main 方法中新增一些存金和退金來測試此程式碼,並遵循建立新 BankAccount 的程式碼:

account.MakeWithdrawal(500, DateTime.Now, "Rent payment");
Console.WriteLine(account.Balance);
account.MakeDeposit(100, DateTime.Now, "Friend paid me back");
Console.WriteLine(account.Balance);

接下來,透過建立具有負數餘額的帳戶,來測試是否能攔截到錯誤情況。 在您剛才新增的上述程式碼之後新增下列程式碼:

// Test that the initial balances must be positive.
BankAccount invalidAccount;
try
{
    invalidAccount = new BankAccount("invalid", -55);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine("Exception caught creating account with negative balance");
    Console.WriteLine(e.ToString());
    return;
}

使用 try-catch 陳述式來標記可能會擲回例外狀況的程式碼區塊,並攔截您所預期的那些錯誤。 您可以使用同樣的技巧來測試會針對負數餘額擲回例外狀況的程式碼。 在 Main 方法中的 invalidAccount 宣告之前新增下列程式碼:

// Test for a negative balance.
try
{
    account.MakeWithdrawal(750, DateTime.Now, "Attempt to overdraw");
}
catch (InvalidOperationException e)
{
    Console.WriteLine("Exception caught trying to overdraw");
    Console.WriteLine(e.ToString());
}

儲存檔案並輸入 dotnet run 來嘗試它。

挑戰 - 記錄所有交易

若要完成此教學課程,您可以撰寫會針對交易記錄建立 stringGetAccountHistory 方法。 將此方法新增到 BankAccount 型別:

public string GetAccountHistory()
{
    var report = new System.Text.StringBuilder();

    decimal balance = 0;
    report.AppendLine("Date\t\tAmount\tBalance\tNote");
    foreach (var item in _allTransactions)
    {
        balance += item.Amount;
        report.AppendLine($"{item.Date.ToShortDateString()}\t{item.Amount}\t{balance}\t{item.Notes}");
    }

    return report.ToString();
}

此歷程記錄使用 StringBuilder 類別來設定針對每個交易包含單一行之字串的格式。 您稍早已經在這些教學課程中看過字串格式設定的程式碼。 一個新的字元是 \t。 它會插入定位字元以設定輸出的格式。

新增下列行以在 Program.cs 中測試它:

Console.WriteLine(account.GetAccountHistory());

執行您的程式以查看結果。

下一步

如果遇到問題,您可以在我們的 GitHub 存放庫 \(英文\) 中查看此教學課程的原始程式碼。

您可以繼續進行物件導向程式設計教學課程。

您可以在下列文章深入了解這些概念: