1. Giới thiệu
Reflection (từ tiếng Anh Reflection) — là khả năng của chương trình tự khám phá cấu trúc của chính nó trong khi chạy. Hãy tưởng tượng chương trình có thể tự hỏi: 'Mình có những phương thức nào? Kiểu này có những thuộc tính nào?' và nhận được câu trả lời tại runtime.
Ví dụ dùng reflection trong thực tế
- Tạo plugin và module: Không cần biết trước bạn sẽ làm việc với lớp nào — chỉ cần load DLL và khám phá phương thức của nó khi chạy.
- Tự động hóa testing: Các framework kiểm thử như xUnit và NUnit tìm các phương thức test bằng reflection.
- Serialization: Dùng reflection có thể tự động biến bất kỳ đối tượng nào thành JSON/XML mà không phải lặp thủ công qua các thuộc tính.
- Validate dữ liệu dựa trên attribute: Ví dụ bạn gắn attribute [Required] lên một thuộc tính — và muốn tự động kiểm tra tất cả các thuộc tính như vậy.
Tại sao nên biết điều này khi phỏng vấn
Kỹ năng dùng reflection thường được hỏi trong phỏng vấn vì nó cho thấy bạn hiểu cách .NET hoạt động và biết giải quyết các bài toán động. Nếu bạn muốn viết framework hoặc extension riêng, reflection là không thể thiếu.
2. Không gian tên và các lớp cơ bản của reflection
Toàn bộ chức năng reflection trong C# có sẵn qua không gian tên System.Reflection. Ở đầu file đừng quên:
using System.Reflection;
Các loại chính trong reflection
| Lớp/Loại | Mô tả |
|---|---|
|
Biểu diễn mô tả của một Type trong .NET (ví dụ class, interface, enum, v.v.) |
|
Thông tin về một thuộc tính của kiểu |
|
Thông tin về một phương thức của kiểu |
|
Thông tin về một trường (field) của kiểu |
|
Thông tin về constructor của kiểu |
|
Biểu diễn một assembly (EXE hoặc DLL) |
Các phương thức chính của lớp Type
| Phương thức | Mô tả |
|---|---|
|
Trả về tất cả các thuộc tính public |
|
Trả về tất cả các field public |
|
Trả về tất cả các phương thức public |
|
Trả về tất cả các constructor public |
|
Trả về tất cả interface mà kiểu này triển khai |
|
Trả về tất cả attribute áp dụng lên kiểu |
Thường có thể truyền BindingFlags cho các phương thức này để truy cập thành viên private hoặc static, nhưng hiện tại ta giới hạn ở kịch bản cơ bản.
3. Bước đầu tiên: lấy đối tượng Type
Mọi kỹ thuật reflection đều dựa trên việc làm việc với đối tượng Type.
Có vài cách để lấy đối tượng này, cùng xem từng cách:
Cách 1: Qua một đối tượng
string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String
Cách 2: Qua tên kiểu cố định
Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32
Cách 3: Theo chuỗi (động)
Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double
Chú ý! Với lớp người dùng từ assembly khác bạn phải chỉ rõ tên đầy đủ của type kèm assembly.
4. Khám phá cấu trúc kiểu: thuộc tính, phương thức, trường và constructor
Giờ là lúc kiểm chứng: ta đã có đối tượng Type — và bây giờ có thể biết mọi thứ mình muốn.
Lấy danh sách thuộc tính
Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}
Kết quả:
Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...
Lấy danh sách phương thức
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}
Các type lớn có thể có rất nhiều phương thức! Thường bạn không cần tất cả mà chỉ cần "của mình" (ví dụ loại trừ các phương thức kế thừa từ object). Chuyện này mình sẽ xử lý sau.
Lấy danh sách trường (fields)
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}
Nhưng hầu hết các lớp bình thường không có field public, vì encapsulation là quan trọng :)
Lấy constructor
ConstructorInfo[] constructors = type.GetConstructors();
foreach (var ctor in constructors)
{
Console.WriteLine($"Hàm khởi tạo: {ctor}");
}
5. Áp dụng reflection thực tế: phân tích ứng dụng của chúng ta
Nhắc lại trong project học của chúng ta có lớp TaskItem, nơi lưu nhiệm vụ. Ta sẽ khám phá nó bằng reflection.
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted;
}
Đây là cách biết toàn bộ "ruột" của kiểu trong runtime:
Type taskType = typeof(TaskItem);
Console.WriteLine("Thuộc tính:");
foreach (var prop in taskType.GetProperties())
Console.WriteLine($" {prop.PropertyType.Name} {prop.Name}");
Console.WriteLine("Trường:");
foreach (var field in taskType.GetFields())
Console.WriteLine($" {field.FieldType.Name} {field.Name}");
Console.WriteLine("Phương thức:");
foreach (var method in taskType.GetMethods())
Console.WriteLine($" {method.ReturnType.Name} {method.Name}()");
Thử thêm thuộc tính mới vào lớp và xem reflection phát hiện chúng như thế nào. Đó là cách serializer và object inspector generic hoạt động.
6. Minh họa: object Type trông như thế nào (sơ đồ)
+-------------------------+
| Type |
+-------------------------+
| .Name |
| .FullName |
| .Namespace |
| .Assembly |
+-------------------------+
| .GetProperties() |
| .GetFields() |
| .GetMethods() |
| .GetConstructors() |
| .GetInterfaces() |
| .GetCustomAttributes() |
+-------------------------+
Đây là "chân dung" của một type — có tên, assembly, tập các thành viên và attributes.
Lấy thông tin cơ bản về một kiểu
Đối tượng Type có thể cho biết đó là kiểu gì:
Type t = typeof(double);
Console.WriteLine(t.Name); // Double
Console.WriteLine(t.FullName); // System.Double
Console.WriteLine(t.Namespace); // System
Console.WriteLine(t.IsClass); // False
Console.WriteLine(t.IsValueType); // True
Console.WriteLine(t.IsEnum); // False
Console.WriteLine(t.IsPrimitive); // True
7. Tìm cụ thể thuộc tính, phương thức hoặc trường theo tên
Thuộc tính
var prop = taskType.GetProperty("Title");
if (prop != null)
{
Console.WriteLine($"Kiểu của thuộc tính 'Title': {prop.PropertyType}");
}
Phương thức
var method = taskType.GetMethod("ToString");
if (method != null)
{
Console.WriteLine($"Phương thức 'ToString' trả về: {method.ReturnType}");
}
Trường
var field = taskType.GetField("IsCompleted");
if (field != null)
{
Console.WriteLine($"Kiểu của trường 'IsCompleted': {field.FieldType}");
}
8. Dùng reflection để truy cập giá trị động
Giờ không chỉ xem mà còn có thể chạm tới giá trị!
Đọc giá trị thuộc tính
TaskItem item = new TaskItem { Id = 1, Title = "Kiểm thử Reflection", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");
if (titleProp != null)
{
object value = titleProp.GetValue(item);
Console.WriteLine($"Tiêu đề: {value}");
}
Gán giá trị cho thuộc tính
titleProp.SetValue(item, "Tiêu đề đã thay đổi");
Console.WriteLine(item.Title);
Làm việc với fields
FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true
Với field và thuộc tính private cần các flag đặc biệt, ví dụ BindingFlags.NonPublic. Mình sẽ nói về chuyện đó sau.
9. Gọi phương thức qua reflection
Bạn có thể gọi phương thức ngay cả khi không biết nó khi viết code!
TaskItem task = new TaskItem { Title = "Reflection — tuyệt!" };
MethodInfo method = task.GetType().GetMethod("ToString");
if (method != null)
{
object result = method.Invoke(task, null);
Console.WriteLine(result);
}
Nếu phương thức có tham số, truyền chúng như một mảng object:
public class Calculator
{
public int Add(int a, int b) => a + b;
}
var calc = new Calculator();
var addMethod = typeof(Calculator).GetMethod("Add");
object[] parameters = { 5, 3 };
object sumResult = addMethod.Invoke(calc, parameters);
Console.WriteLine(sumResult); // 8
10. Tạo đối tượng động qua constructor
Reflection cho phép tạo đối tượng mà không cần toán tử new!
Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// Đây là cùng một TaskItem (nhưng kiểu là object)
Console.WriteLine(newTask.GetType().Name); // TaskItem
Có thể truyền tham số cho constructor:
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "Alice");
Console.WriteLine(((User)user).Name); // Alice
11. Lỗi thường gặp khi làm việc với System.Reflection
Lỗi #1: Truy cập thành viên private mà không dùng BindingFlags.
Mặc định reflection chỉ thấy các thành viên public. Nếu không dùng BindingFlags.NonPublic thì không thể truy cập field hoặc phương thức private.
Lỗi #2: Bỏ qua kiểm tra trên null.
Các phương thức GetProperty, GetMethod và những phương thức khác có thể trả về null nếu không tìm thấy thành viên. Không kiểm tra sẽ dẫn tới NullReferenceException.
Lỗi #3: Chọn sai overload của phương thức.
Khi có nhiều overload, GetMethod có thể trả về overload không mong muốn. Dùng GetMethod với kiểu tham số để chỉ rõ hơn.
Lỗi #4: Dùng reflection trong code yêu cầu hiệu năng cao.
Reflection chậm hơn so với gọi trực tiếp. Với code cần hiệu năng, dùng cache metadata hoặc delegate.
GO TO FULL VERSION