CodeGym /課程 /C# SELF /使用 reflection

使用 reflection

C# SELF
等級 63 , 課堂 1
開放

1. 介紹

在 C# 裡任何 class(或 struct)都是一組 members。它們包含:

  • Fields (欄位): 儲存資料的變數。
  • Properties (屬性): 有 get/set 的「智慧」包裝。
  • Methods (方法): 功能性程式碼。
  • Constructors (建構子): 建立實例的方式。
  • Events (事件): 用來發布-訂閱的機制。

想像你有一把魔術放大鏡,可以看透任何型別、知道它的內容,甚至改變它。那就是 reflection。

命名空間與主要的 reflection 型別

開始前別忘了引用命名空間:

using System.Reflection;

重點類別與介面:

類別/介面 說明
Type
主要入口 — 一切從它開始
FieldInfo
欄位的資訊
PropertyInfo
屬性的資訊
MethodInfo
方法的資訊
ConstructorInfo
建構子的資訊
EventInfo
事件的資訊
MemberInfo
以上各者的共通「父類」

2. 用 reflection 取得型別的成員

取得 fields 與 properties

範例:列舉欄位與屬性

假設我們有這個 class:

public class Person
{
    public string Name { get; set; }
    public int Age;
    private string Secret = "Sekretnoye slovo";
}

取得欄位:

Type personType = typeof(Person);

