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

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

[<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<double> and Point<double>

如果我在dotpeek中查看已编译的F#代码,它表示类型+上的Point<T>运算符具有签名+(Point<???>,Point<???>): Point<???>。我想这是我不得不用'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;

最佳答案

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

inline函数由F#编译器内联,用编译时已知的类型替换通用参数。泛型函数通常在运行时不可调用。
一些例外情况,其中实现会进行动态分配(例如,参见AdditionDynamic)在运行时可以运行,但比其inline d等效项要慢。另一个例外是非通用inline函数,其中inline元数据仅被C#编译器忽略。

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

更新:没错,在不实际使用SRTP(Statically Resolved Type Parameters)的情况下标记运算符inline无效:类型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#中,可以将它们inline d。实际上,如果我们看一下反编译的源代码:

// 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 -> 'T)约束是在结构上还是在运算符上没有任何区别:我们将始终以通用运算符结束。

关于c# - 在C#中可见带有算术运算符的F#结构,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/52779926/

10-16 09:09