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 方法 |
像一道家传秘方,祖父传给父亲,父亲改良后封印:"到我这就是最终配方了,孙子不能再改" |
没 sealed 的 virtual 方法 |
像开源食谱,每一代都可以改,谁也拦不住 |
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 —— 水火不容
sealed 和 abstract 是一对死对头,它们表达的意思完全相反:
| 关键字 | 含义 | 能否实例化 | 能否被继承 |
|---|---|---|---|
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。 - ② 错误:
abstract和sealed不能一起用,它们语义互斥。 - ③ 正确:
sealed类可以继承普通类。 - ④ 错误:
B是sealed类,不能被继承。
练习 2:设计一个"最终版计算器"
需求:设计一个计算系统,满足以下条件:
- 有一个基类
Calculator,包含virtual方法Calculate - 中间类
StandardCalculator重写Calculate并密封掉! - 最终类
PrecisionCalculator尝试继承StandardCalculator - 再设计一个
sealed类FinalMath,包含各种数学方法
点击查看参考代码
// 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() { } // ③
}
点击查看答案
- ① ❌ 不能编译 —
MethodA在Level1中被sealed override封住了 - ② ✅ 能编译 —
MethodB只是普通override,没有被封 - ③ ✅ 能编译 —
MethodC从Level0到Level2之间没被任何类重写过,可以直接 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刚好相反的概念,看看它们如何相辅相成构建灵活的类层次结构。