FieldInfo[] fields = personType.GetFields(
    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

foreach (var field in fields)
{
    Console.WriteLine($"欄位: {field.Name}, 類型: {field.FieldType.Name}, 存取: {field.Attributes}");
}

取得屬性:

PropertyInfo[] properties = personType.GetProperties(
    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

foreach (var property in properties)
{
    Console.WriteLine($"屬性: {property.Name}, 類型: {property.PropertyType.Name}");
}

說明

  • BindingFlags — 一組「旗標」,用來指定要搜尋哪些成員(public/private、instance/static 等)。
  • 欄位和屬性不同:field 是資料片段,property 則是背後的 get/set 方法,封裝了欄位。

取得方法

MethodInfo[] methods = personType.GetMethods(
    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

foreach (var method in methods)
{
    Console.WriteLine($"方法: {method.Name}");
}

你會驚訝會列出多少方法。那裡會有屬性的 getter/setter,還有來自 object 的系統方法!

實用過濾:要只取得你自己寫的「顯式」方法,可以依名稱或 attribute 做過濾。

不同情況下該搭配哪些 BindingFlags

要找什麼 呼叫範例 說明
公有欄位
GetFields(BindingFlags.Public | ...)
所有對外可見的
私有欄位
GetFields(BindingFlags.NonPublic | ...)
隱藏的內部欄位
只有實例成員
BindingFlags.Instance
只有非 static 的
只有 static
BindingFlags.Static
只有靜態成員
全部
BindingFlags.Public | NonPublic | Instance | Static
完整清單

3. 動態建立型別的實例

為什麼這很酷?
你可以在編譯時不知道確切型別的情況下建立物件。

簡單方式 — 用 Activator.CreateInstance

// 以某種方式取得 Type,例如:
Type myType = typeof(Person);
// 建立物件:
object obj = Activator.CreateInstance(myType);
// 現在 obj 是 Person 的實例,但型別是 object

如果 class 沒有無參數的建構子,可以傳參數:

public class Animal
{
    public string Name { get; }
    public Animal(string name) { Name = name; }
}

// ...
Type animalType = typeof(Animal);
object dog = Activator.CreateInstance(animalType, "Sharik");

重要!

Console.WriteLine(dog.GetType().Name); // Animal

更客製的方式 — 用 ConstructorInfo

ConstructorInfo ctor = animalType.GetConstructor(new Type[] { typeof(string) });
object dog = ctor.Invoke(new object[] { "Sharik" });

實務上通常用 Activator,但有時需要精確的參數配對或操作 private constructors,這時 ConstructorInfo 會有用。

4. 依名稱存取欄位與屬性

取得並修改欄位

Person p = new Person();
Type t = p.GetType();

FieldInfo field = t.GetField("Age");
field.SetValue(p, 42);
Console.WriteLine(field.GetValue(p)); // 42

操作 private 欄位:

FieldInfo secret = t.GetField("Secret", BindingFlags.NonPublic | BindingFlags.Instance);
if (secret != null)
{
    secret.SetValue(p, "Oj, sekret raskryt");
    Console.WriteLine(secret.GetValue(p));
}
else
{
    Console.WriteLine("欄位 Secret 未找到");
}

小心!不要濫用存取 private 欄位 — 這會破壞封裝並可能引入 bug。

取得並修改屬性

PropertyInfo pi = t.GetProperty("Name");
pi.SetValue(p, "Vasya");
Console.WriteLine(pi.GetValue(p)); // Vasya

屬性不是欄位,而是背後的 get/set 方法。

為什麼有時候會失敗?

如果你的欄位或屬性是 private,務必加上對應的 BindingFlags(例如 NonPublic)。如果屬性沒有 setter,透過 reflection 就不能寫入 — 這個規則仍然適用。

5. 依名稱呼叫方法

呼叫無參數的方法

public class Greeter
{
    public void SayHello()
    {
        Console.WriteLine("Privet!");
    }
}

Greeter g = new Greeter();
Type t = g.GetType();
MethodInfo mi = t.GetMethod("SayHello");
mi.Invoke(g, null); // Privet!

呼叫帶參數的方法

public class MathOp
{
    public int Add(int x, int y) => x + y;
}

MathOp calc = new MathOp();
Type t = calc.GetType();
MethodInfo mi = t.GetMethod("Add");
object result = mi.Invoke(calc, new object[] { 7, 5 });
Console.WriteLine(result); // 12

如果方法是 static,傳給 Invoke 的第一個參數請傳 null

處理 overload 的方法

MethodInfo mi = t.GetMethod(
    "Add", 
    new Type[] { typeof(int), typeof(int) }
);

或者你可以列舉並手動過濾:

foreach(var method in t.GetMethods()) {
    if (method.Name == "Add" && method.GetParameters().Length == 2) 
    {
        // ... 我們的目標方法!
    }
}

6. 好用的小技巧

分析型別時常用的成員、方法與屬性

類別成員 取得資訊的方法 回傳類別 如何讀/寫值
欄位 (field)
GetField, GetFields
FieldInfo
.GetValue(obj), .SetValue(obj, x)
屬性 (prop)
GetProperty, GetProperties
PropertyInfo
.GetValue(obj), .SetValue(obj, x)
方法 (method)
GetMethod, GetMethods
MethodInfo
.Invoke(obj, parameters)
建構子
GetConstructor, GetConstructors
ConstructorInfo
.Invoke(parameters)

實務上這些功能有什麼用?

  • 在 ORM frameworks(例如 Entity Framework)裡會根據你的 models 建 SQL 查詢。
  • 在序列化/解析器(例如 JSON serializers)裡會用到。
  • 在通用的 logging、testing、UI 生成庫會很常用。
  • 面試題常問「你怎麼寫一個通用的物件複製器?」之類的問題。
  • 寫 plugin 系統,當程式要載入並呼叫其他人寫的程式碼時很有用。

即使現在覺得這像駭客電影裡的魔術,實際上很實用,會用到的時候你會感謝自己學過。

7. 常見錯誤與注意事項

開發者常在 BindingFlags 上搞混 — 看不到 private 欄位,或是反而抓到太多方法。務必檢查旗標組合:private 欄位用 NonPublic,靜態用 Static 等等。

透過 Invoke 呼叫方法時,參數的順序與類型要和宣告一致。錯誤的順序/類型會導致 TargetParameterCountExceptionArgumentException。回傳值會以 object 回來 — 別忘了做類型轉換(或用 pattern matching)。

別忘了效能:reflection 比直接呼叫慢。在熱路徑(hot paths)要把找到的 Type/MethodInfo/PropertyInfo 快取起來,或預先產生 delegate。

你會常看到像 get_PropertyNameset_PropertyName 的系統方法。那是屬性的存取方法 — 不需要你手動去寫它們。

若你去改變 private 欄位或屬性,就是在破壞封裝。誤用可能造成難以偵測的 bug。正如 Ben 叔叔說的:「有了更大的力量就有更大的責任」— 在 library 類的專案中尤其要小心使用。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION