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()!
}
要消除这个错误,Dog和Cat必须重写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 关键字 |
|---|---|---|---|
| 出现位置 | 普通类或抽象类里 | 只能在抽象类里 | 在子类(派生类)里 |
| 方法体 | 有方法体(默认实现) | 没有方法体(以;结尾) | 有方法体(新的实现) |
| 目的 | 提供默认实现,允许子类改写 | 声明契约:子类必须自己实现 | 为virtual或abstract方法提供具体实现 |
| 能不能创建基类实例? | 可以(如果基类不是抽象类) | 不行(如果基类是抽象类) | 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();
}
}
}
这段代码发生了什么?
- 我们把Animal声明成了abstract class。这告诉编译器:“这个类是模板,不能直接创建对象,但可以被继承。”
- 我们在Animal里声明了public abstract void MakeSound();。这就是说:“任何继承Animal的类(不是抽象类)必须实现MakeSound()方法。”这就是契约!
- Dog、Cat和Fish都老老实实地实现了MakeSound()。如果我们漏掉任何一个,编译器都不会放过我们。
- 在Main方法里,我们创建了一个Animal[]数组。虽然Animal是抽象的,数组可以存放子类对象的引用(Dog、Cat、Fish),因为它们本质上都是Animal!
- 当我们用foreach遍历数组并调用animal.MakeSound()时,多态性让C#“知道”该调用哪个MakeSound():Dog.MakeSound()、Cat.MakeSound()还是Fish.MakeSound()。调用的是对象的实际类型的方法,而不是引用类型的方法。这就是多态的魅力!
- 而animal.Sleep()则调用了基类Animal里的具体实现,因为这个方法没被标记为virtual或abstract,子类也没重写它。
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就很明确:“这里没实现,必须让子类来!”
优化代码架构:抽象类帮你把通用逻辑和特定逻辑分开。通用的(比如LogTransaction在PaymentProcessor里)放基类,特定的(比如ProcessPayment)放子类。这样代码更清晰、好维护、好测试。框架和库的设计者可以明确哪些地方必须让用户自己实现。
6. 常见错误和小细节
错误1:试图创建抽象类的实例。
这肯定编译不过。抽象类就是个概念,不是具体对象。你可以用它做引用类型,但不能写new Animal()。
错误2:忘了重写抽象方法。
如果子类不是抽象类,它就必须实现所有abstract方法。不然就编译错误。唯一能绕过的办法是让子类也变成抽象类,但大多数时候你不会这么做。
错误3:把abstract和virtual搞混。
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
GO TO FULL VERSION