我正在尝试为3维点实现一个简单的结构。出于性能原因,我希望将其作为结构。我想让它通用(至少对于System.Int32
和System.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#编译器独有的功能。您将(至少)明确定义int32
和double
的运算符。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/