目 录CONTENT

文章目录

CSharp(二十七)密封类(Sealed)完全指南

CSharp(二十七)密封类(Sealed)完全指南

1. 先看一个故事:为什么需要 sealed

假设你是一家银行的程序员,你写了一个计算利息的核心类:

// 你写的 "官方版本"
public class InterestCalculator
{
    public virtual decimal Calculate(decimal principal, double rate)
    {
        // 银行规定的标准算法
        return principal * (decimal)(1 + rate);
    }
}

结果,隔壁组的同事"继承"了你的类,把计算方法"改"了:

// 别人悄悄继承后改掉了...
public class HackedCalculator : InterestCalculator
{
    public override decimal Calculate(decimal principal, double rate)
    {
        // 多加了一个零!把钱算多了!
        return principal * (decimal)(1 + rate) * 10;
    }
}

更糟糕的是,由于多态的存在,即使别人拿 InterestCalculator 类型的变量来引用 HackedCalculator 对象,调用的仍然是那个被改坏的版本!这就埋下了安全隐患。

sealed 就是来解决这个问题的——加上 sealed,谁也继承不了,谁也改不了。

public sealed class InterestCalculator  // ← 加一个 sealed
{
    public decimal Calculate(decimal principal, double rate)
    {
        return principal * (decimal)(1 + rate);
    }
}

// public class HackedCalculator : InterestCalculator { }
// ↑ 编译错误!sealed 类不能被继承

2. sealed 是什么?一句话理解

sealed = 封印术。给类贴上一个"禁止继承"的封印,给方法贴上一个"禁止再重写"的封印。

