深入 OOP(第 4 天):多态与继承(C# 中的抽象类全解析)






4.89/5 (134投票s)
C# 中的抽象类全解析
1. 引言
我们已经学了很多关于多态和继承的知识。在本系列的“深入 OOP”文章中,我们将讨论 OOP 在 C# 中最热门、最激动人心的话题,即抽象类。抽象类的概念对于任何其他语言都一样,但在 C# 中,我们处理它的方式略有不同。抽象类在多态和继承中扮演着不同且非常有趣的角色。我们将通过实际操作的实验室和理论相结合的方式,涵盖抽象类的所有方面,以解释我们得到的结果。我们还将在文章末尾列出需要记住的要点。
先决条件
希望我们正在处理的学习目标中的第四部分。现在我对读者唯一的期望就是享受这个系列。
2. 路线图
让我们回顾一下我们的路线图
- 深入OOP(第1天):多态性和继承(早期绑定/编译时多态)
- 深入OOP(第2天):多态性和继承(继承)
- 深入 OOP(第 3 天):多态与继承(动态绑定/运行时多态)
- 深入OOP(第4天):多态性和继承(C#中抽象类的一切)
- 深入OOP(第5天):C#中访问修饰符的一切(Public/Private/Protected/Internal/Sealed/Constants/Readonly字段)
- 深入 OOP (第六天):理解 C# 中的枚举 (实践方法)
- 深入OOP(第7天):C#中的属性(一种实用方法)
- 深入OOP(第8天):C#中的索引器(一种实用方法)
- 深入OOP(第9天):理解C#中的事件(洞察)
- 学习C#(第10天):C#中的委托(一种实用方法)
- 学习C#(第11天):C#中的事件(一种实用方法)
3. 抽象类
让我们从 MSDN 获取定义
“abstract 关键字允许您创建不完整且必须在派生类中实现的类和类成员。抽象类不能被实例化。抽象类的目的是提供一个基类的通用定义,多个派生类可以共享该定义。例如,类库可以定义一个抽象类,该类用作其许多函数的参数,并要求使用该库的程序员通过创建派生类来提供自己的类实现。
抽象类也可以定义抽象方法。这是通过在方法的返回类型前加上 abstract 关键字来完成的。”
4. 抽象类实战
在您的 Visual Studio 中添加一个名为“InheritanceAndPolymorphism
”的控制台应用程序。您将获得一个名为 Program.cs 的类,只需再添加一个名为 ClassA.cs 的类,请注意 ClassA
应被标记为 abstract
,并将以下代码添加到 ClassA.cs 和 Program.cs 文件中。
using System;
namespace InheritanceAndPolymorphism
{
public abstract class ClassA
{
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassA classA = new ClassA();
Console.ReadKey();
}
}
}
编译代码。
输出
Compile time error: Cannot create an instance of the abstract class or interface 'InheritanceAndPolymorphism.ClassA'
要点:我们不能使用 new
关键字创建 abstract
类的对象。
现在我们开始理解这个概念。没有任何力量可以阻止 abstract
关键字写在类前面。它充当类的修饰符。我们不能使用 new
关键字创建 abstract
类的对象。看起来这个类对我们来说没有用,因为我们不能像以前那样将其用于其他实际目的。
5. 抽象类中的非抽象方法定义
让我们向我们的 abstract
类添加一些代码
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassA classA = new ClassA();
Console.ReadKey();
}
}
我们再次看到了之前遇到的错误。它再次提醒我们,如果我们已经使用了 abstract
修饰符,就不能使用 new
。
6. 抽象类作为基类
现在让我们再添加一个类
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA
/// </summary>
public class ClassB:ClassA
{
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
我们没有收到错误?一个类可以派生自 abstract
类。创建 ClassB
的对象不会给我们带来任何错误。
要点:一个类可以派生自 abstract
类。
要点:可以使用 new 创建派生自抽象类的类的对象。
注意:本文中的每个代码片段都经过了尝试和测试。
7. 抽象类中的非抽象方法声明
另一种情况
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
我们在 abstract
类 ClassA
中声明了一个名为 YYY()
的方法。
编译代码,我们得到
输出
Compile time error: 'InheritanceAndPolymorphism.ClassA.YYY()'
must declare a body because it is not marked abstract, extern, or partial
InheritanceAndPolymorphism
是我为控制台应用程序使用的命名空间,所以您可以忽略它,无需在意逻辑。
在上面的代码中,我们只是在 abstract
类中添加了一个方法声明。abstract
方法表示该方法的实际定义或代码是在别处创建的。根据 C# 的规则,在 abstract
类中声明的方法原型也必须声明为 abstract
。
8. 抽象类中的抽象方法声明
将 YYY()
方法在 ClassA
中声明为 abstract
。
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
输出
Compiler error: 'InheritanceAndPolymorphism.ClassB' does not implement
inherited abstract member 'InheritanceAndPolymorphism.ClassA.YYY()'
要点:如果我们将在 abstract
类中的任何方法声明为 abstract
,那么派生类有责任提供该 abstract
方法的实现,除非为该 abstract
方法提供了实现,否则我们无法创建该派生类的对象。
在上述场景中,我们在 ClassA
中将方法 YYY()
声明为 abstract
。由于 ClassB
派生自 ClassA
,因此现在 ClassB
有责任提供该 abstract
方法的实现,否则我们将无法创建 ClassB
的对象。
9. 派生类中的抽象方法实现
现在为 ClassB
中的方法 YYY()
提供实现。看看会发生什么
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
public void YYY()
{
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
现在一切看起来都很好,但不是吗?编译代码,我们得到
输出
这次有两个编译时错误
Compile time error: 'InheritanceAndPolymorphism.ClassB' does not implement
inherited abstract member 'InheritanceAndPolymorphism.ClassA.YYY()'
Compile time warning: 'InheritanceAndPolymorphism.ClassB.YYY()' hides
inherited member 'InheritanceAndPolymorphism.ClassA.YYY()'.
要使当前成员覆盖该实现,请添加 override
关键字。否则,添加 new
关键字。
我们一直在尝试编译代码,但至今仍未成功。编译器错误清楚地表明,我们的基类和派生类都包含同名的方法 YYY()
。
如果我们的派生类和基类都包含同名的方法,总是会发生错误。克服这个错误的唯一方法是派生类显式地在其方法签名中添加 override 修饰符。我们在“深入 OOP”系列文章的先前部分中已经讨论过这种情况。
让我们在派生类方法 YYY()
前添加 override
关键字。
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
public override void YYY()
{
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
现在我们没有警告或错误了?
10. 派生类中带不同返回类型的抽象方法实现
要点:当我们在派生类中重写 abstract
或 virtual
方法时,我们不能更改传递给它的参数或被重写方法的返回类型。
让我们更改派生类中方法 YYY()
的返回类型
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
public override int YYY()
{
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
我们将派生类中方法 YYY
的返回类型从 void
更改为 int
。编译代码。
输出
Compile time error: 'InheritanceAndPolymorphism.ClassB.YYY()': return type must be 'void'
to match overridden member 'InheritanceAndPolymorphism.ClassA.YYY()'
因此,又一个限制。
要点:Abstract
类意味着该类不完整,不能直接使用。Abstract
类只能用作其他类的基类,供其派生。
让我们看看“要点”中第二行的实现,
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
abstract public void YYY1();
abstract public void YYY2();
abstract public void YYY3();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
public override void YYY()
{
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
编译器错误
'InheritanceAndPolymorphism.ClassB' does not implement
inherited abstract member 'InheritanceAndPolymorphism.ClassA.YYY3()'
'InheritanceAndPolymorphism.ClassB' does not implement inherited
abstract member 'InheritanceAndPolymorphism.ClassA.YYY2()'
'InheritanceAndPolymorphism.ClassB' does not implement inherited
abstract member 'InheritanceAndPolymorphism.ClassA.YYY1()'
如果我们将在派生类中实现这三个方法,我们就不会收到错误。
11. 抽象类中的变量初始化
因此,如前所述,如果我们对 abstract
类使用 new
关键字,我们会收到一个错误。如果我们像使用 a
那样,不在 abstract
类中初始化变量,它将自动具有默认值 0
,这就是编译器不断警告我们的原因。我们可以将 ClassA
的 int
变量 a
初始化为我们想要的任何值。abstract
类中的变量与任何其他普通类中的变量作用相似。
12. 抽象类的强大之处
每当一个类保持不完整时,即,我们没有某些方法的代码,我们就将这些方法标记为 abstract
,并将类本身也标记为 abstract
。因此,我们可以编译我们的类而不会出现任何错误或障碍。任何其他类都可以从我们的 abstract
类派生,但它们必须实现 abstract
,即我们 abstract
类中的不完整方法。
因此,Abstract
使我们能够编写类的一部分代码,并允许其他人(派生类)完成其余的代码。
13. 非抽象类中的抽象方法
让我们再看一个代码块
/// <summary>
/// Abstract class ClassA
/// </summary>
public class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
public override void YYY()
{
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
编译代码。
输出
Compiler error: 'InheritanceAndPolymorphism.ClassA.YYY()' is abstract
but it is contained in non-abstract class 'InheritanceAndPolymorphism.ClassA'
注意:本文中的每个代码片段都经过了尝试和测试。
我们只是从类 ClassA
中移除了 abstract
关键字。错误清楚地传达了一个信息,即如果一个方法在一个类中被标记为 abstract
,那么该类也必须被标记为 abstract
。
要点:如果一个类甚至有一个 abstract
方法,那么该类也必须被声明为 abstract
。
要点:Abstract
方法也不能使用 static
或 virtual
等修饰符。
我们只能在 abstract
类中拥有 abstract
方法。任何派生自 abstract
类的类都必须为其 abstract
方法提供实现。默认情况下,new
修饰符会添加到派生类方法中,使其成为一个新/不同的方法。
14. 抽象基类方法
/// <summary>
/// Abstract class ClassA
/// </summary>
public abstract class ClassA
{
public int a;
public void XXX()
{
}
abstract public void YYY();
}
/// <summary>
/// Derived class.
/// Class derived from abstract class ClassA.
/// </summary>
public class ClassB:ClassA
{
public override void YYY()
{
base.YYY();
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassB classB = new ClassB();
Console.ReadKey();
}
}
输出
Compile time error : Cannot call an abstract base member:
'InheritanceAndPolymorphism.ClassA.YYY()'
我们无法从基类 ClassA
调用方法 YYY()
,因为它不包含任何实现/代码,并且已被声明为 abstract
。常识占主导地位?而 C# 当然不允许我们调用不包含代码的方法。
15. 抽象类既可作为派生类也可作为基类
让我们稍微修改一下我们的代码,准备我们的类结构如下:
/// <summary>
/// Base class ClassA
/// </summary>
public class ClassA
{
public virtual void XXX()
{
Console.WriteLine("ClassA XXX");
}
}
/// <summary>
/// Derived abstract class.
/// Class derived from base class ClassA.
/// </summary>
public abstract class ClassB:ClassA
{
public new abstract void XXX();
}
public class ClassC:ClassB
{
public override void XXX()
{
System.Console.WriteLine("ClassC XXX");
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
ClassA classA = new ClassC();
ClassB classB = new ClassC();
classA.XXX(); classB.XXX();
}
}
编译代码并运行。
输出
ClassA XXX
ClassC XXX
我们创建了一个名为 ClassA
的基类,它不是 abstract
的,并向其添加了一个虚拟方法 XXX
。由于该方法不是 abstract
的而是标记为 virtual
,因此必须在其派生类中进行重写。我们添加了另一个名为 ClassB
的类,并将其标记为 abstract
,请注意该类派生自 ClassA
。所以这个类可以选择重写基类中标记为虚拟的方法。但我们将做一些不同寻常且棘手的事情,
我们将此派生类中的 XXX
方法标记为 new abstract
,并且没有给这个方法任何实现。现在呢?我们将再添加一个类 ClassC
,它将派生自 ClassB
。ClassC
别无选择,只能重写方法 XXX
。因此,我们在 ClassC
中重写了方法 XXX
。
在 main 方法中,我们创建了两个对象 ClassA classA = new ClassC();
和 ClassB classB = new ClassC();
第一个对象看起来像 ClassC
的,但引用 ClassA
;第二个对象看起来也像 ClassC
的,但引用 ClassB
。
在 classA.XXX()
的情况下,它肯定会首先查找 ClassA
类。在这里,它会找到标记为 virtual
的方法 XXX
。这类场景我们在之前关于运行时多态的文章中已经讨论过 N 次了。然后 C# 会找到 ClassB
类。在这里,它会感到震惊,因为方法 XXX()
是 abstract
,也就是说,方法 XXX()
没有任何代码或实现,并且它被标记为 new
,从而切断了与基类的所有联系。因此,流程停止,ClassA
中的方法 XXX()
被执行。
在 b.XXX()()
的情况下,由于该方法是 new
,与基类的链接被断开,我们别无选择,只能调用 ClassC
中的方法,因为它说 override
。
我们不能将 abstract
类 ClassB
中的方法 XXX()
的 new
修饰符替换为 override
关键字。
让我们将 ClassC
中的 override
修饰符替换为“new
”,如下所示:
public class ClassC:ClassB
{
public new void XXX()
{
System.Console.WriteLine("ClassC XXX");
}
}
输出
Compile time error: 'InheritanceAndPolymorphism.ClassC' does not implement
inherited abstract member 'InheritanceAndPolymorphism.ClassB.XXX()'
错误表明,由于方法 XXX
没有代码。请记住,ClassA
中的 XXX()
方法与 ClassB
和 ClassC
中的方法没有任何关系。
还有一点需要记住。
要点:虚拟方法比非虚拟方法运行得慢。
16. 抽象类是否可以被密封?
让我们考虑这个最终问题。我们来通过一个例子来测试一下。
/// <summary>
/// sealed abstract class ClassA
/// </summary>
public sealed abstract class ClassA
{
public abstract void XXX()
{
Console.WriteLine("ClassA XXX");
}
}
/// <summary>
/// Program: used to execute the method.
/// Contains Main method.
/// </summary>
public class Program
{
private static void Main(string[] args)
{
}
}
编译代码。
输出
Compile time error: 'InheritanceAndPolymorphism.ClassA':
an abstract class cannot be sealed or static
因此,我们得到两个要点。
要点:Abstract
类不能是 sealed
类。
要点:Abstract
类不能是 static
类。
17. 要点回顾
让我们总结一下所有要点
- 我们不能使用
new
关键字创建abstract
类的对象。 - 一个类可以派生自
abstract
类。 - 可以使用 new 创建派生自 abstract 类的对象。
- 如果我们将在
abstract
类中的任何方法声明为abstract
,那么派生类有责任提供该abstract
方法的实现,除非为该abstract
方法提供了实现,否则我们无法创建该派生类的对象。 - 当我们在派生类中重写
abstract
或virtual
方法时,我们不能更改传递给它的参数或被重写方法的返回类型。 Abstract
类意味着该类不完整,不能直接使用。Abstract
类只能用作其他类的基类,供其派生。- 如果一个类甚至有一个
abstract
方法,那么该类也必须被声明为abstract
。 Abstract
方法也不能使用static
或virtual
等修饰符。Virtual
方法比非虚拟方法运行得慢。Abstract
类不能是sealed
类。Abstract
类不能是static
类。
18. 结论
通过这篇文章,我们完成了对继承和多态的理解。我们已经涵盖了多态和继承的几乎所有方面。Abstract
类是我最喜欢的之一,所以我想单独介绍它们。我希望我的读者也喜欢这篇文章,并了解了 C# 中的 abstract
类。
在我接下来的系列文章中,我们将以 C# 的方式讨论其他 OOP 特性,并进行完整的实践操作和大量的讨论。
继续编码,尽情阅读。
如果我的文章在任何方面对您有帮助,请不要忘记对我的文章进行评分/评论/点赞。这有助于我获得动力,并鼓励我写得更多。
我的其他系列文章
MVC: https://codeproject.org.cn/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu
RESTful WebAPIs: https://codeproject.org.cn/Articles/990492/RESTful-Day-sharp-Enterprise-Level-Application
祝您编码愉快!