等价隐式算子:为什么它们合法?

更新!

请参阅我对以下C#规范的一部分的剖析; 我想我必须遗漏一些东西,因为对我而言 ,我在这个问题中描述的行为实际上违反了规范。

更新2!

好的,经过进一步的反思,并根据一些评论,我想我现在明白了发生了什么。 规范中的“源类型”一词指的是从中转换的类型 – 即下面的示例中的Type2 – 这只是意味着编译器能够将候选缩小到两个定义的运算符(因为Type2是两种来源类型)。 但是,它无法进一步缩小选择范围。 所以规范中的关键词(适用于这个问题)是“源类型” ,我之前误解(我认为)是指“声明类型”。


原始问题

说我定义了这些类型:

 class Type0 { public string Value { get; private set; } public Type0(string value) { Value = value; } } class Type1 : Type0 { public Type1(string value) : base(value) { } public static implicit operator Type1(Type2 other) { return new Type1("Converted using Type1's operator."); } } class Type2 : Type0 { public Type2(string value) : base(value) { } public static implicit operator Type1(Type2 other) { return new Type1("Converted using Type2's operator."); } } 

然后说我这样做:

 Type2 t2 = new Type2("B"); Type1 t1 = t2; 

显然这是不明确的,因为不清楚应该使用哪个implicit运算符。 我的问题是 – 因为我看不出任何方法来解决这种歧义(它不像我可以执行一些显式演员来澄清我想要的版本),但上面的类定义确实编译 – 为什么编译器允许那些匹配implicit运算符的人呢?


解剖

好的,我将逐步完成Hans Passant引用的C#规范的摘录,试图理解这一点。

找到将考虑用户定义的转换运算符的类型集D. 该集由S(如果S是类或结构),S的基类(如果S是类)和T(如果T是类或结构)组成。

我们正在 Type2S )转换 Type1T )。 因此,似乎这里D将包括示例中的所有三种类型: Type0 (因为它是S的基类), Type1T )和Type2S )。

找到适用的用户定义转换运算符集合U.此集合由D中的类或结构体声明的用户定义的隐式转换运算符组成,它们从包含S的类型转换为包含在T中的类型。如果U为空,转换未定义,发生编译时错误。

好吧,我们有两个运营商满足这些条件。 Type1声明的版本符合要求,因为Type1D中 ,它从Type2 (显然包含S )转换为Type1 (显然包含在T中 )。 Type2的版本完全符合相同的原因。 所以U包括这两个运算符。

最后,关于在U中找到运算符的最具体的“源类型” SX

如果U中的任何运算符从S转换,那么SX是S.

现在, U中的 两个运算符S转换 – 所以这告诉我SXS.

这是不是意味着应该使用Type2版本?

可是等等! 我糊涂了!

难道我不能定义Type1的运算符版本,在这种情况下,唯一剩下的候选者是Type1的版本,但根据规范, SX会是Type2吗? 这似乎是一种可能的情况,其中规范要求不可能的东西(即,在实际上它不存在时应该使用在Type2声明的转换)。

我们真的不希望它只是一个编译时错误,只是为了定义可能导致歧义的转换。 假设我们更改Type0以存储double,并且由于某种原因,我们希望为有符号整数和无符号整数提供单独的转换。

 class Type0 { public double Value { get; private set; } public Type0(double value) { Value = value; } public static implicit operator Int32(Type0 other) { return (Int32)other.Value; } public static implicit operator UInt32(Type0 other) { return (UInt32)Math.Abs(other.Value); } } 

编译很好,我可以使用两个转换

 Type0 t = new Type0(0.9); int i = t; UInt32 u = t; 

但是,尝试float f = t是一个编译错误,因为任何隐式转换都可以用于获取整数类型,然后可以将其转换为float。

我们只希望编译器在实际使用时抱怨这些更复杂的歧义,因为我们希望编译上面的Type0。 为了保持一致性,更简单的歧义也应该在您使用它时产生错误,而不是在您定义它时。

编辑

由于汉斯删除了引用该规范的答案,因此这里快速浏览一下C#规范中确定转换是否含糊不清的部分,将U定义为可能完成工作的所有转换的集合:

  • 找到U中运算符的最具体的源类型SX:
    • 如果U中的任何运算符从S转换,那么SX是S.
    • 否则,SX是U中运算符的组合目标类型集中包含程度最大的类型。如果找不到包含最多的类型,则转换不明确并且发生编译时错误。

换句话说,我们更喜欢直接从S转换的转换,否则我们更喜欢将S转换为“最简单”的类型。 在这两个示例中,我们都有两个来自S的转换。 如果Type2没有转换,我们更喜欢从Type0转换为object转换。 如果没有一种类型显然是转换的更好选择,我们在这里失败了。

  • 找到U中运算符的最具体的目标类型TX:
    • 如果U中的任何运算符转换为T,则TX为T.
    • 否则,TX是U中运算符的组合目标类型集中最包含的类型。如果找不到包含最多的类型,则转换是不明确的,并且发生编译时错误。

同样,我们更倾向于直接转换为T,但我们会选择转换为T的“最简单”类型。在Dan的示例中,我们有两个转换为T可用。 在我的示例中,可能的目标是Int32UInt32 ,并且两者都不是比另一个更好的匹配,因此这是转换失败的地方。 编译器无法知道float f = t是表示float f = (float)(Int32)t还是float f = (float)(UInt32)t

  • 如果U只包含一个用户定义的转换运算符,它将从SX转换为TX,那么这是最具体的转换运算符。 如果不存在这样的运算符,或者如果存在多个这样的运算符,则转换是不明确的并且发生编译时错误。

在Dan的例子中,我们在这里失败了,因为我们有两次从SX到TX的转换。 如果我们在决定SX和TX时选择不同的转换,我们可能没有从SX到TX的转换。 例如,如果我们有一个从Type1派生的Type1a ,那么我们可能有从Type2Type1a以及从Type0Type1转换。这些仍然会给我们SX = Type2和TX = Type1,但我们实际上没有从Type2转换到Type1。 这没关系,因为这真的很模糊。 编译器不知道是将Type2转换为Type1a然后转换为Type1,还是首先转换为Type0,以便它可以将该转换用于Type1。

最终,它无法取得圆满成功。 你和我可以发布两个程序集。 他们我们可以开始使用彼此的组件,同时更新我们自己的组件。 然后我们可以在每个程序集中定义的类型之间提供隐式转换。 只有当我们发布下一个版本时,才能捕获它,而不是在编译时。

不试图禁止不能被禁止的东西是有利的,因为它使得清晰度和一致性(并且立法者有一个教训)。