404 Not Found

404 Not Found


nginx

多态

多态概述

多态(Polymorphism)是面向对象编程的核心特性之一,指同一接口在不同对象上表现出不同行为。C# 中多态分为两种:

本课重点讲解运行时多态,它是面向对象设计中最关键的能力。

virtual 与 override

基类用 virtual 标记一个方法为"可重写",派生类用 override 提供新实现。运行时根据对象的实际类型(而非变量声明类型)决定调用哪个方法,这就是动态派发。

示例

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes a sound");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

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

class Program
{
    static void Main()
    {
        Animal a = new Dog();
        a.Speak();

        Animal b = new Cat();
        b.Speak();
    }
}
▶ 试一试
TEXT
Dog barks
Cat meows

变量声明为 Animal,但实际对象是 DogCat,运行时调用了各自重写后的版本——这就是多态的核心效果。

多态的实际应用:统一接口不同实现

多态最常见的场景是用基类引用数组或集合管理不同派生类对象,统一调用接口,各对象自行响应。

示例

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes a sound");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks");
    }
}

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

class Cow : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Cow moos");
    }
}

class Program
{
    static void MakeThemSpeak(Animal[] animals)
    {
        foreach (Animal a in animals)
        {
            a.Speak();
        }
    }

    static void Main()
    {
        Animal[] animals = { new Dog(), new Cat(), new Cow() };
        MakeThemSpeak(animals);
    }
}
▶ 试一试
TEXT
Dog barks
Cat meows
Cow moos

MakeThemSpeak 方法只依赖 Animal 类型,无需知道具体是哪种动物,新增派生类也不需要修改此方法。

override 与 new 的区别

派生类中使用 new 关键字可以隐藏基类方法,但这不参与多态。override 重写参与动态派发,new 隐藏只在编译时按声明类型选择方法。

示例

CSHARP
using System;

class Base
{
    public virtual void Print()
    {
        Console.WriteLine("Base.Print");
    }
}

class OverrideDerived : Base
{
    public override void Print()
    {
        Console.WriteLine("OverrideDerived.Print");
    }
}

class NewDerived : Base
{
    public new void Print()
    {
        Console.WriteLine("NewDerived.Print");
    }
}

class Program
{
    static void Main()
    {
        Base obj1 = new OverrideDerived();
        obj1.Print();

        Base obj2 = new NewDerived();
        obj2.Print();

        NewDerived obj3 = new NewDerived();
        obj3.Print();
    }
}
▶ 试一试
TEXT
OverrideDerived.Print
Base.Print
NewDerived.Print
💡 需要多态行为时始终使用 overridenew 仅用于特殊情况下的方法隐藏。

abstract 方法与抽象类

abstract 方法没有实现,强制派生类必须重写。包含抽象方法的类本身也必须声明为 abstract,不能直接实例化。

示例

CSHARP
using System;

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

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

    public Circle(double radius)
    {
        Radius = radius;
    }

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

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

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

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

class Program
{
    static void PrintArea(Shape shape)
    {
        Console.WriteLine($"Area: {shape.Area():F2}");
    }

    static void Main()
    {
        Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
        foreach (Shape s in shapes)
        {
            PrintArea(s);
        }
    }
}
▶ 试一试
TEXT
Area: 28.27
Area: 20.00

抽象类定义了"是什么",派生类决定"怎么做",所有形状通过统一的 Area() 接口计算面积。

方法链:多层重写

virtual/override 可以沿继承链逐层重写,派生类可通过 base. 调用父类实现并在此基础上扩展。

示例

CSHARP
using System;

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal speaks");
    }
}

class Dog : Animal
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("Dog barks");
    }
}

class Puppy : Dog
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("Puppy yips");
    }
}

class Program
{
    static void Main()
    {
        Animal a = new Puppy();
        a.Speak();
    }
}
▶ 试一试
TEXT
Animal speaks
Dog barks
Puppy yips

Puppy.Speak() 调用 base.Speak() 触发 Dog.Speak(),后者再调用 base.Speak() 触发 Animal.Speak(),形成从基类到派生类的链式输出。

ToString 重写

C# 中所有类型都继承自 object,其 ToString() 方法是 virtual 的。重写它可以让对象在输出时展示有意义的文本。

示例

CSHARP
using System;

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public override string ToString()
    {
        return $"{Name} (Age: {Age})";
    }
}

class Program
{
    static void Main()
    {
        Student s = new Student("Alice", 20);
        Console.WriteLine(s);
        Console.WriteLine(s.ToString());
    }
}
▶ 试一试
TEXT
Alice (Age: 20)
Alice (Age: 20)

Console.WriteLine 内部自动调用 ToString(),重写后输出从默认类型名变为自定义格式。

里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的基本原则:派生类对象必须能够替换其基类对象,而程序的行为不变。

⚠️ 违反 LSP 的典型情况:派生类重写方法时抛出基类未声明的异常,或返回不符合基类契约的结果。

示例

CSHARP
using System;

class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Bird flies");
    }
}

class Swallow : Bird
{
    public override void Fly()
    {
        Console.WriteLine("Swallow flies high");
    }
}

class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Penguin cannot fly");
    }
}

class Program
{
    static void LetFly(Bird bird)
    {
        bird.Fly();
    }

    static void Main()
    {
        LetFly(new Swallow());
        LetFly(new Penguin());
    }
}
▶ 试一试
TEXT
Swallow flies high

Unhandled Exception: System.NotSupportedException: Penguin cannot fly

Penguin 继承 Bird 但重写 Fly 抛出异常,破坏了基类"鸟会飞"的契约,违反 LSP。正确做法是重新设计继承层次,如将"飞行能力"提取为独立接口。

❓ 常见问题

Q virtual 方法可以不重写吗?
A 可以,不重写则使用基类默认实现。
Q override 方法还能再被重写吗?
A 可以,override 方法隐含 virtual 语义,派生类可继续重写。
Q newoverride 哪个更好?
A 需要多态行为时用 overridenew 仅在必须隐藏且确认不需要多态时使用。
Q 抽象类和接口有什么区别?
A 抽象类可有字段和实现代码,接口只定义契约;类只能单继承抽象类,但可实现多个接口。
Q 不重写 ToString() 会怎样?
A 默认返回类型的全限定名,如 Student 输出 Student

📖 小节

📝 作业

  1. 定义 Vehicle 基类,包含 virtual void Move() 方法,派生 CarBicycle 类重写 Move(),在 Main 中用 Vehicle[] 数组统一调用
  2. 定义抽象类 Payment,包含 abstract void Pay(decimal amount),派生 CashPaymentCardPayment 实现各自的支付逻辑
  3. 创建 Book 类并重写 ToString(),输出书名和价格,验证 Console.WriteLine(book) 的效果
  4. 编写代码演示 overridenew 的区别:用基类引用分别调用两个派生类对象,观察输出差异
  5. 思考:如果 Square 继承 Rectangle 但重写 SetWidth/SetHeight 使边长始终相等,是否违反里氏替换原则?说明理由
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