【问题标题】:Is it possible to get the name of a function from a reference to it stored in a struct?是否可以从存储在结构中的函数的引用中获取函数的名称?
【发布时间】:2019-12-31 19:41:22
【问题描述】:

正如您在下面的代码中看到的(我的失败尝试,在this question 中得到我想要的答案),我尝试从对存储在结构中的函数的引用中获取函数的名称,它本身存储在一个数组中。

但是我遇到了错误信息:

错误 FS3155:引用可能不涉及对捕获的局部变量的赋值或获取地址

那么,如果这不是我能做到的,有没有人有想法,如何做到?

open Microsoft.FSharp.Quotations

type DocumentGetter =
    static member GetName([<ReflectedDefinition>]x:Expr<_->_>) = 
        match x with
        | DerivedPatterns.Lambdas(_, Patterns.Call(_,methodInfo,_)) ->
            methodInfo.Name
        | _ -> "<unknown>"

type Foo = 
    struct 
        val func: int -> int
        new(f: int -> int) = { func = f }
    end

let f1 (a: int) : int = a

let lookupTable =
    [|
        Foo(f1)
    |]

let getNameOfEntry (index : int) : string =
    DocumentGetter.GetName(lookupTable.[index].func)

【问题讨论】:

  • 是什么导致你需要反思?反射是一个后门,使用反射的 F# 代码不太安全,重命名会影响行为并且 Liskov 替换不成立。
  • 我有一段代码我现在移植到 4 种不同的语言。用于基准测试。我名单上的最后一种语言是 rust。在 rust 中,带有函数值的结构的表的语法是如此不同,以至于我不得不做很多打字......相反,我有想法只使用我的 F# 模块并快速编写.fsx 脚本,用于从我在 F# 中已有的表中创建 Rust 语法。
  • 啊,我明白了。所以你想使用编译器的中间结果。我忍不住,但这是有道理的。
  • 顺便说一句,在 Julia 语言中,它的工作方式如下:typeof(lookupTable[1].func).name.mt.name。由于我的代码也有 julia 版本,因此我将使用 Julia 生成 Rust 内容。

标签: f#


【解决方案1】:

如果您可以允许自己存储表达式而不是函数对象,那么更改代码以返回函数名称是相当容易的。

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&lt;_, _&gt; 对象中,该对象的定义类似于 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# 编译器团队可能决定随时更改的特定类和代码布局,但对于只需要运行一次或用于特定内部目的的代码,它可能很有用。

【讨论】:

    猜你喜欢
    • 2016-06-22
    • 1970-01-01
    • 1970-01-01
    • 2019-05-31
    • 1970-01-01
    • 2018-02-12
    • 2011-05-29
    • 2011-08-12
    • 1970-01-01
    相关资源
    最近更新 更多