C--面向对象编程入门指南-二-

45 阅读1小时+

C# 面向对象编程入门指南(二)

原文:Beginning C# object-oriented programming

协议:CC BY-NC-SA 4.0

七、创建类层次结构

在第六章中,您学习了如何创建类、添加属性和方法,以及在客户端代码中实例化类的对象实例。本章介绍了继承和多态的概念。

继承是任何 OOP 语言最强大和最基本的特性之一。使用继承,您可以创建封装通用功能的基类。其他类可以从这些基类派生。派生类继承基类的属性和方法,并根据需要扩展功能。

第二个基本的 OOP 特性是多态。多态让基类定义必须由任何派生类实现的方法。基类定义了派生类必须遵守的消息签名,但是方法的实现代码留给了派生类。多态的强大之处在于,客户知道他们可以用同样的方式实现基类的方法。即使方法的内部处理可能不同,客户端也知道方法的输入和输出是相同的。

阅读本章后,您将了解以下内容:

  • 如何创建和使用基类
  • 如何创建和使用派生类
  • 访问修饰符如何控制继承
  • 如何覆盖基类方法
  • 如何实现接口
  • 如何通过继承和接口实现多态

理解继承

任何 OOP 语言最强大的特性之一就是继承。继承是创建基类的能力,该基类具有可在从基类派生的类中使用的属性和方法。

创建基类和派生类

继承的目的是创建一个基类,它封装了相同类型的派生类可以使用的属性和方法。例如,您可以创建一个基类Account。在Account类中定义了一个GetBalance方法。然后,您可以创建两个单独的类:SavingsAccountCheckingAccount。因为SavingsAccount类和CheckingAccount类使用相同的逻辑来检索余额信息,所以它们从基类Account继承了GetBalance方法。这使您能够创建一个更易于维护和管理的通用代码库。

然而,派生类不限于基类的属性和方法。派生类可能需要额外的方法和属性,这些方法和属性是它们所特有的。例如,从支票账户提款的业务规则可能要求维持最低余额。然而,从储蓄账户提款可能不需要最低余额。在这个场景中,派生的CheckingAccountSavingsAccount类都需要它们自己对Withdraw方法的唯一定义。

要在 C# 中创建一个派生类,需要输入类名,后跟一个冒号(:)和基类名。以下代码演示了如何创建一个从Account基类派生的CheckingAccount类:

class Account
{
    long _accountNumber;

    public long AccountNumber
    {
        get { return _accountNumber; }
        set { _accountNumber = value; }
    }
    public double GetBalance()
    {
        //code to retrieve account balance from database
        return (double)10000;
    }
}

class CheckingAccount : Account
{
    double _minBalance;

    public double MinBalance
    {
        get { return _minBalance; }
        set { _minBalance = value; }
    }
    public void Withdraw(double amount)
    {
        //code to withdraw from account
    }
}

下面的代码可以由创建一个对象实例CheckingAccount的客户端实现。注意,客户端感觉不到对GetBalance方法的调用和对Withdraw方法的调用之间的区别。在这种情况下,客户端不知道Account类;相反,这两种方法似乎都是由CheckingAccount定义的。

CheckingAccount oCheckingAccount = new CheckingAccount();
double balance;
oCheckingAccount.AccountNumber = 1000;
balance = oCheckingAccount.GetBalance();
oCheckingAccount.Withdraw(500);

创建密封类

默认情况下,任何 C# 类都可以被继承。当创建可以被继承的类时,你必须小心不要以这样的方式修改它们,以至于派生类不再像预期的那样工作。如果不小心,您可能会创建难以管理和调试的复杂继承链。例如,假设您基于Account类创建了一个派生的CheckingAccount类。另一个程序员可以基于CheckingAccount创建一个派生类,并以你从未想过的方式使用它。(这很容易发生在沟通和设计方法不佳的大型编程团队中。)

通过使用sealed修饰符,您可以创建您知道不会从其派生的类。这种类型的班级通常被称为密封最终班级。通过使一个类不可继承,可以避免与修改基类代码相关的复杂性和开销。下面的代码演示了在构造类定义时如何使用sealed修饰符:

sealed class CheckingAccount : Account

创建抽象类

在这个例子中,客户端可以通过派生的CheckingAccount类的实例或者直接通过基类Account的实例来访问GetBalance方法。有时,你可能想要一个不能被客户端代码实例化的基类。必须通过派生类来访问该类的方法和属性。在这种情况下,您使用abstract修饰符来构造基类。以下代码显示了带有abstract修饰符的Account类定义:

abstract class Account

这使得Account类成为一个抽象类。为了让客户端能够访问GetBalance方法,它们必须创建一个派生的CheckingAccount类的实例。

在基类中使用访问修饰符

当使用继承设置类层次结构时,必须管理如何访问类的属性和方法。到目前为止,你看到的两个访问修饰符是 public 和 private。如果基类的方法或属性被公开为 public,则派生类和派生类的任何客户端都可以访问它。如果将基类的属性或方法公开为私有,则派生类或客户端不能直接访问它。

您可能希望向派生类公开基类的属性或方法,而不是向派生类的客户端公开。在这种情况下,使用受保护的访问修饰符。下面的代码演示了 protected 访问修饰符的用法:

protected double GetBalance()
{
    //code to retrieve account balance from database
    return (double)10000;
}

通过将GetBalance方法定义为受保护的,它可以被派生类CheckingAccount访问,但是不能被访问CheckingAccount类实例的客户端代码访问。

活动 7-1。使用基类和派生类实现继承 NCE

在本活动中,您将熟悉以下内容:

  • 创建基类和继承其方法的派生类
  • 使用受保护的访问修饰符来限制基类方法的使用
  • 创建抽象基类

创建基类和派生类

要创建Account类,请遵循以下步骤:

  1. 启动 Visual Studio。选择文件image打开image项目。

  2. 导航到 Activity7_1Starter 文件夹,单击 Activity7_1.sln 文件,然后单击“打开”。当项目打开时,它将包含一个出纳员表单。稍后您将使用这个表单来测试您创建的类。

  3. 在“解决方案资源管理器”窗口中,右键单击项目节点并选择“添加image类”。

  4. 在“添加新项”对话框中,将类文件重命名为 Account.cs,然后单击“打开”。Account.cs 文件被添加到项目中,Account类定义代码被添加到该文件中。

  5. Add the following code to the class definition file to create the private instance variable (private is the default modifier for instance variables):

    int _accountNumber;

  6. Add the following GetBalance method to the class definition:

    public double GetBalance(int accountNumber)

    {

    _accountNumber = accountNumber;

    //Data normally retrieved from database.

    if (_accountNumber == 1)

    {

    return 1000;

    }

    else if (_accountNumber == 2)

    {

    return 2000;

    }

    else

    {

    return -1; //Account number is incorrect

    }

    }

  7. After the Account class, add the following code to create the CheckingAccount and SavingsAccount derived classes:

    class CheckingAccount : Account

    {

    }

    class SavingsAccount : Account

    {

    }

  8. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

测试类别

要测试这些类,请遵循以下步骤:

  1. 在代码编辑器中打开柜员表单,找到btnGetBalance点击事件代码。

  2. Inside the event procedure, prior to the try block, declare and instantiate a variable of type CheckingAccount called oCheckingAccount, a variable of type SavingsAccount called oSavingsAccount, and a variable of type Account called oAccount:

    CheckingAccount oCheckingAccount = new CheckingAccount();

    SavingsAccount oSavingsAccount = new SavingsAccount();

    Account oAccount = new Account();

  3. Depending on which radio button is selected, call the GetBalance method of the appropriate object and pass the account number value from the AccountNumber text box. Show the return value in the Balance text box. Place the following code in the try block prior to the catch statement:

    if (rdbChecking.Checked)

    {

    txtBalance.Text =

    oCheckingAccount.GetBalance(int.Parse(txtAccountNumber.Text)).ToString();

    }

    else if (rdbSavings.Checked)

    {

    txtBalance.Text =

    oSavingsAccount.GetBalance(int.Parse(txtAccountNumber.Text)).ToString();

    }

    else if (rdbGeneral.Checked)

    {

    txtBalance.Text =

    oAccount.GetBalance(int.Parse(txtAccountNumber.Text)).ToString();

    }

  4. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  5. 选择调试image开始运行项目。输入账号 1,并单击支票账户类型的获取余额按钮。你应该得到 1000 英镑的余额。测试其他帐户类型。您应该得到相同的结果,因为所有的类都在使用基类中定义的相同的GetBalance函数。

  6. 测试后,关闭窗体,这将停止调试器。

将基类方法的使用限制在其派生类中

此时,基类的GetBalance方法是公共的,这意味着它可以被派生类及其客户端访问。让我们改变这一点,使GetBalance方法只能被派生类单独访问,而不能被它们的客户端访问。要以这种方式保护GetBalance方法,请遵循以下步骤:

  1. 找到Account类的GetBalance方法。

  2. GetBalance方法的访问修饰符从public改为protected

  3. 切换到 frmTeller 代码编辑器,找到btnGetBalance click 事件代码。

  4. 将光标悬停在对oCheckingAccount对象的GetBalance方法的调用上。您将看到一条警告,指出它是一个受保护的函数,在此上下文中不可访问。

  5. 注释掉trycatch语句之间的代码。

  6. 切换到 Account.cs 代码编辑器。

  7. Add the following code to create the following private instance variable to the SavingsAccount class definition file:

    double _dblBalance;

  8. Add the following Withdraw method to the SavingsAccount class. This function calls the protected method of the Account base class:

    public double Withdraw(int accountNumber, double amount)

    {

    _dblBalance = GetBalance(accountNumber);

    if (_dblBalance >= amount)

    {

    _dblBalance - = amount;

    return _dblBalance;

    }

    else

    {

    Return -1; //Not enough funds

    }

    }

  9. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

测试受保护的基类方法

要测试Withdraw方法,请遵循以下步骤:

  1. 在代码编辑器中打开 frmTeller 表单,找到btnWithdraw click 事件代码。

  2. Inside the event procedure, prior to the try block, declare and instantiate a variable of type SavingsAccount called oSavingsAccount.

    SavingsAccount oSavingsAccount = new SavingsAccount();

  3. Call the Withdraw method of the oSavingsAccount. Pass the account number value from the AccountNumber text box and the withdrawal amount from the Amount text box. Show the return value in the Balance text box. Place the following code in the try block prior to the catch statement:

    txtBalance.Text = oSavingsAccount.Withdraw

    (int.Parse(txtAccountNumber.Text),double.Parse(txtAmount.Text)).ToString();

  4. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  5. 选择调试image开始运行项目。

  6. 通过输入账号 1 和取款金额 200 来测试SavingsAccount类的Withdraw方法。点击撤销按钮。您应该会得到 800 英镑的余额。

  7. 输入账号 1,取款金额 2000。点击撤销按钮。您应该得到-1,表示资金不足。

  8. 测试完Withdraw方法后,关闭表单,这将停止调试器。

将基类的所有成员限制在其派生类中使用

