从零构建C# OOP 知识体系

文章目录

面向对象的三大特性
封装:隐藏内部实现,稳定外部接口
继承:子类继承父类成员,实现代码复用
多态:不同子类对同一个消息做出不同的反应

OOP

  • 封装:把数据和逻辑包起来,保证安全。
  • 继承:复用代码,建立父子关系。
  • 多态:在父子关系的基础上,让代码更灵活。
  • 抽象:定义高层标准,让系统易于扩展。

封装

封装(Encapsulation)是面向对象编程的第一大支柱。它的核心思想是**"隐藏细节,暴露接口"**。
你可以把封装想象成一个自动售货机:你不需要知道机器内部的电路、电机和出货逻辑,你只需要通过屏幕(接口)选择商品并支付,机器就会把东西给你。机器内部的零件是受保护的,你无法直接触摸。

1.为什么要封装

防止对实现细节的访问

可以保护或防止代码(数据)被我们无意中破坏

我们在使用的时候只需要了解如何通过类的接口使用类,而不用关心类的内部数据结构和数据组织方法,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别。面向对象程序设计一直追求高内聚低耦合 ,用封装 恰恰可以减少耦合

  • 数据安全:防止外部代码随意篡改对象内部的关键数据。
  • 降低耦合:内部逻辑的改变不会影响到调用它的外部代码。
  • 隐藏复杂度:使用者只需关心"怎么用",不需要关心"怎么实现的"。

2.封装的实现

主要通过**访问修饰符(Access Modifiers)属性(Properties)**来实现封装。

糟糕的设计(未封装)
csharp 复制代码
public class BankAccount
{
    public decimal Balance; // 外部可以直接改成 -1000000,不安全
}
正确的设计(封装)
csharp 复制代码
public class BankAccount
{
    // 私有字段:隐藏数据
    private decimal _balance;

    // 公有属性:控制访问逻辑
    public decimal Balance
    {
        get { return _balance; }
        private set
        {
            if (value >= 0)
                _balance = value;
            else
                Console.WriteLine("余额不能为负数!");
        }
    }

    public void Deposit(decimal amount)
    {
        if (amount > 0) Balance += amount;
    }
}

3.封装的层级(访问修饰符)

封装通过不同的"锁"来控制代码的可见性:

修饰符 范围说明
private 私有
public 公有
protected 受保护
internal 内部

C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。

一个 访问修饰符 定义了一个类成员的范围和可见性。C# 支持的访问修饰符如下所示:

  • public:所有对象都可以访问;public 访问修饰符允许一个类将其成员变量和成员函数暴露给其他的函数和对象。任何公有成员可以被外部的类访问。
  • private:对象本身在对象内部可以访问;Private 访问修饰符允许一个类将其成员变量和成员函数对其他的函数和对象进行隐藏。只有同一个类中的函数可以访问它的私有成员。即使是类的实例也不能访问它的私有成员。
  • protected:只有该类对象及其子类对象可以访问
  • internal:同一个程序集的对象可以访问;
  • protected internal:同项目&任意子类,访问限于当前程序集或派生自包含类的类型。

类成员 包括变量方法 。如果希望其他类 能够访问成员变量的值,就必须定义成公有的,而将变量设为公有public,那这个成员变量的就可以被任意访问(包括修改,读取),这样不利于数据安全。那怎么办呢?

C#通过属性 特性读取写入字段 (成员变量),而不直接直接读取和写入,以此来提供对类中字段的保护。属性可用于类内部封装字段。属性是C#面向对象技术中封装性的体现

注意:字段就是类的成员变量,为配合属性而这样称呼的。

属性和字段的区别

属性是逻辑字段;属性是字段的扩展,源于字段 ;属性并不占用实际的内存,字段占内存位置及空间。属性可以被其它类访问,而大部分字段不能直接访问。属性可以对接收的数据范围作限定,而字段不能。(也就是增加了数据的安全性)最直接的说:属性是被"外部使用",字段是被"内部使用"

4.示例

一般封装和属性可以放到一块去理解

csharp 复制代码
private int age; // private: 私有的,仅供内部进行访问
public int Age // public: 公有的,任何地方都可以访问
{
    // 获取或读取字段值
    get { return age; } // 属性的读取
    set { age = value; } // 属性赋值 (value 为关键字)
}

