等价隐式算子:为什么它们合法?
更新!
请参阅我对以下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是类或结构)组成。
我们正在从 Type2
( S )转换为 Type1
( T )。 因此,似乎这里D将包括示例中的所有三种类型: Type0
(因为它是S的基类), Type1
( T )和Type2
( S )。
找到适用的用户定义转换运算符集合U.此集合由D中的类或结构体声明的用户定义的隐式转换运算符组成,它们从包含S的类型转换为包含在T中的类型。如果U为空,转换未定义,发生编译时错误。
好吧,我们有两个运营商满足这些条件。 Type1
声明的版本符合要求,因为Type1
在D中 ,它从Type2
(显然包含S )转换为Type1
(显然包含在T中 )。 Type2
的版本也完全符合相同的原因。 所以U包括这两个运算符。
最后,关于在U中找到运算符的最具体的“源类型” SX :
如果U中的任何运算符从S转换,那么SX是S.
现在, U中的 两个运算符都从S转换 – 所以这告诉我SX是S.
这是不是意味着应该使用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可用。 在我的示例中,可能的目标是Int32
和UInt32
,并且两者都不是比另一个更好的匹配,因此这是转换失败的地方。 编译器无法知道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
,那么我们可能有从Type2
到Type1a
以及从Type0
到Type1
转换。这些仍然会给我们SX = Type2和TX = Type1,但我们实际上没有从Type2转换到Type1。 这没关系,因为这真的很模糊。 编译器不知道是将Type2转换为Type1a然后转换为Type1,还是首先转换为Type0,以便它可以将该转换用于Type1。
最终,它无法取得圆满成功。 你和我可以发布两个程序集。 他们我们可以开始使用彼此的组件,同时更新我们自己的组件。 然后我们可以在每个程序集中定义的类型之间提供隐式转换。 只有当我们发布下一个版本时,才能捕获它,而不是在编译时。
不试图禁止不能被禁止的东西是有利的,因为它使得清晰度和一致性(并且立法者有一个教训)。