请参阅下面我对部分C#规范的剖析;我想我必须遗漏一些东西,因为我看起来我在这个问题中描述的行为实际上违反了规范。
好的,经过进一步思考,根据一些评论,我想我现在明白了发生了什么。规范中的“源类型”一词指的是从转换的类型 - 即我在下面的示例中的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 是取值
这是否意味着应该使用Type2
版本?
但是等等!我很困惑!
我不能仅定义Type1
版本的运算符,在这种情况下,唯一剩下的候选者将是Type1
的版本,但是根据规范 SX 将是Type2
?这似乎是一种可能的情况,其中规范强制要求不可能的东西(即,Type2
中声明的转换应该被使用,而实际上它不存在)。
答案 0 :(得分:2)
最终,不能取得圆满成功。你和我可以发布两个程序集。他们我们可以开始使用彼此的组件,同时更新我们自己的组件。然后我们可以在每个程序集中定义的类型之间提供隐式转换。只有当我们发布下一个版本时,才能捕获它,而不是在编译时。
有一个优势就是不要试图禁止那些不能被禁止的东西,因为这样可以保持清晰和一致性(并且这对立法者来说也是一个教训)。
答案 1 :(得分:1)
我们真的不希望它只是一个编译时错误,只是为了定义可能引起歧义的转换。假设我们更改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。为了保持一致性,更简单的歧义也应该在您使用它时产生错误,而不是在您定义错误时。
修改强>
由于Hans删除了引用该规范的答案,因此这里快速浏览一下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的转换。例如,如果我们从[{1}}派生了Type1a
,那么我们可能会从Type1
转换为Type2
,从Type1a
转换为Type0
这些仍然会给我们SX = Type2和TX = Type1,但我们实际上没有从Type2到Type1的任何转换。这没关系,因为这真的很模糊。编译器不知道是将Type2转换为Type1a然后强制转换为Type1,还是首先强制转换为Type0,以便它可以将该转换用于Type1。