first.Age = 21;
Console.WriteLine("年龄为:{0}", first.Age);
// 年龄为:21
无返回值
csharp 复制代码
public void List(double num1, double num2, string fuhao)
{
    double count;
    switch (fuhao)
    {
        case "+":
            count = num1 + num2;
            Console.WriteLine("您要计算的值为:{0}", count);
            break;
        case "-":
            count = num1 - num2;
            Console.WriteLine("您要计算的值为:{0}", count);
            break;
        case "x":
            count = num1 * num2;
            Console.WriteLine("您要计算的值为:{0}", count);
            break;
        case "÷":
            count = num1 / num2;
            Console.WriteLine("您要计算的值为:{0}", count);
            break;
    }
}

// 分别实现四个功能
first.List(20, 10, "x");
first.List(20, 10, "+");
first.List(20, 10, "-");
first.List(20, 10, "÷");
有返回值
csharp 复制代码
public int Then(int[] list)
{
    int k = 0;
    for (int i = 0; i < list.Length; i++)
    {
        k += list[i];
    }
    return k; // 将计算结果返回给调用者
}

int[] arry = { 1, 2, 3, 5, 4, 6, 9, 8, 7 };
int arry1 = first.Then(arry); // 接收返回值并存储在 arry1 中
Console.WriteLine(arry1);

继承

本质是代码复用和建立类之间的层级关系

1. 继承的核心本质

继承是 "is-a"(是一个) 的关系。它的核心意义在于:

  • 代码复用:派生类自动获得基类的非私有成员。
  • 特化与泛型 :基类是泛化 (通用的概念,如 Shape),派生类是特化(具体的实现,如 Rectangle)。

在 C# 中,我们使用 : 符号来实现继承。

  • 基类 (Base Class / Parent Class):被继承的类,提供通用的特征。
  • 派生类 (Derived Class / Child Class):继承基类的类,可以拥有自己的新特征。

继承允许我们根据一个类来定义另一个类,可以节省创建和维护程序的时间。

创建一个类的时候,不需要完全编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有类的成员即可。

一个类可以派生自多个类或接口,可以从多个基类或接口下面继承数据或者函数。

csharp 复制代码
<访问修饰符符> class <基类>
{
 ...
}
class <派生类> : <基类>
{
 ...
}

假如一个基类是Shape,一个派生类是Rectangle。

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace week2._1
{
    // 基类
    class Shape
    {
        public void setLength(int len)//方法1
        {
            length = len;
        }
        public void setWidth(int wid)//方法2
        {
            width = wid;
        }
        protected int length;
        protected int width;
    }
    //派生类
    class Rectangle : Shape
    {
        private int height;
        public int getArea()
        {
            return length * width;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            //创建对象
            Rectangle rect = new Rectangle();
            rect.setLength(5);//可以调用基类的方法
            rect.setWidth(6);
            Console.WriteLine(rect.getArea());
            Console.ReadLine();
        }
    }
}//Console 30

2.基类的初始化

派生类继承了基类的成员变量和成员方法。因此父类对象应在子类对象创建之前被创建。您可以在成员初始化列表中进行父类的初始化。

csharp 复制代码
父类 person
{
    walking();
    showMessage();
}

子类Jason                        子类Mary
{                               {
    walking();                      showMessage();
}                               }

父类也叫基类,子类叫派生类。基类是派生类的泛化,子类是父类的特化

3. 类成员的可见性边界

在继承中,访问修饰符决定了"谁能拿到家产":

访问修饰符protected,基类使用protected访问权限修饰词,只有派生类可以存取

除了使用protected访问权限修饰符来继承之外,子类使用this关键字来获取父类成员

修饰符 当前类 派生类 外部类 专家说明
public 公开接口,完全开放。
protected 继承专用,专门给后代使用的"家传宝"。
private 绝对私有,后代也无权干涉。

尽量少用 private 字段,如果希望子类能修改,请改用 protected 属性。

4. 基类与派生类的初始化序列

这是开发中最容易出错的地方。请牢记:先有父,后有子

  • 隐式调用:如果基类有无参构造函数,子类实例化时会自动调用。
  • 显式调用 ( base) :如果基类定义了带参数的构造函数,子类必须 显式调用 base(...)

5.继承的3(4)个关键字

抽象(abstract)

