抽象类及其用法






4.94/5 (26投票s)
本文描述了 .NET 中抽象类的概念。
引言
初学者对抽象类有很多困惑。了解其语法很容易,但是何时以及为何使用抽象类困扰着大多数初级开发人员。我将尝试用一个易于理解的例子来解释这个概念。希望这能有所帮助!
背景
抽象类有很多定义,我经常发现文章在解释概念时,更多地关注抽象类的实际语法。例如,抽象类不能有私有虚成员,等等。
本文将尝试解释“是什么”和“何时”。
理解抽象类
抽象类是不能被实例化的类,这意味着您不能创建抽象类的对象,它只能作为基类,其他类可以从它派生。那么,如果一个类不能创建对象,它存在的理由是什么呢?
我们以银行为例来解释。稍后,我们将通过代码来实现它。
假设您去银行开户。让我们看看下面申请人和银行工作人员之间发生了什么。
银行工作人员:欢迎光临。
申请人:我想开一个账户。
银行工作人员:好的,先生,您想开什么类型的账户?
申请人:我只是想开一个账户。
银行工作人员:您是想开储蓄账户还是活期账户?
申请人:我不想开任何特定账户,就开一个账户。
银行工作人员:先生,这不可能,我们需要创建一个特定类型的账户。除非您告诉我账户类型,否则我无法帮助您。
根据上述讨论,我们为银行应用程序软件得出了以下推论。
- 银行有开户功能
- 银行账户应为特定类型(储蓄/活期)
- 银行不能开设通用账户
为了解释这个概念,让我们创建一个银行应用程序。此应用程序仅用于演示目的,不应作为设计指南。
Using the Code
我们有一个 BankAccount
类,由于上述原因,它是一个抽象类。
无论账户类型如何,都有某些成员/行为是所有账户类型通用的,因此应该作为基类的一部分,在我们的案例中是 BankAccount
类。
银行账户通用成员
- 账户所有者属性将包含账户持有人的姓名
- 账户号码唯一标识一个银行账户
- 最低余额将包含账户的最低阈值
- 最大存款金额将包含一次性可存入的最大金额
- 所有银行账户都需要利率
- 交易摘要记录账户中发生的所有交易
银行账户通用行为
- 您可以存钱。
- 您可以从中取钱。
- 应该有计算利息的功能。
- 用户可以生成报告/摘要以查看交易。
Deposit
和 Withdraw
:这两个是抽象方法。存款/取款功能似乎对 SavingAccount
和 CurrentAccount
应该相同,但并非总是如此,就像我们的情况一样。这些方法是抽象的,因为我们希望子类提供自己的实现。(然而,这两个方法也可以创建为虚方法,但为了演示,让它们作为抽象方法。)
CalculateInterest()
方法在抽象类中实现,子类将重用此功能,这显然是抽象类相对于接口在代码重用方面的优势。
public abstract class BankAccount
{
// Name of the Account Owner, Its common for all derived classes
public string AccountOwnerName { get; set; }
// Account Number field is a common field for all the account types
public string AccountNumber { get; set; }
// A field to hold the Account Balance
public decimal AccountBalance { get; protected set; }
// A field to hold the MinimumAccount Balance
protected decimal MinAccountBalance { get; set; }
// A field to hold the Max Deposit Amount Balance
protected decimal MaxDepositAmount { get; set; }
protected decimal InteresetRate { get; set; }
// this variable will hold the summary of all the transaction that took place
protected string TransactionSummary { get; set; }
protected BankAccount(string accountOwnerName, string accountNumber)
{
AccountOwnerName = accountOwnerName;
AccountNumber = accountNumber;
TransactionSummary = string.Empty;
}
// Deposit is an abstract method so that Saving/Current Account must override
// it to give their specific implementation.
public abstract void Deposit(decimal amount);
// Withdraw is an abstract method so that Saving/Current Account must override
// it to give their specific implementation.
public abstract void Withdraw(decimal amount);
public decimal CalculateInterest()
{
return (this.AccountBalance * this.InteresetRate) / 100;
}
// This method adds a Reporting functionality
public virtual void GenerateAccountReport()
{
Console.WriteLine("Account Owner:{0}, Account Number:{1}, AccountBalance:{2}",
this.AccountOwnerName, this.AccountNumber, this.AccountBalance);
Console.WriteLine("Interest Amount:{0}", CalculateInterest());
Console.WriteLine("{0}", this.TransactionSummary);
}
}
构造函数:尽管 BankAccount
类是抽象类,但它仍然有一个构造函数。如果不能创建抽象类的实例,那么构造函数有什么用呢?构造函数在我们创建 SavingAccount
或 CurrentAccount
实例时使用,因此可以在抽象类构造函数中初始化抽象类中定义的变量。请记住,每当实例化子类时,首先调用其基类的构造函数,然后调用派生类的构造函数。
有些字段是 protected
,有些是 public
,我这样设置没有特别的原因。TransactionSummary
被设置为 protected
,以便只有子类才能查看和更改它。
GenerateAccountReport()
方法将显示账户详细信息,包括交易摘要。它是一个虚方法。通过将其设置为虚方法,我们声明任何子类都可以覆盖它以提供自己的实现;但是,默认实现由基类提供。现在让我们转向子类,即 SavingAccount
和 CurrentAccount
public class SavingBankAccount : BankAccount
{
protected int withdrawCount = 0;
public SavingBankAccount(string accountOwnerName, string accountNumber)
:base(accountOwnerName,accountNumber)
{
this.MinAccountBalance = 20000m;
this.MaxDepositAmount = 50000m;
InteresetRate = 3.5m;
}
public override void Deposit(decimal amount)
{
if (amount >= MaxDepositAmount)
{
throw new Exception(string.Format("You can not deposit amount
greater than {0}", MaxDepositAmount.ToString()));
}
AccountBalance = AccountBalance + amount;
TransactionSummary = string.Format("{0}\n Deposit:{1}",
TransactionSummary, amount);
}
public override void Withdraw(decimal amount)
{
// some hard coded logic that withdraw count should not be greater than 3
if (withdrawCount > 3)
{
throw new Exception("You can not withdraw amount more than thrice");
}
if (AccountBalance - amount <= MinAccountBalance)
{
throw new Exception("You can not withdraw amount from your
Savings Account as Minimum Balance limit is reached");
}
AccountBalance = AccountBalance - amount;
withdrawCount++;
TransactionSummary = string.Format("{0}\n Withdraw:{1}",
TransactionSummary, amount);
}
// This method adds details to the base class Reporting functionality
public override void GenerateAccountReport()
{
Console.WriteLine("Saving Account Report");
base.GenerateAccountReport();
// Send an email to user if Savings account balance is less
// than user specified balance this is different than MinAccountBalance
if(AccountBalance > 15000)
{
Console.WriteLine("Sending Email for Account {0}", AccountNumber);
}
}
}
让我们看看我们的 CurrentAccount
类,它也派生自抽象基类。
public class CurrentBankAccount : BankAccount
{
public CurrentBankAccount(string accountOwnerName, string accountNumber)
:base(accountOwnerName,accountNumber)
{
this.MinAccountBalance = 0m;
this.MaxDepositAmount = 100000000m;
InteresetRate = .25m;
}
public override void Deposit(decimal amount)
{
AccountBalance = AccountBalance + amount;
TransactionSummary = string.Format("{0}\n Deposit:{1}",
TransactionSummary, amount);
}
public override void Withdraw(decimal amount)
{
if (AccountBalance - amount <= MinAccountBalance)
{
throw new Exception("You can not withdraw amount from
your Current Account as Minimum Balance limit is reached");
}
AccountBalance = AccountBalance - amount;
TransactionSummary = string.Format("{0}\n Withdraw:{1}",
TransactionSummary, amount);
}
// This method adds details to the base class Reporting functionality
public override void GenerateAccountReport()
{
Console.WriteLine("Current Account Report");
base.GenerateAccountReport();
}
}
让我们深入研究子类。SavingAccount
和 CurrentAccount
的构造函数都根据它们的需求初始化一些变量,但是某些通用变量由抽象类设置,这解释了抽象类中需要构造函数的原因。
Withdraw
和 Deposit
方法非常简单,不需要详细解释。这两个类都已覆盖它们以提供自己的实现。
如果存款金额大于指定限制,SavingAccount
的 Deposit
方法会抛出异常。
SavingAccount
的 Withdraw
方法在抛出异常之前检查提款次数。
SavingAccount
的 GenerateAccountReport
方法添加报告头,调用基类方法以实现通用功能,然后发送账户报告电子邮件。
要使用上面的代码,这是我们的 Main
方法,它分别创建了一个储蓄账户和一个活期账户的实例。用于存储这些实例的变量类型为 BankAccount
,从而允许我们拥有多态行为。
public static void Main(string[] args)
{
BankAccount savingAccount = new SavingBankAccount("Sarvesh", "S12345");
BankAccount currentAccount = new CurrentBankAccount("Mark", "C12345");
savingAccount.Deposit(40000);
savingAccount.Withdraw(1000);
savingAccount.Withdraw(1000);
savingAccount.Withdraw(1000);
// Generate Report
savingAccount.GenerateAccountReport();
Console.WriteLine();
currentAccount.Deposit(190000);
currentAccount.Withdraw(1000);
currentAccount.GenerateAccountReport();
Console.ReadLine();
}
输出如下
Saving Account Report
Account Owner:Sarvesh, Account Number:S12345, AccountBalance:37000
Interest Amount:1295.0
Deposit:40000
Withdraw:1000
Withdraw:1000
Withdraw:1000
Sending Email for Account S12345
Current Account Report
Account Owner:Mark, Account Number:C12345, AccountBalance:189000
Interest Amount:472.50
Deposit:190000
Withdraw:1000
何时使用抽象类
让我们回到银行应用程序的例子。
尽管我们不能开设通用账户,但所有账户都将具有上述某些成员和行为。此外,我们希望所有类型的账户都应符合这些属性和行为。
总结一下,在以下情况下创建抽象类:
- 类表达了一个过于通用的理念,并且其在应用程序中独立(单独)存在是不需要的,例如
BankAccount
。 - 存在一个形成层次结构的类型家族。基类和其他派生类之间存在“IS-A”关系。例如:
- 储蓄账户是银行账户
- 活期账户是银行账户
- 如果所有类都有某些通用的成员/行为,这些应该放在抽象类中,例如
AccountNumber
、Deposit()
等。 - 如果存在应该实现或必须存在于所有类中的行为/属性,则将它们声明为抽象类中的抽象方法,例如
CalculateInterest()
。
为什么不用接口代替 BankAccount 类?
在我们的例子中,您可能会争辩说我们可以使用接口而不是 BankAccount
抽象类,如下所示:
public class SavingBankAccount : IBankAccount
{
void Deposit(decimal amount);
void Withdraw(decimal amount);
decimal CalculateInterest();
}
首先,BankAccount
和 SavingAccount
之间存在层次结构和密切关系。此外,我们能够找出所有子类中存在的某些共同特征,因此抽象类将有助于代码重用。接口更像是类之间的契约。抽象类和接口之间存在许多语法差异,稍加谷歌搜索可能会有很大帮助,因此我在本文中没有涉及。
结论
我通过一个例子解释了什么是抽象类,如何使用它以及何时使用它。