CodeGym /课程 /C# SELF /多态性与抽象方法的关系

多态性与抽象方法的关系

C# SELF
第 21 级 , 课程 4
可用

1. 入门

咱们回到咱们的动物园。我们有一个基类Animal(动物),还有它的子类:Dog(狗)、Cat(猫)、Fish(鱼)。

在前面的课里,我们给Animal加了一个virtual方法MakeSound()

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public virtual void MakeSound() // 虚方法
    {
        Console.WriteLine("某种动物发出了声音。"); // 默认实现
    }

    public void Sleep() // 普通方法
    {
        Console.WriteLine($"{Name} 睡觉了。");
    }
}

public class Dog : Animal
{
    public override void MakeSound() // 重写狗的叫声
    {
        Console.WriteLine("汪汪!");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // 重写猫的叫声
    {
        Console.WriteLine("喵!");
    }
}

这样用起来很爽!当我们创建Dog或者Cat并调用MakeSound()时,我们能听到它们独特的声音。但如果我们只创建一个Animal呢?

Animal genericAnimal = new Animal();
genericAnimal.Name = "未知生物";
genericAnimal.MakeSound(); // 会输出:"某种动物发出了声音。"

看起来挺合理的。但有时候,基类的默认实现其实没啥意义。比如Animal其实不是具体的动物,而是一个概念?“动物”本身并不会发出具体的声音,只有具体的动物种类才会。或者想象一下,我们有个Shape(图形)类,它有个CalculateArea()(计算面积)方法。只是图形该怎么算面积?没法算!圆有面积,正方形有面积,但抽象的“图形”没有面积。

这种情况下,如果基类不能(或者不该)给出有意义的默认实现,但又强制所有子类都必须实现这个方法,那就轮到抽象方法抽象类出场了。

2. 抽象类:图纸还不是房子

想象你是个建筑师,你画了一张标准房子的图纸。但这不是具体的房子,而是“概念房子”。它有共同的特征:墙、屋顶、地基。但你还不知道它是一层小别墅还是高层大厦。有些图纸细节是确定的(比如一楼的层高),有些只是个提示(比如“楼层数”,以后再定)。

在C#世界里,这种“概念房子”就叫抽象类
抽象类就是用abstract关键字标记的类。


public abstract class Animal // Animal 现在是抽象类
{
    // ...
}
抽象类的声明

抽象类的关键点:

  • 不能直接创建对象。你不能写new Animal()。为啥?因为Animal现在只是个概念。你不能建“概念房子”,只能建具体的别墅或大厦。
    如果你试试new Animal(),编译器会立马警告你:
    Cannot create an instance of the abstract type or interface 'Animal'
    (无法创建抽象类型或接口 'Animal' 的实例)
    这个限制很重要!
  • 可以包含抽象成员。这才是最有意思的!

3. 抽象方法:只有契约没有实现

如果抽象类是“概念房子”,那抽象方法就是图纸上写着“要做这个”,但没说“怎么做”的那部分。比如“建地基”——这是房子的必备部分,但具体尺寸和材料得看房子的类型。

抽象方法就是:

  • abstract关键字标记。
  • 没有方法体(没有{}代码块),直接以;结尾。
  • 只能在抽象类里声明。

来,把我们的MakeSound()方法变成抽象的:


public abstract class Animal // Animal 变成抽象类了
{
    public string Name { get; set; }
    public int Age { get; set; }

    public abstract void MakeSound(); // 这就是抽象方法!没有方法体!

    public void Sleep() // 这个方法还是普通的,“具体的”
    {
        Console.WriteLine($"{Name} 睡觉了。");
    }
}

看看MakeSound()变成啥样了!它没有大括号和默认实现了。它只是在说:“任何动物必须会发出声音。怎么发——让继承我的类自己决定。”

重要规则:如果你的类继承了抽象类,而且自己不是抽象类,那它必须override重写所有基类的抽象方法。这不是选项,这是强制契约!C#编译器在这方面很严格。你要是忘了,它会立马提醒你:

public class Dog : Animal // 普通的、非抽象类
{
    // 编译错误!
    // 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
    // (类 'Dog' 没有实现继承的抽象成员 'Animal.MakeSound()')
    // 编译器等着我们用override写MakeSound()!
}

要消除这个错误,DogCat必须重写MakeSound()

public class Dog : Animal
{
    public override void MakeSound() // 必须重写!
    {
        Console.WriteLine("汪汪!");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // 这里也一样!
    {
        Console.WriteLine("喵!");
    }
}

virtual、abstract和override的对比

特性 virtual 方法 abstract 方法 override 关键字
出现位置 普通类或抽象类里 只能在抽象类里 在子类(派生类)里
方法体 有方法体(默认实现) 没有方法体(以;结尾) 有方法体(新的实现)
目的 提供默认实现,允许子类改写 声明契约:子类必须自己实现 virtualabstract方法提供具体实现
能不能创建基类实例? 可以(如果基类不是抽象类) 不行(如果基类是抽象类) N/A(针对方法,不是类)
子类的义务 可选重写(override 必须重写(override),除非子类本身也是抽象类 N/A

4. 抽象方法下的多态性实战

现在才是最有意思的!我们已经知道,多态性让我们可以通过基类引用操作不同的子类对象。即使基类是抽象的,这也完全没问题!

虽然我们不能直接创建Animal的实例(记住,new Animal()会报错),但我们可以用Animal类型作为引用指向子类对象。这很强大!

继续我们的动物园。假设我们有个农场,里面住着各种动物。我们想让每只动物都发出自己的声音。


using System;

// 抽象类 Animal
public abstract class Animal
{
    public string Name;
    public int Age;
    public Animal(string name, int age) { Name = name; Age = age; }
    public abstract void MakeSound();
    public void Sleep() { Console.WriteLine($"{Name} 睡觉了。"); }
}

public class Dog : Animal
{
    public Dog(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("汪汪!"); }
}

public class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("喵!"); }
}

public class Fish : Animal
{
    public Fish(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("咕噜咕噜"); }
}

class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("Sharik", 3),
            new Cat("Murzik", 5),
            new Fish("Nemo", 1)
        };

        foreach (Animal animal in animals)
        {
            Console.WriteLine($"\n你好,我是{animal.Name},我{animal.Age}岁。");
            animal.MakeSound();
            animal.Sleep();
        }
    }
}