抽象方法,抽象类,抽象属性

  • 动作 :基类只给签名,不给肉体。用来限定类时,类中的方法不能有方法实体;
  • 用来限定方法时,同样也不能有方法实体,
  • 要求:派生类必须完成实现,除非派生类也是抽象类。
虚拟(virtual)

虚方法,虚拟类,虚拟属性

动作:基类虚设,子类更替。

效果:运行时多态。无论拿的是父类还是子类的引用,执行的都是子类重写后的逻辑。

用来指示类中的方法可以被子类的同名方法覆盖或者共存,覆盖时,子类中使用override关键字;共存时,使用new关键字。

密封(sealed)

动作:锁死继承链。

意义:安全与性能优化,防止核心逻辑被意外篡改。

关键字 角色 含义 子类的自由度
virtual 虚成员 提供默认逻辑,允许子类改进。 (可改可不改)
abstract 抽象成员 只有定义没有逻辑,强制子类实现。 (必须实现)
sealed 密封成员 锁死逻辑,禁止子类再次重写或继承。 (不准再动)
隐藏(new)

动作:子类另起炉灶。

效果:切断联系。拿父类引用就执行父类的,拿子类引用就执行子类的。

应用场景:当你不想修改基类逻辑,但又必须定义一个同名方法时使用。

子类中用于屏蔽 父类对子类中同样签名子类中的方法和父类中的方法名字和参数都一模一样)方法的影响。示例如下,C#中基类(父类)中的某方法若想在派生类(子类)中被重写(override),必须将基类中的方法定义为virtual,即虚函数。

若派生类将方法修饰为new,即有意隐藏基类中的方法。

在用作声明修饰符时,new 关键字可以显式隐藏从基类继承的成员。 隐藏继承的成员时,该成员的派生版本将替换基类版本。 虽然可以不使用 new 修饰符来隐藏成员,但将收到编译器警告。 如果使用 new 来显式隐藏成员,将禁止此警告。基类中的成员不管是否有virtual声明,子类都可以用new替换。

在子类方法中的用法是指子类中与父类存在的同名方法在方法列表敏感词存 ,并不覆盖父类的方法 ,这就达到了多态机制下并非一味覆盖的效果

派生类方法前端加上new修饰词,表示他是派生类所定义的,与基类方法无关

override (重写):是"狸猫换太子"。无论你用父类变量还是子类变量去调用,执行的都是子类的方法。

new (隐藏):是"各过各的"。你用父类变量调用的就是父类的,用子类变量调用的就是子类的。

csharp 复制代码
public class BaseClass
{
    public void ShowTime() => Console.WriteLine("这是基类时间");
}

public class DerivedClass : BaseClass
{
    // 使用 new 隐藏基类方法
    new public void ShowTime() => Console.WriteLine("这是派生类的新时间");
}

// 以下是一种写法,效果一样
// public new void run() (最常见的写法,符合"权限 + 行为"的逻辑)
// new public void run() (完全合法,但相对少见)

// 调用实验
BaseClass obj = new DerivedClass();
obj.ShowTime(); // 输出:这是基类时间 (多态失效了!)

DerivedClass realObj = new DerivedClass();
realObj.ShowTime(); // 输出:这是派生类的新时间

属性new的场景

csharp 复制代码
public class User
{
    public object Info { get; set; }
}

public class SuperUser : User
{
    // 隐藏父类的 object 类型,改为更具体的自定义类型
    new public DetailedInfo Info { get; set; }
}
new & override
  1. 不管是重写还是覆盖都不会影响父类自身的功能。
  2. 当用子类创建父类的时候,如 A c = new B(),重写会改变父类的功能,即调用子类的功能;而覆盖不会,仍然调用父类功能。
  3. 虚方法、实方法都可以被覆盖(new),抽象方法(还没实现,覆盖个寂寞)、接口不可以。
  4. 抽象方法(用重写来实现)、接口、虚方法可以被重写(override),实方法不可以。
  5. 重写使用的频率比较高,实现多态;覆盖用的频率比较低,用于对以前无法修改的类进行继承的时候。

6.base 与 this 的角色分工

这两个关键字是你在类内部导航的"指南针":

  • this:指向当前实例。用于访问本类定义的成员,或者从父类继承来的成员。
  • base:指向直接基类 。用于:
    • 在构造函数中调用父类构造函数。
    • 在重写方法中调用父类的原始实现(如:base.Hello())。
