CodeGym /课程 /C# SELF /继承和重写时常见的错误

继承和重写时常见的错误

C# SELF
第 25 级 , 课程 1
可用

1. 引言

当新手听到继承这个词时,可能觉得很简单:拿个现成的类,稍微扩展或改改就行了!但实际上细节超多。继承时的错误可能各种各样:方法签名不对、忘了关键字、类层级设计烂——这些都会带来很难查的bug。

有些错误一上来就暴露了:代码直接编译不过。还有些——只有在运行时才会发现,比如你新写的SuperMegaLogger根本没按预期输出内容,甚至啥都不输出。难受、失望、调试一整天——是不是很熟悉?

咱们一起来看看继承和重写方法时最常见的坑,顺便一路“治愈”我们的代码。

2. 忘记virtual:为什么方法不能重写?

问题

在C#里,只有基类里用virtualabstract或者override声明的方法,子类才能用override重写。如果基类方法没这些关键字,你在子类里写override会直接编译报错。


class Animal
{
    public void Speak()
    {
        Console.WriteLine("动物说了点啥。");
    }
}

class Cat : Animal
{
    // 编译错误!基类方法不是virtual、abstract或override。
    public override void Speak()
    {
        Console.WriteLine("喵!");
    }
}

错误大概会是这样: "'Cat.Speak()': 不能重写继承的成员 'Animal.Speak()',因为它没有被标记为virtual、abstract或override"

怎么避免踩坑?

想让方法能被重写,基类里一定要加virtual。顺便说下,如果你设计的类是给别人扩展用的,最好提前想好哪些方法可能需要重写。


class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("动物发出声音。");
    }
}

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("喵!");
    }
}

为啥要这样?
把方法声明成virtual,就是明确告诉子类可以改它的行为,而且编译器会帮你防止误重写“错的方法”。

3. overridenew关键字搞混

有时候反过来:子类作者想“重写”方法,但基类方法不是virtual。这时编译器不让你用override,但可以用new。不过这俩完全不是一回事!


class Dog : Animal
{
    // 这不是override,而是隐藏(hiding)基类方法。
    public new void Speak()
    {
        Console.WriteLine("汪!");
    }
}

如果用Dog类型的引用调用这个方法——没问题:


Dog dog = new Dog();
dog.Speak(); // "汪!"

但如果用基类类型的引用,调用的是基类方法:


Animal dog2 = new Dog();
dog2.Speak(); // "动物发出声音。"

解释:
new关键字不是重写方法,而是把父类方法“藏起来”了。这叫“hiding”。这样做容易让人迷惑,因为多态行为不会像你想的那样工作。

怎么避免newoverride的坑?

如果你想要经典的多态重写——用virtual/override。如果你真的是要加全新功能(或者故意想隐藏基类方法,慎用!),那就用new

场景 关键字 多态生效? 用基类类型调用时的行为
改变行为 override 调用子类方法
隐藏/替换方法 new 调用基类方法

4. 方法签名不一致

新手,甚至老手,有时会把参数类型或返回值类型在子类方法里改了。比如基类方法是public virtual void Print(string message),子类“重写”成public override void Print(object message),这其实不是override,而是新定义了个方法。


class Printer
{
    public virtual void Print(string msg)
    {
        Console.WriteLine("基类打印机: " + msg);
    }
}

class SmartPrinter : Printer
{
    // 编译错误!方法签名和基类不一致。
    public override void Print(object msg)
    {
        Console.WriteLine("智能打印机: " + msg);
    }
}

小提示:
方法名、返回值类型、参数类型、参数数量和顺序都必须一模一样。

如果你不小心改了参数类型或拼错名字——编译器会提醒你。

5. 访问修饰符不一致

还有个新手常踩的坑——访问修饰符。子类方法不能比基类方法更“封闭”。比如这样就不行:


public class Vehicle
{
    public virtual void StartEngine() { /* ... */ }
}

public class Car : Vehicle
{
    // 错误!'private'比基类的'public'更严格。
    private override void StartEngine() { /* ... */ }
}

怎么办?
子类方法的访问修饰符要和基类一样,或者更“开放”。大多数时候用publicprotected

6. 忘记/多写了abstract方法

如果基类方法声明为abstract,那子类必须override它,否则子类也会变成抽象类(不能实例化)。


abstract class Shape
{
    public abstract double Area();
}

class Circle : Shape
{
    // 错误!没有实现抽象方法Area()
}

解决办法:
实现这个方法就行:


class Circle : Shape
{
    public override double Area()
    {
        return 3.14 * 2 * 2; // 大概这样...
    }
}

7. 调用基类实现:base.Method()

有时候你不想完全替换方法实现,只是想加点东西。这时经常会忘了(或者不知道)可以在子类方法里用base调用基类实现。


class Logger
{
    public virtual void Log(string msg)
    {
        Console.WriteLine("基础日志: " + msg);
    }
}

class FancyLogger : Logger
{
    public override void Log(string msg)
    {
        // 可以加点“花活”再调用基类方法:
        Console.WriteLine("[FANCY] " + msg);
        base.Log(msg);
    }
}

解释:
如果不调用base.Log(msg),基类里的逻辑就全没了。

8. 调用基类构造函数(base

如果基类构造函数需要参数,子类必须用base显式调用对应的构造函数。


class Engine
{
    public Engine(int cylinders)
    {
        Console.WriteLine("Engine有气缸数: " + cylinders);
    }
}

class RaceEngine : Engine
{
    // 编译错误!Engine没有无参构造函数。
    public RaceEngine() { }
}

// 正确写法:
class RaceEngine2 : Engine
{
    public RaceEngine2() : base(8) // 显式调用基类构造函数
    {
        Console.WriteLine("赛车引擎准备好了!");
    }
}

9. 忘记/没用sealed关键字

有时候你想禁止子类再重写某个方法——这时C#里可以用sealed配合override。如果你不用,别人就能继续重写你的方法,可能会破坏你的逻辑。


class Hero
{
    public virtual void Attack() => Console.WriteLine("英雄攻击!");
}
class Warrior : Hero
{
    public sealed override void Attack() => Console.WriteLine("战士出击!");
}
class Mutant : Warrior
{
    // 错误!Attack方法上面已经sealed了。
    // public override void Attack() { ... }
}

解释:
这样你就把实现“封死”在当前层级,下面的类不能再改它了。

10. 误用重载(overloading)当重写(overriding)

有些同学会把重载overloading)和重写overriding)搞混。重载是同名不同参数的方法,和多态没关系。


class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("动物在吃。");
    }
}

class Panda : Animal
{
    // 这不是override!只是新加了个重载方法。
    public void Eat(string what)
    {
        Console.WriteLine("熊猫吃: " + what);
    }
}

...

Animal a = new Panda();
a.Eat(); // 如果方法被重写了,会调用子类实现。否则用基类的
// a.Eat("竹子"); // 编译错误:Animal没有这个方法

想要多态行为,必须用重写,不是重载。

11. 设计上的形式和非形式错误

如果类设计得很烂,继承链会变得很乱:

  • 内部“旋转木马”式的override,没好好用base.
  • 修饰符用得不一致,
  • 层级太深或者结构不清晰,
  • 方法逻辑分散在不同继承层,
  • 虚方法/抽象方法没写注释,
  • 有些副作用很隐晦(比如在基类构造函数里调用虚方法,这时子类还没初始化完)。

建议:
如果你觉得自己已经晕了——不妨用纸画一下(老派但有效!)类层级,标注每个方法在哪,谁重写了谁,谁调用了谁,哪里要用base.
实际项目里,文档里也要写清楚每个方法重写时的预期行为。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION