Skip to content

18-继承和多态

继承和多态是面向对象编程的重要特性。继承允许一个类从另一个类获得成员,多态允许同一操作在不同对象上有不同的行为。


一、继承(Inheritance)

1. 基本概念

通过继承,子类(派生类)可以获取父类(基类)的成员,并添加新成员或重写父类方法。

csharp
// 基类(父类)
public class Animal
{
    public string Name { get; set; }

    public void Eat()
    {
        Console.WriteLine($"{Name} 在吃东西");
    }
}

// 派生类(子类)
public class Dog : Animal  // 使用 : 表示继承
{
    public void Bark()
    {
        Console.WriteLine($"{Name} 在汪汪叫");  // Name 从 Animal 继承
    }
}

2. 继承的特性

特性说明
单继承C# 类只能继承一个直接基类
传递性A → B → C,C 拥有 A 和 B 的成员
可访问成员子类可访问父类的 publicprotected 成员
不继承构造函数、析构函数、静态构造函数
sealed 类不能被继承

3. 继承链的构造函数调用

csharp
public class Animal
{
    public string Name { get; set; }

    public Animal()
    {
        Console.WriteLine("Animal 默认构造函数");
    }

    public Animal(string name)
    {
        Name = name;
        Console.WriteLine($"Animal 参数构造函数:{name}");
    }
}

public class Dog : Animal
{
    public string Breed { get; set; }

    // 默认调用父类的无参构造函数
    public Dog()
    {
        Console.WriteLine("Dog 默认构造函数");
    }

    // 指定调用父类的哪个构造函数
    public Dog(string name, string breed) : base(name)  // 调用 Animal(string)
    {
        Breed = breed;
        Console.WriteLine($"Dog 参数构造函数:{name}, {breed}");
    }
}

// 执行顺序:
// 1. 父类构造函数
// 2. 子类构造函数
var dog = new Dog("旺财", "金毛");
// 输出:
// Animal 参数构造函数:旺财
// Dog 参数构造函数:旺财, 金毛

4. base 关键字

base 用于在子类中访问父类成员。

csharp
public class Employee
{
    public string Name { get; set; }
    public decimal BaseSalary { get; set; }

    public Employee(string name, decimal baseSalary)
    {
        Name = name;
        BaseSalary = baseSalary;
    }

    public virtual decimal CalculateSalary()
    {
        return BaseSalary;
    }
}

public class Manager : Employee
{
    public decimal Bonus { get; set; }

    public Manager(string name, decimal baseSalary, decimal bonus)
        : base(name, baseSalary)  // 调用父类构造函数
    {
        Bonus = bonus;
    }

    public override decimal CalculateSalary()
    {
        // 调用父类方法 + 额外逻辑
        return base.CalculateSalary() + Bonus;
    }
}

var mgr = new Manager("张三", 10000, 5000);
Console.WriteLine($"总工资:{mgr.CalculateSalary()}");  // 15000

5. 密封类(sealed)

csharp
public sealed class FinalClass
{
    // 这个类不能被继承
}

// 编译错误
// public class Derived : FinalClass { }

二、多态(Polymorphism)

多态通过 虚方法(virtual)重写(override) 实现。

1. 虚方法和重写

csharp
public class Animal
{
    public string Name { get; set; }

    // virtual:允许子类重写
    public virtual void MakeSound()
    {
        Console.WriteLine("动物发出声音");
    }
}

public class Dog : Animal
{
    // override:重写父类方法
    public override void MakeSound()
    {
        Console.WriteLine("汪汪汪");
    }
}

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

public class Duck : Animal
{
    // 没有重写——会使用父类的默认实现
}

2. 多态的威力

csharp
// 父类类型引用子类对象
Animal[] animals = new Animal[]
{
    new Dog { Name = "旺财" },
    new Cat { Name = "咪咪" },
    new Duck { Name = "唐老鸭" },
    new Dog { Name = "来福" }
};

// 同一行代码,不同行为
foreach (Animal animal in animals)
{
    Console.Write($"{animal.Name}:");
    animal.MakeSound();
}
// 输出:
// 旺财:汪汪汪
// 咪咪:喵喵喵
// 唐老鸭:动物发出声音(没有重写,使用父类默认)
// 来福:汪汪汪

3. 方法隐藏(new 关键字)

csharp
public class BaseClass
{
    public void Show()
    {
        Console.WriteLine("父类方法");
    }
}

public class DerivedClass : BaseClass
{
    public new void Show()  // 隐藏(不是重写)
    {
        Console.WriteLine("子类方法");
    }
}

// 重点:隐藏 vs 重写的区别
BaseClass obj1 = new DerivedClass();
obj1.Show();  // 输出:父类方法(没有多态行为!)

DerivedClass obj2 = new DerivedClass();
obj2.Show();  // 输出:子类方法

virtual / override / new 对比

关键字使用位置效果
virtual父类方法声明方法可以被重写
override子类方法重写父类的 virtual 方法(实现多态)
new子类方法隐藏父类方法(不实现多态)
sealed override子类方法重写后禁止更下层的子类再重写

三、抽象类(Abstract Class)

抽象类使用 abstract 声明,不能实例化,必须被继承。可以包含抽象方法(无实现)和普通方法。

csharp
public abstract class Shape
{
    public string Color { get; set; }

    // 抽象方法:没有方法体,子类必须重写
    public abstract double GetArea();

    // 抽象属性
    public abstract string ShapeType { get; }

    // 普通方法——有实现
    public void Display()
    {
        Console.WriteLine($"{ShapeType}:颜色={Color},面积={GetArea():F2}");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public override string ShapeType => "圆形";

    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override string ShapeType => "矩形";

    public override double GetArea()
    {
        return Width * Height;
    }
}

// 使用
// Shape s = new Shape();  // ❌ 不能实例化抽象类

Shape[] shapes = new Shape[]
{
    new Circle { Color = "红", Radius = 5 },
    new Rectangle { Color = "蓝", Width = 4, Height = 6 }
};

foreach (var shape in shapes)
{
    shape.Display();
}
// 输出:
// 圆形:颜色=红,面积=78.54
// 矩形:颜色=蓝,面积=24.00

抽象类与普通类的区别

对比项抽象类普通类
new 实例化❌ 不能✅ 能
抽象方法可以有不能有
普通方法可以有可以有
子类要求子类必须实现所有抽象成员子类可选择重写 virtual 方法

四、类型检查和转换

is 运算符

检查对象是否是某个类型(或实现了某个接口)。

csharp
Animal animal = new Dog();

if (animal is Dog)
{
    Console.WriteLine("这是一只狗");
}

if (animal is Animal)
{
    Console.WriteLine("这是动物");
}

// C# 7.0+:模式匹配
if (animal is Dog dog)
{
    Console.WriteLine($"狗名:{dog.Name}");
}

// switch 模式匹配
string GetAnimalSound(Animal animal)
{
    return animal switch
    {
        Dog => "汪汪",
        Cat => "喵喵",
        _ => "未知声音"
    };
}

as 运算符

安全类型转换,失败返回 null(不抛异常)。

csharp
Animal animal = new Dog();

// as 转换
Dog? dog = animal as Dog;
if (dog != null)
{
    dog.Bark();  // 安全使用
}

// as 转换失败返回 null
Cat? cat = animal as Cat;
Console.WriteLine(cat == null);  // True(animal 不是 Cat)

is vs as vs 强制转换

方式失败时行为适用场景
is 检查不转换,返回 bool只需要判断类型
as 转换返回 null安全的类型转换
强制转换 (T)obj抛出异常确定类型一定正确时

五、里氏替换原则(LSP)

Liskov Substitution Principle: 子类对象应能替换父类对象使用,且程序行为不变。

csharp
// 符合 LSP 的设计
public class Bird
{
    public virtual void Move()
    {
        Console.WriteLine("移动");
    }
}

public class Sparrow : Bird
{
    public override void Move()
    {
        Console.WriteLine("飞翔");
    }
}

// 不符合 LSP 的设计
public class Penguin : Bird
{
    public override void Move()
    {
        throw new NotSupportedException("企鹅不会飞");  // 违反了 LSP!
    }
}

六、密封方法

sealed 不仅可以作用于类,还可以作用于重写的方法,禁止更下层的子类再次重写。

csharp
public class Animal
{
    public virtual void Eat() { }
}

public class Mammal : Animal
{
    public sealed override void Eat()  // 禁止后续子类重写
    {
        Console.WriteLine("哺乳动物在吃东西");
    }
}

public class Dog : Mammal
{
    // public override void Eat() { }  // ❌ 编译错误:Eat 已被密封
}

七、综合案例:员工薪资系统

csharp
// 抽象基类
public abstract class Employee
{
    public string Name { get; set; }
    public string EmployeeId { get; set; }

    protected Employee(string name, string employeeId)
    {
        Name = name;
        EmployeeId = employeeId;
    }

    public abstract decimal CalculateMonthlySalary();  // 抽象方法
    public abstract string Position { get; }           // 抽象属性

    public virtual void DisplayInfo()                  // 虚方法(可重写)
    {
        Console.WriteLine($"\n=== {Position} ===");
        Console.WriteLine($"姓名:{Name}");
        Console.WriteLine($"工号:{EmployeeId}");
        Console.WriteLine($"月薪:{CalculateMonthlySalary():C}");
    }
}

// 正式员工
public class FullTimeEmployee : Employee
{
    public decimal BaseSalary { get; set; }
    public decimal Bonus { get; set; }

    public FullTimeEmployee(string name, string id, decimal baseSalary, decimal bonus = 0)
        : base(name, id)
    {
        BaseSalary = baseSalary;
        Bonus = bonus;
    }

    public override string Position => "正式员工";

    public override decimal CalculateMonthlySalary() => BaseSalary + Bonus;

    public override void DisplayInfo()
    {
        base.DisplayInfo();
        Console.WriteLine($"底薪:{BaseSalary:C},奖金:{Bonus:C}");
    }
}

// 兼职员工
public class PartTimeEmployee : Employee
{
    public decimal HourlyRate { get; set; }
    public int HoursWorked { get; set; }

    public PartTimeEmployee(string name, string id, decimal hourlyRate, int hoursWorked)
        : base(name, id)
    {
        HourlyRate = hourlyRate;
        HoursWorked = hoursWorked;
    }

    public override string Position => "兼职员工";

    public override decimal CalculateMonthlySalary() => HourlyRate * HoursWorked;
}

// 实习生
public class Intern : Employee
{
    public decimal Stipend { get; set; }

    public Intern(string name, string id, decimal stipend) : base(name, id)
    {
        Stipend = stipend;
    }

    public override string Position => "实习生";

    public override decimal CalculateMonthlySalary() => Stipend;

    // 不使用 override,使用 new 隐藏父类方法
    public new void DisplayInfo()
    {
        Console.WriteLine($"\n=== {Position} ===");
        Console.WriteLine($"姓名:{Name}");
        Console.WriteLine($"月薪:{CalculateMonthlySalary():C}(实习津贴)");
    }
}

// 使用多态
class Program
{
    static void Main()
    {
        List<Employee> employees = new List<Employee>
        {
            new FullTimeEmployee("张三", "E001", 10000, 3000),
            new PartTimeEmployee("李四", "E002", 80, 120),
            new Intern("王五", "E003", 2000),
            new FullTimeEmployee("赵六", "E004", 15000, 5000)
        };

        Console.WriteLine("===== 员工薪资报表 =====");

        decimal total = 0;
        foreach (var emp in employees)
        {
            emp.DisplayInfo();
            total += emp.CalculateMonthlySalary();
        }

        Console.WriteLine($"\n总薪资支出:{total:C}");

        // 特定类型员工的操作
        foreach (var emp in employees)
        {
            if (emp is FullTimeEmployee fullTime && fullTime.Bonus > 2000)
            {
                Console.WriteLine($"\n{fullTime.Name} 获得高额奖金 {fullTime.Bonus:C}");
            }
        }
    }
}

核心知识点总结

关键字速查

关键字用途
:继承/实现接口语法
base在子类中访问父类成员
virtual声明可重写的方法
override重写父类方法
new隐藏父类方法
abstract声明抽象类/抽象方法
sealed密封类(禁止继承)或密封方法(禁止继续重写)

设计决策

场景使用
需要共享基础实现普通基类
需要强制子类实现某些方法抽象类
只需要定义能力/合约接口(见下一章)
禁止被继承sealed
需要多态行为virtual + override

注意事项

  1. C# 只支持单继承——一个类只能有一个父类
  2. 构造函数不可继承——子类必须通过 base() 调用父类构造函数
  3. override 才有多态——new 关键字只是隐藏,不是重写
  4. 抽象类不能实例化——但可以有构造函数(用于子类调用)
  5. 遵循 LSP——子类应能无缝替换父类
  6. 优先使用组合而非继承——继承层级过深会增加维护成本

Released under the MIT License.