因为Account基类是公共的,所以它可以被派生类的客户端实例化。您可以通过使Account基类成为抽象类来改变这一点。抽象类只能被它的派生类访问,不能被它们的客户端实例化和访问。要创建并测试抽象类的可访问性,请遵循以下步骤:

  1. 在 Account.cs 代码中找到Account类定义。

  2. Add the abstract keyword to the class definition code, like so:

    abstract class Account

  3. Select Build image Build Solution. You should receive a build error in the Error List window. Find the line of code causing the error.

    Account oAccount = new Account();

  4. 注释掉该行代码,然后再次选择 Build image Build Solution。它现在应该没有任何错误。

  5. 保存并关闭项目。

重写基类的方法

当派生类从基类继承方法时,它继承该方法的实现。作为基类的设计者,您可能希望让派生类以自己独特的方式实现该方法。这就是所谓的重写基类方法。

默认情况下,派生类不能覆盖其基类的实现代码。要允许覆盖基类方法,必须在方法定义中包含关键字virtual。在派生类中,使用相同的方法签名定义一个方法,并使用override关键字指示它正在重写一个基类方法。下面的代码演示了在Account基类中创建一个可重写的Deposit方法:

public virtual  void Deposit(double amount)
{
    //Base class implementation
}

要覆盖派生的CheckingAccount类中的Deposit方法,请使用以下代码:

public override void Deposit(double amount)
{
    //Derived class implementation
}

需要注意的一个场景是当一个派生类从基类继承,第二个派生类从第一个派生类继承。当一个方法重写基类中的一个方法时,它在默认情况下变成可重写的。为了限制重写方法在继承链上被重写,您必须在派生类的方法定义中在override关键字之前包含sealed关键字。如果CheckingAccount类派生自,CheckingAccount类中的以下代码防止覆盖Deposit方法:

public sealed override void Deposit(double amount)
{
    //Derived class implementation
}

当您指示基类方法是可重写的时,派生类可以选择重写该方法或使用基类提供的实现。在某些情况下,您可能希望使用基类方法作为派生类的模板。基类没有实现代码,但用于定义派生类中使用的方法签名。这种类型的类被称为抽象基类。你用抽象关键字定义类和方法。以下代码用于通过抽象Deposit方法创建抽象Account基类:

public abstract class Account
{
    public abstract void Deposit(double amount);
}

注意,因为在基类中没有为Deposit方法定义实现代码,所以省略了方法体。

从基类调用派生类方法

可能会出现这样的情况:从基类的另一个方法调用基类中可重写的方法,而派生类重写基类的方法。当从派生类的实例调用基类方法时,基类将调用派生类的重写方法。下面的代码显示了这种情况的一个例子。一个CheckingAccount基类包含一个可重写的GetMinBalance方法。从CheckingAccount类继承而来的InterestBearingCheckingAccount类覆盖了GetMinBalance方法。

class CheckingAccount
{
    private double _balance = 2000;

    public double Balance
    {
        get { return _balance; }
    }
    public virtual double GetMinBalance()
    {
        return 200;
    }
    public virtual void Withdraw(double amount)
    {
        double minBalance = GetMinBalance();
        if (minBalance < (Balance - amount))
        {
            _balance -= amount;
        }
        else
        {
            throw new Exception("Minimum balance error.");
        }
    }
}
class InterestBearingCheckingAccount : CheckingAccount
{
    public override double GetMinBalance()
    {
        return 1000;
    }
}

客户端创建了一个InterestBearingCheckingAccount类的对象实例,并调用了Withdraw方法。在这种情况下,执行InterestBearingCheckingAccount类的被覆盖的GetMinimumBalance方法,并使用最小余额 1000。

InterestBearingCheckingAccount oAccount = new InterestBearingCheckingAccount();
oAccount.Withdraw(500);

当调用Withdraw方法时,您可以在它前面加上this限定符:

double minBalance = this.GetMinBalance();

因为如果没有使用限定符,那么this限定符就是默认的限定符,所以代码的执行方式和前面演示的一样。执行该方法的最大派生类实现(已实例化)。换句话说,如果客户端实例化了一个InterestBearingCheckingAccount类的实例,如前所述,基类对GetMinimumBalance的调用是针对派生类的实现的。另一方面,如果客户创建了一个CheckingAccount类的实例,基类对GetMinimumBalance的调用是针对它自己的实现的。

从派生类中调用基类方法

在某些情况下,您可能希望开发一个派生类方法,该方法仍然使用基类中的实现代码,但也用自己的实现代码来扩充它。在这种情况下,您在派生类中创建一个重写方法,并使用base限定符调用基类中的代码。下面的代码演示了base限定符的用法:

public override void Deposit(double amount)
{
    base.Deposit(amount);
    //Derived class implementation.
}

基类的重载方法

派生类继承的方法可以被重载。重载类的方法签名必须使用与重载方法相同的名称,但参数列表必须不同。这与重载同一类的方法是一样的。下面的代码演示派生方法的重载:

class CheckingAccount
{
    public void Withdraw(double amount)
    {
    }
}
class InterestBearingCheckingAccount : CheckingAccount
{
    public void Withdraw(double amount, double minBalance)
    {
    }
}

实例化InterestBearingCheckingAccount实例的客户端代码可以访问两个Withdraw方法。

InterestBearingCheckingAccount oAccount = new InterestBearingCheckingAccount();
oAccount.Withdraw(500);
oAccount.Withdraw(500, 200);

隐藏基类方法

如果派生类中的方法与基类方法具有相同的方法签名,但是没有用关键字override、标记,那么它实际上隐藏了基类的方法。虽然这可能是有意的行为,但有时它可能会在无意中发生。虽然代码仍然可以编译,但是 IDE 会发出警告,询问这是否是预期的行为。如果您打算隐藏基类方法,您应该在派生类的方法定义中显式使用new关键字。使用new关键字将向 IDE 表明这是预期的行为,并消除警告。下面的代码演示隐藏基类方法:

class CheckingAccount
{
    public virtual void Withdraw(double amount)
    {
    }
}

class InterestBearingCheckingAccount : CheckingAccount
{
    public new void Withdraw(double amount)
    {
    }
    public void Withdraw(double amount, double minBalance)
    {
    }
}

活动 7-2。重写基类方法

在本活动中,您将熟悉以下内容:

  • 覆盖基类的方法
  • 在派生类中使用基限定符

覆盖基类方法

要覆盖Account类,请遵循以下步骤:

  1. 开始 VS .选择文件image打开image项目。

  2. 导航到 Activity7_2Starter 文件夹,单击 Activity7_2.sln 文件,然后单击“打开”。当项目打开时,它将包含一个出纳员表单。稍后您将使用这个表单来测试您将创建的类。该项目还包含一个 BankClasses.cs 文件。这个文件包含了基类Account和派生类SavingsAccountCheckingAccount的代码。

  3. 检查基类Account中定义的Withdraw方法。该方法检查帐户中是否有足够的资金,如果有,则更新余额。您将在CheckingAccount类中覆盖这个方法,以确保维持最小的平衡。

  4. Change the Withdraw method definition in the Account class to indicate it is overridable, like so:

    public virtual double Withdraw(double amount)

  5. Add the following GetMinimumBalance method to the CheckingAccount class definition:

    public double GetMinimumBalance()

    {

    return 200;

    }

  6. Add the following overriding Withdraw method to the CheckingAccount class definition. This method adds a check to see that the minimum balance is maintained after a withdrawal.

    public override double Withdraw(double amount)

    {

    if (Balance >= amount + GetMinimumBalance())

    {

    _balance - = amount;

    return Balance;

    }

    else

    {

    return -1; //Not enough funds

    }

    }

  7. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

测试被覆盖的方法

要测试您已经创建的修改过的Withdraw方法,请遵循以下步骤:

  1. 在代码编辑器中打开 frmTeller 表单,找到btnWithdraw click 事件代码。

  2. Depending on which radio button is selected, call the Withdraw method of the appropriate object and pass the value of the txtAmount text box. Add the following code in the try block to show the return value in the txtBalance text box:

    if (rdbChecking.Checked)

    {

    oCheckingAccount.AccountNumber = int.Parse(txtAccountNumber.Text);

    txtBalance.Text = oCheckingAccount.Withdraw(double.Parse(txtAmount.Text)).ToString();

    }

    else if (rdbSavings.Checked)

    {

    oSavingsAccount.AccountNumber = int.Parse(txtAccountNumber.Text);

    txtBalance.Text = oSavingsAccount.Withdraw(double.Parse(txtAmount.Text)).ToString();

    }

  3. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  4. 选择调试image开始运行项目。

  5. 输入帐号 1,选择“检查”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。

  6. 输入取款金额 200,然后单击取款按钮。您应该会得到 800 英镑的余额。

  7. 输入取款金额 700,然后点击取款按钮。您应该得到-1(表示资金不足),因为最终余额将小于最小余额 200。

  8. 输入帐号 1,选择“储蓄”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。

  9. 输入取款金额 600,然后点击取款按钮。您应该会得到 400 英镑的余额。

  10. 输入取款金额 400,然后点击取款按钮。您应该得到一个 0 的结果余额,因为对于使用Account基类的Withdraw方法的储蓄帐户,没有最小余额。

  11. 测试后,关闭窗体,这将停止调试器。

使用基本限定符调用基类方法

此时,CheckingAccount类的Withdraw方法覆盖了Account类的Withdraw方法。不执行基类的方法中的任何代码。现在您将修改代码,这样当执行CheckingAccount类的代码时,它也会执行基类的Withdraw方法。遵循这些步骤:

  1. 找到Account类的Withdraw方法。

  2. Change the implementation code so that it decrements the balance by the amount passed to it.

    public virtual double Withdraw(double amount)

    {

    _balance - = amount;

    return Balance;

    }

  3. Change the Withdraw method of the CheckingAccount class so that after it checks for sufficient funds, it calls the Withdraw method of the Account base class.

    public override double Withdraw(double amount)

    {

    if (Balance > = amount + GetMinimumBalance())

    {

    return base.Withdraw(amount);

    }

    else

    {

    return -1; //Not enough funds.

    }

    }

  4. Add a Withdraw method to the SavingsAccount class that is similar to the Withdraw method of the CheckingAccount class, but does not check for a minimum balance.

    public override double Withdraw(double amount)

    {

    if (Balance > = amount)

    {

    return base.Withdraw(amount);

    }

    else

    {

    return -1; //Not enough funds.

    }

    }

  5. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

测试基础改性剂的使用

要测试Withdraw方法,请遵循以下步骤:

  1. 选择调试image开始。
  2. 输入帐号 1,选择“检查”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。
  3. 输入取款金额 600,然后点击取款按钮。您应该会得到 400 英镑的余额。
  4. 输入取款金额 300,然后单击取款按钮。您应该得到 a -1(资金不足),因为最终余额将小于 200 的最小值。
  5. 输入帐号 1,选择“储蓄”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。
  6. 输入取款金额 600,然后点击取款按钮。您应该会得到 400 英镑的余额。
  7. 输入取款金额 300,然后单击取款按钮。您应该得到 100 的结果余额,因为使用Account基类的Withdraw方法的储蓄账户没有最小余额。
  8. 测试后,关闭窗体,这将停止调试器。

实现接口

如前所述,您可以创建一个抽象基类,它不包含任何实现代码,但定义了从基类继承的任何类必须使用的方法签名。当使用抽象类时,从它派生的类必须实现它的继承方法。您可以使用另一种技术来实现类似的结果。在这种情况下,不是定义一个抽象类,而是定义一个定义方法签名的接口。

实现接口的类被合同要求实现接口签名定义,并且不能改变它。这种技术有助于确保使用这些类的客户端代码知道哪些方法可用、应该如何调用它们以及预期的返回值。下面的代码显示了如何声明接口定义:

public interface IAccount
{
   string GetAccountInfo(int accountNumber);
}

类通过在类名后使用分号后跟接口名来实现接口。当一个类实现一个接口时,它必须为该接口定义的所有方法提供实现代码。下面的代码演示了CheckingAccount如何实现IAccount接口:

public class CheckingAccount : IAccount
{
    public string GetAccountInfo(int accountNumber)
    {
        return "Printing checking account info";
    }
}

因为实现接口和从抽象基类继承是相似的,所以你可能会问为什么要使用接口。使用接口的一个好处是一个类可以实现多个接口。那个 .NET Framework 不支持从多个类继承。作为多重继承的一种变通方法,实现多个接口的能力也包括在内。接口对于跨不同类型的类实施通用功能也很有用。

理解多态

多态是从同一基类继承的派生类以自己独特的方式响应同一方法调用的能力。这简化了客户端代码,因为客户端代码不需要担心它引用的是哪个类类型,只要这些类类型实现相同的方法接口。

例如,假设您希望银行应用中的所有帐户类都包含一个GetAccountInfo方法,该方法具有相同的接口定义,但基于帐户类型有不同的实现。客户端代码可以遍历一组 account-type 类,编译器将在运行时确定需要执行哪个特定的 account-type 实现。如果您后来添加了一个实现了GetAccountInfo方法的新帐户类型,那么您不需要修改现有的客户端代码。

您可以通过使用继承或实现接口来实现多态。下面的代码演示了继承的用法。首先,定义基类和派生类。

public abstract class Account
{
   public abstract string GetAccountInfo();
}

public class CheckingAccount : Account
{
   public override string GetAccountInfo()
   {
       return "Printing checking account info";
   }
}
public class SavingsAccount : Account
{
    public override string GetAccountInfo()
    {
        return "Printing savings account info";
    }
}

然后创建一个类型为Account的列表,并添加一个CheckingAccount和一个SavingsAccount

List <Account> AccountList = new List <Account> ();
CheckingAccount oCheckingAccount = new CheckingAccount();
SavingsAccount oSavingsAccount = new SavingsAccount();
AccountList.Add(oCheckingAccount);
AccountList.Add(oSavingsAccount);

然后循环遍历List并调用每个AccountGetAccountInfo方法。每个Account类型都将实现自己的GetAccountInfo实现。

foreach (Account a in AccountList)
{
    MessageBox.Show(a.GetAccountInfo());
}

您也可以通过使用接口获得类似的结果。不是从基类Account继承,而是定义并实现一个IAccount接口。

public interface IAccount
 {
    string GetAccountInfo();
 }

public class CheckingAccount : IAccount
{
    public string GetAccountInfo()
    {
        return "Printing checking account info";
    }
}
public class SavingsAccount : IAccount
{
    public string GetAccountInfo()
    {
        return "Printing savings account info";
    }
}

然后创建一个类型为IAccount的列表,并添加一个CheckingAccount和一个SavingsAccount

List<IAccount>AccountList = new List<IAccount>();
CheckingAccount oCheckingAccount = new CheckingAccount();
SavingsAccount oSavingsAccount = new SavingsAccount();
AccountList.Add(oCheckingAccount);
AccountList.Add(oSavingsAccount);

然后循环遍历AccountList并调用每个AccountGetAccountInfo方法。每个Account类型都将实现自己的GetAccountInfo实现。

foreach (IAccount a in AccountList)
{
    MessageBox.Show(a.GetAccountInfo());
}

活动 7-3。实现多态

在本活动中,您将熟悉以下内容:

  • 通过继承创建多态
  • 通过接口创建多态

使用继承实现多态

要使用继承实现多态,请遵循以下步骤:

  1. 启动 Visual Studio。选择文件image新建image项目。

  2. 选择 C# 模板下的控制台应用模板。将项目命名为活动 7_3。

  3. 该项目包含一个 Program.cs 文件。这个文件包含一个启动 Windows 控制台应用的Main方法。在解决方案资源管理器窗口中右键单击项目节点,并选择 Add image class。将文件命名为 Account.cs。

  4. In the Account.cs file alter the code to create an abstract base Account class. Include an accountNumber property and an abstract method GetAccountInfo that takes no parameters and returns a string.

    public abstract class Account

    {

    private int _accountNumber;

    public int AccountNumber

    {

    get { return _accountNumber; }

    set { _accountNumber = value; }

    }

    public abstract string GetAccountInfo();

    }

  5. Add the following code to create two derived classes: CheckingAccount and SavingsAccount. These classes will override the GetAccountInfo method of the base class.

    public class CheckingAccount : Account

    {

    public override string GetAccountInfo()

    {

    return "Printing checking account info for account number "

    + AccountNumber.ToString();

    }

    }

    public class SavingsAccount : Account

    {

    public override string GetAccountInfo()

    {

    return "Printing savings account info for account number "

    + AccountNumber.ToString();

    }

    }

  6. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

测试多态继承方法

要测试多态方法,请遵循以下步骤:

  1. 在代码编辑器中打开 Program.cs 文件,找到Main方法。

  2. Create an instance of a list of account types.

    List <Account > AccountList = new List <Account> ();

  3. Create instances of CheckingAccount and SavingsAccount.

    CheckingAccount oCheckingAccount = new CheckingAccount();

    oCheckingAccount.AccountNumber = 100;

    SavingsAccount oSavingsAccount = new SavingsAccount();

    oSavingsAccount.AccountNumber = 200;

  4. Add the oCheckingAccount and oSavingsAccount to the list using the Add method of the list.

    AccountList.Add(oCheckingAccount);

    AccountList.Add(oSavingsAccount);

  5. Loop through the list and call the GetAccountInfo method of each account type in the list and show the results in a console window.

    foreach (Account a in AccountList)

    {

    Console.WriteLine(a.GetAccountInfo());

    }

    Console.ReadLine();

  6. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  7. 选择调试image开始运行项目。您应该看到一个控制台窗口,显示列表中每个对象的GetAccountInfo方法的返回字符串。

  8. 测试完多态后,按 enter 键关闭控制台窗口,这将停止调试器。

使用接口实现多态

要使用接口实现多态,请遵循以下步骤:

  1. 在代码编辑器中查看 Account.cs 文件的代码。

  2. 注释掉AccountCheckingAccountSavingsAccount类的代码。

  3. Define an interface IAccount that contains the GetAccountInfo method.

    public interface IAccount

    {

    string GetAccountInfo();

    }

  4. Add the following code to create two classes: CheckingAccount and SavingsAccount. These classes will implement the IAccount interface.

    public class CheckingAccount : IAccount

    {

    private int _accountNumber;

    public int AccountNumber

    {

    get { return _accountNumber; }

    set { _accountNumber = value; }

    }

    public string GetAccountInfo()

    {

    return "Printing checking account info for account number "

    + AccountNumber.ToString();

    }

    }

    public class SavingsAccount : IAccount

    {

    private int _accountNumber;

    public int AccountNumber

    {

    get { return _accountNumber; }

    set { _accountNumber = value; }

    }

    public string GetAccountInfo()

    {

    return "Printing savings account info for account number "

    + AccountNumber.ToString();

    }

    }

  5. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

测试多态接口方法

要测试多态方法,请遵循以下步骤:

  1. 在代码编辑器中打开 Program.cs 文件,找到Main方法。

  2. Change the code to create an instance of a list of IAccount types.

    List <IAccount > AccountList = new List <IAccount> ();

  3. Change the code for each loop to loop through the list and call the GetAccountInfo method of each IAccount type in the list.

    foreach (IAccount a in AccountList)

    {

    Console.WriteLine(a.GetAccountInfo());

    }

    Console.ReadLine();

  4. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  5. 选择调试image开始运行项目。您应该看到一个控制台窗口,显示列表中每个对象的GetAccountInfo方法的返回字符串。

  6. 测试完多态后,按 enter 键关闭控制台窗口,这将停止调试器。

摘要

本章向您介绍了 OOP 的两个最强大的特性:继承和多态。不管使用什么语言,知道如何实现这些特性是成为一名成功的面向对象程序员的基础。

在第八章中,你将仔细观察应用中的对象是如何协作的。涵盖的主题包括对象如何相互传递消息,事件如何驱动程序,数据如何在类的实例之间共享,以及如何处理异常。

八、实现对象协作

在第七章中,你学习了如何在 C# 中创建和使用类层次结构。那一章还介绍了继承、多态和接口的概念。在这一章中,你将学习如何让应用的对象一起工作来执行任务。您将看到对象如何通过消息传递进行通信,以及事件如何启动应用处理。您还将了解对象如何响应和交流在执行分配给它们的任务时可能出现的异常。

阅读本章后,您应该熟悉以下内容:

  • 通过消息传递进行对象通信的过程
  • 可能出现的不同类型的消息传递
  • 如何在 C# 应用中使用委托
  • 对象如何响应事件并发布自己的事件
  • 发布和响应异常的过程
  • 如何在同一个类的几个实例之间创建共享数据和过程
  • 如何异步发出消息调用

通过消息传递进行交流

OOP 的优势之一是它的许多方面都模仿了现实世界。之前,您看到了如何使用公司中的雇员来表示一个Employee类的实例。我们可以将这种类比扩展到类(对象)的实例在应用中如何交互。例如,在大公司中,员工执行专门的功能。一个人负责应付账款的处理,另一个人负责应收账款的操作。当一名员工需要请求服务时——例如带薪休假(PTO)——该员工向她的经理发送一条消息。我们可以将这种交互视为请求者(员工)和服务提供者(经理)之间的服务请求。这个请求可以只涉及一个对象(自助请求),两个对象,或者它可以是一个复杂的请求者/服务提供者请求链。例如,员工向她的经理申请 PTO,经理反过来与人力资源(HR)部门核实该员工是否有足够的累积时间。在这种情况下,经理既是员工的服务提供者,也是人力资源部门的服务请求者。

定义方法签名

当消息在请求者和服务提供者之间传递时,请求者可能会也可能不会期待响应。例如,当一个员工请求 PTO 时,她希望得到一个表示批准或拒绝的响应。然而,当会计部门发放工资时,员工们并不期望公司里的每个人都会发一封回复邮件来感谢他们!

发布消息时的一个常见要求是包含执行请求所必需的信息。当员工请求 PTO 时,她的经理希望她向他提供她请求的休假日期。在 OOP 术语中,您将方法的名称(请求的服务)和输入参数(请求者提供的信息)称为方法签名。

下面的代码演示了如何在 C# 中定义方法。访问修饰符后面首先是返回类型(如果没有返回值,则使用void),然后是方法名。参数类型和名称列在括号中,用逗号分隔。方法的主体包含在左花括号和右花括号中。

public int AddEmployee(string firstName,string lastName)
{
    //Code to save data to database
}
public void LogMessage(string message)
{
    //Code to write to log file.
}

传递参数

当在类中定义方法时,还必须指示参数是如何传递的。参数可以通过值或引用传递。

