在C++基础上理解CSharp-5

1:学习目标

  • 彻底理解 C# 的单继承机制,掌握继承的语法和规则
  • 深入理解多态的本质,掌握virtual/override的底层原理
  • 明确区分方法重写 (override) 与方法隐藏 (new) 的核心差异
  • 掌握抽象类与抽象方法的定义与使用场景
  • 深入理解接口的概念,掌握接口的定义、实现与多态应用
  • 理解显式接口实现的用法与适用场景
  • 掌握密封类与密封方法的作用
  • 所有知识点均与 C++ 对应概念进行深度对比,消除认知误区

2:继承的基本概念

继承是面向对象编程的三大特性之一(封装、继承、多态),允许我们创建一个新类来复用、扩展和修改已有类的行为。

1:C#继承的核心机制

C# 的继承机制与 C++ 有一个根本性的区别

  • C# 只支持单继承:一个类只能直接继承自一个基类
  • C++ 支持多继承:一个类可以同时继承自多个基类
  • C# 通过接口来实现类似多继承的功能,这是 C# 设计的核心原则之一

C# 继承的其他规则:

  1. 继承是可传递的:如果 C 继承自 B,B 继承自 A,那么 C 同时继承 B 和 A 的成员
  2. 派生类可以添加新的成员,但不能移除继承的成员
  3. 构造函数和终结器不能被继承
  4. 派生类可以通过base关键字访问基类的成员
  5. 所有类最终都直接或间接继承自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关键字用于在派生类中访问基类的成员:

  1. 调用基类的构造函数
  2. 调用基类的方法
  3. 访问基类的字段和属性

调用基类构造函数:

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# 通过virtualoverride关键字实现运行时多态。

1:虚方法和重写的底层原理

当你将一个方法声明为virtual时,编译器会在类的方法表中添加一个条目。当调用虚方法时,CLR 会根据对象的实际类型来决定调用哪个版本的方法,这称为动态绑定后期绑定

C# 的虚方法机制与 C++ 的虚函数机制几乎完全相同,都基于虚函数表 (vtable) 实现:

  • 每个包含虚方法的类都有一个虚函数表
  • 虚函数表中存储了类的所有虚方法的地址
  • 每个对象都有一个指向其类虚函数表的指针
  • 调用虚方法时,通过对象的虚函数表指针找到对应的方法地址

2:override和new的核心区别

这是 C# 面向对象中最容易混淆的知识点,必须彻底理解:

对比维度 override new
作用 重写基类的虚方法 隐藏基类的方法
要求 基类方法必须是virtualabstractoverride 基类方法可以是任何方法
多态性 支持运行时多态 不支持多态,是编译时绑定
调用方式 根据对象的实际类型调用 根据变量的声明类型调用
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:方法重写的规则

  • 重写方法必须与基类方法具有相同的签名(方法名、参数列表、返回值类型)
  • 重写方法的访问修饰符不能比基类方法更严格
  • 不能重写staticprivate方法
  • 重写方法可以使用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();
}

接口的特点:

  1. 接口不能包含字段
  2. 接口的所有成员默认都是public abstract(不能显式指定访问修饰符)
  3. 接口不能包含构造函数和终结器
  4. 接口不能包含静态成员(C# 8.0 之前)
  5. 一个类可以实现多个接口
  6. 接口可以继承自其他接口

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");
    }
}

显式接口实现的适用场景:

  1. 实现多个接口且有同名方法
  2. 隐藏接口方法,只允许通过接口引用调用
  3. 实现接口的同时保留类自己的同名方法

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
// {
// }

密封类的使用场景:

  1. 类包含敏感信息,不希望被继承修改
  2. 类是静态工具类,不需要被继承
  3. 为了性能优化,密封类的方法调用比非密封类更快

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关键字可以防止类被继承或方法被重写
相关推荐
xufengzhu1 小时前
第三方 Python 库 redis-py + hiredis 的使用
开发语言·redis·python
jingling5551 小时前
go | 环境安装和快速入门
开发语言·后端·golang
yuan199971 小时前
欧拉梁静力与屈曲计算的 MATLAB 实现(有限差分法 + 解析解)
开发语言·算法·matlab
llxxyy卢2 小时前
polar夏季赛部分题目
开发语言·python
AI玫瑰助手2 小时前
Python模块:from...import...导入指定内容
开发语言·python·信息可视化
石山代码2 小时前
JavaScript 进阶核心知识点
开发语言·javascript·ecmascript
FL16238631292 小时前
[cmake]基于C++使用纯opencv部署ppocrv5v6的onnx模型
开发语言·c++·opencv
玖玥拾2 小时前
C/C++ 数据结构(六)链表迭代器与底层
c语言·数据结构·c++·链表·stl库
牛油果子哥q2 小时前
AVL平衡树与红黑树深度精讲对比,平衡因子、四大旋转原理、着色规则、平衡策略、性能差异与面试手撕全解
数据结构·c++·面试