这段代码发生了什么?

  1. 我们把Animal声明成了abstract class。这告诉编译器:“这个类是模板,不能直接创建对象,但可以被继承。”
  2. 我们在Animal里声明了public abstract void MakeSound();。这就是说:“任何继承Animal的类(不是抽象类)必须实现MakeSound()方法。”这就是契约!
  3. DogCatFish都老老实实地实现了MakeSound()。如果我们漏掉任何一个,编译器都不会放过我们。
  4. Main方法里,我们创建了一个Animal[]数组。虽然Animal是抽象的,数组可以存放子类对象的引用DogCatFish),因为它们本质上都是Animal
  5. 当我们用foreach遍历数组并调用animal.MakeSound()时,多态性让C#“知道”该调用哪个MakeSound()Dog.MakeSound()Cat.MakeSound()还是Fish.MakeSound()。调用的是对象的实际类型的方法,而不是引用类型的方法。这就是多态的魅力!
  6. animal.Sleep()则调用了基类Animal里的具体实现,因为这个方法没被标记为virtualabstract,子类也没重写它。

5. 现实生活中有啥用?

“好吧,动物园、动物……可我以后写银行或商店的应用,这玩意有啥用?”——你可能会问。这问题问得好!抽象类和抽象方法是设计灵活、可扩展系统的超级利器。

强制实现契约:这是最主要的好处。假设你要开发一个支付系统框架。有个基类abstract class PaymentProcessor(支付处理器)。你很清楚,任何支付处理器都得会ProcessPayment()(处理支付)、RefundPayment()(退款)和CheckStatus()(查状态)。但PayPal、银行卡、比特币的实现完全不同。
你把这些方法在PaymentProcessor里声明成abstract


public abstract class PaymentProcessor
{
    public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
    public abstract bool RefundPayment(string transactionId);
    public abstract string CheckStatus(string transactionId);
    // ... 其它可以是具体的,比如日志
    public void LogTransaction(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public class PayPalProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // 这里是和PayPal API打交道的复杂逻辑
        Console.WriteLine($"PayPal: 处理 {amount} {currency}...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "已完成"; }
}

public class CreditCardProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // 这里是和银行收单行打交道的逻辑
        Console.WriteLine($"CreditCard: {amount} {currency} 用卡号 {cardNumber.Substring(0,4)}XXXX...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "处理中"; }
}

现在,任何开发者想写新的支付处理器(比如BitcoinProcessor),都必须实现这三个方法。他不可能忘了RefundPayment(),因为编译器根本不让他这么干!这样你的系统就很一致。

灵活和可扩展:你可以写只和PaymentProcessor打交道的代码,根本不用关心具体实现。比如在网店购物车里,你只管调用currentProcessor.ProcessPayment(),系统会根据用户选的支付方式自动用对的处理器。明天又有新支付方式?只要新建个类继承PaymentProcessor,实现抽象方法,主业务代码都不用改!

避免空实现:如果用virtual方法而不是abstract,你得给它们一个空实现,这可能会让人误解abstract就很明确:“这里没实现,必须让子类来!”

优化代码架构:抽象类帮你把通用逻辑和特定逻辑分开。通用的(比如LogTransactionPaymentProcessor里)放基类,特定的(比如ProcessPayment)放子类。这样代码更清晰、好维护、好测试。框架和库的设计者可以明确哪些地方必须让用户自己实现。

6. 常见错误和小细节

错误1:试图创建抽象类的实例。
这肯定编译不过。抽象类就是个概念,不是具体对象。你可以用它做引用类型,但不能写new Animal()

错误2:忘了重写抽象方法。
如果子类不是抽象类,它就必须实现所有abstract方法。不然就编译错误。唯一能绕过的办法是让子类也变成抽象类,但大多数时候你不会这么做。

错误3:把abstractvirtual搞混。
abstract强制重写,没有方法体。virtual有默认实现,可以重写。不能声明有方法体的abstract方法,也不能声明没方法体的virtual方法——语法错误。

错误4:用new而不是override
如果你不用override,而是在子类里写了同名方法,你不是重写,而是隐藏了基类方法。这样多态调用时会出现意外:会调用基类的方法。


public class Base
{
    public void DoSomething() { Console.WriteLine("Base"); }
}

public class Derived : Base
{
    public new void DoSomething() { Console.WriteLine("Derived"); }
}

Base obj = new Derived();
obj.DoSomething(); // 会输出:Base
1
调查/小测验
多态的概念第 21 级,课程 4
不可用
多态的概念
多态和方法重载
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION