首页 / C# 入门教程 / 继承

C# 入门教程

继承

本教程共 100 篇 · 第 40 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

C#C# 入门教程继承派生类base多态基础

40. 继承

本节目标:学完你能用 : 派生子类,用 base 复用基类逻辑,并分清继承与隐藏。

继承(inheritance)让一个新类「复用并扩展」一个已有类。被复用的是基类(base class),新类叫派生类(derived class)。它的好处是:公共代码只写一次,子类按需加料。

用冒号派生

语法是 class 派生类 : 基类。派生类自动拥有基类的 publicprotected 成员,不用重写一遍。

namespace CSharpDemo;

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public void Greet() => Console.WriteLine($"我是 {Name}{Age} 岁");
}

class Student : Person
{
    public int Id { get; set; }
    public int Score { get; set; }
}

var s = new Student { Name = "小明", Age = 18, Id = 1, Score = 95 };
s.Greet();                 // 继承自 Person
Console.WriteLine(s.Score);

Student 没写 Greet,却能用,因为从 Person 继承了过来。

Note

派生类不继承构造函数,也看不见基类的 private 成员。但它能通过基类提供的 public/protected 方法间接触达那些私有数据。

base 调用基类构造函数

基类若只有带参构造,派生构造必须用 : base(...) 把参数传上去,否则编译报错。这保证了「基类先被正确初始化」。

namespace CSharpDemo;

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name;
}

class Student : Person
{
    public int Score { get; }
    public Student(string name, int score) : base(name)
    {
        Score = score;
    }
}

var s = new Student("小红", 90);
Console.WriteLine($"{s.Name} 考了 {s.Score}");

base(name) 先在基类里跑完 Person(string),再回来执行 Student 构造体。

base 调用基类方法

当子类想「先做事、再调用父类原版」时,用 base.方法名()

namespace CSharpDemo;

class Person
{
    public virtual void Introduce() => Console.WriteLine("我是一个人");
}

class Student : Person
{
    public override void Introduce()
    {
        base.Introduce();           // 先调用基类版本
        Console.WriteLine("我还是学生");
    }
}
Tip

这里用了 virtual/override,属于「重写」,会在第 41 章细讲。继承章节先记住:base 就是「找父类那一份」的钥匙。

成员隐藏:new 关键字

若子类写了和基类同名、但没用 virtual/override 的成员,C# 会报警,提示你故意隐藏时加 newnew 表示「我有意遮盖基类这个成员」。

namespace CSharpDemo;

class Base
{
    public void Show() => Console.WriteLine("基类版本");
}

class Derived : Base
{
    public new void Show() => Console.WriteLine("派生类版本");
}

var d = new Derived();
d.Show();                      // 派生类版本
Base b = d;
b.Show();                      // 基类版本(隐藏是编译期绑定)
Note

new 是「隐藏(hide)」,不是「重写(override)」。用基类类型的变量去调用时,看到的是基类那份;用派生类变量才看到派生那份。它是编译期决定的,和多态不同。

单继承与多接口

C# 的类只支持单一继承:一个类只能有一个直接基类。但一个类可以同时实现多个接口(class A : Base, I1, I2)。接口将在第 44 章展开。

class Student : Person, IComparable<Student> { }
Tip

新手踩坑:以为能写 class A : B, C(两个类)。不行,只能跟一个类,其余必须是接口。

继承解决了什么

没有继承时,每类相似数据都要复制一遍;有了继承,共性上提、个性下沉。比如 Person 放名字年龄,StudentTeacher 各自只加自己专属字段,维护点集中到一处。

继承表达的是「是一个」

判断该不该继承,问一句:「派生类是不是一种基类?」Student 是一种 Person,所以继承合理;若只是「学生有一张书桌」,那是「拥有」关系,不该用继承,而该用字段组合。错用继承会让类型关系失真、难以维护。

class Person { }
class Student : Person { }     // Student 是一种 Person,合理

多层继承时 base 的调用顺序

继承可以多层:C : B : A。构造时从最派生类(C)的 base 一路向上,A 的构造先跑,最后才跑 C 自己的体。

namespace CSharpDemo;

class A { public A() => Console.WriteLine("A"); }
class B : A { public B() : base() => Console.WriteLine("B"); }
class C : B { public C() : base() => Console.WriteLine("C"); }

new C();   // 输出 A → B → C

protected:给派生类留的口子

protected 成员对外部不可见,但派生类内部可以访问。它介于 privatepublic 之间,专门用来给子类留扩展点。

namespace CSharpDemo;

class Person
{
    protected string IdCard;     // 子类能碰,外部不能
}

class Student : Person
{
    public void SetId(string id) => IdCard = id;   // 合法
}

隐藏(new)与重写(override)对比

对比new(隐藏)override(重写)
基类需 virtual不需要需要
绑定时机编译期(看变量类型)运行期(看对象实际类型)
用基类变量调用看到基类版本看到派生版本

new 是「我有意换掉」,但用父类类型看还是老样子;override 是真正的多态,父类变量也能看到子类新行为。重写将在第 41 章展开。

Note

新手踩坑:想实现多态却忘了在基类标 virtual、子类标 override,只写了 new,结果用基类变量调用时看不到新逻辑。

继承的代价

继承会带来耦合:基类一改,所有子类都可能受影响(俗称「脆基类问题」)。所以能用电字段「组合」解决的,未必非要继承。继承适合真正的「是一种」且需要复用行为时。

字段也能被 new 隐藏

不仅方法,new 也能隐藏基类的字段。但隐藏字段通常意味着设计混乱——基类和子类各有一份同名数据,极易读错。能用 protected 复用就别用 new 字段。

namespace CSharpDemo;

class Base
{
    public int Value = 1;
}
class Derived : Base
{
    public new int Value = 2;   // 隐藏基类字段
}

var d = new Derived();
Console.WriteLine(d.Value);          // 2
Console.WriteLine(((Base)d).Value);  // 1,基类的那份还在

基类可以禁止被继承

若你设计了一个类,不希望别人再派生(比如要保证某些不变式),可用 sealed 修饰。关于 sealed 的更多细节见第 46 章。

namespace CSharpDemo;

sealed class FixedTool
{
    public void Run() => Console.WriteLine("不可被继承");
}
// class X : FixedTool { }   // 报错:sealed 类不能作基类

继承还是组合

继承并非复用代码的唯一手段。「有一个」关系用组合(把另一个对象当字段)往往更灵活、耦合更低。例如 CarEngine,就让 Car 持有一个 Engine 字段,而不是让 Car 继承 Engine

Tip

经验法则:能用组合清楚表达时优先组合;只有当两端确实是「是一种」且需要复用行为时,才用继承。滥用继承会让基类一改、子类全崩。

常见误区小结

  • 忘记在基类标 virtual、子类标 override,只写 new,多态不生效。
  • 把「有一个」误写成「是一种」,硬上继承。
  • 在构造函数里调用虚方法(第 41 章内容),可能访问到未初始化完的派生状态。

所有类都隐式继承自 object

在 C# 里,若不写基类,类会隐式继承 object(即 System.Object)。所以 ToString()Equals() 这些方法每个类都有,因为它们来自 object。继承链的最顶端永远是 object,这也是第 47 章的主题。

namespace CSharpDemo;

class Person { public string Name { get; set; } }

var p = new Person { Name = "小明" };
Console.WriteLine(p.ToString());   // 默认输出类型名,可重写
Note

理解「万物皆 object」有助于看懂很多通用写法:集合之所以能装任意类型,正是因为一切最终都源于 object

继承的设计初衷与适用边界

继承解决的核心问题是「复用 + 归类」:把多个类型的共性上提到基类,个性下沉到派生类,做到「公共逻辑只写一处」,同时用「是一种(is-a)」表达类型之间的层次。但它不是免费的——基类一旦改动,所有子类都受影响(人称「脆基类问题」)。所以现代设计更推崇「优先组合、谨慎继承」:能用「有一个(has-a)」字段组合清楚表达的,就别硬上继承。

Note

判断口诀:「是一种」才继承,「有一个」用组合。例如 Student 是一种 Person 合理;CarEngine,应让 Car 持有一个 Engine 字段,而不是让 Car 继承 Engine

最佳实践汇总

  1. 基类要稳:放进基类的成员应该是「长期不变」的,否则一改牵动全部子类。
  2. sealedsealed:不希望被派生的类用 sealed 封死,保护不变式。
  3. 想多态别漏 virtual/override:只在子类写 new 是「隐藏」,用基类变量调用看不到新行为;要运行期多态必须基类 virtual、子类 override(下一章展开)。
  4. 构造里别急着调虚方法:基类构造函数执行时,派生类还没初始化完,此时调 virtual 方法可能访问到未就绪的状态(第 41 章细说)。
  5. 所有类终归继承自 object:不写基类时隐式继承 System.ObjectToStringEqualsGetHashCode 等方法人人都有,可重写。
Warning

滥用继承会让类型关系失真、耦合变紧。若两个类只是「凑巧有同名字段」,别为了省几行就拉继承;那是组合或各自独立更合适。

小结

继承用 class 派生 : 基类 表达「是一种」关系,派生类自动获得基类的 public/protected 成员,但不继承构造函数、也看不到 private。若不显式写基类,类会隐式继承 object,因此 ToStringEquals 等方法人皆有之。base(...) 在派生构造里调用基类构造(多层时从最基类往上执行),base.成员 调用基类方法。protected 给派生类留扩展口子,sealed 可禁止类被继承。子类写同名成员想故意遮盖时用 new(字段和方法都能隐藏),它是编译期的「隐藏」而非运行期的「重写」(重写见第 41 章)。能用组合表达「有一个」时优先组合,C# 只支持单类继承,但可实现多个接口。