第 7 课 · 约 14 分钟 · ← 第 6 课
继承与接口
类型之间有两种关系:继承("狗是一种动物——继承它的一切")和接口("机器人会 Speak()——承诺一种能力")。两者存在的目的都是让一份代码通过同一个形状操作许多类型。
virtual / override:基类允许你替换行为
class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
public virtual void Speak() // virtual:子类可以替换
{
Console.WriteLine(Name + " makes a sound");
}
}
class Dog : Animal // Dog 继承 Animal
{
public Dog(string name) : base(name) { } // base(...):先跑 Animal 的构造器
public override void Speak() // 替换它
{
Console.WriteLine(Name + " says woof");
}
}
Animal a = new Dog("Rex"); // 狗是一种动物——这个赋值合法
a.Speak(); // 分发跟着对象走:Dog 的版本执行
Animal b = new Animal("Generic");
b.Speak();
最后两行是全部要点:变量类型是 Animal,但执行的方法是实际对象上的那个——虚分发。一个遍历混合数组的循环,挨个调 Speak(),每次都叫得恰到好处。
base:在父类的基础上盖楼
base(...) 调父类构造器;base.Method() 调父类版本——标准的"扩展而非替换"形状:
class Animal
{
public virtual void Speak() { Console.WriteLine("..."); }
}
class Cat : Animal
{
public override void Speak()
{
base.Speak(); // 先做动物都会做的……
Console.WriteLine("meow"); // ……再加猫的
}
}
is:"它真的是那个吗?"——顺便绑定变量
class Animal { }
class Dog : Animal
{
public void Fetch()
{
Console.WriteLine("fetching!");
}
}
Animal x = new Dog(); // 向上转:没问题,Dog 是 Animal
if (x is Dog d) // 测试 + 声明一步到位
{
d.Fetch(); // 只有狗会捡东西
}
安静的兄弟 as 负责转换、失败时给 null:Dog dd = x as Dog;——然后检查 dd != null。只用来分支用 is;转换后还要用值用 as。
接口:是承诺,不是血缘
interface IGreeter
{
void Greet(); // 一纸契约:没有方法体,只有承诺
}
class EnglishGreeter : IGreeter
{
public void Greet() { Console.WriteLine("Hello"); }
}
class SpanishGreeter : IGreeter
{
public void Greet() { Console.WriteLine("Hola"); }
}
IGreeter[] greeters = { new EnglishGreeter(), new SpanishGreeter() }; // 互不相干的类,同一个承诺
foreach (IGreeter g in greeters)
{
g.Greet(); // 各自履行各自的行为
}
两个问候者没有共同基类——接口不是 ancestry,是共同的承诺。这就是为什么 C# 允许一个类实现很多接口、却只能继承一个基类:血缘排他,承诺不排他。
哪个工具,什么时候?
| 场景 | 用 |
|---|---|
| "X 是一种 Y",共享代码 | 继承(class Dog : Animal) |
| "X 能做 Z",横跨不同家族 | 接口(: IComparable) |
| 子类微调父类行为 | virtual + override |
| 子类扩展父类行为 | override + base.Method() |
| 安全向下转后再用额外成员 | is(分支)/ as(保留) |
现在就练
- 建
Shape带virtual double Area() { return 0; },再建Square和Circle重写它——遍历Shape[]打印每个面积。(练习场的示例里有一份起步代码。) - 只给其中一个形状加
IScalable(含void Scale(double factor))——证明接口可以横切家族树。 - 下一课:第 8 课 — 集合。