使用类和对象探索面向对象的编程Explore object oriented programming with classes and objects

本教程要求你有一台可用于开发的计算机。This tutorial expects that you have a machine you can use for development. .NET 教程 Hello World 10 分钟入门介绍了如何在 Windows、Linux 或 macOS 上设置本地开发环境。The .NET tutorial Hello World in 10 minutes has instructions for setting up your local development environment on Windows, Linux, or macOS. 熟悉开发工具不仅简要概述了将用到的命令,还收录了详细信息链接。A quick overview of the commands you'll use is in the Become familiar with the development tools with links to more details.

创建应用程序Create your application

使用终端窗口,创建名为 classes 的目录。Using a terminal window, create a directory named classes. 可以在其中生成应用程序。You'll build your application there. 将此目录更改为当前目录,并在控制台窗口中键入 dotnet new consoleChange to that directory and type dotnet new console in the console window. 此命令可创建应用程序。This command creates your application. 打开 Program.cs 。Open Program.cs. 应如下所示:It should look like this:

using System;

namespace classes
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

在本教程中,将要新建表示银行帐户的类型。In this tutorial, you're going to create new types that represent a bank account. 通常情况下,开发者都会在不同的文本文件中定义每个类。Typically developers define each class in a different text file. 这样可以更轻松地管理不断增大的程序。That makes it easier to manage as a program grows in size. 在 classes 目录中,新建名为 BankAccount.cs 的文件。Create a new file named BankAccount.cs in the classes directory.

此文件包含“银行帐户”定义。This file will contain the definition of a bank account. 面向对象的编程组织代码的方式为,创建类形式的类型。Object Oriented programming organizes code by creating types in the form of classes. 这些类包含表示特定实体的代码。These classes contain the code that represents a specific entity. BankAccount 类表示银行帐户。The BankAccount class represents a bank account. 代码通过方法和属性实现特定操作。The code implements specific operations through methods and properties. 在本教程中,银行帐户支持以下行为:In this tutorial, the bank account supports this behavior:

  1. 用一个 10 位数唯一标识银行帐户。It has a 10-digit number that uniquely identifies the bank account.
  2. 用字符串存储一个或多个所有者名称。It has a string that stores the name or names of the owners.
  3. 可以检索余额。The balance can be retrieved.
  4. 接受存款。It accepts deposits.
  5. 接受取款。It accepts withdrawals.
  6. 初始余额必须是正数。The initial balance must be positive.
  7. 取款后的余额不能是负数。Withdrawals cannot result in a negative balance.

定义银行帐户类型Define the bank account type

首先,创建定义此行为的类的基本设置。You can start by creating the basics of a class that defines that behavior. 具体如下所示:It would look like this:

using System;

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)
        {
        }
    }
}

继续操作前,先来看看已经生成的内容。Before going on, let's take a look at what you've built. 借助 namespace 声明,可以按逻辑组织代码。The namespace declaration provides a way to logically organize your code. 由于本教程的篇幅较小,因此所有代码都将添加到一个命名空间中。This tutorial is relatively small, so you'll put all the code in one namespace.

public class BankAccount 定义要创建的类或类型。public class BankAccount defines the class, or type, you are creating. 类声明后面 {} 中的所有内容定义了类行为。Everything inside the { and } that follows the class declaration defines the behavior of the class. BankAccount 类有五个成员。There are five members of the BankAccount class. 前三个成员是属性。The first three are properties. 属性是数据元素,可以包含强制执行验证或其他规则的代码。Properties are data elements and can have code that enforces validation or other rules. 最后两个成员是方法。The last two are methods. 方法是执行一个函数的代码块。Methods are blocks of code that perform a single function. 读取每个成员的名称应该能够为自己或其他开发者提供了解类用途的足够信息。Reading the names of each of the members should provide enough information for you or another developer to understand what the class does.

打开新帐户Open a new account

要实现的第一个功能是打开银行帐户。The first feature to implement is to open a bank account. 打开帐户时,客户必须提供初始余额,以及此帐户的一个或多个所有者的相关信息。When a customer opens an account, they must supply an initial balance, and information about the owner or owners of that account.

新建 BankAccount 类型的对象意味着,定义可分配这些值的构造函数。Creating a new object of the BankAccount type means defining a constructor that assigns those values. 构造函数是与类同名的成员。A constructor is a member that has the same name as the class. 用于初始化相应类类型的对象。It is used to initialize objects of that class type. 将以下构造函数添加到 BankAccount 类型中:Add the following constructor to the BankAccount type:

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

