使用 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 期望的内容,它将返回正确的方法信息。

这解决了上面列出的所有问题。