如果选择按值传递参数,参数数据的副本将从调用例程传递给请求的方法。被请求的方法使用副本,如果对数据进行了更改,被请求的方法必须将副本传递回调用例程,以便调用者可以选择放弃更改或复制它们。回到公司的类比,想想更新你的员工档案的过程。人力资源部门不会让你直接接触文件;相反,它会向您发送文件中值的副本。您对副本进行更改,然后将其发送回人力资源部门。然后,人力资源部门决定是否将这些更改复制到实际的员工档案中。在 C# 中,默认情况下通过值传递参数,因此不使用关键字。在下面的方法中,参数通过值传递:

public int AddEmployee(string firstName)
{
    //Code to save data to database
}

传递参数的另一种方式是通过引用。在这种情况下,请求代码不会传入数据的副本,而是传递数据所在位置的引用。以前面的例子为例,当您想要进行更新时,人力资源部门不会向您发送员工文件中数据的副本,而是通知您文件的位置,并告诉您去该部门进行更改。在这种情况下,显然,通过引用传递参数会更好。在 C# 代码中,当通过引用传递参数时,使用了ref关键字。下面的代码演示如何定义通过引用传递值的方法:

public int AddEmployee(ref string firstName)
{
    //Code to save data to database
}

当应用被设计为跨处理边界通信的组件时,甚至在不同的计算机上托管时,通过值而不是通过引用传递参数是有利的。通过引用传递参数会导致开销增加,因为当服务提供对象必须使用参数信息时,它需要跨处理边界和网络进行调用。单个处理请求可能会导致请求者和服务器提供者之间的多次来回调用。在维护数据完整性时,通过引用传递值也会导致问题。请求者在服务器不知情或无法控制的情况下,为要操作的数据打开了通道。

另一方面,当调用代码和被请求的方法在同一个处理空间中(可以说,它们占用同一个“隔间”)并且具有明确建立的信任关系时,通过引用传递值可能是更好的选择。在这种情况下,允许直接访问内存存储位置并通过引用传递参数可以提供优于通过值传递参数的性能优势。

通过引用传递参数可能有好处的另一种情况是,如果对象是复杂数据类型,例如另一个对象。在这种情况下,复制数据结构并跨进程和网络边界传递它的开销超过了跨网络重复调用的开销。

image 这个 .NET Framework 通过在 XML 结构中序列化和反序列化复杂数据类型,允许您有效地复制和传递这些类型,从而解决了复杂数据类型的问题。

理解事件驱动编程

到目前为止,您已经看到了对象之间的消息传递,其中直接请求启动了消息交互。如果你想一想你在现实生活中是如何与物体互动的,你会经常收到信息来回应已经发生的事件。以办公室为例,当卖三明治的小贩走进大楼时,对讲机会发出信息通知员工午餐车已经到了。这种类型的消息传递被称为广播消息传递。发出一条消息,接收者决定是忽略还是响应这条消息。

发布此事件消息的另一种方式是,接待员向一组员工发送一封电子邮件,这些员工有兴趣知道三明治供应商何时出现。在这种情况下,感兴趣的员工将向接待员订阅以接收事件消息。这种类型的消息传递通常被称为基于订阅的消息传递。

用 .NET 框架是面向对象的、事件驱动的程序。如果您跟踪应用中出现的请求/服务提供者处理链,您就可以确定启动处理的事件。在 Windows 应用的情况下,与 GUI 交互的用户通常会启动事件。例如,用户可以通过单击按钮启动将数据保存到数据库的过程。应用中的类也可以启动事件。当检测到无效登录时,安全类可以广播事件消息。您还可以订阅外部事件。您可以创建一个 Web 服务,当您在股市中跟踪的股票发生变化时,该服务将发出事件通知。您可以编写一个订阅服务并响应事件通知的应用。

理解委托

为了在 C# 中实现基于事件的编程,您必须首先了解委托。委托是指通过调用服务提供者的方法来请求服务。然后,服务提供者将这个服务请求重新路由到另一个服务该请求的方法。委托类可以检查服务请求,并在运行时动态确定将请求路由到哪里。回到公司的类比,当一个经理收到一个服务请求时,她经常将它委托给她部门的一个成员。(事实上,许多人会认为,成功经理的一个共同特点是知道何时以及如何授权。)

创建委托方法时,首先定义委托方法的签名。因为委托方法实际上并不服务于请求,所以它不包含任何实现代码。下面的代码显示了一个用于比较整数值的委托方法:

public delegate Boolean CompareInt(int I1, int I2);

一旦定义了委托方法的签名,就可以创建要委托的方法。这些方法必须与委托方法具有相同的参数和返回类型。下面的代码显示了委托方法将委托给的两个方法:

private Boolean AscendOrder(int I1, int I2)
{
    if (I1 < I2)
        { return true;}
    else
        { return false; }
}
private Boolean DescendOrder(int I1, int I2)
{
    if (I1 > I2)
        { return true; }
    else
        { return false; }
}

一旦定义了委托及其委托方法,就可以使用委托了。下面的代码显示了排序例程的一部分,它根据作为参数传入的SortType来确定调用哪个委托方法:

public void SortIntegers(SortType sortDirection, int[] intArray)
{
    CompareInt CheckOrder;
    if (sortDirection == SortType.Ascending)
        { CheckOrder = new CompareInt(AscendOrder); }
    else
        { CheckOrder = new CompareInt(DescendOrder); }
    // Code continues ...
}

实施事件

在 C# 中,当你想发布事件消息时,首先你要为事件声明一个委托类型。委托类型定义将传递给处理事件的方法的一组参数。

public delegate void DataUpdateEventHandler(string msg);

一旦声明了委托,就声明了委托类型的事件。

public event DataUpdateEventHandler DataUpdate;

当您想要引发事件时,可以通过传入适当的参数来调用事件。

public void SaveInfo()
{
    try
    {
        DataUpdate("Data has been updated");
    }
    catch
    {
        DataUpdate("Data could not be updated");
    }
}

响应事件

为了在客户端代码中使用事件,声明了一个事件处理方法,该方法执行程序逻辑以响应事件。此事件处理程序必须与发出事件的类中声明的事件委托具有相同的方法签名。

void odata_DataUpdate(string msg)
{
    MessageBox.Show(msg);
}

此事件处理程序使用+=运算符向事件源注册。这个过程被称为事件连接。以下代码连接了先前声明的DataUpdate事件的事件处理程序:

Data odata = new Data();
odata.DataUpdate += new DataUpdateEventHandler(odata_DataUpdate);
odata.SaveInfo();

Windows 控件事件处理

Windows 窗体还通过使用+=运算符将事件处理程序绑定到事件来实现事件处理程序。下面的代码将一个按钮绑定到一个点击事件,将一个文本框绑定到一个鼠标按下事件:

this.button1.Click += new System.EventHandler(this.button1_Click);
this.textBox1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.textBox1_MouseDown);

控件事件的事件处理程序方法有两个参数:第一个参数sender,提供对引发事件的对象的引用。第二个参数传递包含特定于正在处理的事件的信息的对象。下面的代码显示了按钮单击事件的事件处理程序方法和文本框鼠标按下事件的事件处理程序。注意如何使用e来确定左键是否被点击。

private void button1_Click(object sender, EventArgs e)
{

}
private void textBox1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        //code goes here.
    }
}

活动 8-1。发布和响应事件消息

在本活动中,您将学习做以下事情:

  • 从服务器类创建和引发事件
  • 处理来自客户端类的事件
  • 处理 GUI 事件

在类定义中添加和引发事件消息

要在类定义文件中添加和引发事件消息,请遵循以下步骤:

  1. 启动 Visual Studio。选择文件image新建image项目。

  2. 选择一个 Windows 窗体应用项目。将项目命名为Activity8_1

  3. A default form is included in the project. Add controls to the form and change the property values, as listed in Table 8-1. Your completed form should look similar to Figure 8-1.

    表 8-1。登录表单和控件属性

    物体属性
    Form1NamefrmLogin
    TextLogin
    Label1NamelblName
    TextName:
    Label2NamelblPassword
    TextPassword:
    Textbox1NametxtName
    Text(empty)
    Textbox2NametxtPassword
    Text(empty)
    PasswordChar*
    Button1NamebtnLogin
    TextLogin
    Button2NamebtnClose
    TextClose

    9781430249351_Fig08-01.jpg

    图 8-1 。完整的登录表单

  4. 选择项目image添加类别。给类命名Employee。在代码编辑器中打开Employee类代码。

  5. Above the class declaration, add the following line of code to define the login event handler delegate. You will use this event to track employee logins to your application.

    public delegate void LoginEventHandler(string loginName, Boolean status);

  6. Inside the class declaration, add the following line of code to define the LoginEvent as the delegate type:

    public event LoginEventHandler LoginEvent;

  7. Add the following Login method to the class, which will raise the LoginEvent:

    public void Login(string loginName, string password)

    {

    //Data normally retrieved from database.

    if (loginName == "Smith" && password == "js")

    {

    LoginEvent(loginName, true);

    }

    else

    {

    LoginEvent(loginName, false);

    }

    }

  8. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

接收客户端类中的事件

要在客户端类中接收事件,请遵循以下步骤:

  1. 在设计窗口中打开 frmLogin。

  2. 双击登录按钮以查看登录按钮 click 事件处理程序。

  3. Add the following code to wire up the Employee class’s LoginEvent with an event handler in the form class:

    private void btnLogin_Click(object sender, EventArgs e)

    {

    Employee oEmployee = new Employee();

    oEmployee.LoginEvent += new LoginEventHandler(oEmployee_LoginEvent);

    oEmployee.Login(txtName.Text, txtPassword.Text);

    }

  4. Add the following event handler method to the form that gets called when the Employee class issues a LoginEvent:

    void oEmployee_LoginEvent(string loginName, bool status)

    {

    MessageBox.Show("Login status :" + status);

    }

  5. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  6. 选择调试image开始运行项目。

  7. 要测试以确保引发了Login事件,请输入登录名 Smith 和密码 js。这应该会触发 true 登录状态。

  8. 测试登录事件后,关闭窗体,这将停止调试器。

用一种方法处理多个事件

要用一种方法处理多个事件,请遵循以下步骤:

  1. 通过在解决方案资源管理器中右键单击 frmLogin 节点并选择“视图设计器”,在窗体设计器中打开 frmLogin。

  2. From the Toolbox, add a MenuStrip control to the form. Click where it says “Type Here” and enter File for the top-level menu and Exit for its submenu, as shown in Figure 8-2.

    9781430249351_Fig08-02.jpg

    图 8-2 。添加 MenuStrip 控件

  3. Add the following method to handle the click event of the menu and the Close button:

    private void FormClose(object sender, EventArgs e)

    {

    this.Close();

    }

  4. Open frmLogin in the designer window. In the properties window, select the exitToolStripMenuItem. Select the event button (lightning bolt) at the top of the properties window to show the events of the control. In the click event drop-down, select the FormClose method (see Figure 8-3).

    9781430249351_Fig08-03.jpg

    图 8-3 。连接事件处理程序

  5. 重复步骤 4 将btnClose按钮点击事件绑定到FormClose方法。

  6. 在解决方案窗口中展开 Form1.cs 节点。右键单击表格 1。Designer.cs 节点并选择“查看代码”。

  7. In the code editor, expand the Windows Form Designer generated code region. Search for the code listed below. This code was generated by the form designer to wire up the events to the FormClose method.

    this.btnClose.Click += new System.EventHandler(this.FormClose);

    this.exitToolStripMenuItem.Click += new System.EventHandler(this.FormClose);

  8. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  9. 选择调试image开始运行项目。测试退出菜单和关闭按钮。

  10. 测试后,保存项目,然后退出 Visual Studio。

