1:学习目标
- 彻底理解 C# 的单继承机制,掌握继承的语法和规则
- 深入理解多态的本质,掌握
virtual/override的底层原理 - 明确区分方法重写 (
override) 与方法隐藏 (new) 的核心差异 - 掌握抽象类与抽象方法的定义与使用场景
- 深入理解接口的概念,掌握接口的定义、实现与多态应用
- 理解显式接口实现的用法与适用场景
- 掌握密封类与密封方法的作用
- 所有知识点均与 C++ 对应概念进行深度对比,消除认知误区
2:继承的基本概念
继承是面向对象编程的三大特性之一(封装、继承、多态),允许我们创建一个新类来复用、扩展和修改已有类的行为。
1:C#继承的核心机制
C# 的继承机制与 C++ 有一个根本性的区别:
- C# 只支持单继承:一个类只能直接继承自一个基类
- C++ 支持多继承:一个类可以同时继承自多个基类
- C# 通过接口来实现类似多继承的功能,这是 C# 设计的核心原则之一
C# 继承的其他规则:
- 继承是可传递的:如果 C 继承自 B,B 继承自 A,那么 C 同时继承 B 和 A 的成员
- 派生类可以添加新的成员,但不能移除继承的成员
- 构造函数和终结器不能被继承
- 派生类可以通过
base关键字访问基类的成员 - 所有类最终都直接或间接继承自
System.Object类
2:继承的基本语法
使用冒号:表示继承关系
cs
// 基类(父类)
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void Eat()
{
Console.WriteLine($"{Name}正在吃东西");
}
}
// 派生类(子类),继承自Animal
public class Dog : Animal
{
// 派生类可以添加新的成员
public string Breed { get; set; }
public void Bark()
{
Console.WriteLine($"{Name}({Breed})汪汪叫");
}
}
3:继承中的访问修饰符
不同访问修饰符的成员在继承中的可访问性:
| 访问修饰符 | 基类内部 | 派生类内部 | 外部代码 |
|---|---|---|---|
public |
✅ | ✅ | ✅ |
protected |
✅ | ✅ | ❌ |
internal |
✅ | ✅(同一程序集内) | ✅(同一程序集内) |
protected internal |
✅ | ✅ | ✅(同一程序集内) |
private |
✅ | ❌ | ❌ |
protected访问修饰符详解: protected成员只能在基类内部和派生类内部访问,外部代码无法访问。这是为了让派生类能够访问基类的内部状态,同时保持封装性。
cs
public class Animal
{
protected string _internalState = "健康";
protected void InternalMethod()
{
Console.WriteLine("这是基类的受保护方法");
}
}
public class Dog : Animal
{
public void ShowState()
{
Console.WriteLine($"动物状态:{_internalState}"); // 可以访问protected成员
InternalMethod(); // 可以调用protected方法
}
}
// 外部代码
Dog dog = new Dog();
// dog._internalState; // 编译错误:无法访问protected成员
// dog.InternalMethod(); // 编译错误:无法访问protected方法
4:base关键字
base关键字用于在派生类中访问基类的成员:
- 调用基类的构造函数
- 调用基类的方法
- 访问基类的字段和属性
调用基类构造函数:
cs
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public Animal(string name, int age)
{
Name = name;
Age = age;
}
}
public class Dog : Animal
{
public string Breed { get; set; }
// 派生类构造函数必须调用基类构造函数
public Dog(string name, int age, string breed) : base(name, age)
{
Breed = breed;
}
}
重要提示:
- 如果基类没有无参数构造函数,派生类必须显式调用基类的带参数构造函数
- 如果没有显式调用
base(),编译器会自动调用基类的无参数构造函数
调用基类方法:
cs
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
base.MakeSound(); // 调用基类的MakeSound方法
Console.WriteLine("汪汪汪");
}
}
3:多态本质:虚方法和重写
多态是指同一个操作作用于不同的对象会产生不同的结果。C# 通过virtual和override关键字实现运行时多态。
1:虚方法和重写的底层原理
当你将一个方法声明为virtual时,编译器会在类的方法表中添加一个条目。当调用虚方法时,CLR 会根据对象的实际类型来决定调用哪个版本的方法,这称为动态绑定 或后期绑定。
C# 的虚方法机制与 C++ 的虚函数机制几乎完全相同,都基于虚函数表 (vtable) 实现:
- 每个包含虚方法的类都有一个虚函数表
- 虚函数表中存储了类的所有虚方法的地址
- 每个对象都有一个指向其类虚函数表的指针
- 调用虚方法时,通过对象的虚函数表指针找到对应的方法地址
2:override和new的核心区别
这是 C# 面向对象中最容易混淆的知识点,必须彻底理解:
| 对比维度 | override |
new |
|---|---|---|
| 作用 | 重写基类的虚方法 | 隐藏基类的方法 |
| 要求 | 基类方法必须是virtual、abstract或override |
基类方法可以是任何方法 |
| 多态性 | 支持运行时多态 | 不支持多态,是编译时绑定 |
| 调用方式 | 根据对象的实际类型调用 | 根据变量的声明类型调用 |
cs
public class BaseClass
{
public virtual void VirtualMethod()
{
Console.WriteLine("基类虚方法");
}
public void NormalMethod()
{
Console.WriteLine("基类普通方法");
}
}
public class DerivedClass : BaseClass
{
public override void VirtualMethod()
{
Console.WriteLine("派生类重写方法");
}
public new void NormalMethod()
{
Console.WriteLine("派生类隐藏方法");
}
}
// 测试代码
BaseClass baseObj1 = new DerivedClass();
baseObj1.VirtualMethod(); // 输出:派生类重写方法(运行时多态)
baseObj1.NormalMethod(); // 输出:基类普通方法(编译时绑定)
DerivedClass derivedObj = new DerivedClass();
derivedObj.VirtualMethod(); // 输出:派生类重写方法
derivedObj.NormalMethod(); // 输出:派生类隐藏方法
3:方法重写的规则
- 重写方法必须与基类方法具有相同的签名(方法名、参数列表、返回值类型)
- 重写方法的访问修饰符不能比基类方法更严格
- 不能重写
static或private方法 - 重写方法可以使用
base关键字调用基类版本
4:抽象类和抽象方法
抽象类是不能被实例化的类,只能作为基类被其他类继承。抽象方法是没有实现的方法,必须在派生类中被重写。
1:抽象类的定义与使用
使用abstract关键字声明抽象类和抽象方法:
cs
// 抽象类
public abstract class Shape
{
// 抽象方法:没有实现,只有声明
public abstract double CalculateArea();
public abstract double CalculatePerimeter();
// 抽象类可以包含非抽象方法
public void PrintInfo()
{
Console.WriteLine($"这是一个形状,面积:{CalculateArea()},周长:{CalculatePerimeter()}");
}
}
// 派生类必须实现所有抽象方法
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public override double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
}
public 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 CalculateArea()
{
return Width * Height;
}
public override double CalculatePerimeter()
{
return 2 * (Width + Height);
}
}
2:抽象类的特点
- 不能被实例化
- 可以包含抽象方法和非抽象方法
- 可以包含字段、属性、构造函数等所有类成员
- 派生类必须实现所有继承的抽象方法
- 抽象类可以继承自另一个抽象类
3:与C++纯虚函数的对比
C++ 的纯虚函数与 C# 的抽象方法非常相似:
cpp
// C++抽象类
class Shape
{
public:
virtual double CalculateArea() = 0; // 纯虚函数
virtual double CalculatePerimeter() = 0;
void PrintInfo()
{
std::cout << "这是一个形状" << std::endl;
}
};
主要区别:
- C++ 中包含纯虚函数的类自动成为抽象类,不需要显式声明
- C# 必须显式使用
abstract关键字声明抽象类和抽象方法 - C++ 的纯虚函数可以有实现,而 C# 的抽象方法不能有实现
5:接口
接口是 C# 中最重要的概念之一,也是 C# 实现多继承的方式。接口定义了一组契约,实现接口的类必须遵守这个契约。
1:接口的定义
使用interface关键字定义接口,接口名通常以I开头(这是 C# 的命名约定):
cs
public interface IShape
{
// 接口方法:默认是public abstract
double CalculateArea();
double CalculatePerimeter();
}
接口的特点:
- 接口不能包含字段
- 接口的所有成员默认都是
public abstract(不能显式指定访问修饰符) - 接口不能包含构造函数和终结器
- 接口不能包含静态成员(C# 8.0 之前)
- 一个类可以实现多个接口
- 接口可以继承自其他接口
2:接口的实现
一个类可以实现一个或多个接口,使用冒号:表示,多个接口用逗号分隔:
cs
// 实现一个接口
public class Circle : IShape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
// 必须实现接口的所有方法
public double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
}
// 实现多个接口
public interface IMovable
{
void Move(int x, int y);
}
public interface IDrawable
{
void Draw();
}
public class Rectangle : IShape, IMovable, IDrawable
{
public double Width { get; set; }
public double Height { get; set; }
public int X { get; set; }
public int Y { get; set; }
// 实现IShape接口
public double CalculateArea() => Width * Height;
public double CalculatePerimeter() => 2 * (Width + Height);
// 实现IMovable接口
public void Move(int x, int y)
{
X = x;
Y = y;
Console.WriteLine($"矩形移动到({X}, {Y})");
}
// 实现IDrawable接口
public void Draw()
{
Console.WriteLine($"绘制矩形,位置({X}, {Y}),大小{Width}x{Height}");
}
}
3:接口的多态性
接口最大的价值在于实现多态。我们可以通过接口引用来调用实现类的方法:
cs
// 接口多态
IShape[] shapes = new IShape[]
{
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 5)
};
foreach (IShape shape in shapes)
{
Console.WriteLine($"面积:{shape.CalculateArea()}");
Console.WriteLine($"周长:{shape.CalculatePerimeter()}");
Console.WriteLine();
}
// 多个接口的多态
IMovable movable = new Rectangle(4, 6);
movable.Move(10, 20);
IDrawable drawable = new Rectangle(4, 6);
drawable.Draw();
4:显示接口实现
当一个类实现多个接口,而这些接口有同名的方法时,就需要使用显式接口实现来区分不同接口的方法。
显式接口实现的语法是接口名.方法名:
cs
public interface IFirstInterface
{
void DoSomething();
}
public interface ISecondInterface
{
void DoSomething();
}
public class MyClass : IFirstInterface, ISecondInterface
{
// 显式实现IFirstInterface的DoSomething
void IFirstInterface.DoSomething()
{
Console.WriteLine("实现IFirstInterface的DoSomething");
}
// 显式实现ISecondInterface的DoSomething
void ISecondInterface.DoSomething()
{
Console.WriteLine("实现ISecondInterface的DoSomething");
}
}
显式接口实现的适用场景:
- 实现多个接口且有同名方法
- 隐藏接口方法,只允许通过接口引用调用
- 实现接口的同时保留类自己的同名方法
5:接口和抽象类的区别
| 对比维度 | 接口 | 抽象类 |
|---|---|---|
| 继承 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类 |
| 成员类型 | 只能包含方法、属性、索引器、事件(C# 8.0 之前) | 可以包含字段、方法、属性、构造函数等所有类成员 |
| 访问修饰符 | 所有成员默认都是 public abstract | 可以使用任何访问修饰符 |
| 构造函数 | 不能有构造函数 | 可以有构造函数 |
| 静态成员 | C# 8.0 之前不能有静态成员 | 可以有静态成员 |
| 实现 | 必须实现所有接口成员 | 只需实现所有抽象成员 |
| 设计目的 | 定义契约,描述 "能做什么" | 定义基类,描述 "是什么" |
使用原则:
- 当你想定义一组类的共同行为,且这些类没有共同的基类时,使用接口
- 当你想在多个相关类之间共享代码,且这些类有共同的本质时,使用抽象类
- 优先使用接口而不是抽象类,因为接口提供了更好的解耦和灵活性
6:密封类与密封方法
使用sealed关键字可以防止类被继承或方法被重写。
1:密封类
密封类不能被其他类继承:
cs
public sealed class SealedClass
{
public void Method()
{
Console.WriteLine("这是密封类的方法");
}
}
// 编译错误:不能从密封类派生
// public class DerivedClass : SealedClass
// {
// }
密封类的使用场景:
- 类包含敏感信息,不希望被继承修改
- 类是静态工具类,不需要被继承
- 为了性能优化,密封类的方法调用比非密封类更快
2:密封方法
密封方法不能在派生类中被重写。密封方法必须是重写方法:
cs
public class BaseClass
{
public virtual void Method()
{
Console.WriteLine("基类方法");
}
}
public class DerivedClass1 : BaseClass
{
public sealed override void Method()
{
Console.WriteLine("派生类1的密封方法");
}
}
public class DerivedClass2 : DerivedClass1
{
// 编译错误:不能重写密封方法
// public override void Method()
// {
// }
}
3:与C++的对比
C++11 引入了final关键字,功能与 C# 的sealed相同:
cpp
class BaseClass
{
public:
virtual void Method() {}
};
class DerivedClass1 : public BaseClass
{
public:
void Method() final {} // 密封方法
};
class DerivedClass2 final : public DerivedClass1 // 密封类
{
};
7:总结
- C# 只支持单继承,通过接口实现多继承的功能
override实现运行时多态,new只是隐藏基类方法- 接口定义契约,抽象类定义基类,优先使用接口
- 显式接口实现用于解决同名方法冲突
sealed关键字可以防止类被继承或方法被重写