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

时间:2010-08-24 22:48:57

标签: c# .net implicit ambiguity implicit-conversion

更新!

请参阅下面我对部分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 取值

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

但是等等!我很困惑!

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

2 个答案:

答案 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可用。在我的示例中,可能的目标是Int32UInt32,两者都不是更好的匹配,因此这是转换失败的地方。编译器无法知道float f = tfloat 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。