this

来获取父类原有属性值

获取父类属性 this[类型 父类属性名称],以中括号围住父类类型和属性名称

base

如果想使用基类构造函数,必须使用base关键字 从派生类以base 调用基类构造函数

csharp 复制代码
class 派生类:基类
{
    public 构造函数():base()
    //构造函数程序段;
}

base关键字必须使用于派生类,才能引用到父类的成员

基类定义了构造函数,派生类也必须定义构造函数

base 关键字用于从派生类中访问基类的成员:

调用基类上已被其他方法重写的方法。

指定创建派生类实例时应调用的基类构造函数。

基类访问只能在构造函数、实例方法或实例属性访问器中进行。

csharp 复制代码
class Father
{
    public Father (string fatherName)
    {
        //父类构造函数程序段;
    }
}
class Son:Father
{
    public Son(string sonName) :base(sonName)
    {
        //子类构造函数程序段
    }
}

csharp 复制代码
public class A
{
    public virtual void Hello()
    {
        Console.WiriteLine("Hello");
    }
}
public class B : A
{
    public override void Hello()
    {
        base.Hello();//调用基类的方法,显示Hello
        Console.WiriteLine("World");
    }
}
// Hello World

csharp 复制代码
// 属性也可以被new
public class BaseClass
{
    public string Name { get; set; } = "基类名称";
}

public class DerivedClass : BaseClass
{
    // 使用 new 隐藏基类的属性
    new public string Name { get; set; } = "派生类名称";

    public void Show()
    {
        // 在子类内部访问
        Console.WriteLine(this.Name); // 派生类名称
        Console.WriteLine(base.Name); // 基类名称
    }
}

7.C# 继承的规则约束

  1. 单继承限制 :一个派生类只能有一个直接基类(单一继承机制)。单继承 :C# 不支持多重继承(一个子类不能同时继承多个父类),这是为了避免"钻石问题"(逻辑冲突)。但可以通过接口(Interface)实现多重行为。派生类只有一个基类,而基类会有多个派生类
  2. 多重接口:虽然只能有一个"爸爸",但可以有无限多个"老师"(一个类可以实现多个接口)。
  3. 避免深层继承:继承层级建议控制在 3-4 层以内。层级过深会导致系统极其难以维护。

8.为什么要有继承机制

  • 减少冗余:公共逻辑写一次,到处使用。扩充系统更为简单
  • 易于维护:修改父类的逻辑,所有子类自动同步。减少系统开发时间
  • 逻辑清晰 :反映了现实世界中 "Is-A" 的关系(例如:汽车 是一种 交通工具)。

多态(Polymorphism)

它允许你使用一个基类引用来操作不同派生类的对象,从而实现灵活和可扩展的代码
多态是同一个行为具有多个不同表现形式或形态的能力。
重载是编译时多态,重写是运行时多态

多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为**"一个接口,多个功能"**。

有多态之前必须要有继承,只有多个类同时继承了同一个类,才有多态这样的说法。

依赖于类之间的继承关系

多态性可以是静态的或动态的。在静态多态性 中,函数的响应是在编译时发生的。在动态多态性中,函数的响应是在运行时发生的。

在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object

多态是指同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果

多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:

1.几种实现多态的方式

基于继承的多态:

  • 这是最经典的多态形式,如你所说,它依赖于类之间的继承关系
  • 基类定义虚方法或抽象方法,派生类重写这些方法,从而实现不同的行为
  • 通过基类引用,可以调用不同派生类对象的重写方法,实现运行时多态
  • 只有多个类同时继承了同一个类,才有多态这样的说法"的体现。

基于接口的多态:

  • 接口定义一组必须实现的成员,实现接口的类提供具体的实现。
  • 通过接口引用,可以调用不同类实现的接口方法,实现多态
  • 这种方式不依赖于类之间的继承关系,只要类实现了相同的接口,就可以实现多态。
  • 这种方式也体现了多态。

隐式多态(运算符重载):

  • C# 允许对运算符进行重载,使得同一个运算符在不同类型的对象上可以有不同的行为。
  • 例如,+ 运算符可以用于整数相加,也可以用于字符串连接。
  • 运算符重载也是一种多态形式,它在编译时确定调用哪个运算符重载。
