Reflection cho phép chương trình xem xét các kiểu trong lúc chạy: lớp có những thuộc tính nào, giá trị của chúng là gì, có những phương thức nào, được gắn attribute nào. Attribute là nửa còn lại: những nhãn khai báo trong dấu ngoặc vuông, như [Obsolete] hay [JsonPropertyName("id")], tự chúng không có ý nghĩa gì cho đến khi trình biên dịch hoặc một đoạn code đọc chúng bằng reflection. Thư viện serialize, ORM, thư viện kiểm tra dữ liệu, framework kiểm thử và cơ chế định tuyến của ASP.NET đều hoạt động theo cách này.
Phần lớn code ứng dụng hiếm khi cần dùng reflection trực tiếp, nhưng hiểu cách nó hoạt động giúp bạn giải thích được rất nhiều hành vi của framework.
Đối tượng Type: typeof và GetType
Mọi thứ bắt đầu từ một System.Type. Có hai cách để lấy nó:
Đầu ra:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
Điểm khác biệt cần nhớ: typeof(X) gọi tên một kiểu bạn đã biết khi viết code, còn obj.GetType() hỏi một đối tượng xem thực sự nó là gì. So sánh GetType() == typeof(Employee) là khớp chính xác, sẽ sai với lớp dẫn xuất, và thường đó không phải điều bạn muốn; is và IsAssignableFrom thì tính đến kế thừa. nameof trông giống nhưng hoàn toàn không phải reflection: trình biên dịch thay nó bằng một hằng chuỗi.
Gọi GetType() trên tham chiếu null sẽ ném NullReferenceException, vì không có đối tượng nào để hỏi. Trên một kiểu giá trị nullable đã boxing, nó trả về kiểu bên dưới: ((int?)5).GetType() là System.Int32.
Đọc và gán thuộc tính
GetProperties() liệt kê các thuộc tính public của một kiểu dưới dạng đối tượng PropertyInfo, mỗi đối tượng có thể đọc và ghi giá trị trên một instance cụ thể:
Đầu ra:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Ví dụ này cho thấy ba điều:
GetValuetrả vềobject, nên kiểu giá trị được trả về ở dạng boxing và bạn phải ép kiểu để dùng.GetPropertyvới một tên không tồn tại sẽ trả vềnull, và lời gọi tiếp theo trên nó némNullReferenceException. Hãy kiểm tra trước khi dùng.BindingFlags.NonPublic | BindingFlags.Instancetruy cập được cả thành viên private. Điều này hợp lý trong công cụ và kiểm thử, nhưng nó phá vỡ tính đóng gói và hỏng một cách âm thầm khi lớp được refactor.
Vòng lặp này về cơ bản là cách một công cụ xuất CSV hay một trình serialize đối tượng sang JSON hoạt động: duyệt các thuộc tính, đọc từng giá trị, định dạng nó.
Gọi phương thức và tạo đối tượng theo tên
GetMethod tìm một phương thức, và Invoke gọi nó với một mảng đối số. Activator.CreateInstance tạo đối tượng từ một Type, đó là cách các hệ thống plugin và container DI dựng những kiểu được chọn lúc chạy:
Đầu ra:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly giới hạn danh sách ở những thành viên được khai báo ngay trong lớp; không có nó, GetMethods còn trả về cả ToString, Equals, GetHashCode và GetType từ object. Type.GetType("Name") cần tên đầy đủ kèm namespace, và với kiểu nằm ở assembly khác thì cần cả tên assembly ("MyApp.Plugins.Csv, MyApp.Plugins").
Nếu phương thức được gọi ném ngoại lệ, Invoke sẽ bọc ngoại lệ đó trong một TargetInvocationException; ngoại lệ gốc nằm trong InnerException của nó.
Attribute: nhãn để trình biên dịch và framework đọc
Attribute được viết trong dấu ngoặc vuông, đặt trước thứ mà nó mô tả. Framework định nghĩa rất nhiều attribute; một vài cái thay đổi cách trình biên dịch làm việc:
public class OrderService
{
[Obsolete("Use PlaceOrderAsync instead.")]
public void PlaceOrder(Order order) { }
// Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
// [Obsolete("...", true)] makes it error CS0619 instead.
[Conditional("DEBUG")]
public void Trace(string message) => Console.WriteLine(message);
// Calls to Trace are removed entirely from builds without the DEBUG symbol.
}
[Flags] enum Channels { None = 0, Email = 1, Sms = 2 } // changes how ToString formats combinations
[Serializable] class Snapshot { } // marks a type for legacy binary serialization
Những attribute khác được thư viện đọc lúc chạy: [JsonPropertyName] và [JsonIgnore] bởi System.Text.Json, [Required] và [MaxLength] bởi cơ chế kiểm tra model của ASP.NET Core và Entity Framework, [HttpGet("orders/{id}")] bởi định tuyến ASP.NET, [Fact] và [Test] bởi các trình chạy kiểm thử. Bản thân attribute không làm gì cả; đoạn code đi tìm nó mới là thứ hành động.
Tên ObsoleteAttribute được rút gọn thành [Obsolete] khi sử dụng: theo quy ước, tên mọi lớp attribute đều kết thúc bằng Attribute, và C# cho phép bỏ hậu tố này.
Khai báo và đọc một attribute tự tạo
Attribute tự tạo là một lớp kế thừa từ Attribute. [AttributeUsage] cho biết nó được gắn vào đâu. Tham số của hàm khởi tạo trở thành đối số theo vị trí, còn các thuộc tính public có thể gán trở thành đối số có tên:
Đầu ra:
Username must be at most 20 characters
Email is required
Keep the city code short
0
Đó là phiên bản thu nhỏ của việc cơ chế kiểm tra model trong ASP.NET Core làm với System.ComponentModel.DataAnnotations. Đối số của attribute phải là hằng lúc biên dịch (số, chuỗi, typeof(...), giá trị enum hoặc mảng của chúng), vì chúng được lưu trong metadata của assembly. GetCustomAttribute<T>() là một extension method trong System.Reflection; ngoài ra còn có IsDefined(typeof(T)) khi bạn chỉ cần biết attribute có được gắn hay không.
Cái giá của reflection
Reflection đánh đổi tốc độ và độ an toàn để lấy sự linh hoạt:
- Tốc độ. Tìm một thành viên theo tên rồi gọi qua
InvokehayGetValuechậm hơn rất nhiều so với gọi trực tiếp, và kiểu giá trị bị boxing. Khi dùng lặp lại, hãy tìmPropertyInfohoặcMethodInfomột lần rồi giữ lại, hoặc biến nó thành một delegate bằngDelegate.CreateDelegatehayMethodInfo.CreateDelegaterồi gọi delegate đó. - An toàn. Một tên viết sai hay chữ ký phương thức bị đổi vẫn biên dịch bình thường và chỉ lỗi lúc chạy. Hãy ưu tiên
nameof(Product.Price)thay cho chuỗi"Price"ở bất cứ đâu có thể, để việc đổi tên được phát hiện. - Trimming và AOT. Ứng dụng trimmed và Native AOT loại bỏ những thành viên có vẻ không được dùng, mà reflection lại che giấu việc sử dụng khỏi phép phân tích đó. Các thư viện hiện đại (
System.Text.Json,GeneratedRegex, logging) đang chuyển sang source generator, làm cùng công việc đó ngay lúc biên dịch.
Hãy dùng reflection cho những phần của chương trình thực sự không biết trước kiểu dữ liệu: plugin, công cụ tổng quát, trình serialize, công cụ hỗ trợ kiểm thử. Khi đã biết kiểu, code thông thường, generic hoặc interface nhanh hơn và được trình biên dịch kiểm tra.
Lỗi thường gặp
- Dùng
GetType() == typeof(Base)để kiểm tra lớp cơ sở. Cách này sai với kiểu dẫn xuất. Hãy dùngishoặcIsAssignableFrom. - Không kiểm tra
null.GetProperty,GetMethodvàType.GetTypetrả vềnullkhi không có gì khớp. - Dùng reflection trong vòng lặp nóng mà không lưu lại. Hãy lưu
MemberInfohoặc biên dịch thành delegate. - Bắt sai ngoại lệ từ
Invoke. Ngoại lệ thật nằm trongInnerExceptioncủaTargetInvocationException. - Dùng chuỗi cứng cho tên thành viên. Hãy dùng
nameof.
Câu hỏi thường gặp
Reflection trong C# là gì?
Reflection là khả năng của chương trình tự xem xét các kiểu lúc chạy: liệt kê thuộc tính và phương thức của một lớp, đọc và gán giá trị theo tên, gọi phương thức, tạo instance và đọc attribute. Nó nằm trong System.Reflection và bắt đầu từ một đối tượng Type. Các thư viện serialize, ORM, container dependency injection và framework kiểm thử đều được xây dựng trên nó.
typeof và GetType trong C# khác nhau thế nào?
typeof(Customer) được xác định lúc biên dịch từ tên kiểu và không cần đối tượng nào. obj.GetType() được gọi trên một instance lúc chạy và trả về kiểu thực sự của đối tượng, có thể là lớp dẫn xuất sâu hơn kiểu khai báo của biến: với Animal a = new Dog();, a.GetType() là Dog. Gọi GetType() trên tham chiếu null sẽ ném NullReferenceException.
Làm sao lấy giá trị thuộc tính theo tên trong C#?
obj.GetType().GetProperty("Price") trả về một PropertyInfo (hoặc null nếu không có thuộc tính public nào như vậy), và .GetValue(obj) đọc nó dưới dạng object. .SetValue(obj, value) thì ghi giá trị. Hãy lưu lại PropertyInfo nếu bạn làm việc này trong vòng lặp, vì bước tìm kiếm mới là phần tốn kém.
Làm sao tạo attribute tự định nghĩa trong C#?
Khai báo một lớp kế thừa từ System.Attribute, đặt tên có hậu tố Attribute, và đánh dấu nơi được phép dùng nó bằng [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Gắn nó dưới dạng [MaxLength(50)] và đọc bằng property.GetCustomAttribute<MaxLengthAttribute>().
Attribute Obsolete trong C# có tác dụng gì?
Gắn [Obsolete("Use PlaceOrderAsync instead")] lên một thành viên khiến trình biên dịch đưa ra cảnh báo CS0618, kèm thông điệp của bạn, ở mọi nơi gọi. [Obsolete("...", true)] biến cảnh báo thành lỗi CS0619. Đây là cách các thư viện loại bỏ dần một API mà không làm hỏng code của bên gọi chỉ sau một đêm.
Reflection trong C# có chậm không?
So với lời gọi trực tiếp thì có: tìm một thành viên theo tên rồi gọi nó qua MethodInfo.Invoke hoặc PropertyInfo.GetValue thường chậm hơn hàng chục đến hàng trăm lần, và kiểu giá trị bị boxing. Dùng lúc khởi động, khi đọc cấu hình hay thỉnh thoảng thì không sao. Với đoạn code chạy liên tục, hãy lưu lại MemberInfo, tạo delegate một lần, hoặc dùng generic hay source generator thay thế.