1. 介紹
反射(英語 Reflection)是程式在執行時檢視自身結構的能力。想像你的程式可以問自己:'我有哪些方法?這個型別有哪些屬性?',並在執行期得到答案。
反射的實際應用範例
- 建立插件與模組:你不需要事先知道會使用哪個類別——只要載入 DLL 並即時檢查它的方法就可以。
- 測試自動化:測試框架,例如 xUnit 和 NUnit,透過反射找到測試方法。
- 序列化:用反射可以自動把任意物件轉成 JSON/XML,而不需要手動列舉屬性。
- 根據 attributes 做資料驗證:比如你在屬性上加了 [Required],想自動檢查所有這類屬性。
為什麼面試會問到這個
會問反射是因為它能展示你對 .NET 運作方式的理解,以及解決動態需求的能力。如果你想寫自己的框架或擴充功能,反射非常實用。
2. 命名空間與反射的基本類別
C# 的所有反射功能都在命名空間 System.Reflection。檔案開頭別忘了:
using System.Reflection;
反射的主要型別
| 類別/型別 | 說明 |
|---|---|
|
代表 .NET 中型別的描述(例如 class、interface、enum 等) |
|
型別的屬性 |
|
型別的方法 |
|
型別的欄位 |
|
型別的建構子 |
|
代表一個組件(EXE 或 DLL) |
類別 Type 的主要方法
| 方法 | 說明 |
|---|---|
|
回傳所有 public 的屬性 |
|
回傳所有 public 的欄位 |
|
回傳所有 public 的方法 |
|
回傳所有 public 的建構子 |
|
回傳型別實作的所有介面 |
|
回傳套用到型別上的所有 attributes |
通常可以傳入 BindingFlags 到這些方法來存取 private 或 static 成員,但我們先從基本情境開始。
3. 第一個步驟:取得 Type 物件
所有反射技巧都建立在 Type 物件之上。
有幾種方式可以取得這個物件,下面逐一說明:
方式 1:透過物件
string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String
方式 2:透過型別名稱
Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32
方式 3:用字串(動態)
Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double
注意!若是其他組件中的自訂類別,需要帶上包含組件名稱的完整型別名稱。
4. 探索型別結構:屬性、方法、欄位和建構子
現在重點來了:取得了 Type 物件後,你可以查到任何你想要的資訊。
取得屬性列表
Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}
輸出:
Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...
取得方法列表
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}
大型型別的方法可能非常多!通常你不需要全部,而只想要「自己的」方法(例如排除繼承自 object 的方法)。我們稍後會處理這類過濾。
取得欄位列表
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}
不過大多數常見類別不會有公開欄位,因為封裝是很重要的 :)
取得建構子
ConstructorInfo[] constructors = type.GetConstructors();
foreach (var ctor in constructors)
{
Console.WriteLine($"建構子: {ctor}");
}
5. 在實務中應用反射:分析我們的應用程式
提醒一下,在我們的教學專案中有一個類別 TaskItem,用來存放任務。我們來透過反射檢查它。
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted;
}
下面示範如何在執行期查看型別的「內部」:
Type taskType = typeof(TaskItem);
Console.WriteLine("屬性:");
foreach (var prop in taskType.GetProperties())
Console.WriteLine($" {prop.PropertyType.Name} {prop.Name}");
Console.WriteLine("欄位:");
foreach (var field in taskType.GetFields())
Console.WriteLine($" {field.FieldType.Name} {field.Name}");
Console.WriteLine("方法:");
foreach (var method in taskType.GetMethods())
Console.WriteLine($" {method.ReturnType.Name} {method.Name}()");
試著在類別中新增屬性,看看反射如何看到它們。這就是通用序列化器和物件檢視器的工作方式。
6. 視覺化:Type 物件的結構(示意圖)
+-------------------------+
| Type |
+-------------------------+
| .Name |
| .FullName |
| .Namespace |
| .Assembly |
+-------------------------+
| .GetProperties() |
| .GetFields() |
| .GetMethods() |
| .GetConstructors() |
| .GetInterfaces() |
| .GetCustomAttributes() |
+-------------------------+
這就是型別的「肖像」——有名稱、組件、成員集合和 attributes。
取得型別的基本資訊
Type 物件可以告訴你這是什麼型別:
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. 根據名稱查找特定的屬性、方法或欄位
屬性
var prop = taskType.GetProperty("Title");
if (prop != null)
{
Console.WriteLine($"屬性 'Title' 的型別: {prop.PropertyType}");
}
方法
var method = taskType.GetMethod("ToString");
if (method != null)
{
Console.WriteLine($"方法 'ToString' 回傳: {method.ReturnType}");
}
欄位
var field = taskType.GetField("IsCompleted");
if (field != null)
{
Console.WriteLine($"欄位 'IsCompleted' 的型別: {field.FieldType}");
}
8. 用反射做動態存取值
現在不只看,還可以真正「操作」值!
讀取屬性值
TaskItem item = new TaskItem { Id = 1, Title = "測試反射", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");
if (titleProp != null)
{
object value = titleProp.GetValue(item);
Console.WriteLine($"標題: {value}");
}
設定屬性值
titleProp.SetValue(item, "已修改的標題");
Console.WriteLine(item.Title);
操作欄位
FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true
若要存取 private 欄位或屬性,需要額外的旗標,例如 BindingFlags.NonPublic。我們會在之後介紹。
9. 用反射呼叫方法
你可以在不知道方法細節的情況下呼叫它!
TaskItem task = new TaskItem { Title = "反射很棒!" };
MethodInfo method = task.GetType().GetMethod("ToString");
if (method != null)
{
object result = method.Invoke(task, null);
Console.WriteLine(result);
}
若方法有參數,要把參數放在 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. 透過建構子動態建立物件
反射可以在沒有 new 的情況下建立物件!
Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// 這仍然是同一個 TaskItem(但型別是 object)
Console.WriteLine(newTask.GetType().Name); // TaskItem
也可以傳入建構子的參數:
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "艾莉絲");
Console.WriteLine(((User)user).Name); // 艾莉絲
11. 使用 System.Reflection 的常見錯誤
錯誤 #1:在沒有使用 BindingFlags 的情況下存取 private 成員。
預設情況下反射只會看到 public 成員。未使用 BindingFlags.NonPublic 就想存取私有欄位或方法是行不通的。
錯誤 #2:忽略 null 檢查。
GetProperty、GetMethod 等方法在找不到成員時會回傳 null,不做檢查會導致 NullReferenceException。
錯誤 #3:沒選對方法的 overload。
當有多個同名的 overload 時,單純用 GetMethod 可能不是你想要的。請使用帶參數型別的 GetMethod 或其他更精確的搜尋方式。
錯誤 #4:在性能敏感的程式中濫用反射。
反射比直接呼叫慢。對於高效能需求的程式,應該快取 metadata 或使用 delegate。
GO TO FULL VERSION