深入理解 OOP(第 1 天):多态与继承(早期绑定/编译时多态)






4.87/5 (327投票s)
本文将涵盖新手/初学者开发者所寻找的几乎每一个 OOP 概念,而且不仅仅是针对初学者,本文的目的也是为了帮助那些有时需要温习概念或准备面试的经验丰富的专业人士。
引言
我写过很多关于高级主题的文章,比如 MVC、实体框架、仓储模式等。我的首要任务始终是全面地涵盖主题,以便读者不必在别处寻找缺失的环节。本文将涵盖新手/初学者开发者可能寻找的几乎每一个 OOP 概念,而且不仅仅是针对初学者,本文的目的也是为了帮助那些有时需要温习概念或准备面试的经验丰富的专业人士。
我将以一种简单直接的方式来探讨这些主题,在需要的地方提供代码片段作为示例。在整个阅读过程中,我们将使用 C# 作为我们的编程语言。
我们将讨论一些棘手的问题,而不是过多地探讨理论。关于理论,您可以参考 MSDN。
- 深入 OOP(第一天):多态与继承(早期绑定/编译时多态)
- 深入OOP(第2天):多态性和继承(继承)
- 深入OOP(第3天):多态性和继承(动态绑定/运行时多态)
- 深入 OOP (第四天):多态与继承 (关于 C# 中的抽象类)
- 深入OOP(第5天):C#中访问修饰符的一切(Public/Private/Protected/Internal/Sealed/Constants/Readonly字段)
- 深入理解 OOP(第 6 天):理解 C# 中的枚举(一种实用方法)
- 深入OOP(第7天):C#中的属性(一种实用方法)
- 深入OOP(第8天):C#中的索引器(一种实用方法)
- 深入OOP(第9天):理解C#中的事件(洞察)
- 学习C#(第10天):C#中的委托(一种实用方法)
- 学习C#(第11天):C#中的事件(一种实用方法)
先决条件
由于这是本系列的第一部分,我的读者应该具备 C# 的基础知识,并了解 OOP 的概念和术语。
OOP
1. 什么是 OOP,它有什么优势?
OOP 代表“面向对象编程”(Object-Oriented Programming)。请记住,是 OOP 而不是 OOPS,'S' 可能代表系统(system)、概要(synopsis)、结构(structure)等。它是一种完全基于对象的编程方法,而不是像过程式语言那样仅仅基于函数和过程。它就像一种围绕对象而非“动作”、围绕数据而非逻辑组织的编程语言模型。在 OOP 语言中,“对象”指的是一个类的特定类型或“实例”。每个对象的结构与类中的其他对象完全相似,但可以拥有独立的属性/值。一个对象也可以调用特定于该对象的方法。
OOP 使开发人员更容易构建和组织软件程序。单个对象可以在不影响程序其他方面的情况下被修改,因此,用面向对象语言编写的程序也更容易更新和更改。随着近年来软件程序规模的不断扩大,OOP 使得开发这些大型程序变得更易于管理和阅读。
2. OOP 的概念有哪些?
以下是 OOP 概念的简要解释,我们将详细探讨这些主题。
- 数据抽象:数据抽象是一个概念,它将逻辑实现的内部和多余细节对最终用户(使用程序的人)隐藏起来。用户可以使用类中的任何数据和方法,而无需了解它是如何创建的或其背后的复杂性。用现实世界的例子来说,当我们骑自行车换挡时,我们不必关心其内部工作原理,比如拉杆是如何拉动的,或者链条是如何设置的。
- 继承:继承是 OOP 中最流行的概念。它为开发人员提供了一个叫做代码可重用性的优势。假设一个类已经编写好了,其中包含具有特定逻辑的函数,那么我们可以将该类派生到我们新创建的类中,而无需为派生类的函数再次编写逻辑,我们可以直接使用它们。
- 数据封装:将一个类的成员数据和成员函数包装在一个单元中称为封装。成员函数和数据成员的可见性通过类中使用的访问修饰符来设置。
- 多态:Poly 意为“多”,morphism 意为“变化”或“可变的”。这个概念以一个对象的多种行为形式引入。
- 消息通信:消息通信指当一个对象将调用传递给类的方法以供执行。
好了,我们讲了很多理论,现在是时候行动了。我希望这会很有趣。我们将按以下系列来涵盖这些主题,
3. 多态
在本文中,我们将涵盖几乎所有编译时多态的场景,详细介绍 params 关键字的用法,并通过案例研究或动手实践来探讨编码时我们脑海中出现的各种可能组合。
方法重载、早期绑定或编译时多态
注意:本文中的每个代码片段都经过了尝试和测试。
- 让我们创建一个名为
InheritanceAndPolymorphism
的简单控制台应用程序,并添加一个名为 Overload.cs 的类,再添加三个名为DisplayOverload
的方法,它们具有不同的参数,如下所示,Overload.cs
-
public class Overload { public void DisplayOverload(int a){ System.Console.WriteLine("DisplayOverload " + a); } public void DisplayOverload(string a){ System.Console.WriteLine("DisplayOverload " + a); } public void DisplayOverload(string a, int b){ System.Console.WriteLine("DisplayOverload " + a + b); } }
在 Program.cs 文件的主方法中,添加以下代码,
Program.cs
class Program { static void Main(string[] args) { Overload overload = new Overload(); overload.DisplayOverload(100); overload.DisplayOverload("method overloading"); overload.DisplayOverload("method overloading", 100); Console.ReadKey(); } }
现在运行应用程序,输出是,
输出
DisplayOverload 100
DisplayOverload method overloading
DisplayOverload method overloading100
Overload
类包含三个名为 DisplayOverload
的方法,它们仅在所含参数的数据类型上有所不同。在 C# 中,我们可以有同名的方法,但它们的参数数据类型必须不同。C# 的这个特性称为方法重载。因此,如果一个方法因行为不同而异,我们不需要记住很多方法名,只需为方法提供不同的参数即可单独调用该方法。
要点: C# 通过方法的参数来识别它,而不仅仅是通过其名称。
一个签名表示方法的完整名称。因此,一个方法的名称或其签名是原始方法名 + 其各个参数的数量和数据类型。
如果我们使用以下代码运行项目,
public void DisplayOverload() { }
public int DisplayOverload(){ }
我们肯定会得到一个编译时错误,
错误:类型 'InheritanceAndPolymorphism.Overload' 已定义了一个名为 'DisplayOverload' 且具有相同参数类型的成员
这里我们有两个函数,它们仅在返回值的类型上有所不同,但我们得到了一个编译时错误。因此,又有一个要点需要记住,
要点:如果方法名相同,方法的返回值/参数类型永远不是方法签名的一部分。所以这不是多态。
如果我们使用以下代码运行项目,
static void DisplayOverload(int a) { } public void DisplayOverload(int a) { } public void DisplayOverload(string a){ }
我们再次得到一个编译时错误,
错误:类型 'InheritanceAndPolymorphism.Overload' 已定义了一个名为 'DisplayOverload' 且具有相同参数类型的成员
你能分辨出上面代码中的修改吗?我们现在有两个 DisplayOverload
方法,都接受一个 int (整数)。唯一的区别是一个方法被标记为 static。这里,方法的签名将被视为相同,因为像 static 这样的修饰符也不被认为是方法签名的一部分。
要点:像 static 这样的修饰符不被视为方法签名的一部分。
如果我们按照以下代码运行程序,考虑到现在方法签名是不同的,
private void DisplayOverload(int a) { }
private void DisplayOverload(out int a)
{
a = 100;
}
private void DisplayOverload(ref int a) { }
我们再次得到一个编译时错误,
错误:无法定义重载方法 'DisplayOverload',因为它与另一个方法的区别仅在于 ref 和 out
方法的签名不仅包括参数的数据类型,还包括参数的类型/种类,如 ref 或 out 等。方法 DisplayOverload
接受一个带有不同访问修饰符(即 out/ref 等)的 int,每个方法的签名都是不同的。
要点:一个方法的签名由其名称、形式参数的数量和类型组成。函数的返回类型不是签名的一部分。两个方法不能有相同的签名,非成员也不能与成员同名。
4. Params 参数在多态中的作用
一个方法可以由四种不同类型的参数调用。
- 按值传递,
- 按引用传递,
- 作为输出参数,
- 使用参数数组。
如前所述,参数修饰符永远不是方法签名的一部分。现在让我们关注参数数组。
一个方法声明意味着在内存中创建一个独立的声明空间。所以任何创建的东西都会在方法结束时丢失。
运行以下代码,
public void DisplayOverload(int a, string a) { }
public void Display(int a)
{
string a;
}
导致编译时错误,
错误1:参数名 'a' 重复
错误2:无法在此作用域中声明名为 'a' 的局部变量,因为它会给 'a' 赋予不同的含义,而 'a' 已在 '父级或当前' 作用域中用于表示其他内容
要点:参数名称必须唯一。并且,在同一个函数中,我们不能有同名的参数名和已声明的变量名。
在按值传递的情况下,传递的是变量的值;而在 ref 和 out 的情况下,传递的是引用的地址。
当我们运行以下代码时,
Overload.cs
public class Overload
{
private string name = "Akhil";
public void Display()
{
Display2(ref name, ref name);
System.Console.WriteLine(name);
}
private void Display2(ref string x, ref string y)
{
System.Console.WriteLine(name);
x = "Akhil 1";
System.Console.WriteLine(name);
y = "Akhil 2";
System.Console.WriteLine(name);
name = "Akhil 3";
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
我们得到输出为,
输出
Akhil
Akhil 1
Akhil 2
Akhil3
我们被允许任意多次传递同一个 ref 参数。在 Display
方法中,字符串 name 的值为 Akhil。然后,通过将字符串 x 改为 Akhil1,我们实际上是将字符串 name 改为 Akhil1,因为 name 是通过引用传递的。变量 x 和 name 指向内存中同一个字符串。改变一个就会改变另一个。同样,改变 y 也会改变 name 变量,因为它们无论如何都指向同一个字符串。因此,变量 x、y 和 name 都指向内存中的同一个字符串。
当我们运行以下代码时,
Overload.cs
public class Overload { public void Display() { DisplayOverload(100, "Akhil", "Mittal", "OOP"); DisplayOverload(200, "Akhil"); DisplayOverload(300); } private void DisplayOverload(int a, params string[] parameterArray) { foreach (string str in parameterArray) Console.WriteLine(str + " " + a); } }
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
我们得到输出,
输出
Akhil 100
Mittal 100
OOP 100
Akhil 200
我们经常会遇到这样一种场景:希望向一个方法传递 n 个参数。由于 C# 在向方法传递参数方面非常严格,如果我们传递一个 int 而期望的是一个 string,程序会立即崩溃。但是 C# 提供了一种向方法传递 n 个参数的机制,我们可以借助 params
关键字来实现它。
要点:此 params
关键字只能应用于方法的最后一个参数。所以这 n 个参数只能在末尾。
在 DisplayOverload
方法的例子中,第一个参数必须是整数,其余的可以是零到无限个字符串。
如果我们添加一个像这样的方法,
private void DisplayOverload(int a, params string[] parameterArray, int b) { }
我们会得到一个编译时错误,
错误:参数数组必须是形式参数列表中的最后一个参数
因此,这证明了 params
关键字将是方法中的最后一个参数,这一点在最新的要点中已经说明。
Overload.cs
public class Overload
{
public void Display()
{
DisplayOverload(100, 200, 300);
DisplayOverload(200, 100);
DisplayOverload(200);
}
private void DisplayOverload(int a, params int[] parameterArray)
{
foreach (var i in parameterArray)
Console.WriteLine(i + " " + a);
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
当我们运行代码时,我们得到,
200 100
300 100
100 200
因此,
要点:C# 非常聪明,能够识别倒数第二个参数和 params
是否具有相同的数据类型。
第一个整数存储在变量 a 中,其余的则成为数组 parameterArray
的一部分。
private void DisplayOverload(int a, params string[][] parameterArray) { } private void DisplayOverload(int a, params string[,] parameterArray) { }
对于上面编写的代码,我们再次得到一个编译时错误,以及一个新的要点,
错误: 参数数组必须是一维数组
要点:与上面的错误相同。
params
参数的数据类型必须是一维数组。因此 [ ][ ]
是允许的,但 [,] 不行。我们也不允许将 params
关键字与 ref 或 out 结合使用。
Overload.cs
public class Overload
{
public void Display()
{
string[] names = {"Akhil", "Ekta", "Arsh"};
DisplayOverload(3, names);
}
private void DisplayOverload(int a, params string[] parameterArray)
{
foreach (var s in parameterArray)
Console.WriteLine(s + " " + a);
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
输出
Akhil 3
Ekta 3
Arsh 3
因此,我们被允许传递一个字符串数组,而不是单个字符串作为参数。这里,names
是一个使用简写形式初始化的字符串数组。在内部,当我们调用 DisplayOverload
函数时,C# 会将字符串数组转换为单个字符串。
Overload.cs
public class Overload
{
public void Display()
{
string [] names = {"Akhil","Arsh"};
DisplayOverload(2, names, "Ekta");
}
private void DisplayOverload(int a, params string[] parameterArray)
{
foreach (var str in parameterArray)
Console.WriteLine(str + " " + a);
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
输出
错误:'InheritanceAndPolymorphism.Overload.DisplayOverload(int, params string[])' 的最佳重载方法匹配有一些无效的参数
错误:参数2:无法从 'string[]' 转换为 'string'
所以,我们得到了两个错误。
对于上述代码,C# 不允许混合匹配。我们假设最后一个字符串“Ekta”会被添加到字符串数组 names 中,或者将 names 转换为单个字符串,然后再将字符串“Ekta”添加进去。这很合乎逻辑。
在调用 DisplayOverload
函数之前,C# 内部会收集所有单个参数,并将它们转换为一个大的数组,以供 params
语句使用。
Overload.cs
public class Overload
{
public void Display()
{
int[] numbers = {10, 20, 30};
DisplayOverload(40, numbers);
Console.WriteLine(numbers[1]);
}
private void DisplayOverload(int a, params int[] parameterArray)
{
parameterArray[1] = 1000;
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
输出
1000
我们看到产生的输出证明了这个概念。数组的成员 parameterArray[1]
的初始值为 20,在 DisplayOverload
方法中,我们将其更改为 1000。所以原始值改变了,这表明数组被传递给了 DisplayOverload
方法,由此得证。
更多文章,请访问 A Practical Approach。
Overload.cs
public class Overload
{
public void Display()
{
int number = 102;
DisplayOverload(200, 1000, number, 200);
Console.WriteLine(number);
}
private void DisplayOverload(int a, params int[] parameterArray)
{
parameterArray[1] = 3000;
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
输出
102
在上述场景中,C# 创建了一个包含 1000、102 和 200 的数组。我们现在将数组的第二个成员更改为 3000,这与变量 number 无关。因为 DisplayOverload
不知道 number 的存在,那么 DisplayOverload
又怎么能改变 int 型变量 number 的值呢?因此它保持不变。
Overload.cs
public class Overload
{
public void Display()
{
DisplayOverload(200);
DisplayOverload(200, 300);
DisplayOverload(200, 300, 500, 600);
}
private void DisplayOverload(int x, int y)
{
Console.WriteLine("The two integers " + x + " " + y);
}
private void DisplayOverload(params int[] parameterArray)
{
Console.WriteLine("parameterArray");
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
Overload overload = new Overload();
overload.Display();
Console.ReadKey();
}
}
输出
parameterArray
两个整数 200 300
parameterArray
现在我们来谈谈方法重载。C# 虽然非常有才华,但也有偏心。它不欣赏 params
语句,并将其视为继子。当我们只用一个整数调用 DisplayOverload
时,C# 只能调用那个接受 params
作为参数的 DisplayOverload
,因为它只匹配一个 int。一个数组也可以只包含一个成员。有趣的是当用两个 int 调用 DisplayOverload
时。这里我们面临一个两难的境地。C# 可以调用带 params
的 DisplayOverload
,也可以调用带两个 int 的 DisplayOverload
。如前所述,C# 将 params
视为二等成员,因此选择带两个 int 的 DisplayOverload
。当有两个以上的 int 时,就像第三次方法调用那样,C# 别无选择,只能不情愿地选择带 params
的 DisplayOverload
。C# 在报错之前,会把 params
作为最后的选择。
现在是一个有点棘手但很重要的例子,
Overload.cs
public class Overload
{
public static void Display(params object[] objectParamArray)
{
foreach (object obj in objectParamArray)
{
Console.Write(obj.GetType().FullName + " ");
}
Console.WriteLine();
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
object[] objArray = { 100, "Akhil", 200.300 };
object obj = objArray;
Overload.Display(objArray);
Overload.Display((object)objArray);
Overload.Display(obj);
Overload.Display((object[])obj);
Console.ReadKey();
}
}
输出
System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double
在第一个实例中,我们向 Display
方法传递了一个看起来像 object 的对象数组。因为所有的类都派生自一个公共基类 object,所以我们可以这样做。Display
方法接收一个对象数组 objectParamArray
。在 foreach
循环中,object 类有一个名为 GetType
的方法,它返回一个看起来像 Type 的对象,该对象也有一个名为 FullName
的方法,返回类型的名称。因此显示了三种不同的类型。在对 Display
的第二次方法调用中,我们将 objArray
强制转换为一个 object。由于没有可用的从 object 到对象数组(即 object[])的转换,所以只创建了一个包含一个元素的 object[]。第三次调用也是同样的情况,最后一次是显式地转换为对象数组。
为了证明这个概念,
Overload.cs
public class Overload
{
public static void Display(params object[] objectParamArray)
{
Console.WriteLine(objectParamArray.GetType().FullName);
Console.WriteLine(objectParamArray.Length);
Console.WriteLine(objectParamArray[0]);
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
object[] objArray = { 100, "Akhil", 200.300 };
Overload.Display((object)objArray);
Console.ReadKey();
}
}
输出
System.Object[]
1
System.Object[]
5. 结论
在我们“深入理解 OOP”系列的这篇文章中,我们学习了编译时多态,它也被称为早期绑定或方法重载。我们涵盖了大多数与多态相关的特定场景。我们还学习了强大的 params 关键字的用法及其在多态中的应用。
为了总结,让我们再次列出所有的要点,
- C# 通过方法的参数来识别它,而不仅仅是通过其名称。
- 如果方法名相同,方法的返回值/参数类型永远不是方法签名的一部分。所以这不是多态。
- 像 static 这样的修饰符不被视为方法签名的一部分。
- 一个方法的签名由其名称、形式参数的数量和类型组成。函数的返回类型不是签名的一部分。两个方法不能有相同的签名,非成员也不能与成员同名。
- 参数名称必须唯一。并且,在同一个函数中,我们不能有同名的参数名和已声明的变量名。
- 在按值传递的情况下,传递的是变量的值;而在 ref 和 out 的情况下,传递的是引用的地址。
- 此 params 关键字只能应用于方法的最后一个参数。所以这 n 个参数只能在末尾。
- C# 非常聪明,能够识别倒数第二个参数和 params 是否具有相同的数据类型。
- 参数数组必须是一维数组。
在接下来的文章中,我们将以同样的方式涵盖其他主题。
我的其他系列文章
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
祝您编码愉快!