中处理异常 .NET 框架

当对象协作时,事情可能会出错。异常是在正常处理过程中不希望发生的事情。例如,当连接失败时,您可能试图通过网络将数据保存到数据库中,或者您可能试图将数据保存到驱动器中没有磁盘的驱动器中。您的应用应该能够优雅地处理应用处理过程中发生的任何异常。

的 .NET 框架使用结构化的异常处理机制。以下是这种结构化异常处理的一些优势:

  • 所有人的共同支持和结构 .NET 语言
  • 支持创建受保护的代码块
  • 过滤异常以创建高效可靠的错误处理的能力
  • 支持终止处理程序,以保证完成清理任务,而不管可能遇到的任何异常

的 .NET Framework 还提供了大量的异常类,用于处理可能发生的常见异常。例如,FileNotFoundException类封装了诸如文件名、错误消息以及当试图访问一个不存在的文件时抛出的异常的来源等信息。此外 .NET Framework 允许创建应用特定的异常类,您可以编写这些异常类来处理应用特有的常见异常。

使用 Try-Catch 块

当创建可能导致异常的代码时,您应该将它放在try块中。放置在try块中的代码被认为是受保护的。如果在受保护代码执行过程中出现异常,代码处理将被转移到catch块,在那里进行处理。下面的代码演示了一个类的方法,该方法尝试从不存在的文件中读取数据。当抛出异常时,它被捕获在catch块中。

public string ReadText(string filePath)
{
    StreamReader sr;
    try
    {
        sr = File.OpenText(filePath);
        string fileText = sr.ReadToEnd();
        sr.Close();
        return fileText;
    }
    catch(Exception ex)
    {
        return ex.Message;
    }
}

所有的try块都需要至少一个嵌套的catch块。您可以使用catch块来捕获try块中可能出现的所有异常,或者您可以使用它根据异常的类型来过滤异常。这使您能够根据异常类型动态响应不同的异常。下面的代码演示了如何根据从磁盘读取文本文件时可能发生的不同异常来筛选异常:

public string ReadText(string filePath)
{
    StreamReader sr;
    try
    {
        sr = File.OpenText(filePath);
        string fileText = sr.ReadToEnd();
        sr.Close();
        return fileText;
    }
    catch (DirectoryNotFoundException ex)
    {
        return ex.Message;
    }
    catch (FileNotFoundException ex)
    {
        return ex.Message;
    }
    catch(Exception ex)
    {
        return ex.Message;
    }
}

添加 Finally 块

此外,您可以在try块的末尾嵌套一个finally块。与catch块不同,finally块的使用是可选的。finally块用于任何需要发生的清理代码,即使遇到异常。例如,您可能需要关闭数据库连接或释放文件。当try块的代码被执行并且异常发生时,处理将评估每个catch块,直到找到合适的捕捉条件。在catch块执行后,将执行finally块。如果try块执行并且没有遇到异常,那么catch块不会执行,但是finally块仍然会被处理。下面的代码显示了一个用于关闭和释放StreamReaderfinally块:

public string ReadText(string filePath)
{
    StreamReader sr = null;
    try
    {
        sr = File.OpenText(filePath);
        string fileText = sr.ReadToEnd();
        return fileText;
    }
    catch (DirectoryNotFoundException ex)
    {
        return ex.Message;
    }
    catch (FileNotFoundException ex)
    {
        return ex.Message;
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
        finally
    {
        if (sr != null)
        {
            sr.Close();
            sr.Dispose();
        }
    }
}

抛出异常

在代码执行期间,当进行不适当的调用时,例如,方法的参数具有无效值或者传递给方法的参数导致异常,您可以抛出异常来通知调用代码违规。在下面的代码中,如果orderDate参数大于当前日期,就会向调用代码抛出一个ArgumentOutOfRangeException,通知它们发生了冲突。

if (orderDate > DateTime.Now)
{
    throw new ArgumentOutOfRangeException ("Order date can not be in the future.");
}
//Processing code...

image 注意如果在 .NET Framework,您可以创建一个从System.Exception类派生的自定义异常类。

嵌套异常处理

在某些情况下,您可能能够纠正发生的异常,并继续处理try块中剩余的代码。例如,可能会出现被零除的错误,将结果赋值为零并继续处理是可以接受的。在这种情况下,try-catch 块可以嵌套在导致异常的代码行周围。处理完异常后,处理将返回到嵌套的try块之后的外层try-catch块中的代码行。下面的代码演示了如何将一个try块嵌套在另一个块中:

try
{
    try
    {
        Y = X1 / X2;
    }
    catch (DivideByZeroException ex)
    {
        Y = 0;
    }
    //Rest of processing code.
}
catch (Exception ex)
{
    //Outer exception processing
}

image 了解更多关于处理异常和 .NET 框架异常类,请参考附录 b。

静态属性和方法

当您声明一个类的对象实例时,该对象实例化它所实现的类的属性和方法的自己的实例。例如,如果你要写一个增加计数器的计数例程,然后实例化该类的两个对象实例,每个对象的计数器将彼此独立;当您增加一个计数器时,另一个不会受到影响。正常情况下,这种对象独立性就是你想要的行为。但是,有时您可能希望一个类的不同对象实例访问相同的共享变量。例如,您可能希望构建一个计数器,记录已经实例化了多少个对象实例。在这种情况下,您将在类定义中创建一个静态属性值。以下代码演示了如何在类定义中创建静态TaxRate属性:

public class AccountingUtilities
{
    private static double _taxRate = 0.06;

    public static double TaxRate
    {
        get { return _taxRate; }
    }
}

要访问静态属性,不需要创建该类的对象实例;相反,您可以直接引用该类。下面的代码显示了一个访问先前定义的静态属性TaxRate的客户端:

public class Purchase
{
    public double CalculateTax(double purchasePrice)
    {
        return purchasePrice * AccountingUtilities.TaxRate;
    }
}

如果您有客户端需要访问的实用函数,但是您不希望通过创建类的对象实例来获得对方法的访问,那么静态方法是非常有用的。请注意,静态方法只能访问静态属性。下面的代码显示了一个静态方法,用于计算当前登录到应用的用户数量:

public class UserLog
{
    private static int _userCount;
    public static void IncrementUserCount()
    {
        _userCount += 1;
    }
    public static void DecrementUserCount()
    {
        _userCount -= 1;
    }
}

当客户端代码访问静态方法时,它通过直接引用该类来实现。下面的代码演示了如何访问前面定义的静态方法:

public class User
{
    //other code ...
    public void Login(string userName, string password)
    {
        //code to check credentials
        //if successful
        UserLog.IncrementUserCount();
    }
}

虽然在应用中创建类时可能不经常使用静态属性和方法,但它们在创建基类库时很有用,并且在整个 .NET Framework 系统类。下面的代码演示了System.String类的Compare方法的使用。这是一个静态方法,按字母顺序比较两个字符串。如果第一个字符串大于,则返回正值;如果第二个字符串大于,则返回负值;如果两个字符串相等,则返回零。

public Boolean CheckStringOrder(string string1, string string2)
{
    if (string.Compare(string1, string2) >= 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}

活动 8-2。实现异常处理和静态方法

在本活动中,您将学习如何执行以下操作:

  • 创建并调用类的静态方法
  • 使用结构化异常处理

创建静态方法

要创建静态方法,遵循以下步骤:

  1. 启动 Visual Studio。选择文件image新建image项目。

  2. 选择一个 Windows 应用项目。将项目命名为 Activity8_2。

  3. Visual Studio creates a default form for the project which you’ll use to create a login form named Logger. Add controls to the form and change the property values, as listed in Table 8-2. Your completed form should look similar to Figure 8-4.

    9781430249351_Fig08-04.jpg

    图 8-4 。已完成的记录器表单

    表 8-2。记录器表单和控件属性

    物体属性
    Form1NamefrmLogger
    TextLogger
    Textbox1NametxtLogPath
    Textc:\Test\LogTest.txt
    Textbox2NametxtLogInfo
    TextTest Message
    Button1NamebtnL ogInfo
    TextLog Info
  4. 选择项目image添加类。给类命名Logger

  5. Because you will be using the System.IO class within the Logger class, add a using statement to the top of the class file:

    using System.IO;

  6. Add a static LogWrite method to the class. This method will write information to a log file. To open the file, create a FileStream object. Then create a StreamWriter object to write the information to the file. Notice the use of the using blocks to properly dispose the FileStream and StreamWriter objects and release the resources.

    public static string LogWrite(string logPath, string logInfo)

    {

    using (FileStream oFileStream = new FileStream(logPath, FileMode.Open, FileAccess.Write))

    {

    using (StreamWriter oStreamWriter = new StreamWriter(oFileStream))

    {

    oFileStream.Seek(0, SeekOrigin.End);

    oStreamWriter.WriteLine(DateTime.Now);

    oStreamWriter.WriteLine(logInfo);

    oStreamWriter.WriteLine();

    }

    }

    return "Info Logged";

    }

  7. Open frmLogger in the visual design editor. Double click the btnLogInfo button to bring up the btnLogInfo_Click event method in the code editor. Add the following code, which runs the LogWrite method of the Logger class and displays the results in the form’s text property. Note that because you designated the LogWrite method as static (in step 6), the client does not need to create an object instance of the Logger class. Static methods are accessed directly through a class reference.

    private void btnLogInfo_Click(object sender, EventArgs e)

    {

    this.Text = Logger.LogWrite(txtLogPath.Text, txtLogInfo.Text);

    }

  8. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  9. 选择调试image运行。当表单启动时,单击 Log Info 按钮。您应该得到一个类型为System.IO.DirectoryNotFoundException的未处理异常消息。停止调试器。

创建结构化异常处理程序

要创建结构化异常处理程序,请遵循以下步骤:

  1. 在代码编辑器中打开Logger类代码。

  2. Locate the LogWrite method and add a try-catch block around the current code. In the catch block, return a string stating the logging failed.

    try

    {

    using (FileStream oFileStream = new FileStream(logPath, FileMode.Open, FileAccess.Write))

    {

    using (StreamWriter oStreamWriter = new StreamWriter(oFileStream))

    {

    oFileStream.Seek(0, SeekOrigin.End);

    oStreamWriter.WriteLine(DateTime.Now);

    oStreamWriter.WriteLine(logInfo);

    oStreamWriter.WriteLine();

    }

    }

    return "Info Logged";}

    catch

    {

    return "Logging Failed";

    }

  3. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  4. 选择调试image运行。当表单启动时,单击 Log Info 按钮。这一次,您应该不会得到异常消息,因为它是由LogWrite方法处理的。您应该会在表单的标题中看到消息“日志记录失败”。关闭表单。

过滤异常

要过滤例外情况,请遵循以下步骤:

  1. Alter the catch block to return different messages, depending on which exception is thrown.

    catch (FileNotFoundException ex)

    {

    return ex.Message;

    }

    catch (IOException ex)

    {

    return ex.Message;

    }

    catch

    {

    return "Logging Failed";

    }

  2. 在 Logger 类的LogWrite方法上设置断点。

  3. 选择调试image开始运行项目。通过单击日志信息按钮测试 catch 块。执行将在断点处停止。单步执行代码,注意它被IOException块捕获。

  4. 测试后,关闭表单。

  5. 使用记事本,在 c 盘上的测试文件夹中创建 LogTest.txt 文件,并关闭该文件。确保文件和文件夹未被标记为只读。

  6. 选择调试image开始运行项目。点击日志信息按钮,测试WriteLog方法。这一次,表单的标题应该表明日志写入成功。

  7. 停止调试器。

  8. 使用记事本打开 LogTest.txt 文件,并验证是否记录了信息。

  9. 保存项目,然后退出 Visual Studio。

使用异步消息传递

当对象通过来回传递消息进行交互时,它们可以同步或异步传递消息。

当客户机对象对服务器对象进行同步消息调用时,客户机暂停处理,并在继续之前等待来自服务器的响应。同步消息传递是最容易实现的,也是 .NET 框架。然而,有时这是一种低效的消息传递方式。例如,同步消息传递模型不太适合长时间运行的文件读写、跨慢速网络进行服务调用,或者在客户端断开连接的情况下进行消息排队。为了更有效地处理这些类型的情况 .NET Framework 提供了在对象之间异步传递消息所需的管道。

当客户端对象异步传递消息时,客户端可以继续处理。服务器完成消息请求后,响应信息将被发送回客户端。

如果你想一想,你与现实世界中的对象进行同步和异步的交互。同步消息传递的一个很好的例子是当你在杂货店排队结账时。当店员不能确定其中一个商品的价格时,他会打电话给经理进行价格检查,并暂停结账过程,直到返回结果。异步消息调用的一个例子是当职员注意到他的零钱不够时。他提醒经理,他很快就需要更改,但他可以继续处理客户的商品,直到更改到来。

为了使异步编程更容易实现 .NET Framework 4.5 引入了基于任务的异步模式(TAP) 。当符合 TAP 时,异步方法返回一个任务对象。此任务对象表示正在进行的操作,并允许与调用者进行通信。当调用异步方法时,客户端只需使用 await 修饰符,编译器负责所有必要的管道代码。

在 .NET Framework 中,当您想要创建一个可以异步调用的方法时,您可以使用async修饰符。异步方法提供了一种在不阻塞调用者的情况下执行潜在的长时间运行流程的方法。如果包含用户界面(UI)的主线程需要调用长时间运行的进程,这将非常有用。如果进程被同步调用,那么 UI 将冻结,直到进程完成。

一个异步方法应该包含至少一个await语句。当遇到await语句时,处理被挂起,控制权返回给调用者(本例中是 UI)。因此用户可以继续与 UI 进行交互。一旦异步任务完成,处理返回 await 语句,调用者可以得到处理完成的提示。为了提醒调用者流程已经完成,您创建了一个Task返回类型。如果 async 方法需要将信息传递回调用者,则使用Task<TResult>。下面的代码显示了一个异步读取文本文件的方法。注意用于调用StreamReader类的ReadToEndAsync方法的async修饰符和await关键字。该方法通过一个字符串结果将一个Task对象传递回调用者。

public static async Task<string> LogReadAsync(string filePath)
{
    StreamReader oStreamReader;
    string fileText;
        try
        {
            oStreamReader = File.OpenText(filePath);
            fileText = await oStreamReader.ReadToEndAsync();
            oStreamReader.Close();
            return fileText;
        }
        catch (FileNotFoundException ex)
        {
            return ex.Message;
        }
        catch (IOException ex)
        {
            return ex.Message;
        }
        catch
        {
            return "Logging Failed";
        }
}

活动 8-3。异步调用方法

在本活动中,您将学习如何执行以下操作:

  • 同步调用方法
  • 异步调用方法

创建一个方法并同步调用它

要创建方法并同步调用它,请遵循以下步骤:

  1. 启动 Visual Studio。选择文件image打开image项目。

  2. 打开您在练习 8_2 中完成的解决方案文件。

  3. Add the buttons shown in Table 8-3 to the frmLogger form. Figure 8-5 shows the completed form.

    9781430249351_Fig08-05.jpg

    图 8-5 。用于同步和异步读取的完整记录器表单

    表 8-3。记录器表单的附加按钮

    物体属性
    Button1NamebtnSyncRead
    TextSync Read
    Button2NamebtnAsyncRead
    TextAsync Read
    Button3NamebtnMessage
    TextMessage
  4. 在代码编辑器中打开Logger类。

  5. Recall that because you are using the System.IO namespace within the Logger class, you added a using statement to the top of the file. You are also going to use System.Threading namespace, so add a using statement to include this namespace.

    using System.Threading;

  6. Add a static LogRead function to the class. This function will read information from a log file. To open the file, create a FileStream object. Then create StreamReader object to read the information from the file. You are also using the Thread class to suspend processing for five seconds to simulate a long call across a slow network.

    public static string LogRead(string filePath)

    {

    StreamReader oStreamReader;

    string fileText;

    try

    {

    oStreamReader = File.OpenText(filePath);

    fileText = oStreamReader.ReadToEnd();

    oStreamReader.Close();

    Thread.Sleep(5000);

    return fileText;

    }

    catch (FileNotFoundException ex)

    {

    return ex.Message;

    }

    catch (IOException ex)

    {

    return ex.Message;

    }

    catch

    {

    return "Logging Failed";

    }

    }

  7. Open frmLogger in the visual design editor. Double click the btnMessage button to bring up the btnMessage_Click event method in the code editor. Add code to display a message box.

    private void btnMessage_Click(object sender, EventArgs e)

    {

    MessageBox.Show("Hello");

    }

  8. Open frmLogger in the visual design editor. Double-click the btnSyncRead button to bring up the btnSyncRead_Click event method in the code editor. Add code that calls the LogRead method of the Logger class and displays the results in a message box.

    private void btnSyncRead_Click(object sender, EventArgs e)

    {

    MessageBox.Show(Logger.LogRead(txtLogPath.Text));

    }

  9. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  10. 选择调试image运行。当表单启动时,单击同步读取按钮。点按“同步阅读”按钮后,尝试点按“邮件”按钮。当点击消息按钮时,您应该不会得到响应,因为您同步调用了ReadLog方法。ReadLog 方法返回结果后,单击消息按钮将会响应。

  11. 完成测试后,关闭表单。

创建和调用异步方法

要创建异步方法,请遵循以下步骤:

  1. 在代码编辑器中打开Logger类代码。

  2. Check for the following using statement at the top of the file. This namespace exposes the Task class and other types that are used to implement asynchronous programming.

    using System.Threading.Tasks;

  3. Create an asynchronous method that reads the text file. The use of the Task’s Delay method is to simulate a long running process.

    public static async Task<string> LogReadAsync(string filePath)

    {

    string fileText;

    try

    {

    using (StreamReader oStreamReader = File.OpenText(filePath))

    {

    fileText = await oStreamReader.ReadToEndAsync();

    }

    await Task.Delay(10000);

    return fileText;

    }

    catch (FileNotFoundException ex)

    {

    return ex.Message;

    }

    catch (IOException ex)

    {

    return ex.Message;

    }

    catch

    {

    return "Logging Failed";

    }

    }

  4. Open frmLogger in the visual design editor. Double-click the btnAsyncRead button to bring up the btnAsyncRead_Click event method in the code editor. Alter the method so that it is asynchronous.

    private async void btnAsyncRead_Click(object sender, EventArgs e)

  5. Add code to call the LogReadAsync method of the Logger class and display the results in a message box.

    btnAsyncRead.Enabled = false;

    string s = await Logger.LogReadAsync(txtLogPath.Text);

    MessageBox.Show(s);

    btnAsyncRead.Enabled = true;

  6. 选择构建image构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。

  7. 选择调试image运行。当表单启动时,单击异步读取按钮。单击异步读取按钮后,单击消息按钮。这一次,您应该得到一个响应,因为您异步调用了ReadLog方法。五秒钟后,您应该会看到一个消息框,其中包含了Logger.LogReadAsync方法的结果。

  8. 完成测试后,关闭表单。

  9. 保存项目,然后退出 Visual Studio。

摘要

本章描述了应用中的对象如何协作。您看到了对象如何相互传递消息,事件如何驱动程序,类的实例如何共享数据,以及如何处理异常。

在第九章中,我们看集合和数组。集合和数组将相似的对象组织成一个组。使用集合是您需要在应用中应用的最常见的编程结构之一。您将研究. NET Framework 中可用的一些基本集合类型,并学习如何在代码中使用集合。

九、使用集合

在前一章中,你已经看到了在面向对象的程序中对象是如何协作和通信的。那一章介绍了消息传递、事件、委托、异常处理和异步编程的概念。在这一章中,你将会看到对象集合是如何被组织和处理的。那个 .NET Framework 包含一组用于创建和管理对象集合的广泛的类和接口。您将看到各种类型的集合结构 .NET 提供并了解它们的设计目的以及何时使用它们。您还将看到如何使用泛型来创建高度可重用、高效的集合。

在本章中,您将学习以下内容:

  • 公开的各种类型的集合 .NET 框架
  • 如何使用数组和数组列表
  • 如何创建通用集合
  • 如何实现队列和堆栈

介绍 .NET Framework 集合类型

程序员经常需要处理类型的集合。例如,如果你在一个工资系统中处理雇员时间记录,你需要按雇员对记录进行分组,循环遍历记录,并把每个记录的时间加起来。

所有的集合都需要一组基本的函数,比如添加对象、移除对象和遍历它们的对象。除了基本集合之外,一些集合还需要额外的专门功能。例如,在向帮助台电子邮件请求集合中添加和删除项目时,该集合可能需要实现先进先出功能。另一方面,如果请求按严重性划分优先级,那么集合需要能够按优先级对其项目进行排序。

那个 .NET Framework 提供了各种基本的和专门的集合类供您使用。System.Collections名称空间包含定义各种类型集合的接口和类,例如列表、队列、哈希表和字典。表 9-1 列出并描述了一些常用的收集类。如果没有找到具有所需功能的集合类,可以扩展. NET Framework 类来创建自己的集合类。

表 9-1。常用集合类

描述
Array为支持强类型数组的语言实现提供基类。
ArrayList使用大小根据需要动态增加的数组表示弱类型对象列表。
SortedList表示按键排序并可按键和索引访问的键/值对的集合。
Queue表示对象的先进先出(FIFO)集合。
Stack表示简单的后进先出(LIFO)的非泛型对象集合。
Hashtable表示基于键的哈希代码组织的键/值对的集合。
CollectionBase为强类型集合提供抽象基类。
DictionaryBase为键/值对的强类型集合提供抽象基类。

表 9-2 描述了这些集合类实现的一些接口。

表 9-2 。集合类接口

界面描述
ICollection为所有非泛型集合定义大小、枚举数和同步方法。
IComparer公开比较两个对象的方法。
IDictionary表示键/值对的非泛型集合。
IDictionaryEnumerator枚举非泛型字典的元素。
IEnumerable公开枚举数,该枚举数支持对非泛型集合的简单迭代。
IEnumerator支持对非泛型集合的简单迭代。
IList表示可以通过索引单独访问的对象的非一般集合。

在本章中,您将使用一些常用的集合类,从ArrayArrayList类开始。

使用数组和数组列表

数组是计算机编程中最常见的数据结构之一。数组保存相同数据类型的数据元素。例如,你可以创建一个整数、字符串或日期的数组。数组通常用于将值作为参数传递给方法。例如,当您使用控制台应用时,通常会提供命令行开关。下面的 DOS 命令用来在你的电脑上复制一个文件:

copy win.ini c:\windows /y

源文件、目标路径和覆盖指示符作为字符串数组传递到复制程序中。

通过数组的索引来访问数组的元素。索引是一个整数,表示元素在数组中的位置。例如,表示一周中各天的字符串数组具有以下索引值:

| 索引 | | | Zero | 在星期日 | | one | 星期一 | | Two | 星期二 | | three | 星期三 | | four | 星期四 | | five | 星期五 | | six | 星期六 |

这个星期几的例子是一个一维数组,这意味着索引由单个整数表示。数组也可以是多维的。多维数组元素的索引是一组等于维数的整数。图 9-1 显示了一个座位表,表示一个二维数组,其中一个学生的名字(值)被一对有序的行号、座位号(索引)引用。

9781430249351_Fig09-01.jpg

图 9-1 。二维数组

当您声明数组的类型时,就实现了数组功能。作为数组实现的常见类型是数字类型,如整数或双精度类型,以及字符和字符串类型。将类型声明为数组时,在类型后使用方括号([]),后跟数组的名称。数组的元素由一个用花括号({})括起来的逗号分隔的列表指定。例如,下面的代码声明了一个类型为int的数组,并用五个值填充它:

int[] intArray = { 1, 2, 3, 4, 5 };

一旦一个类型被声明为数组,就暴露了Array类的属性和方法。一些功能包括查询数组的上限和下限、更新数组的元素以及复制数组的元素。Array类包含许多用于处理数组的静态方法,比如清除、反转和排序数组元素的方法。

下面的代码演示如何声明和使用整数数组。它还使用了几个由Array类公开的静态方法。注意用于列出数组值的foreach循环。foreach循环提供了一种遍历数组元素的方法。

int[] intArray = { 1, 2, 3, 4, 5 };
Console.WriteLine("Upper Bound");
Console.WriteLine(intArray.GetUpperBound(0));
Console.WriteLine("Array elements");
foreach (int item in intArray)
{
Console.WriteLine(item);
}
Array.Reverse(intArray);
Console.WriteLine("Array reversed");
foreach (int item in intArray)
{
    Console.WriteLine(item);
}
Array.Clear(intArray, 2, 2);
Console.WriteLine("Elements 2 and 3 cleared");
foreach (int item in intArray)
{
    Console.WriteLine(item);
}
intArray[4] = 9;
Console.WriteLine("Element 4 reset");
foreach (int item in intArray)
{
    Console.WriteLine(item);
}
Console.ReadLine();

图 9-2 显示了控制台窗口中这段代码的输出。

9781430249351_Fig09-02.jpg

图 9-2 。一维数组输出

虽然一维数组是您将遇到的最常见的类型,但是您应该理解如何处理偶尔出现的多维数组。二维数组用于存储(在活动内存中)和处理适合表的行和列的数据。例如,您可能需要处理几天内每小时进行的一系列测量(温度或辐射水平)。要创建多维数组,可以在方括号内放置一个或多个逗号来表示维数。一个逗号表示两个维度;两个逗号表示三维,依此类推。填充多维数组时,花括号中的花括号定义了元素。下面的代码声明并填充一个二维数组:

int[,] twoDArray = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
//Print the index and value of the elements
for (int i = 0; i <= twoDArray.GetUpperBound(0); i++)
{
     for (int x = 0; x <= twoDArray.GetUpperBound(1); x++)
     {
          Console.WriteLine("Index = [{0},{1}]  Value = {2}", i, x, twoDArray[i, x]);
     }
}

图 9-3 显示了控制台窗口中该代码的输出。

9781430249351_Fig09-03.jpg

图 9-3 。二维数组输出

当您使用集合时,通常直到运行时才知道它们需要包含的项数。这就是ArrayList类适合的地方。数组列表的容量会根据需要自动扩展,内存重新分配和元素复制会自动执行。ArrayList类还提供了Array类没有提供的处理数组元素的方法和属性。下面的代码演示了其中的一些属性和方法。请注意,随着更多姓名的添加,列表的容量会动态扩展。

ArrayList nameList = new ArrayList();
nameList.Add("Bob");
nameList.Add("Dan");
nameList.Add("Wendy");
Console.WriteLine("Original Capacity");
Console.WriteLine(nameList.Capacity);
Console.WriteLine("Original Values");
foreach (object name in nameList)
{
    Console.WriteLine(name);
}

nameList.Insert(nameList.IndexOf("Dan"), "Cindy");
nameList.Insert(nameList.IndexOf("Wendy"), "Jim");
Console.WriteLine("New Capacity");
Console.WriteLine(nameList.Capacity);
Console.WriteLine("New Values");
foreach (object name in nameList)
{
    Console.WriteLine(name);
}

图 9-4 显示了控制台窗口中的输出。

9781430249351_Fig09-04.jpg

图 9-4 。数组列表输出

虽然使用数组列表通常比使用数组更容易,但是数组列表只能有一维。此外,特定类型的数组比数组列表提供更好的性能,因为ArrayList的元素属于类型Object。当类型被添加到数组列表中时,它们被转换为通用的Object类型。当从列表中检索项目时,必须再次将它们转换为特定类型。

活动 9-1。使用数组和数组列表

在本活动中,您将熟悉以下内容:

  • 创建和使用数组
  • 使用多维数组
  • 使用数组列表

创建和使用数组

要创建并填充一个数组,遵循以下步骤:

  1. 启动 Visual Studio。选择文件image新建image项目。

  2. 选择控制台应用项目。将项目命名为 Activity9_1。控制台应用包含一个名为 Program 的类,它有一个 Main 方法。Main 方法是应用启动时访问的第一个方法。

  3. Notice that the Main method accepts an input parameter of a string array called args. The args array contains any command line arguments passed in when the console application is launched. The members of the args array are separated by a space when passed in.

    static void Main(string[] args)

    {

    }

  4. Add the following code to the Main method to display the command line arguments passed in:

    Console.WriteLine("parameter count = {0}", args.Length);

    for (int i = 0; i < args.Length; i++)

    {

    Console.WriteLine("Arg[{0}] = [{1}]", i, args[i]);

    }

    Console.ReadLine();

  5. In Solution Explorer, right-click the project node and choose Properties. In the project properties window, select the Debug tab. In the command line arguments field, enter “C# coding is fun” (see Figure 9-5).

    9781430249351_Fig09-05.jpg

    图 9-5 。添加命令行参数

  6. Select Debug image Start to run the project. The console window should launch with the output shown in Figure 9-6. After viewing the output, stop the debugger.

    9781430249351_Fig09-06.jpg

    图 9-6 。控制台输出为阵列

  7. Add the following code before the Console.ReadLine method in the Main method. This code clears the value of the array at index 1 and sets the value at index 3 to “great”.

    Array.Clear(args, 1, 1);

    args[3] = "great";

    for (int i = 0; i < args.Length; i++)

    {

    Console.WriteLine("Arg[{0}] = [{1}]", i, args[i]);

    }

  8. Select Debug image Start to run the project. The console window should launch with the additional output shown in Figure 9-7. After viewing the output, stop the debugger.

    9781430249351_Fig09-07.jpg

    图 9-7 。更新阵列的控制台输出

使用多维数组

要创建和填充多维数组,请遵循以下步骤:

  1. 注释掉Main方法中的代码。

  2. Add the following code to the Main method to create and populate a two-dimensional array:

    string[,] seatingChart = new string[2,2];

    seatingChart[0, 0] = "Mary";

    seatingChart[0, 1] = "Jim";

    seatingChart[1, 0] = "Bob";

    seatingChart[1, 1] = "Jane";

  3. Add the following code to loop through the array and print the names to the console window:

    for (int row = 0; row < 2; row++)

    {

    for (int seat = 0; seat < 2; seat++)

    {

    Console.WriteLine("Row: {0} Seat: {1} Student: {2}",

    (row + 1),(seat + 1),seatingChart[row, seat]);

    }

    }

    Console.ReadLine();

  4. Select Debug image Start to run the project. The console window should launch with the output that shows the seating chart of the students (see Figure 9-8).

    9781430249351_Fig09-08.jpg

    图 9-8 。二维数组的控制台输出

  5. 查看输出后,停止调试器。

使用数组列表

虽然您刚刚创建的二维数组可以工作,但是将每个学生的座位分配信息存储在座位分配类中,然后将这些对象组织到一个数组列表结构中可能更直观。要创建和填充座位分配的数组列表,请按照下列步骤操作:

  1. 向名为 SeatingAssignment.cs 的项目添加一个类文件。

  2. Add the following code to create the SeatingAssignment class. This class contains a Row, Seat, and Student property. It also contains an overloaded constructor to set these properties.

    public class SeatingAssignment

    {

    int _row;

    int _seat;

    string _student;

    public int Row

    {

    get { return _row; }

    set { _row = value; }

    }

    public int Seat

    {

    get { return _seat; }

    set { _seat = value; }

    }

    public string Student

    {

    get { return _student; }

    set { _student = value; }

    }

    public SeatingAssignment(int row, int seat, string student)

    {

    this.Row = row;

    this.Seat = seat;

    this.Student = student;

    }

    }

  3. In the Main method of the Program class, comment out the previous code and add the following using statement to the top of the file:

    using System.Collections;

  4. Add the following code to create an ArrayList of seating assignments:

    ArrayList seatingChart = new ArrayList();

    seatingChart.Add(new SeatingAssignment(0, 0, "Mary"));

    seatingChart.Add(new SeatingAssignment(0, 1, "Jim"));

    seatingChart.Add(new SeatingAssignment(1, 0, "Bob"));

    seatingChart.Add(new SeatingAssignment(1, 1, "Jane"));

  5. After the ArrayList is populated, add the following code to write the SeatingAssignment information to the console window.

    foreach (SeatingAssignment sa in seatingChart)

    {

    Console.WriteLine("Row: {0} Seat: {1} Student: {2}",

    (sa.Row + 1), (sa.Seat + 1), sa.Student);

    }

    Console.ReadLine();

  6. 选择调试image开始运行项目。控制台窗口应启动,输出与图 9-8 (学生座位表)所示的相同。

  7. One of the advantages of the ArrayList class is the ability to add and remove items dynamically. Add the following code after the code in step 4 to add two more students to the seating chart:

    seatingChart.Add(new SeatingAssignment(2, 0, "Bill"));

    seatingChart.Add(new SeatingAssignment(2, 1, "Judy"));

  8. 选择调试image开始运行项目。控制台窗口应该启动,输出显示新学生。

  9. 完成后,停止调试器,并关闭 Visual Studio。

使用泛型集合

使用集合是应用编程的常见要求。我们处理的大多数数据都需要组织成一个集合。例如,您可能需要从数据库中检索客户,并将他们加载到 UI(用户界面)的下拉列表中。客户信息由一个客户类表示,客户被组织到一个客户集合中。然后可以对集合进行排序、过滤和循环处理。

除了少数强类型的用于保存字符串的专用集合之外,由 .NET 框架是弱类型的。集合持有的项目属于类型Object,因此它们可以是任何类型,因为所有类型都是从类型Object派生的。

弱类型集合会导致应用的性能和维护问题。一个问题是没有内在的安全措施来限制存储在集合中的对象类型。同一个集合可以包含任何类型的项,包括日期、整数或自定义类型,如 employee 对象。如果您构建并公开了一个整数集合,而该集合无意中被传递了一个日期,那么代码很可能会在某个时候失败。

幸运的是,C# 支持泛型 .NET Framework 在System.Collections.Generic命名空间中提供了基于泛型的集合。泛型允许您定义一个类,而无需指定它的类型。类型是在类被实例化时指定的。使用泛型集合提供了类型安全的优势和强类型集合的性能,同时还提供了与弱类型集合相关联的代码重用。

下面的代码展示了如何使用Generic.List类创建客户的强类型集合。列表类型(在本例中为Customer)放在尖括号(<>)之间。Customer 对象被添加到集合中,然后检索集合中的客户,并将客户信息写出到控制台。(你会在第十一章看到将集合绑定到 UI 控件。)

List<Customer> customerList = new List<Customer>();
customerList.Add(new Customer
    ("WHITC", "White Clover Markets", "Karl Jablonski"));
customerList.Add(new Customer("RANCH", "Rancho grande", "Sergio Gutiérrez"));
customerList.Add(new Customer("ALFKI","Alfreds Futterkiste","Maria Anders"));
customerList.Add
    (new Customer("FRANR", "France restauration", "Carine Schmitt"));
foreach (Customer c in customerList)
{
    Console.WriteLine("Id: {0} Company: {1} Contact: {2}",
                    c.CompanyId, c.CompanyName, c.ContactName);
}

有时,您可能需要扩展由 .NET 框架。例如,您可能需要能够按照CompanyIdCompanyName对客户集合进行排序。要实现排序,您需要定义一个实现IComparer接口的排序类。IComparer接口确保排序类用适当的签名实现了一个Compare方法。(接口在第七章中讨论过。)下面的CustomerSorter类按照CompanyName对一个Customer的列表进行排序。注意,因为CompanyName属性是一个字符串,所以可以使用字符串比较器对它们进行排序。

public class CustomerSorter : IComparer<Customer>
{
    public int Compare(Customer customer1, Customer customer2)
    {
        return customer1.CompanyName.CompareTo(customer2.CompanyName);
    }
}

现在您可以按CompanyName对客户进行排序,然后显示它们。

customerList.Sort(new CustomerSorter());

输出如图 9-9 所示。

9781430249351_Fig09-09.jpg

图 9-9 。客户排序列表的控制台输出

活动 9-2。实现和扩展通用集合

在本活动中,您将熟悉以下内容:

  • 实现泛型集合
  • 扩展泛型集合以实现排序

要创建和填充通用列表,请遵循以下步骤:

  1. 启动 Visual Studio。选择文件image新建image项目。

  2. 选择控制台应用项目。将项目命名为 Activity9_2。

  3. 选择项目image添加类别。将该类文件命名为 Request.cs。

  4. Add the following properties to the Request class:

    public class Request

    {

    string _requestor;

    int _priority;

    DateTime _date;

    public string Requestor

    {

    get { return _requestor; }

    set { _requestor = value; }

    }

    public int Priority

    {

    get { return _priority; }

    set { _priority = value; }

    }

    public DateTime Date

    {

    get { return _date; }

    set { _date = value; }

    }

  5. Overload the constructor of the Request class to set the properties in the constructor.

    public Request(string requestor, int priority, DateTime date)

    {

    this.Requestor = requestor;

    this.Priority = priority;

    this.Date = date;

    }

  6. Add a method to override the ToString method of the base Object class. This will return the request information as a string when the method is called.

    public override string ToString()

    {

    return String.Format("{0}, {1}, {2}",this.Requestor,

    this.Priority.ToString(), this.Date);

    }

  7. Open the Program class in the code editor and add the following code to the Main method. This code populates a generic list of type Request and displays the values in the console window.

    static void Main(string[] args)

    {

    List<Request> reqList = new List<Request>();

    reqList.Add(new Request("Dan",2 ,new DateTime(2011,4,2)));

    reqList.Add(new Request("Alice", 5, new DateTime(2011, 2, 5)));

    reqList.Add(new Request("Bill", 3, new DateTime(2011, 6, 19)));

    foreach (Request req in reqList)

    {

    Console.WriteLine(req.ToString());

    }

    Console.ReadLine();

    }

  8. 选择调试image开始运行项目。控制台窗口应该启动,请求项按照添加到reqList的顺序列出。

  9. 选择项目image添加类。给类命名DateSorter

  10. Add the following code to the DateSorter class. This class implements the IComparer interface and is used to enable sorting requests by date.

`public class DateSorter:IComparer<Request>`
`{`

`public int Compare(Request R1, Request R2)`

`{`

`return R1.Date.CompareTo(R2.Date);`

`}`

`}`

11. Add the following code in the Main method of the Program class prior to the Console.ReadLine method. This code sorts the reqList by date and displays the values in the console window.

`Console.WriteLine("Sorted by date.");`

`reqList.Sort(new DateSorter());`

`foreach (Request req in reqList)`

`{`

`Console.WriteLine(req.ToString());`

`}`

12. Select Debug image Start to run the project. The console window should launch with the output shown in Figure 9-10. After viewing the output, stop the debugger and exit Visual Studio.

![9781430249351_Fig09-10.jpg](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b2b79c429a6345baaffba913715f7974~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772277887&x-signature=ENLCYD1UVFQY94Y83QDoS0B6i4Q%3D)

图 9-10 。未排序和按日期排序的通用集合

使用堆栈和队列编程

编程中经常使用的两种特殊类型的集合是堆栈和队列。堆栈是后进先出的对象集合。队列代表先进先出的对象集合。

栈是一个很好的方法来维护一个国际象棋游戏中的移动列表。当用户想要撤销他的移动时,他从他最近的移动开始,这是最后一个添加到列表中的移动,也是第一个检索到的移动。使用堆栈的另一个例子发生在程序执行一系列方法调用时。堆栈维护方法的地址,执行以调用方法的相反顺序返回方法。将项目放入堆栈时,使用push方法。 pop方法从堆栈中移除项目。peek方法返回堆栈顶部的对象,但不移除它。下面的代码演示如何在堆栈中添加和移除项。在这种情况下,您使用泛型来实现一堆ChessMove对象。RecordMove方法将最近的移动添加到堆栈中。GetLastMove方法返回堆栈上最近的移动。

Stack<ChessMove> moveStack = new Stack<ChessMove>();
void RecordMove(ChessMove move)
{
   moveStack.Push(move);
}
   ChessMove GetLastMove()
{
   return moveStack.Pop();
}

为帮助台请求提供服务的应用是何时使用队列的一个很好的例子。集合维护发送到应用的帮助台请求列表。当从集合中检索请求进行处理时,中的第一个请求应该是最先检索到的请求。Queue类使用enqueuedequeue方法来添加和移除项目。它还实现了peek方法来返回队列开头的项,而不删除该项。下面的代码演示了在PaymentRequest队列中添加和删除项目。AddRequest方法将请求添加到队列中,而GetNextRequest方法将请求从队列中移除。

Queue<PaymentRequest> payRequest = new Queue<PaymentRequest>();
void AddRequest(PaymentRequest request)
{
   payRequest.Enqueue(request);
}
PaymentRequest GetNextRequest()
{
   return payRequest.Dequeue();
}

活动 9-3。实现堆栈和队列

在本活动中,您将熟悉以下内容:

  • 实现堆栈集合
  • 实现队列集合

要创建和填充通用列表,请遵循以下步骤:

  1. 启动 Visual Studio。选择文件image新建image项目。

  2. 选择控制台应用项目。将项目命名为活动 9_3。

  3. Add the following code to the Main method of the Program class.This code creates a stack of strings and loads it with strings representing moves in a game. It then uses the Peek method to write out the move stored at the top of the stack to the console window.

    Stack<string> moveStack = new Stack<string>();

    Console.WriteLine("Loading Stack");

    for (int i = 1; i <= 5; i++)

    {

    moveStack.Push("Move " + i.ToString());

    Console.WriteLine(moveStack.Peek().ToString());

    }

  4. Add the following code to the Main method of the Program class after the code in step 3. This code removes the moves from the stack using the Pop method and writes them to the console window.

    Console.WriteLine("Press the enter key to unload the stack.");

    Console.ReadLine();

    for (int i = 1; i <= 5; i++)

    {

    Console.WriteLine(moveStack.Pop().ToString());

    }

    Console.ReadLine();

  5. Select Debug image Start to run the project. The console window should launch with the output shown in Figure 9-11. Notice the last-in, first-out pattern of the stack. After viewing the output, stop the debugger.

    9781430249351_Fig09-11.jpg

    图 9-11 。堆栈的后进先出模式

  6. Comment out the code entered in steps 3 and 4. Add the following code to the Main method of the Program class. This code creates a queue of strings and loads it with strings representing requests to a consumer help line. It then uses the Peek method to write out the request stored at the beginning of the queue of to the console window.

    Queue<string> requestQueue = new Queue<string>();

    Console.WriteLine("Loading Queue");

    for (int i = 1; i <= 5; i++)

    {

    requestQueue.Enqueue("Request " + i.ToString());

    Console.WriteLine(requestQueue.Peek().ToString());

    }

  7. Add the following code to the Main method of the Program class after the code in step 6. This code removes the requests from the queue using the Dequeue method and writes them to the console window.

    Console.WriteLine("Press the enter key to unload the queue.");

    Console.ReadLine();

    for (int i = 1; i <= 5; i++)

    {

    Console.WriteLine(requestQueue.Dequeue().ToString());

    }

    Console.ReadLine();

  8. Select Debug image Start to run the project. The console window should launch with the output shown in Figure 9-12. Notice that as the queue is loaded the first request stays at the top of the queue. Also notice the first-in, first-out pattern of the queue. After viewing the output, stop the debugger and exit Visual Studio.

    9781430249351_Fig09-12.jpg

    图 9-12 。队列的先进先出模式

摘要

在本章中,您研究了由 .NET 框架。您学习了如何使用数组、数组列表、队列、堆栈和泛型集合。

本章是向您介绍各种 OOP 结构(如类、继承和多态)的系列文章的最后一章。您应该对 C# 中的类结构、对象协作和集合是如何实现的有一个很好的理解。已经向您介绍了 Visual Studio IDE,并且您已经练习了使用它。现在,您已经准备好将这些部分放在一起,开发一个可工作的应用。

第十章是你将发展的系列中的第一章 .NET 应用。在此过程中,您将使用 ADO.NET 和实体框架调查数据访问;使用 Windows 演示基础(WPF)和新的 Windows 8 应用商店创建基于 Windows 的 GUI 使用 web 窗体和 ASP 创建基于 Web 的 GUI。网;,并使用 ASP.NET 通信基金会(WCF)和 web API 创建 Web 服务。