如果您可以允许自己存储表达式而不是函数对象,那么更改代码以返回函数名称是相当容易的。
let hello (a: int) : int = a
let there (a: int) : int = a
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.DerivedPatterns
type [<Struct>] Function =
| F of Expr
static member Create ([<ReflectedDefinition>] x: Expr<_ -> _>) = x :> Expr |> F
let getName (F e) =
match e with
| Lambdas(_, Patterns.Call(_,methodInfo,_)) -> methodInfo.Name
| _ -> failwithf "Invalid pattern, expected Lambda(Call (...)) but found %A" e
let functions =
[|
Function.Create hello
Function.Create there
|]
let getNameOf i = functions.[i] |> getName
Function.Create hello 在这里将hello 表达式转换为getName 可以从中分解和提取名称的lambda 表达式。
现在,并不总是可以存储表达式,您可能希望从函数对象中提取函数。
困境在于,因为函数对象在 .NET 中并不真正存在,所以 F# 将函数包装在 FSharpFunc<_, _> 对象中,该对象的定义类似于 this:
[<AbstractClass>]
type FSharpFunc<'T,'U> =
/// <summary>Construct an instance of an F# first class function value </summary>
/// <returns>The created F# function.</returns>
new : unit -> FSharpFunc<'T,'U>
/// <summary>Invoke an F# first class function value with one argument</summary>
/// <param name="func"></param>
/// <returns>'U</returns>
abstract member Invoke : func:'T -> 'U
它没有告诉我们Invoke 是如何实现的,尽管在这种情况下它可能只是对我们关心的函数的调用。使用反编译器,我们可以提取其外观示例:
[Serializable]
internal sealed class functions@30 : FSharpFunc<int, int>
{
// Token: 0x06000025 RID: 37 RVA: 0x000024E8 File Offset: 0x000006E8
[CompilerGenerated]
[DebuggerNonUserCode]
internal functions@30()
{
}
// Token: 0x06000026 RID: 38 RVA: 0x000024F0 File Offset: 0x000006F0
public override int Invoke(int a)
{
return Program.hello(a);
}
}
Invoke 方法知道它需要调用的方法很明显。我们可以破解我们的方式。这种代码我通常不会发布,但如果它只是我需要为测试或一些内部工具工作的东西,我会考虑它。
使用 NuGet 上可用的dnlib(与dnSpy 反编译器后面的人相同)我们可以加载函数对象的类型,找到调用方法并处理指令以找到第一个调用指令并从中获取被调用方法的MethodInfo。 System.Reflection 为我们提供了我们需要的所有工具除了解析 IL 流很痛苦,所以 dnlib 很有价值。
open System.Reflection
open dnlib.DotNet
open dnlib.DotNet.Emit
let hello (a: int) : int = a
let there (a: int) : int = a
type [<Struct>] Function = F of (int -> int)
let create (f : int -> int) = F f
let getName (F d) =
let n = UTF8String "Invoke"
let t = d.GetType ()
// Check that the base type is FSharpFunc<_, _>
let bt = t.BaseType
let ebt = typeof<FSharpFunc<_, _>>.GetGenericTypeDefinition ()
let hasCorrectBase = bt.IsGenericType && (bt.GetGenericTypeDefinition () = ebt)
if not hasCorrectBase then
failwithf "Function object base type is expected to be FSharpFunc<_, _> but was %A" t.BaseType.AssemblyQualifiedName
// Loads the Module where the type of the function object is defined
let md = ModuleDefMD.Load t.Module
// Finds the type of the function object
let td = md.GetTypes () |> Seq.find (fun tt -> tt.AssemblyQualifiedName = t.AssemblyQualifiedName)
// Finds the Invoke method (assuming no overloads)
let md = td.Methods |> Seq.find (fun mm -> mm.Name = n)
let bd = md.Body
// Find the first call instruction, *likely* the call to method we are looking for the name for
let ins = bd.Instructions |> Seq.find (fun i -> i.OpCode = OpCodes.Call)
let op = ins.Operand :?> MethodDef
op.Name.ToString ()
let functions =
[|
create hello
create there
|]
let getNameOf i = functions.[i] |> getName
上述方法的一个巨大缺点是,它依赖于 F# 编译器团队可能决定随时更改的特定类和代码布局,但对于只需要运行一次或用于特定内部目的的代码,它可能很有用。