C#入门详解 重写与多态
多态指的是在⼀个继承体系中⼀个类的对象可有多种状态,每种状态下有可能表现不同;
多态指的是在面向对象编程(OOP)中,同一个方法或操作可能会被应用于多个不同的类的实例上,并且每个实例都能够根据其不同的类型作出合适的响应。换句话说,就是通过父类或接口来引用不同子类的实例,使得相同的方法或操作可以适用于不同的对象。
多态性可以大大提高代码的复用性和灵活性。当我们使用多态时,程序可以基于不同的对象表现出不同的行为,而这些行为有一个共同的接口。这样可以大大提高程序的可扩展性和可维护性,同时也可以方便地组织代码,实现封装、抽象和分层等设计原则。
例如,我们可以定义一个 Animal 类,并从它派生出 Cat 和 Dog 类。Cat 和 Dog 都具有 Speak() 方法,然而它们所谓的“说话”却不一样。通常情况下,如果我们要让 Cat 或者 Dog 说话,就需要针对不同的对象调用不同的方法。但是多态允许我们通过 Animal 引用指向对应子类的对象,然后通过调用标准的 Speak() 方法来实现多态效果,从而避免了控制流的复杂性和代码的重复性。
实现多态(Polymorphism)的方法有以下几种:
方法重载(Method Overloading):在同一个类中,通过定义名称相同但参数类型或个数不同的多个方法来实现多态。编译器会根据传入的参数类型或个数自动选择对应的方法进行调用。 方法重写(Method Overriding):在子类中重写父类或接口定义的方法,使得同样的方法可以产生不同的行为。 接口(Interface)实现:任何实现相同接口的类都具有相同的行为特点。程序可以成本较低地替换操作对象,同时保持良好的组织结构和代码可读性。 抽象类(Abstract class):抽象类不能实例化,只能被派生子类继承并实现其方法,通过动态绑定机制实现多态性。 泛型(Generics):泛型允许在编译时确定数据类型,同时让我们编写的通用代码适用于多种类型的数据。这种多态由 C# 编译器在编译时通过类型擦除技术实现。
以上是常见的实现多态特性的方法,它们在面向对象编程中都有着广泛的应用。需要注意的是,不同的实现方式有着不同的适用场景和使用方法,开发者需要结合具体问题进行灵活选择和使用。
多态下我们要解决⼀个问题:如果⽗类和⼦类有相同的函数(⽅法签名完全相同),在⽗类引⽤指向⼦类对象的时候应该如何访问该⽅法?
多态实现的步骤:
- ⽗类的引⽤指向⼦类的对象
- ⽗类的同名函数使⽤virtual关键字修饰(V)
- ⼦类需要使⽤override关键字重写⽗类的同名函数(O)
- ⼦类中可以通过base关键字访问⽗类的可⻅成员(B)
为了⽅便⼤家进⾏代码的对⽐,我们先来看⼀个没有多态的情况,参考代码如下:
//定义怪物基类 class Monster { // 怪物有攻击的⽅法 public void Atk() { Console.WriteLine("Monster的攻击⽅法."); } } // 定义⽯头⼈怪物类 class StoneMonster : Monster { // 定义⽯头怪物攻击⽅法:使⽤new关键字可以覆盖继承下来的⽗类的Atk⽅法 public new void Atk() { Console.WriteLine("StoneMonster的攻击⽅法."); } } public class MainClass { public static void Main(string[] args) { // ⽗类指向⽗类 Monster monster = new Monster(); monster.Atk(); // ⽗类指向⼦类 Monster stoneMonster = new StoneMonster(); stoneMonster.Atk(); // ⼦类指向⼦类 StoneMonster stone = new StoneMonster(); stone.Atk(); } } // 运⾏结果: Monster的攻击⽅法. Monster的攻击⽅法. StoneMonster的攻击⽅法.
如果我们需要当⽗类引⽤指向⼦类对象且重写了⽗类的⽅法的时候,如果调⽤该⽅法依旧执⾏⼦类重写过的⽅法,那么就需要使
⽤多态,参考代码如下:
// 定义怪物基类 class Monster { // ①定义为虚⽅法:virtual⽤于定义虚函数,该函数可以被重写 public virtual void Atk() { Console.WriteLine("Monster的攻击⽅法."); } } // 定义⽯头⼈怪物类 class StoneMonster : Monster { // ②重写⽗类的虚⽅法:override⽤于重写⽗类中的虚函数 public override void Atk() { // ③通过base可以调⽤⽗类的该⽅法 base.Atk(); Console.WriteLine("StoneMonster的攻击⽅法."); } } public class MainClass { public static void Main(string[] args) { // ⽗类指向⽗类 Monster monster = new Monster(); monster.Atk(); // ④ ⽗类引⽤指向⼦类对象 Monster stoneMonster = new StoneMonster(); stoneMonster.Atk(); // ⼦类指向⼦类 StoneMonster stone = new StoneMonster(); stone.Atk(); } }
⼩技巧: 如果⼤家理解起来有困难,这⾥给⼤家⼀个⼩妙招,记住VOB即可,V是Virtual,就是先定义⽗类中的虚⽅法,O是 Override就是再重写⽗类的虚⽅法,B就是Base可以调⽤⽗类的⽅法,最后使⽤⽗类的引⽤指向⼦类的对象即可实现多态。
多态
当用一个父类的变量去引用一个子类的对象实例时:调到的版本永远是跟子类实例相关的最新的版本。换句话说:继承的最新版本。多态是面向对象编程中的一个概念,它允许使用一个基类或接口的引用来调用不同派生类对象的方法,实现代码的灵活性和可扩展性。
在 C# 中,多态通过使用关键字virtual和override来实现。在基类中声明一个方法为virtual,表示该方法可以被派生类重写(覆盖)。派生类中使用override关键字来重写基类中的虚方法。当使用基类引用来调用该方法时,实际上是调用了派生类中的重写方法。
下面是一个简单的例子,演示了多态的用法:
public interface IAnimal { void Speak(); } public class Cat : IAnimal { public void Speak() { Console.WriteLine("Meow!"); } } public class Dog : IAnimal { public void Speak() { Console.WriteLine("Woof!"); } } public class MainClass { public static void Main() { // 多态示例 IAnimal animal1 = new Cat(); IAnimal animal2 = new Dog(); animal1.Speak(); // 输出 "Meow!" animal2.Speak(); // 输出 "Woof!" } }
在上面的例子中,Cat 类和 Dog 类都实现了 IAnimal 接口,并且分别重写了 Speak 方法。在 Main 方法中,我们创建了 Cat 和 Dog 的实例,并将它们存储在 IAnimal 类型的变量中。这是多态的用法,因为 IAnimal 可以引用任何实现了 IAnimal 接口的对象。
当我们调用 Speak 方法时,实际上调用的是 Cat 或 Dog 类中的 Speak 方法,具体取决于 animal1 和 animal2 引用的对象类型。这就是多态的优势:我们可以编写通用的代码,而不必关心实际的对象类型。
通过这些对象调用 Speak方法时,由于多态的作用,基类引用会根据对象的实际类型自动调用相应的派生类方法。这样就可以避免在代码中大量使用if/else或switch语句来判断对象的类型。
接口可以不被继承,直接使用嘛?
是的,接口可以直接被使用,不需要被继承。接口是一种约定,定义了一个类应该提供哪些方法和属性,但并不提供这些方法和属性的具体实现。一个类可以实现一个或多个接口,并提供接口中定义的方法和属性的具体实现,从而符合接口的约定。
使用接口的好处在于它提供了一种非常灵活的方式来定义类的行为,使得不同的类可以实现相同的接口并具有相同的行为,从而使得这些类可以被用来处理相同类型的数据。此外,接口还可以提供一种标准化的方式来编写代码,使得不同的开发者可以更容易地协同工作并共享代码。
以下是一个示例,演示如何使用接口来定义一个简单的日志记录器:
点击查看代码
// 定义一个日志记录器接口 public interface ILogger { void Log(string message); } // 定义一个使用 ILogger 接口的类 public class FileLogger : ILogger { public void Log(string message) { // 具体的日志记录代码 } } // 定义另一个使用 ILogger 接口的类 public class ConsoleLogger : ILogger { public void Log(string message) { // 具体的日志记录代码 } } // 使用 ILogger 接口 class Program { static void Main(string[] args) { ILogger logger = new FileLogger(); logger.Log("文件日志记录器记录一条日志"); logger = new ConsoleLogger(); logger.Log("控制台日志记录器记录一条日志"); } }
在上面的示例中,我们定义了一个接口ILogger,它定义了一个Log方法。我们还定义了两个实现ILogger接口的类FileLogger和ConsoleLogger,它们提供了Log方法的具体实现。
在Main方法中,我们使用ILogger接口定义了一个logger变量,并分别使用FileLogger和ConsoleLogger来实例化这个变量,并调用它们的Log方法。由于这些类都实现了ILogger接口,所以它们的Log方法可以被logger变量调用,从而实现了一种灵活的日志记录方式。
值得注意的是, Main函数没有继承ILogger ,而是直接“使用ILogger接口定义了一个logger变量,并分别使用FileLogger和ConsoleLogger来实例化这个变量,并调用它们的Log方法。”
接口在 C# 中默认是可以被继承的,没有像类一样的sealed关键字来限制继承。
如果你想要定义一个接口,不允许其他类继承它,可以使用接口中定义的所有成员都是private访问修饰符,这样就只能在接口内部访问这些成员,而无法在接口外部继承接口或实现它的成员。
例如:
public interface IMyInterface
{
private void MyPrivateMethod()
{
// 具体实现代码
}
}
不过,需要注意的是,这种方式并不常用,因为接口的主要作用是定义行为和契约,而不是提供实现细节。如果要限制接口的继承,可以考虑在文档中记录并明确告知不允许继承该接口。
重写与隐藏的发生条件:
函数成员
可见:对子类可见:public,protected。
签名一致:方法名和参数。 签名 指的是 方法名和参数列表