构造函数是在使用 new 创建对象时进行调用。Constructors are called when you create an object using new. 将 Program.cs 中的代码行 Console.WriteLine("Hello World!"); 替换为以下代码行(将 <name> 替换为自己的名称):Replace the line Console.WriteLine("Hello World!"); in Program.cs with the following line (replace <name> with your name):

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

键入 dotnet run,看看会发生什么。Type dotnet run to see what happens.

有没有注意到帐号为空?Did you notice that the account number is blank? 是时候解决这个问题了。It's time to fix that. 帐号应在构造对象时分配。The account number should be assigned when the object is constructed. 但不得由调用方负责创建。But it shouldn't be the responsibility of the caller to create it. BankAccount 类代码应了解如何分配新帐号。The BankAccount class code should know how to assign new account numbers. 这样做的简单方法是从一个 10 位数开始。A simple way to do this is to start with a 10-digit number. 帐号随每个新建的帐户而递增。Increment it when each new account is created. 最后,在构造对象时,存储当前的帐号。Finally, store the current account number when an object is constructed.

将以下成员声明添加到 BankAccount 类中:Add the following member declaration to the BankAccount class:

private static int accountNumberSeed = 1234567890;

此为数据成员。This is a data member. 它是 private,这意味着只能通过 BankAccount 类中的代码访问它。It's private, which means it can only be accessed by code inside the BankAccount class. 这是一种分离公共责任(如拥有帐号)与私有实现(如何生成帐号)的方法。它也是 static,这意味着它由所有 BankAccount 对象共享。It's a way of separating the public responsibilities (like having an account number) from the private implementation (how account numbers are generated.) It is also static, which means it is shared by all of the BankAccount objects. 非静态变量的值对于 BankAccount 对象的每个实例是唯一的。The value of a non-static variable is unique to each instance of the BankAccount object. 将下面两行代码添加到构造函数,以分配帐号:Add the following two lines to the constructor to assign the account number:

this.Number = accountNumberSeed.ToString();
accountNumberSeed++;

键入 dotnet run 看看结果如何。Type dotnet run to see the results.

创建存款和取款Create deposits and withdrawals

银行帐户类必须接受存款和取款,才能正常运行。Your bank account class needs to accept deposits and withdrawals to work correctly. 接下来,将为银行帐户创建每笔交易日记,实现存款和取款。Let's implement deposits and withdrawals by creating a journal of every transaction for the account. 与仅更新每笔交易余额相比,这样做有一些优点。That has a few advantages over simply updating the balance on each transaction. 历史记录可用于审核所有交易,并管理每日余额。The history can be used to audit all transactions and manage daily balances. 通过在需要时根据所有交易的历史记录计算余额,单笔交易中修正的任何错误将会在下次计算余额时得到正确体现。By computing the balance from the history of all transactions when needed, any errors in a single transaction that are fixed will be correctly reflected in the balance on the next computation.

接下来,先新建表示交易的类型。Let's start by creating a new type to represent a transaction. 这是一个没有任何责任的简单类型。This is a simple type that doesn't have any responsibilities. 但需要有多个属性。It needs a few properties. 新建名为 Transaction.cs 的文件。Create a new file named Transaction.cs. 向新文件添加以下代码:Add the following code to it:

using System;

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)
        {
            this.Amount = amount;
            this.Date = date;
            this.Notes = note;
        }
    }
}

现在,将 Transaction 对象的 List<T> 添加到 BankAccount 类中。Now, let's add a List<T> of Transaction objects to the BankAccount class. 添加以下声明:Add the following declaration:

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

List<T> 类要求导入不同的命名空间。The List<T> class requires you to import a different namespace. 在 BankAccount.cs 的开头,添加以下代码:Add the following at the beginning of BankAccount.cs:

using System.Collections.Generic;

现在,更改 Balance 的报告方式。Now, let's change how the Balance is reported. 可以通过对所有交易的值进行求和计算余额。It can be found by summing the values of all transactions. BankAccount 类中 Balance 的声明修改为如下所示:Modify the declaration of Balance in the BankAccount class to the following:

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

        return balance;
    }
}

