【问题标题】:F# struct with arithmetic operators visible in C#带有算术运算符的 F# 结构在 C# 中可见
【发布时间】:2019-03-17 17:00:30
【问题描述】:

我正在尝试为 3 维点实现一个简单的结构。出于性能原因,我想把它作为一个结构。我想让它通用(至少对于System.Int32System.Double),并定义算术运算符。我打算在混合的 F#/C# 解决方案中使用它。

为了简化代码,将所有内容简化为 1D,这是我开始的:

[<Struct>]
type Point<'T> = 
    val X: 'T
    new(x) = { X = x}
    static member inline (+) (p1: Point<'U> when 'U: (static member (+): 'U * 'U -> 'U), p2: Point<'U>): Point<'U> = 
        Point<_>(p1.X + p2.X)

(+) 操作符上的类型参数需要写成 'U 而不是 'T,否则编译器会抱怨类型约束应该在 Point 的 'T 参数上。

这在 F# 中运行良好,我可以编写

let p1 = Point(2.0)
let sum = p1 + p1

在 C# 中:

var p = new Point<double>(1);
var sum = p + p;

那不编译,说Operator + cannot by applied to operands of type Point&lt;double&gt; and Point&lt;double&gt;

如果我在 dotpeek 中查看已编译的 F# 代码,就会发现 Point&lt;T&gt; 类型上的 + 运算符具有签名 +(Point&lt;???&gt;,Point&lt;???&gt;): Point&lt;???&gt;。我认为这是因为我必须用 'U 来编写类型约束 - 并且可能这也会触发 C# 编译器找不到运算符。

我可以通过定义一个带有操作符的 F# 模块来解决这个问题:

module Ops = 
    let inline Add(p1, p2: Point<_>) = p1 + p2

有了它,我可以通过 Ops.Add(p1,p2) 在 C# 中进行加法 - 但这显然不像 + 运算符那么容易阅读。

如果我尝试在顶层附加类型约束以进行添加,如下所示:

[<Struct>]
type Point<'T when 'T: (static member (+): 'T * 'T -> 'T)> = 
    val X: 'T
    new(x) = { X = x}
    static member inline (+) (p1: Point<'T>, p2: Point<'T>): Point<'T> = 
        Point<_>(p1.X + p2.X)

然后我在new(x) = { X = x} 收到编译器错误,说This code is not sufficiently generic. The type variable ^T when ^T: (static member...) could not be generalized because it would escape its scope

有没有什么方法可以让 C# 编译器满意地暴露 + 运算符?

更新: 运算符被标记为inline 的事实对结果没有重大影响:我可以定义

[<Struct>]
type Nothing<'T> =
    val X: 'T
    new(x) = { X = x}
    static member inline (+) (p1: Nothing<'T>, p2: Nothing<'T>): Nothing<'T> = 
        Nothing<_>(p1.X)

在 C# 中使用这个 + 操作符就好了:

var p1 = new Nothing<double>(1);
var sum = p1 + p1;

【问题讨论】:

    标签: c# f# operator-overloading


    【解决方案1】:

    inline 是 C# 不支持的 F# 编译器独有的功能。您必须(至少)明确定义 int32double 的运算符。

    inline 函数被 F# 编译器内联,将泛型参数替换为 编译时已知 类型。 generic 函数一般在运行时不可调用。 一些例外情况,其中实现执行动态调度(参见例如 AdditionDynamicdo 在运行时工作,但比它们的 inlined 等效项慢。另一个例外是非泛型 inline 函数,其中 inline 元数据被 C# 编译器简单地忽略。

    inline 具有传染性,所有调用inline 函数的函数都必须是inline 本身,在调用树中,被调用者的所有类型参数在编译时都是已知的。这解释了... would escape its scope 错误。因此,如果 inline 最终位于程序集边界,则这些函数不能从非 F# 项目中使用。

    更新:你是对的,标记运算符inline而不实际使用SRTP(Statically Resolved Type Parameters)没有效果:T类型没有约束,所以不需要在编译时已知:

    let inline Add(p1: Nothing<'T>, p2: Nothing<'T>) = Nothing<_>(p1.X)
    

    有签名

    val inline Add : p1:Nothing<'T> * p2:Nothing<'T> -> Nothing<'T>
    

    一旦你真正使用了inline 功能(这里是为了能够使用+),T 就会有一些限制:

    let inline Add(p1: Nothing<_>, p2: Nothing<_>) = p1.X + p2.X
    

    有签名

    val inline Add :
      p1:Nothing< ^a> * p2:Nothing< ^b> ->  ^c
        when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c)
    

    从 C# 的角度来看:

    // normal (runtime) generics: works
    let inline Add(p1: Nothing<'T>, p2: Nothing<'T>) = Nothing<_>(p1.X)
    // SRTPs decalred only: works
    let inline Add(p1: Nothing<(^T)>, p2: Nothing<(^T)>) = Nothing<_>(p1.X)
    // SRTPs (requires member (+)), needs type annotation, slow (using AdditionDynamic, that is reflection), may fail at runtime (if no + operator)
    let inline Add(p1: Nothing<(^T)>, p2: Nothing<(^T)>) = Nothing<_>(p1.X + p2.X)
    // needs type annotation, guaranteed failure at runtime (no dynamic polyfill)
    let inline Add(p1: Nothing<(^T)>, p2: Nothing<(^T)>) = Nothing<_>(p1.X %% p2.X) 
    

    现在,如果我们使用这些函数之一作为运算符,则只有非 SRTP 可以工作:

    static member inline (*) (p1: Nothing<'T>, p2: Nothing<'T>) : Nothing<'T> = Nothing(p1.X) // fine
    

    拥有 SRTP 声明就足够了:

    static member inline (+) (p1: Nothing<(^a)>, p2: Nothing<(^a)>) : Nothing<(^a)> = Nothing(p1.X) // can not be used fom C#
    

    这是为什么呢? C# 根本不支持泛型运算符dotnet/csharplang 中有一些请求),而在 F# 中它们可以是 inlined。事实上,如果我们查看反编译的源代码:

    // introduces new generic parameter `a`
    public static Nothing<a> operator +(Nothing<a> p1, Nothing<a> p2)
    // uses T from containing struct
    public static Nothing<T> operator *(Nothing<T> p1, Nothing<T> p2)
    

    'T when 'T: (static member (+): 'T * 'T -&gt; 'T) 约束是在结构上还是在操作符上没有影响:我们总是会得到一个 generic 操作符。

    【讨论】:

    • 我似乎无法证实 inline 是罪魁祸首,请参阅我对问题的更新。
    • @AntonSchwaighofer 我怀疑这可行,因为不涉及实际的内联函数。今天晚些时候我会检查的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-09-30
    • 2013-06-27
    • 2012-10-10
    • 2021-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多