用在哪儿 效果
这个类不能再被任何类继承
方法 这个方法不能被子类再重写(必须配合 override
属性 这个属性不能被子类再重写(必须配合 override
索引器 这个索引器不能被子类再重写(必须配合 override

大白话版本

  • sealed 类 = "绝后类"(不会有子类)
  • sealed 方法 = "最终版本"(到此为止,不许再改了)

类比理解

概念 现实类比
sealed 像一款绝版跑车,设计师说了:"这是最终版,不允许你拿它做改装"
sealed 方法 像一道家传秘方,祖父传给父亲,父亲改良后封印:"到我这就是最终配方了,孙子不能再改"
sealedvirtual 方法 开源食谱,每一代都可以改,谁也拦不住

3. 密封类(sealed class)—— 禁止被继承

3.1 基本语法

// 在 class 前面加 sealed 关键字即可
public sealed class FinalCalculator
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
}

3.2 尝试继承会怎样?

// ❌ 编译错误:CS0509 "MyCalc": 无法从密封类型 "FinalCalculator" 派生
// public class MyCalc : FinalCalculator { }

编译器会直接报错,连编译都通不过——这就是编译时安全

3.3 密封类里能有什么?

密封类只是"不能当爸爸",但它自己可以是一个正常的类:

public sealed class SealedExample
{
    // ✅ 可以有字段
    private int data;

    // ✅ 可以有属性(包括自动属性)
    public string Name { get; set; }

    // ✅ 可以有构造函数
    public SealedExample(string name)
    {
        Name = name;
    }

    // ✅ 可以有自己的方法
    public void DoWork()
    {
        Console.WriteLine($"{Name} 正在工作...");
    }

    // ✅ 可以实现接口
    // (密封类可以实现接口——见下文)
}

3.4 密封类可以实现接口

public interface ILoggable
{
    void Log(string message);
}

// sealed 类照样可以实现接口
public sealed class FileLogger : ILoggable
{
    public void Log(string message)
    {
        // 写入文件...
        Console.WriteLine($"[文件日志] {message}");
    }
}

别搞混了sealed 阻止的是"被继承",不阻止"实现接口"。

3.5 密封类可以有自己的父类

密封类只限制"下面没儿子",不限制"上面有爸爸":

// 普通父类
public class BaseLogger
{
    public virtual void Log(string msg)
    {
        Console.WriteLine($"[LOG] {msg}");
    }
}

// 密封类可以继承别人
public sealed class DatabaseLogger : BaseLogger
{
    public sealed override void Log(string msg)
    {
        Console.WriteLine($"[数据库日志] {msg}");
        // 把数据写到数据库...
    }
}

// ❌ 但别人不能继承密封类
// public class CloudLogger : DatabaseLogger { }  // 编译错误!

3.6 图解:密封类的继承限制

         BaseLogger(普通类)
              ↑
              │ 继承
              │
       DatabaseLogger(sealed 类)
              ↑
              │  ❌ 不能再继承!
              │
         想继承它的类

4. 密封方法(sealed method)—— 阻止再重写

4.1 重要前提

sealed 方法必须配合 override 使用!

你不能在一个普通方法上单独加 sealed。因为 sealed 的意思是"到此为止,不能再重写",那前提是这个方法本身就是从父类重写来的。

正确链: virtual(父类) → override(子类) → sealed override(子类再重写后封住)
错误链: sealed 单独出现 → 编译错误!

4.2 基本示例

// ==================== 第一层:祖父类 ====================
public class Animal
{
    // virtual:允许后代重写
    public virtual void MakeSound()
    {
        Console.WriteLine("动物发出声音...");
    }

    public virtual void Eat()
    {
        Console.WriteLine("动物吃东西...");
    }
}

// ==================== 第二层:父类 ====================
public class Dog : Animal
{
    // 普通 override:孙子可以继续改
    public override void Eat()
    {
        Console.WriteLine("狗吃狗粮!");
    }

    // sealed override:封住!孙子不能再改
    public sealed override void MakeSound()
    {
        Console.WriteLine("汪汪汪!");
    }
}

// ==================== 第三层:孙子类 ====================
public class GoldenRetriever : Dog
{
    // ❌ 编译错误!MakeSound 被 sealed 封住了
    // public override void MakeSound() { Console.WriteLine("嘤嘤嘤"); }

    // ✅ Eat 没有被 sealed,可以继续重写
    public override void Eat()
    {
        Console.WriteLine("金毛优雅地吃狗粮~");
    }
}

4.3 图解调用链

调用引用的类型    →    实际对象的类型    →    调用哪个方法?

Animal a = new GoldenRetriever();
a.MakeSound();   // 输出 "汪汪汪!"(Dog 的 sealed 版本,金毛没法重写)
a.Eat();         // 输出 "金毛优雅地吃狗粮~"(层层 override 到最底)

4.4 完整的多层示例

using System;

public class Vehicle
{
    public virtual void Start()
    {
        Console.WriteLine("【Vehicle】启动引擎...");
    }

    public virtual void Honk()
    {
        Console.WriteLine("【Vehicle】滴滴!");
    }

    public virtual string GetFuelType()
    {
        return "未知燃料";
    }
}

public class Car : Vehicle
{
    // 普通重写——后代可以继续改
    public override string GetFuelType()
    {
        return "汽油";
    }

    // 密封重写——到此为止
    public sealed override void Start()
    {
        Console.WriteLine("【Car】一键启动!发动机轰鸣!");
    }

    // 没有重写 Honk,保留 Vehicle 原版
}

public class ElectricCar : Car
{
    // ✅ 可以重写 GetFuelType(没有被 sealed)
    public override string GetFuelType()
    {
        return "电力";
    }

    // ❌ 不能重写 Start(被 sealed 封住了)
    // public override void Start() { }

    // ✅ 可以重写 Honk(Vehicle 定义 virtual,中间没人封)
    public override void Honk()
    {
        Console.WriteLine("【ElectricCar】静音喇叭:滴滴滴(小声)");
    }
}

// ========== 测试 ==========
class Program
{
    static void Main()
    {
        Vehicle v = new ElectricCar();

        v.Start();          // 输出:【Car】一键启动!发动机轰鸣!
                            //      (ElectricCar 想重写但被封住了,用的是 Car 的版本)

        v.Honk();           // 输出:【ElectricCar】静音喇叭:滴滴滴(小声)
                            //      (成功重写,中间没人封住)

        Console.WriteLine(v.GetFuelType());  // 输出:电力
                                             //      (成功重写,没被封住)
    }
}

4.5 为什么要封住某个方法?

场景 说明
业务规则固定 比如"利息计算方式",上级确定了就不许再改
安全原因 比如"权限验证",封住以防绕过
数据结构一致 比如"哈希计算",改了就破坏唯一性
行为有副作用 比如"记录审计日志",封住以防被子类偷偷去掉

5. 密封属性和密封索引器

从 C# 开始,sealed 也可以用在属性和索引器的 override 上。

5.1 密封属性

public class Shape
{
    // 父类的虚属性
    public virtual string Description => "这是一个形状";
}

public class Circle : Shape
{
    private double radius;

    // 重写并密封:子类不能再改这个属性的行为
    public sealed override string Description
    {
        get
        {
            return $"这是一个圆形,半径:{radius}";
        }
    }

    // 普通属性,也可以被密封
    public virtual double Area => Math.PI * radius * radius;
}

public class FilledCircle : Circle
{
    // ❌ 编译错误!Description 被 sealed 封住了
    // public override string Description => "实心圆";

    // ✅ Area 没有被 sealed,可以重写
    public override double Area
    {
        get
        {
            // 填充风格,面积不变但是显示不同
            Console.Write("【填充圆面积】");
            return base.Area;
        }
    }
}

5.2 密封索引器

public class DataCollection
{
    public virtual int this[int index]
    {
        get { return index * 2; }
    }
}

public class FixedCollection : DataCollection
{
    // 密封索引器
    public sealed override int this[int index]
    {
        get { return index * 10; }
    }
}

public class ExtendedCollection : FixedCollection
{
    // ❌ 编译错误!索引器被 sealed 封住了
    // public override int this[int index] => index * 100;
}

6. sealed 与 abstract —— 水火不容

sealedabstract 是一对死对头,它们表达的意思完全相反:

关键字 含义 能否实例化 能否被继承
abstract 还没做完,必须靠子类来完成 ❌ 不能 ✅ 必须被继承
sealed 已经做完,禁止再改动 ✅ 可以 ❌ 不能被继承

它们的冲突

// ❌ 编译错误!
// abstract 说"必须被继承",sealed 说"不能被继承"
// 它们不能一起用在同一个类上
// public abstract sealed class Impossible { }

那什么时候该用哪个?

如果你设计一个基类,有东西还没确定 → abstract
如果你设计一个最终版本,不想让别人改 → sealed

7. sealed 与 override —— 最佳搭档

sealed 在方法上必须和 override 一起出现:

public class Parent
{
    public virtual void Method() { }
}

public class Child : Parent
{
    // ✅ 正确:sealed override 在一起
    public sealed override void Method() { }
}

// ❌ 错误:sealed 不能单独用
// public sealed void Method() { }    // 缺少 override

// ❌ 错误:sealed 不能和 new 一起用
// public sealed new void Method() { }

sealed override vs 普通 override 对比

public class Parent
{
    public virtual void Show() => Console.WriteLine("Parent");
}

public class ChildA : Parent
{
    // 普通 override:后代可以继续重写
    public override void Show() => Console.WriteLine("ChildA");
}

public class GrandChildA : ChildA
{
    // ✅ 可以继续重写
    public override void Show() => Console.WriteLine("GrandChildA");
}

// ================================

public class ChildB : Parent
{
    // sealed override:到此为止
    public sealed override void Show() => Console.WriteLine("ChildB");
}

public class GrandChildB : ChildB
{
    // ❌ 编译错误!Show 被封住了
    // public override void Show() => Console.WriteLine("GrandChildB");
}

8. static 类天然是 sealed

如果你定义了一个 static 静态类,它自动就是 sealed 的(虽然你没有写 sealed 关键字):

// 这两个写法实际效果几乎一样:
public static class Utility    // static 类 → 自动 sealed
{
    public static void DoWork() { }
}

public sealed class Utility2   // sealed 类 → 不能 new,但可以有实例成员
{
    // 但是 sealed 普通类可以有实例成员:
    public string Name { get; set; }  // ✅ 可以有

    // 而 static 类不能有实例成员:
    // public string Name { get; set; }  // ❌ 编译错误
}
特性 static class sealed class
不能被继承 ✅ 自动 ✅ 显式
不能实例化 ❌(可以 new)
可以有实例成员
可以有静态构造函数

简单记static 类 = sealed + 不能 new + 全是静态成员。


9. 什么时候该用 sealed?实战场景分析

场景 1:工具类 / 辅助类(最常用)

/// <summary>
/// 字符串处理工具 — 不需要被继承
/// </summary>
public sealed class StringHelper
{
    public static bool IsNullOrEmpty(string value)
    {
        return string.IsNullOrEmpty(value);
    }

    public static string Truncate(string value, int maxLength)
    {
        if (string.IsNullOrEmpty(value)) return value;
        return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
    }
}

场景 2:第三方库的核心类(防篡改)

/// <summary>
/// 加密工具 — 算法固定,不允许修改
/// </summary>
public sealed class Crypto
{
    public string Encrypt(string plainText)
    {
        // AES 加密逻辑...
        return Convert.ToBase64String(
            System.Text.Encoding.UTF8.GetBytes(plainText)
        );
    }

    public string Decrypt(string cipherText)
    {
        // AES 解密逻辑...
        return System.Text.Encoding.UTF8.GetString(
            Convert.FromBase64String(cipherText)
        );
    }
}

场景 3:值类型包装类(不可变性)

/// <summary>
/// 金额类 — 行为固定,防止被篡改
/// </summary>
public sealed class Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (other.Currency != Currency)
            throw new InvalidOperationException("币种不一致!");
        return new Money(Amount + other.Amount, Currency);
    }

    public override string ToString() => $"{Currency} {Amount:N2}";
}

场景 4:不期待被继承的小类

/// <summary>
/// 坐标点 — 简单数据类,继承没意义
/// </summary>
public sealed class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(Point other)
    {
        int dx = X - other.X;
        int dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

场景 5:框架中的 "最终方法"

/// <summary>
/// 用户认证基类,VerifyPassword 是最终实现,不允许子类修改
/// </summary>
public abstract class AuthBase
{
    // 子类必须实现这个
    public abstract string GetAuthToken();

    // 这个是最终实现,不许改
    public virtual bool VerifyPassword(string input, string hashed)
    {
        // 标准密码验证逻辑(不许改动!)
        return BCrypt.Net.BCrypt.Verify(input, hashed);
    }
}

public class ApiAuth : AuthBase
{
    public override string GetAuthToken()
    {
        return "api-token-xxx";
    }

    // 密封关键方法
    public sealed override bool VerifyPassword(string input, string hashed)
    {
        // 加上额外日志
        Console.WriteLine($"[审计] 密码验证时间:{DateTime.Now}");
        return base.VerifyPassword(input, hashed);
    }
}

// ApiAuth 的子类不能再改 VerifyPassword

场景 6:设计为组合优于继承

/// <summary>
/// 订单处理引擎 — 设计意图是组合使用,不是继承扩展
/// </summary>
public sealed class OrderProcessor
{
    private readonly IOrderValidator _validator;
    private readonly IPaymentGateway _payment;
    private readonly INotificationService _notification;

    // 通过构造函数注入依赖(组合),而不是让子类重写方法(继承)
    public OrderProcessor(
        IOrderValidator validator,
        IPaymentGateway payment,
        INotificationService notification)
    {
        _validator = validator;
        _payment = payment;
        _notification = notification;
    }

    public async Task ProcessOrder(Order order)
    {
        // 验证
        if (!_validator.Validate(order))
            throw new ValidationException("订单无效");

        // 支付
        var result = await _payment.Charge(order);
        if (!result.Success)
            throw new PaymentException("支付失败");

        // 通知
        await _notification.SendOrderConfirmation(order);

        // 状态更新
        order.Status = OrderStatus.Completed;
    }
}

10. sealed 对性能有什么影响?

简短回答:有影响,但在绝大多数情况下不需要关注。如果你在写一般的业务代码,先考虑"语义正确",再考虑性能。

10.1 编译器优化

当编译器看到 sealed 类时,可以做以下优化:

sealed class SealedDemo
{
    public virtual void Method() { }
}

class NonSealedDemo
{
    public virtual void Method() { }
}

// 场景对比:
SealedDemo s = new SealedDemo();
s.Method();
// 编译器知道:s 的类型 100% 是 SealedDemo(因为没人能继承它)
// 所以可以直接调用,不需要查虚方法表 → 更快

NonSealedDemo ns = new NonSealedDemo();
ns.Method();
// 编译器不能确定 ns 是不是某个子类的实例
// 所以必须通过虚方法表查找 → 略慢

10.2 运行时性能

情况 sealed 类 非 sealed 类
类型检查 is / as ✅ 更快(无需检查继承链) 需要遍历继承链
虚方法调用 ✅ 可去虚拟化(devirtualization) 必须查虚方法表
JIT 内联 ✅ 更激进 保守
类型转换 ✅ 更简单 检查层级

10.3 一个简单的基准测试

using System;
using System.Diagnostics;

public class BaseClass
{
    public virtual int Calculate(int x) => x + 1;
}

public sealed class SealedClass : BaseClass
{
    public override int Calculate(int x) => x * 2;
}

public class NonSealedClass : BaseClass
{
    public override int Calculate(int x) => x * 2;
}

class Program
{
    static void Main()
    {
        const int iterations = 10_000_000;

        var sealedObj = new SealedClass();
        var nonSealedObj = new NonSealedClass();

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
            sealedObj.Calculate(i);
        sw.Stop();
        Console.WriteLine($"sealed 类耗时:{sw.ElapsedMilliseconds}ms");

        sw.Restart();
        for (int i = 0; i < iterations; i++)
            nonSealedObj.Calculate(i);
        sw.Stop();
        Console.WriteLine($"非 sealed 类耗时:{sw.ElapsedMilliseconds}ms");

        // 典型结果:sealed 类比非 sealed 类快约 5%~15%
    }
}

结论:把类标记为 sealed 在语义正确的前提下,顺便还能获得一丢丢性能提升。但不要为了性能盲目使用——先考虑设计意图。


11. 常见误区与注意事项

误区 1:"sealed 类不能有派生类,但可以被派生"

❌ 错误理解:sealed 类也可以有父类,所以能被继承
✅ 正确理解:sealed 类只能"向上看"(可以有父类),不能"向下看"(不能有子类)

误区 2:"sealed 类不能包含 virtual 方法"

public sealed class Demo
{
    // ✅ 可以有 virtual 方法!
    // 虽然这个类没有被继承的可能,但 virtual 本身不报错
    // 只是这样做没有实际意义(因为永远不会有子类来重写)
    public virtual void UnusedVirtualMethod()
    {
        Console.WriteLine("这个方法永远不会被重写");
    }
}

误区 3:"private 方法可以用 sealed"

❌ private 方法天然就是 sealed(不能被外部的类重写)
✅ sealed 只能用在 protected / public 的 override 方法上

误区 4:"sealed 类不能用 using"

❌ sealed 和 using 没有关系
✅ sealed 类是普通类,照常可以用 new 创建,也可以用 using 语句
public sealed class DatabaseConnection : IDisposable
{
    public void Dispose() { /* 释放资源 */ }
}

// ✅ 正常使用
using (var conn = new DatabaseConnection())
{
    // ...
}

注意事项汇总

注意点 说明
sealed 类不能是 abstract 两者互斥
sealed 方法必须跟 override 不能单独出现在"首次定义"的方法上
sealed 不能撤销 一旦 sealed,连作者自己也不能再通过继承来修改
struct 天然 sealed 值类型(struct)不能被继承,所以隐式是 sealed
sealed 不影响接口实现 sealed 类仍然可以实现任意多个接口
sealed 不阻止 new 关键字隐藏 子类仍可用 new 定义同名方法(但不是重写)

12. 综合练习

练习 1:找出下面的错误

public class A
{
    public sealed virtual void M1() { }    // ①
    public virtual void M2() { }
    public abstract sealed void M3();      // ②
}

public sealed class B : A                  // ③
{
    public override void M2() { }
}

public class C : B                         // ④
{
    public override void M2() { }
}
点击查看答案
  • ① 错误:sealed 不能和 virtual 一起用。sealed 必须跟 override
  • ② 错误:abstractsealed 不能一起用,它们语义互斥。
  • ③ 正确:sealed 类可以继承普通类。
  • ④ 错误:Bsealed 类,不能被继承。

练习 2:设计一个"最终版计算器"

需求:设计一个计算系统,满足以下条件:

  1. 有一个基类 Calculator,包含 virtual 方法 Calculate
  2. 中间类 StandardCalculator 重写 Calculate密封掉!
  3. 最终类 PrecisionCalculator 尝试继承 StandardCalculator
  4. 再设计一个 sealedFinalMath,包含各种数学方法
点击查看参考代码
// 1. 基类
public class Calculator
{
    public virtual double Calculate(double a, double b)
    {
        return a + b;  // 默认加法
    }
}

// 2. 中间类:重写并密封
public class StandardCalculator : Calculator
{
    public sealed override double Calculate(double a, double b)
    {
        // 标准计算:加权平均
        return (a * 0.6) + (b * 0.4);
    }
}

// 3. 尝试继承 StandardCalculator
// ❌ 不能继承 sealed 方法
// public class PrecisionCalculator : StandardCalculator
// {
//     public override double Calculate(double a, double b)
//     {
//         // 更高精度的计算...
//     }
// }

// 4. sealed 工具类
public sealed class FinalMath
{
    public static double Square(double x) => x * x;
    public static double Cube(double x) => x * x * x;
    public static double Average(double[] values)
    {
        if (values == null || values.Length == 0) return 0;
        double sum = 0;
        foreach (var v in values) sum += v;
        return sum / values.Length;
    }
}

练习 3:密封链的继承权限判断

public class Level0
{
    public virtual void MethodA() { }
    public virtual void MethodB() { }
    public virtual void MethodC() { }
}

public class Level1 : Level0
{
    public sealed override void MethodA() { }   // 封住 A
    public override void MethodB() { }          // 没封住 B
    // 没有重写 MethodC
}

public class Level2 : Level1
{
    // 问题:下面哪些可以正常编译?
    // public override void MethodA() { }   // ①
    // public override void MethodB() { }   // ②
    // public override void MethodC() { }   // ③
}
点击查看答案
  • ① ❌ 不能编译 — MethodALevel1 中被 sealed override 封住了
  • ② ✅ 能编译 — MethodB 只是普通 override,没有被封
  • ③ ✅ 能编译 — MethodCLevel0Level2 之间没被任何类重写过,可以直接 override

13. 本节小结

核心要点

要点 一句话总结
sealed 类 class 前加 sealed,这个类就不能再被继承了
sealed 方法 必须配合 override 使用,封住后子类不能再重写
sealed 属性/索引器 同样配合 override,阻止子类再重写
sealed vs abstract 死对头,不能同时出现
static 类 天然 sealed
设计原则 如果不希望类被继承,就用 sealed

知识图谱

                          sealed
                            │
          ┌─────────────────┼─────────────────┐
          │                 │                 │
     sealed class      sealed method     sealed property
          │                 │                 │
    不能被继承          必须+override      必须+override
          │                 │                 │
    ┌─────┴─────┐     ┌─────┴─────┐     ┌─────┴─────┐
    │           │     │           │     │           │
  可以有父类  可以new  封住虚方法   封住虚属性  封住虚索引器
  可以实现接口        阻止再重写   阻止再重写  阻止再重写

一句话记住 sealed

sealed = "就此封存,不准再改"。类被封存不能继承,方法被封存不能重写。


下一篇预告abstract 抽象类与抽象方法 — 和 sealed 刚好相反的概念,看看它们如何相辅相成构建灵活的类层次结构。

0
博主关闭了当前页面的评论