csharp 复制代码
class Vector
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector(double x, double y)
    {
        X = x;
        Y = y;
    }

    // 重载 + 运算符
    // public static 返回类型 operator +(参数列表)。
    public static Vector operator +(Vector v1, Vector v2)
    {
        return new Vector(v1.X + v2.X, v1.Y + v2.Y);
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main(string[] args)
    {
        Vector v1 = new Vector(1, 2);
        Vector v2 = new Vector(3, 4);

        Vector v3 = v1 + v2; // 使用重载的 + 运算符

        Console.WriteLine($"v1: {v1}"); // 输出:v1: (1, 2)
        Console.WriteLine($"v2: {v2}"); // 输出:v2: (3, 4)
        Console.WriteLine($"v3: {v3}"); // 输出:v3: (4, 6)
    }
}
operator + 方法:使用 public static 关键字声明,表示这是一个静态方法,并且可以被外部访问。
使用 operator + 语法来重载 + 运算符。
接受两个 Vector 类型的参数 v1 和 v2,表示要相加的两个向量。
返回一个新的 Vector 对象,其 X 和 Y 属性分别为 v1 和 v2 的 X 和 Y 属性之和。

operator + 是 C# 中用于重载 + 运算符的特殊语法
运算符重载允许你为自定义类型(类或结构体)定义运算符(如 +、-、*、/ 等)的行为。

泛型(Generics):

  • 泛型允许你编写可以处理不同类型数据的代码,而无需为每种类型编写单独的代码。
  • 通过泛型,你可以编写通用的算法和数据结构,它们可以适用于多种类型。
  • 虽然泛型本身不是多态,但它可以与接口和继承结合使用,实现更灵活的多态。

动态类型(Dynamic Type):

  • dynamic 关键字允许你在运行时确定变量的类型。
  • 通过动态类型,你可以调用对象的方法和属性,而无需在编译时知道对象的具体类型。
  • 动态类型可以实现运行时多态,但也可能导致运行时错误。

2.多态的两个核心前提

在 C# 中要实现多态,必须满足两个条件:

  1. 继承:子类必须继承自同一个父类(或接口)。
  2. 重写 (Override) :子类必须重新定义父类的虚方法(virtual)或抽象方法(abstract)。

3.不用多态 vs 使用多态

不用多态(代码臃肿): 如果你要给一堆动物喂食,你得不停地判断:

csharp 复制代码
if (animal is Dog)
{
    ((Dog)animal).Eat();
}
else if (animal is Cat)
{
    ((Cat)animal).Eat();
}
// 每增加一种动物,你都要改这段逻辑,非常痛苦

使用多态(代码优雅):

csharp 复制代码
public class Animal
{
    public virtual void Eat() => Console.WriteLine("动物吃东西");
}

public class Dog : Animal
{
    public override void Eat() => Console.WriteLine("狗啃骨头");
}

public class Cat : Animal
{
    public override void Eat() => Console.WriteLine("猫吃鱼");
}

List<Animal> animals = [new Dog(), new Cat()];
foreach (var animal in animals)
{
    animal.Eat(); // 自动识别:第一遍输出狗啃骨头,第二遍输出猫吃鱼
}

4.多态带来的好处

  • 可扩展性(开闭原则) :如果你明天想增加一个"猪",你只需要新建一个 Pig 类继承 Animal 即可。你原有的喂食逻辑代码(foreach 那段)一行都不用改
  • 接口统一:复杂的系统被简化成一套通用的指令。
特性 重写 (Override) 重载 (Overload)
多态类型 运行时多态 (Dynamic Polymorphism) 编译时多态 (Static Polymorphism)
决定权 运行时根据对象是谁决定 编译时根据参数是谁决定
关联性 必须有继承关系 发生在同一个类
口诀 同样的动作,不同的对象 同样的动作,不同的参数

5.覆盖/重写(Override)

派生类定义方法名可以与基类方法相同,基类派生类方法重载
覆盖基类,将基类原有方法扩充通过派生类进一步修改继承的方法,加上overload关键字 来声明覆盖方法 ,同样基类必须加上virtual关键字
overload ==> virtual

  • 派生类方法前面加上overload关键字,表示调用自己的方法,而非基类的方法
  • 使用相同权限的访问权限修饰词
  • 修饰词virtual overload放在访问权限修饰词后面,返回数据类型前面
  • 父类使用public 子类必须也使用public
  • 静态方法不能覆盖,被覆盖的基类方法必须冠上virtual,abstract或override关键字

