抽象类在C#中的地位有些特殊——它不能直接实例化,只允许被继承。换句话说,只要一个类里出现了abstract标识的方法,这个类就必须声明为abstract class,否则编译器会直接报错。这可不是什么小细节,而是整个抽象机制的设计起点。
先搞清楚一个基本规则:abstract class是不能用new来创建实例的。想试试new Animal()?编译器会直接甩你一脸CS0144:“无法创建抽象类的实例”。抽象类存在唯一的意义,就是作为基类被继承。它允许你定义一部分可复用的逻辑(比如Sleep()),同时强制子类实现关键行为(比如MakeSound())。
具体来说,抽象类有几个值得注意的特性:
- 抽象类可以有构造函数,子类初始化时会自动调用它(哪怕你不写
base()) - 抽象类可以包含字段、属性、普通方法、静态成员,甚至
protected或internal成员 - 但不能用
sealed修饰抽象类——这俩语义冲突:一个要求必须被继承,一个禁止被继承
抽象方法:只有契约,没有实现
抽象方法的声明形式是public abstract void Run();,结尾是分号,没有{}。它不提供任何实现,只划出契约边界。一旦父类中存在这样的方法,所有非抽象子类都必须用override显式实现它。
这里有几点必须注意:
- 不能加
private——子类根本访问不到,还怎么重写? - 不能加
static或virtual——abstract已隐含virtual语义,二者重复;static属于类型而非实例,和“由子类实现”的设计目标矛盾 - 抽象方法可以是
protected,但不推荐:外部无法通过变量调用,削弱了多态价值
什么时候选抽象类,什么时候选接口
这是个经典问题。当你要共享代码实现、需要字段或构造逻辑、或者想约束继承链深度(只能单继承)时,优先考虑抽象类。举个例子,所有Shape都要记录CreatedTime字段,都有默认的ToString()实现,但面积计算必须由子类决定——这种场景抽象类再合适不过。
对比来看:
- 接口更适合定义“能做什么”(
IComparable、IDisposable),抽象类更适合定义“是什么+部分怎么做”(Stream、Exception) - C# 8.0+支持接口默认方法,但接口仍不能有字段或构造器,这点和抽象类有本质区别
- 一个类可以
: Animal, IFlyable, ISwimmable,但只能: Mammal——继承结构越清晰,越该用抽象类做骨架
一个容易被忽略的细节:抽象类可以没有抽象方法
很多人在这里卡壳。比如abstract class Logger完全可以不写任何abstract方法,只提供通用日志格式化逻辑,并用abstract修饰自身来防止被误实例化。编译器只检查“有没有abstract成员”来决定是否强制子类实现,不检查“有没有abstract方法”。
还有几点补充:
- 反过来,只要有一个
abstract方法,类就必须是abstract——没得商量 - 抽象子类可以不实现父类的抽象方法,但再下一级非抽象子类必须补全
- 抽象类的构造函数不能是
abstract,但它可以是protected,这是最常见也最合理的做法
