EnriqueMark
設計模式與架構

反射

2026-04 → 2026-09 更新 繁體中文

反射在有些地方看上去有點像是委託,但兩者的差異還是蠻大的。委託的關鍵是編譯器安全,而反射則是犧牲了編譯時的安全檢查,而帶來了執行時的靈活性:它通過字串來呼叫或取到指定的物件,而可以讓程式在執行時獲取或修改任何程式碼內容——本質上來說,反射是一種「元操作」,用程式碼來操作程式碼,也是字面意義上的「反射」。

就拿我之前寫的排程場景來說:假設我的排程是子類繼承,下級只關心具體的job,但如果我的場景是判斷有多少個job繼承了基類——目前的做法是用反射去掃。在我剛瞭解到委託機制後,我的設想是,能不能在基類裡面採用生命週期管理的模式,給子類包在一個run裡,排程實際執行的是run,然後基類定義一個事件,這個run就是——到這一步時,我就突然發現了——事實上在基類裡預定義的委託屬性,在每次被繼承並例項化時,這個屬性都會重置,那這樣的話事件佇列就空了。

因此啟動時直接掃描程式本身,去確認有多少子類繼承了這個基類,才能交給排程框架去統一註冊。否則要知道job有多少,就得先獲取job,要獲取job就得先註冊,註冊就得知道到底有多少job——最終就是沒有任何job會被註冊。例項化之前框架就得知道有幾個,那我不可能在這之後才讓job進事件,所以必須在啟動前就知道,那這個只能靠反射去掃有多少類繼承了base。這是一個型別發現問題,而不是委託所承載的物件之間的互動(訂閱者通過釋出者暴露的委託來操作)問題。

順帶一說,反射有一個地方在用法上跟泛型很像,而且更加靈活。比如在編譯期,我可以完全不指定型別,反而是通過執行時給什麼就接什麼的形式來承接型別。但這裡有代價,如果我寫 where T : IConverter ,那好歹是規定了介面的結構到底啥樣,寫的有問題,編譯期間就會報錯。而反射的話,我只能進行型別轉換——那就是我和你約定,這裡需要是這個型別,但你到底是不是真用這個型別,我是沒法硬性限制的(下面的介面來自 控制反轉):

Type type = Type.GetType("MyApp.XmlConverter");
IConverter converter = (IConverter)Activator.CreateInstance(type);
// 如果 XmlConverter 根本沒實現 IConverter,這裡直接 InvalidCastException

這裡萬一給進來的就是不符合約定,那只有執行的時候才知道錯誤。但如果我用的是泛型,那編譯期間就會直接報錯提示,根本就不會到最後階段。因此反射其實多少有了點動態語言的味道——就跟python一樣,不到執行的時候你是不知道會不會報錯的。這也是靈活性和穩健性的權衡,在有些場景下確實需要靈活性,但必須明確的知道其代價和邊界。如果可以的話,提前在這種靈活性上做好錯誤封裝:就算結果不可預料,也要控制損失範圍,使其盡可能最小化。