使用 F 引號的強大反射

反射很有用但很脆弱。考慮一下:

let mi  = typeof<System.String>.GetMethod "StartsWith"

這種程式碼的問題是:

  1. 程式碼不起作用,因為 String.StartsWith 有幾個過載
  2. 即使現在沒有任何過載,庫的更高版本也可能會新增導致執行時崩潰的過載
  3. Rename methods 這樣的重構工具被反射破壞了。

這意味著我們會遇到編譯時已知的執行時崩潰。這似乎不是最理想的。

使用 F# 引用可以避免上述所有問題。我們定義了一些輔助函式:

open FSharp.Quotations
open System.Reflection

let getConstructorInfo (e : Expr<'T>) : ConstructorInfo =
  match e with
  | Patterns.NewObject (ci, _) -> ci
  | _ -> failwithf "Expression has the wrong shape, expected NewObject (_, _) instead got: %A" e

let getMethodInfo (e : Expr<'T>) : MethodInfo =
  match e with
  | Patterns.Call (_, mi, _) -> mi
  | _ -> failwithf "Expression has the wrong shape, expected Call (_, _, _) instead got: %A" e

我們使用這樣的函式:

printfn "%A" <| getMethodInfo <@ "".StartsWith "" @>
printfn "%A" <| getMethodInfo <@ List.singleton 1 @>
printfn "%A" <| getConstructorInfo <@ System.String [||] @>

這列印:

Boolean StartsWith(System.String)
Void .ctor(Char[])
Microsoft.FSharp.Collections.FSharpList`1[System.Int32] Singleton[Int32](Int32)

<@ ... @> 意味著不是在 F# 中執行表示式而是生成表示表示式的表示式樹。<@ "".StartsWith "" @> 生成一個如下所示的表示式樹:Call (Some (Value ("")), StartsWith, [Value ("")])。此表示式樹匹配 getMethodInfo 期望的內容,它將返回正確的方法資訊。

這解決了上面列出的所有問題。