Chuyển tới nội dung chính

Sealed Classes

Định nghĩa (Definition)

Sealed class (Lớp bị phong ấn) là một class không thể bị kế thừa. Đánh dấu một class với từ khóa sealed ngăn chặn việc dẫn xuất và cho phép trình biên dịch JIT tối ưu hóa các lệnh gọi phương thức, kiểm tra kiểu và thao tác mảng.

public sealed class Husky : Animal
{
public override void DoNothing() { }
public override int GetAge() => 11;
}

Xem OOP để biết cú pháp và cách sử dụng cơ bản của sealed class. Trang này tập trung vào tác động hiệu suất (Performance Implications).

Tại sao Sealed Class nhanh hơn

1. Hủy ảo hóa (Devirtualization) — Lệnh gọi trực tiếp

Khi gọi một virtual method trên một open class, runtime phải sử dụng phân phối ảo (Virtual Dispatch) — nó tra cứu bảng phương thức (Method Table) để tìm override chính xác. Việc này liên quan đến các lệnh mov thêm trong assembly được JIT biên dịch.

Với một sealed class, JIT biết rằng không có override tiếp theo. Nó có thể thay thế lệnh gọi gián tiếp bằng một lệnh gọi trực tiếp (Direct Call) đến địa chỉ bộ nhớ của phương thức — hủy ảo hóa (Devirtualization).

Benchmark (BenchmarkDotNet):

Phương thứcTrung bìnhGhi chú
Sealed_VoidMethod0.0030 nsLệnh gọi trực tiếp — gần như miễn phí
Open_VoidMethod0.6350 nsOverhead của phân phối ảo
// JIT thấy sealed → hủy ảo hóa → lệnh gọi trực tiếp
[Benchmark]
public void Sealed_VoidMethod() => _husky.DoNothing();

// JIT thấy open class → phải sử dụng phân phối ảo
[Benchmark]
public void Open_VoidMethod() => _bear.DoNothing();

Mẫu tương tự cũng đúng cho các phương thức có giá trị trả về:

Phương thứcTrung bình
Sealed_IntMethod0.0857 ns
Open_IntMethod0.5081 ns
JIT có thể hủy ảo hóa open class too

Nếu JIT có thể xác định chính xác kiểu tại thời điểm biên dịch (ví dụ: object được tạo và sử dụng trong cùng một phương thức), nó sẽ hủy ảo hóa ngay cả open class. Khoảng cách hiệu suất biến mất trong trường hợp đó. Sealed class đảm bảo tối ưu hóa này bất kể ngữ cảnh.

2. Kiểm tra kiểu và ép kiểu nhanh hơn (Faster Type Checking and Casting)

Các toán tử isas nhanh hơn đáng kể trên các kiểu sealed vì runtime chỉ cần so sánh với một kiểu cụ thể duy nhất. Đối với open class, nó phải duyệt toàn bộ hệ thống phân cấp kiểu (Type Hierarchy).

Kiểm tra kiểu (is):

Phương thứcTrung bình
Sealed_TypeCheck0.3880 ns
Open_TypeCheck2.0330 ns

Ép kiểu (as):

Phương thứcTrung bình
Sealed_Casting0.2757 ns
Open_Casting2.0827 ns
private readonly Animal _animal = new Animal();

// Sealed: so sánh một kiểu duy nhất
[Benchmark]
public Husky? Sealed_Casting() => _animal as Husky;

// Open: phải kiểm tra toàn bộ hệ thống phân cấp
[Benchmark]
public Bear? Open_Casting() => _animal as Bear;

3. Loại bỏ kiểm tra hiệp biến mảng (Array Covariance Check Elimination)

Lưu trữ phần tử trong mảng kiểu tham chiếu yêu cầu kiểm tra hiệp biến (Covariance Check) — runtime phải xác minh kiểu phần tử có thể gán được. Sealed class bỏ qua kiểm tra này vì không có kiểu con (Subtype).

Phương thứcTrung bình
Sealed_AddToArray4.322 ns
Open_AddToArray5.629 ns

4. Lệnh gọi phương thức tĩnh (Static Method Calls) — Không khác biệt

Static method không thể bị ghi đè, nên không có phân phối ảo bất kể class có sealed hay không:

Phương thứcTrung bình
Sealed_StaticMethod0.0183 ns
Open_StaticMethod0.0340 ns

5. ToString() — Cải thiện nhỏ

Phương thứcTrung bình
Sealed_ToString5.731 ns
Open_ToString7.149 ns

Phát hiện code không thể truy cập (Unreachable Code Detection)

Trình biên dịch có thể phát hiện các ép kiểu và kiểm tra kiểu không thể xảy ra trên sealed class tại thời điểm biên dịch:

public interface IFlyingAnimal { }

// Lỗi biên dịch: Husky là sealed và không implement IFlyingAnimal
var flying = _animal as Husky; // trình biên dịch biết điều này không bao giờ có thể là IFlyingAnimal

// Cảnh báo: điều kiện luôn false
if (_animal is Husky && _animal is IFlyingAnimal) { } // không thể truy cập

Điều này giúp giảm code chết (Dead Code) và phát hiện lỗi logic sớm.

Đánh đổi (Trade-offs)

Khó khăn khi Mock (Mocking Difficulty)

Sealed class không thể bị mock bởi hầu hết các mocking framework (ví dụ: Moq):

// System.NotSupportedException — Kiểu cần mock phải là interface,
// abstract class hoặc non-sealed class
var mock = new Mock<Husky>();

Cách giải quyết: Lập trình theo interface thay vì kiểu sealed cụ thể, sau đó mock interface đó.

Khi nào nên Seal

Tình huốngCó Seal?
Class không có lý do hợp lý để bị kế thừa
Đường dẫn code nhạy cảm hiệu suất (Hot Paths)
Class quan trọng bảo mật không được phép mở rộng
Class cần mock trong unit testCân nhắc trừu tượng hóa interface trước
Class là một phần của public API được thiết kế để mở rộngKhông

Tổng kết tác động hiệu suất (Summary of Performance Impact)

Thao tácLợi ích của Sealed
Lệnh gọi virtual methodĐược hủy ảo hóa — lệnh gọi trực tiếp, không tra cứu vtable
Kiểm tra kiểu (is)Nhanh hơn ~5 lần
Ép kiểu (as)Nhanh hơn ~7.5 lần
Thao tác mảngNhanh hơn ~20% (không kiểm tra hiệp biến)
Static methodKhông khác biệt
ToString()Cải thiện nhỏ
Nguyên tắc chung

Đánh dấu class là sealed theo mặc định trừ khi bạn cố ý thiết kế cho kế thừa. Lợi ích hiệu suất là miễn phí, và nó truyền đạt ý định thiết kế một cách rõ ràng.

Lỗi thường gặp (Common Pitfalls)

  • Seal quá sớm trong public API — nếu người tiêu dùng cần mở rộng các kiểu của bạn, sealing sẽ phá vỡ khả năng tương thích. Cân nhắc bề mặt API (API Surface) trước khi seal.
  • Ma sát khi mock — sealed class yêu cầu trừu tượng hóa interface để có thể kiểm tra. Hãy tính đến điều này trong thiết kế của bạn.
  • Cho rằng static method được hưởng lợi — static method đã là non-virtual; sealing không cung cấp thêm lợi ích cho chúng.

Câu hỏi phỏng vấn (Interview Questions)

Q: Tại sao sealed class nhanh hơn open class trong C#? A: Trình biên dịch JIT có thể hủy ảo hóa (Devirtualize) các lệnh gọi phương thức trên kiểu sealed — nó thay thế tra cứu vtable gián tiếp bằng các lệnh gọi trực tiếp vì biết rằng không có override tiếp theo. Kiểu sealed cũng cho phép kiểm tra kiểu nhanh hơn (is/as) vì runtime chỉ so sánh với một kiểu duy nhất thay vì duyệt hệ thống phân cấp.

Q: Nên seal class theo mặc định không? A: Có, trừ khi class được thiết kế có chủ đích cho kế thừa. Sealing truyền đạt rằng class là kiểu lá (Leaf Type), cho phép tối ưu hóa JIT và ngăn chặn việc phân lớp con không chủ ý. Hầu hết các class trong một codebase điển hình không bao giờ bị kế thừa.

Q: Hạn chế của việc sử dụng sealed class là gì? A: Sealed class không thể bị kế thừa hoặc mock dễ dàng bởi các mocking framework tiêu chuẩn. Để duy trì khả năng kiểm tra, code nên phụ thuộc vào interface thay vì kiểu sealed cụ thể.

Q: Sealing ảnh hưởng đến thao tác mảng như thế nào? A: Lưu trữ kiểu tham chiếu trong mảng yêu cầu kiểm tra hiệp biến runtime (Runtime Covariance Check) để xác minh khả năng tương thích kiểu. Sealed class bỏ qua kiểm tra này vì chúng không có kiểu con, dẫn đến việc gán mảng nhanh hơn một chút.

Q: JIT có luôn sử dụng phân phối ảo cho non-sealed class không? A: Không. Nếu JIT có thể xác định kiểu runtime chính xác tại thời điểm biên dịch (ví dụ: object được tạo và sử dụng cục bộ trong một phương thức duy nhất), nó có thể hủy ảo hóa lệnh gọi ngay cả cho non-sealed class. Sealing đảm bảo tối ưu hóa này bất kể ngữ cảnh.

Tài liệu tham khảo (References)