6

更新!

请参阅下面我对 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的基类)、Type1T)和Type2S)。

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

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

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

如果 U 中的任何运算符从 S 转换,则 SX 是 S。

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

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

可是等等!我很困惑!

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

4

2 回答 2

2

最终,它不能完全成功地被禁止。你和我可以发布两个程序集。我们可以开始使用彼此的程序集,同时更新我们自己的程序集。然后我们每个人都可以在每个程序集中定义的类型之间提供隐式转换。只有当我们发布下一个版本时,才能捕捉到这一点,而不是在编译时。

不试图禁止不能禁止的东西是有好处的,因为它有助于清晰和一致(这对立法者来说是一个教训)。

于 2010-08-24T23:52:57.627 回答
1

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

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因为任何一个隐式转换都可以用来获取一个整数类型,然后可以将其转换为浮点数。

我们只希望编译器在实际使用这些更复杂的歧义时抱怨它们,因为我们希望上面的 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 的转换可用。在我的示例中,可能的目标是Int32and UInt32,两者都不是比另一个更好的匹配,所以这就是转换失败的地方。编译器无法知道是float f = t手段float f = (float)(Int32)t还是float f = (float)(UInt32)t.

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

在 Dan 的示例中,我们在这里失败了,因为我们还有两个从 SX 到 TX 的转换。如果我们在决定 SX 和 TX 时选择不同的转换,我们可能没有从 SX 到 TX 的转换。例如,如果我们有一个Type1a派生自Type1,那么我们可能会有从Type2toType1a和 from Type0to的转换。Type1这些仍然会给我们 SX=Type2 和 TX=Type1,但我们实际上没有从 Type2 到 Type1 的任何转换。这没关系,因为这确实是模棱两可的。编译器不知道是先将 Type2 转换为 Type1a,然后再转换为 Type1,还是先转换为 Type0,以便它可以使用该转换为 Type1。

于 2010-08-25T00:12:51.073 回答