此示例反映了属性的一个重要方面。This example shows an important aspect of properties. 现在,可以在其他程序员要求获取余额时计算值。You're now computing the balance when another programmer asks for the value. 计算会枚举所有交易,总和即为当前余额。Your computation enumerates all transactions, and provides the sum as the current balance.

接下来,实现 MakeDepositMakeWithdrawal 方法。Next, implement the MakeDeposit and MakeWithdrawal methods. 这些方法将强制执行最后两条规则:初始余额必须为正数,且取款后的余额不能是负数。These methods will enforce the final two rules: that the initial balance must be positive, and that any withdrawal must not create a negative balance.

这就引入了异常的概念。This introduces the concept of exceptions. 指明方法无法成功完成工作的标准方法是抛出异常。The standard way of indicating that a method cannot complete its work successfully is to throw an exception. 异常类型及其关联消息描述了错误。The type of exception and the message associated with it describe the error. 在此示例中,如果存款金额为负数,MakeDeposit 方法会抛出异常。Here, the MakeDeposit method throws an exception if the amount of the deposit is negative. 如果取款金额为负数,或取款后的余额为负数,MakeWithdrawal 方法会抛出异常:The MakeWithdrawal method throws an exception if the withdrawal amount is negative, or if applying the withdrawal results in a negative balance:

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 语句将引发异常 。The throw statement throws an exception. 当前块执行结束,将控制权移交给在调用堆栈中发现的第一个匹配的 catch 块。Execution of the current block ends, and control transfers to the first matching catch block found in the call stack. 添加 catch 块可以稍后再测试一下此代码。You'll add a catch block to test this code a little later on.

构造函数应进行一处更改,更改为添加初始交易,而不是直接更新余额。The constructor should get one change so that it adds an initial transaction, rather than updating the balance directly. 由于已编写 MakeDeposit 方法,因此通过构造函数调用它。Since you already wrote the MakeDeposit method, call it from your constructor. 完成的构造函数应如下所示:The finished constructor should look like this:

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

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

DateTime.Now 是返回当前日期和时间的属性。DateTime.Now is a property that returns the current date and time. Main 方法中添加几个存款和取款,对此进行测试:Test this by adding a few deposits and withdrawals in your Main method:

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

接下来,尝试创建初始余额为负数的帐户,测试能否捕获到错误条件:Next, test that you are catching error conditions by trying to create an account with a negative balance:

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

使用 trycatch 语句,标记可能会引发异常的代码块,并捕获预期错误。You use the try and catch statements to mark a block of code that may throw exceptions and to catch those errors that you expect. 可以使用相同的技术,测试代码能否在取款后的余额为负数时引发异常:You can use the same technique to test the code that throws an exception for a negative balance:

// 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,试运行看看。Save the file and type dotnet run to try it.

挑战 - 记录所有交易Challenge - log all transactions

为了完成本教程,可以编写 GetAccountHistory 方法,为交易历史记录创建 stringTo finish this tutorial, you can write the GetAccountHistory method that creates a string for the transaction history. 将此方法添加到 BankAccount 类型中:Add this method to the BankAccount type:

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

    report.AppendLine("Date\tAmount\tNote");
    foreach (var item in allTransactions)
    {
        report.AppendLine($"{item.Date.ToShortDateString()}\t{item.Amount}\t{item.Notes}");
    }

    return report.ToString();
}

上面的代码使用 StringBuilder 类,设置包含每个交易行的字符串的格式。This uses the StringBuilder class to format a string that contains one line for each transaction. 在前面的教程中,也遇到过字符串格式设置代码。You've seen the string formatting code earlier in these tutorials. 新增的一个字符为 \tOne new character is \t. 这用于插入选项卡,从而设置输出格式。That inserts a tab to format the output.

添加以下代码行,在 Program.cs 中对它进行测试:Add this line to test it in Program.cs:

Console.WriteLine(account.GetAccountHistory());

键入 dotnet run 看看结果如何。Type dotnet run to see the results.

后续步骤Next Steps

如果遇到问题,可以在 GitHub 存储库中查看本教程的源代码If you got stuck, you can see the source for this tutorial in our GitHub repo

恭喜,你已完成我们的所有 C# 简介教程。Congratulations, you've finished all our introduction to C# tutorials. 若要了解详细信息,请继续学习我们的教程If you're eager to learn more, try more of our tutorials