🔮 反射與特性 (Reflection & Attributes)
📌 什麼是反射(Reflection)?
反射就像一面魔鏡,讓你在程式執行時可以「照」出任何物件的內部結構:它有哪些屬性、方法、欄位,甚至可以動態呼叫它們。
📌 取得型別資訊
// 方法 1:透過 typeof 取得型別(編譯時期就知道型別)
Type stringType = typeof(string); // 取得 string 的型別資訊
Console.WriteLine(stringType.FullName); // 印出 "System.String"
// 方法 2:透過物件實例的 GetType() 取得(執行時期才知道)
object myObj = "Hello"; // 宣告一個 object 變數
Type objType = myObj.GetType(); // 取得實際的型別資訊
Console.WriteLine(objType.Name); // 印出 "String"
// 方法 3:透過型別名稱字串取得(完全動態)
Type typeByName = Type.GetType("System.Int32"); // 用字串取得 int 的型別
Console.WriteLine(typeByName.Name); // 印出 "Int32"
📌 檢查屬性與方法
// 定義一個簡單的類別作為示範
public class Student
{
// 學生姓名
public string Name { get; set; }
// 學生年齡
public int Age { get; set; }
// 私有欄位:學號
private string _studentId = "S001";
// 公開方法:打招呼
public string SayHello()
{
return $"我是 {Name},今年 {Age} 歲"; // 回傳自我介紹
}
// 私有方法:取得學號
private string GetStudentId()
{
return _studentId; // 回傳學號
}
}
// 使用反射檢查 Student 類別
Type studentType = typeof(Student); // 取得 Student 的型別資訊
// 取得所有公開屬性
Console.WriteLine("=== 公開屬性 ==="); // 標題
foreach (var prop in studentType.GetProperties()) // 走訪所有屬性
{
// 印出屬性名稱和型別
Console.WriteLine($" {prop.Name} ({prop.PropertyType.Name})");
}
// 取得所有公開方法
Console.WriteLine("=== 公開方法 ==="); // 標題
foreach (var method in studentType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) // 只取自己宣告的公開實例方法
{
// 印出方法名稱和回傳型別
Console.WriteLine($" {method.Name}() -> {method.ReturnType.Name}");
}
// 取得私有成員(需要特殊的 BindingFlags)
Console.WriteLine("=== 私有欄位 ==="); // 標題
foreach (var field in studentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) // 取得私有實例欄位
{
// 印出私有欄位名稱
Console.WriteLine($" {field.Name}");
}
📌 動態建立物件與呼叫方法
// 動態建立物件(不用直接 new)
Type type = typeof(Student); // 取得型別
object instance = Activator.CreateInstance(type); // 動態建立 Student 實例
// 動態設定屬性值
PropertyInfo nameProp = type.GetProperty("Name"); // 取得 Name 屬性的資訊
nameProp.SetValue(instance, "小明"); // 把 "小明" 設定給 Name 屬性
PropertyInfo ageProp = type.GetProperty("Age"); // 取得 Age 屬性的資訊
ageProp.SetValue(instance, 20); // 把 20 設定給 Age 屬性
// 動態呼叫方法
MethodInfo sayHello = type.GetMethod("SayHello"); // 取得 SayHello 方法的資訊
object result = sayHello.Invoke(instance, null); // 呼叫方法(null 表示沒有參數)
Console.WriteLine(result); // 印出 "我是 小明,今年 20 歲"
// 動態呼叫私有方法
MethodInfo getId = type.GetMethod("GetStudentId", BindingFlags.NonPublic | BindingFlags.Instance); // 取得私有方法
object id = getId.Invoke(instance, null); // 呼叫私有方法
Console.WriteLine($"學號:{id}"); // 印出 "學號:S001"
📌 特性(Attributes)
特性就像貼在程式碼上的便利貼,用來標記額外的資訊。程式本身不會直接受影響,但其他程式碼(例如框架)可以讀取這些便利貼。
內建特性
// [Obsolete] 標記方法已過時
[Obsolete("請改用 NewMethod(),此方法將在 v3.0 移除")] // 標記為過時
public void OldMethod()
{
// 舊的實作方式
Console.WriteLine("這是舊方法");
}
// [Serializable] 標記類別可以被序列化
[Serializable] // 標記此類別的實例可以被序列化
public class Config
{
// 設定名稱
public string Name { get; set; }
}
自訂特性
// 建立自訂特性類別
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] // 限制只能用在屬性上,且不能重複
public class ValidateRangeAttribute : Attribute // 繼承 Attribute 基底類別
{
// 最小值
public int Min { get; }
// 最大值
public int Max { get; }
// 建構函式,設定範圍
public ValidateRangeAttribute(int min, int max)
{
Min = min; // 儲存最小值
Max = max; // 儲存最大值
}
// 驗證方法
public bool IsValid(int value)
{
return value >= Min && value <= Max; // 檢查值是否在範圍內
}
}
// 使用自訂特性
public class Product
{
// 商品名稱
public string Name { get; set; }
// 價格必須在 1 到 99999 之間
[ValidateRange(1, 99999)] // 使用自訂特性標記驗證規則
public int Price { get; set; }
// 數量必須在 0 到 1000 之間
[ValidateRange(0, 1000)] // 使用自訂特性標記驗證規則
public int Quantity { get; set; }
}
// 用反射讀取自訂特性並執行驗證
public static bool ValidateProduct(Product product)
{
Type type = product.GetType(); // 取得物件的型別資訊
foreach (var prop in type.GetProperties()) // 走訪所有屬性
{
// 嘗試取得 ValidateRange 特性
var attr = prop.GetCustomAttribute<ValidateRangeAttribute>();
if (attr != null) // 如果有標記 ValidateRange
{
int value = (int)prop.GetValue(product); // 取得屬性的值
if (!attr.IsValid(value)) // 驗證是否在範圍內
{
// 驗證失敗,印出錯誤訊息
Console.WriteLine($"{prop.Name} 的值 {value} 不在 {attr.Min}~{attr.Max} 範圍內");
return false; // 回傳驗證失敗
}
}
}
return true; // 所有驗證通過
}
📌 何時該用反射?何時不該用?
// ✅ 適合使用反射的情境:
// 1. 框架與函式庫開發(例如 ASP.NET MVC 的 Model Binding)
// 2. 序列化 / 反序列化(JSON、XML)
// 3. 依賴注入容器(DI Container)
// 4. 單元測試中存取私有成員
// 5. 外掛(Plugin)系統,動態載入組件
// ❌ 不適合使用反射的情境:
// 1. 效能敏感的程式碼(反射比直接呼叫慢 10~100 倍)
// 2. 可以用介面(Interface)或泛型解決的問題
// 3. 簡單的物件建立(直接 new 就好)
// 4. 頻繁呼叫的路徑(hot path)
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:BindingFlags 用錯
// ❌ 錯誤寫法:想取得私有方法,但沒加正確的 BindingFlags
Type type = typeof(Student); // 取得型別
MethodInfo method = type.GetMethod("GetStudentId"); // ❌ 回傳 null!預設只搜尋公開方法
method.Invoke(new Student(), null); // 💥 NullReferenceException!
// ✅ 正確寫法:加上 NonPublic 和 Instance 旗標
Type type = typeof(Student); // 取得型別
MethodInfo method = type.GetMethod(
"GetStudentId",
BindingFlags.NonPublic | BindingFlags.Instance // ✅ 指定搜尋非公開的實例方法
);
if (method != null) // 先確認方法存在
{
object result = method.Invoke(new Student(), null); // 安全地呼叫
Console.WriteLine(result); // 印出結果
}
解釋: GetMethod() 預設只搜尋公開方法。要找私有方法必須明確告訴它搜尋範圍,就像在圖書館找書要去對的樓層。
❌ 錯誤 2:反射效能問題
// ❌ 錯誤寫法:在迴圈中反覆使用反射取得屬性
for (int i = 0; i < 100000; i++) // 十萬次迴圈
{
Type type = student.GetType(); // 每次都重新取得型別(浪費效能)
PropertyInfo prop = type.GetProperty("Name"); // 每次都重新搜尋屬性
string name = (string)prop.GetValue(student); // 每次都透過反射取值
}
// ✅ 正確寫法:快取反射結果,避免重複搜尋
Type type = student.GetType(); // 只取得一次型別
PropertyInfo prop = type.GetProperty("Name"); // 只搜尋一次屬性
for (int i = 0; i < 100000; i++) // 十萬次迴圈
{
string name = (string)prop.GetValue(student); // 重複使用已快取的 PropertyInfo
}
解釋: 反射的搜尋過程很耗效能。每次在迴圈裡重新搜尋就像每次打電話都重新查電話簿,直接把號碼記下來(快取)會快很多。
❌ 錯誤 3:AttributeUsage 設定錯誤
// ❌ 錯誤寫法:特性標記在錯誤的目標上
[AttributeUsage(AttributeTargets.Method)] // 限制只能用在方法上
public class MyAttribute : Attribute { }
[MyAttribute] // 💥 編譯錯誤!MyAttribute 只能用在方法上,不能用在類別上
public class MyClass { }
// ✅ 正確寫法:根據需求設定正確的 AttributeTargets
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] // ✅ 允許用在類別和方法上
public class MyAttribute : Attribute { }
[MyAttribute] // ✅ 可以用在類別上
public class MyClass
{
[MyAttribute] // ✅ 也可以用在方法上
public void MyMethod() { }
}
解釋: AttributeTargets 決定你的便利貼可以貼在哪裡。貼錯地方就像把「小心地滑」的牌子掛在天花板上。