定义

  • 覆盖是指在派生类中重新定义基类中的虚方法(virtual)或抽象方法(abstract)。
  • 派生类使用 override 关键字来表示对基类方法的覆盖。

特点

  • 覆盖要求派生类的方法签名 (方法名、参数列表和返回类型)必须与基类中的虚方法或抽象方法完全相同。
  • 覆盖用于实现运行时多态性,即在运行时根据对象的实际类型来调用相应的方法
  • 覆盖发生在继承关系中,即派生类和基类之间。

用途

  • 允许派生类提供基类方法的特定实现。
  • 实现多态性,使得可以使用基类引用来操作派生类对象。
  • 在继承关系中,用于子类需要改变父类方法的行为时。
csharp 复制代码
// 基类
class Animal
{
    // 虚方法
    public virtual void MakeSound()
    {
        Console.WriteLine("动物发出叫声");
    }
}

// 派生类
class Dog : Animal
{
    // 覆盖基类方法
    public override void MakeSound()
    {
        Console.WriteLine("狗发出汪汪声");
    }
}

6.重载(Overload)

定义

  • 重载是指在同一个类中定义多个同名方法 ,但这些方法的参数列表不同(参数类型、参数个数或参数顺序)。
  • 重载不需要使用任何特殊的关键字。

特点

  • 重载要求方法的参数列表必须不同 ,但方法的返回类型可以相同也可以不同
  • 重载用于实现编译时多态性,即在编译时根据方法调用时提供的参数来确定调用哪个方法。
  • 重载发生在同一个类中,与继承无关。

用途

  • 提供具有不同参数的同名方法,方便调用者根据需要选择合适的方法。
  • 提高代码的灵活性和可读性。
  • 在同一个类中,用于实现相似功能 ,但是参数不同的方法。

为什么重载也叫多态?

很多人认为只有重写才是多态,这是一种误解。 多态(Polymorphism) 的字面意思是"多种形态"。

  • 重载:让一个方法名拥有了"处理不同数据"的多种形态。

  • 重写:让一个方法名拥有了"展现不同人格"的多种形态。

    class Calculator
    {
    // 重载方法
    public int Add(int a, int b)
    {
    return a + b;
    }

    复制代码
      // 重载方法
      public double Add(double a, double b)
      {
          return a + b;
      }

    }

7.覆盖和重载的主要区别

发生范围

  • 覆盖发生在继承关系中(派生类和基类之间)。
  • 重载发生在同一个类中。

方法签名

  • 覆盖要求方法签名完全相同。
  • 重载要求参数列表不同。

多态性

  • 覆盖实现运行时多态性。
  • 重载实现编译时多态性。

关键字

  • 覆盖需要使用override关键字。
  • 重载不需要。
特性 虚方法 (Virtual) 抽象类 (Abstract) 接口 (Interface)
定义关键字 virtual abstract class / abstract interface
是否有方法体 (默认实现) (仅限抽象方法) (传统语法下)
子类是否必须重写 否(可选)
重写关键字 override override 无(直接实现)
多继承 否(单继承) 否(单继承) (可实现多个接口)
实例化 可以实例化父类 禁止实例化 禁止实例化

抽象(Abstract)

本质: 只关注"做什么",不关注"怎么做"。 抽象是封装的进阶版。它通过**抽象类(Abstract Class)或接口(Interface)**定义规范,强制要求子类必须实现某些功能。

1.抽象类

抽象化 是为了让描述的对象具体化,简单化,编写oop程序时将细节隐藏,保留使用的接口,使用abstract关键字。

定义抽象类是基类为通用定义提供给多个派生类(子类)共享 ,也就是声明为抽象类的基类无法实例化的对象,必须由继承的派生类实现

复制代码
abstract class 类名称
{
  //定义抽象成员
  public abstract 数据类型 属性名称 {get,set};
  public abstract 返回值类型 方法名称1 (参数列表)
}
继承抽象类的子类必须搭配override关键字来实现抽象方法,抽象类的实现方法可以被覆盖
eg:以override修饰词覆盖父类所定义的抽象的属性...

2.密封类

Sealed Class 意义就是不能被继承,简单说就是无法产生派生类,不能被当做基类使用,也不能把它声明为抽象类。

csharp 复制代码
sealed class Person
{
    public void Run()
    {
        Console.WriteLine("我会跑哦~");
    }
}

// 报错:Cannot inherit from sealed class 'Person'
class Student : Person
{
}
// 密封方法
class Father
{
    public virtual void Work() { }
}

class Son : Father
{
    // 密封重写后的方法,孙子类就不能再 override 它了
    public sealed override void Work()
    {
        Console.WriteLine("这是我的最终实现");
    }
}

声明密封类必须要放在class前或访问修饰符后

关键字类型 作用对象 效果
sealed class 整个类不能被继承
sealed override 方法 特定方法不能被孙子类再次重写
abstract class 必须被继承才能使用(与 sealed 正好相反)

3.接口

接口用于描述一组类的公共方法/公共属性

接口中的方法没有具体实现,也就是没有方法体,必须由继承者去实现而且必须全部实现。

接口中的方法不需要修饰符,默认就是公有的(Default / Public)

接口可以包含方法、属性、索引器、事件。不能包含任何其他的成员,例如:常量、字段、域、构造函数、析构函数、静态成员

抽象类和接口的区别

抽象类是关于"它是谁",接口是关于"它能做什么"。

特性 抽象类 (Abstract Class) 接口 (Interface)
本质 对象的"底座",表示 Is-A(是一个)关系。 行为的"契约",表示 Can-Do(能做)关系。
继承限制 一个类只能继承 一个 抽象类(单继承)。 一个类可以实现 多个 接口(多接口)。
成员实现 可以包含有实现的代码(字段、方法体)。 传统上不包含实现(C# 8.0 后支持默认实现)。
构造函数 可以有构造函数。 不可以有构造函数。
字段/变量 可以定义私有或公有字段。 不可以定义字段(只能定义属性、方法、事件)。
示例
抽象类:定义"它是谁"
csharp 复制代码
public abstract class Vehicle
{
    public string EngineNumber { get; set; } // 抽象类可以存数据

    public void Start() // 抽象类可以写具体逻辑
    {
        Console.WriteLine("引擎点火...");
    }

    public abstract void Move(); // 强制子类必须实现的个性化逻辑
}
接口:定义"它能做什么"
csharp 复制代码
public interface IAutopilot
{
    void SelfDrive(); // 只定义标准,不关心怎么开
}

public interface IFlyable
{
    void TakeOff();
}
最终整合
csharp 复制代码
// 汽车是一个交通工具,且具备自动驾驶功能
public class Tesla : Vehicle, IAutopilot
{
    public override void Move() => Console.WriteLine("四轮驱动行驶");

    public void SelfDrive() => Console.WriteLine("特斯拉视觉算法自动驾驶中");
}

// 飞机也是交通工具,且具备自动驾驶和飞行功能
public class Boeing747 : Vehicle, IAutopilot, IFlyable
{
    public override void Move() => Console.WriteLine("滑行");

    public void SelfDrive() => Console.WriteLine("航线自动巡航");

    public void TakeOff() => Console.WriteLine("起飞");
}

易混淆

继承与接口

在软件开发中,继承 (Inheritance) 解决的是"它是什么"的问题,而 接口 (Interface) 解决的是"它能做什么"的问题。

1. 核心差异:本质属性 vs 行为契约

你可以把继承 看作"血缘关系",把接口看作"职业技能"。

特性 继承 (Inheritance) 接口 (Interface)
逻辑关系 Is-A (是一个):猫是一个动物 Can-Do (能做):猫能抓老鼠
代码复用 子类直接获得父类的代码实现 只定义规范,不强制提供实现(C# 8.0+ 支持默认实现但不推荐作为核心逻辑)
数量限制 C# 中类只能单继承(一个爹) 一个类可以实现多个接口(多项技能)
耦合度 强耦合(父类改动,子类全受影响) 低耦合(只要符合规范,怎么实现随便你)

2. 为什么要写接口?

如果不写接口,你的程序就是一坨硬编码。接口存在的意义在于解耦 (Decoupling)

  • 制定标准 :作为架构师,我先定义好 IMessageSender (消息发送器) 的接口。不管你后面是用邮件、短信还是微信发,只要你实现了这个接口,我的主逻辑就能直接调用。
  • 依赖倒置 (Dependency Inversion):高层逻辑不应该依赖底层细节。通过接口,高层只管调用方法,不用关心具体实现类的内部构造。
  • 单元测试 (Unit Testing):在测试时,我们可以用一个"假的"接口实现(Mock 对象)替换掉真实的数据库或网络请求,从而实现独立测试。

3. 什么情况下设计成接口?

当你发现多个互不相关的物体,拥有共同的行为时,就该上接口了。

  • 跨种族的共同行为
    • "麻雀"和"飞机"都能飞。它们没有共同的祖先,但你可以定义 IFlyable 接口。
  • 多态性的需求
    • 你需要处理一组对象,只关心它们能否被"持久化",而不关心它们具体是什么。定义 ISaveable
  • 插件化/扩展性设计
    • 系统需要支持第三方插件。你提供一个接口规范,别人按规范写好 DLL 丢进来就能运行。

4. 逻辑架构图

抽象与接口

很多开发者在这里会产生混淆,因为抽象类 (Abstract Class) 同时也具备了继承和接口的一部分特性。

从底层本质上看,抽象类是关于**"身份"的半成品,而接口是关于"能力"**的契约。

1. 核心区别对比

特性 抽象类 (Abstract Class) 接口 (Interface)
本质意义 强调 Is-A(是一个),是对同类事物的共有特征提取 强调 Can-Do(能做),是对行为的统一建模
多继承 单继承。一个类只能有一个亲爹(父类) 多实现。一个类可以有多个证件(接口)
字段与成员 可以有成员变量(字段)、构造函数、属性 不能有字段。只能定义方法签名、属性、事件
实现细节 可以包含已实现的方法(逻辑复用) 主要是方法定义(虽然 C# 8.0 引入默认实现,但应慎用)
访问修饰符 成员可以有 protected、internal 等 默认全是 public(毕竟是给外面的契约)

2. 为什么要有这两种东西?

抽象类是为了"代码复用"和"统一管理"

  • 你有一群"鸟"。所有鸟都要呼吸、都有羽毛、都要产卵。你不需要在"麻雀"、"老鹰"、"企鹅"里各写一遍呼吸的代码。你在抽象类 Bird 里写好 Breathe() 的逻辑,大家直接继承拿走。

接口是为了"灵活契约"和"跨界协作"

  • 你突然发现"飞机"也能飞,"麻雀"也能飞。你不能让飞机继承"鸟",这逻辑就乱了。于是你发明了 IFlyable。飞机和麻雀各自实现这个接口。对于塔台(调用者)来说,它只管对方是不是 IFlyable,而不管对方是生物还是机器。

3. 如何选择?

如果你在犹豫该写成哪种,可以跑一下这个逻辑:

  1. 你是想给一系列相关的子类提供公共的代码逻辑吗?抽象类
  2. 你是想定义一个通用的行为规范,且这个规范可能被完全不相关的类使用吗?接口
  3. 你需要强制单继承约束吗?抽象类
  4. 你需要一个类具备多种身份/能力吗?接口

4. 逻辑架构图

相关推荐
唐青枫2 小时前
C#.NET ConcurrentStack<T> 深入解析:无锁栈原理、LIFO 语义与使用边界
c#·.net
aini_lovee5 小时前
C# 实现邮件发送源码(支持附件)
开发语言·javascript·c#
七夜zippoe7 小时前
OpenClaw 接入 Discord:从零开始
大数据·人工智能·microsoft·discord·openclaw
WarrenMondeville9 小时前
1.Unity面向对象-单一职责原则
unity·设计模式·c#
LSL666_10 小时前
MybatisPlus条件构造器(下)
java·开发语言·windows·mybatisplus
寒风暖哥10 小时前
Oracle视图查询返回空数据集的分析
oracle·c#
万兴丶11 小时前
Unity用C#完成抖音小游戏接入引力引擎(Gravity Engine)完整指南,一篇文章讲清楚!
unity·c#·游戏引擎·抖音
PascalMing12 小时前
openEuler 25.09 安装 .NET 10(二进制 tar.gz 包)教程
.net·openeuler
手揽回忆怎么睡12 小时前
win11显卡NVIDIA GeForce GTX 1660的ComfyUI_windows_portable_nvidia_cu126的节点包安装不上
windows