C#入门详解 重写与多态

【068-多态】 B站视频 Up:IT萌叔Jack

多态指的是在⼀个继承体系中⼀个类的对象可有多种状态,每种状态下有可能表现不同;

多态指的是在面向对象编程(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。
签名一致:方法名和参数。 签名 指的是 方法名和参数列表

posted @ 2022-06-23 14:42  专心Coding的程侠  阅读(111)  评论(0编辑  收藏  举报