等效的隐式运算符:为什么它们是合法的?

发布于 2024-09-15 16:49:43 字数 2750 浏览 2 评论 0原文

更新!

请参阅下面我对 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;

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


剖析

好的,我将逐步浏览 Hans Passant 在一个例子中引用的 C# 规范的摘录。尝试理解这一点。

找到类型集 D,从中 用户定义的转换运算符将 予以考虑。该套装由 S (如果 S 是类或结构),基数 S 的类别(如果 S 是一个类别)和 T (如果 T 是类或结构)。

我们正在将 Type2 (S) 转换为 Type1 ( T)。因此,这里的 D 似乎会包含示例中的所有三种类型:Type0(因为它是 S 的基类)、类型1 (T) 和类型2 (S)。

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

好吧,我们有两个满足这些条件的操作员。 Type1 中声明的版本满足要求,因为 Type1 位于 D 中,并且它从 Type2 转换而来(显然包含S)到Type1(显然包含在T中)。由于完全相同的原因,Type2 中的版本也满足要求。因此 U 包括这两个运算符。

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

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

现在,U 中的两个运算符都从 S< 转换而来/strong>——所以这告诉我SXS

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

但是等等!我很困惑!

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

Update!

See my dissection of a portion of the C# spec below; I think I must be missing something, because to me it looks like the behavior I'm describing in this question actually violates the spec.

Update 2!

OK, upon further reflection, and based on some comments, I think I now understand what's going on. The words "source type" in the spec refer to the type being converted from -- i.e., Type2 in my example below -- which simply means that the compiler is able to narrow the candidates down to the two operators defined (since Type2 is the source type for both). However, it cannot narrow the choices any further. So the key words in the spec (as it applies to this question) are "source type", which I previously misinterpreted (I think) to mean "declaring type."


Original Question

Say I have these types defined:

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.");
    }
}

Then say I do this:

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

Obviously this is ambiguous, as it is not clear which implicit operator should be used. My question is -- since I cannot see any way to resolve this ambiguity (it isn't like I can perform some explicit cast to clarify which version I want), and yet the class definitions above do compile -- why would the compiler allow those matching implicit operators at all?


Dissection

OK, I'm going to step through the excerpt of the C# spec quoted by Hans Passant in an attempt to make sense of this.

Find the set of types, D, from which
user-defined conversion operators will
be considered. This set consists of S
(if S is a class or struct), the base
classes of S (if S is a class), and T
(if T is a class or struct).

We're converting from Type2 (S) to Type1 (T). So it seems that here D would include all three types in the example: Type0 (because it is a base class of S), Type1 (T) and Type2 (S).

Find the set of applicable
user-defined conversion operators, U.
This set consists of the user-defined
implicit conversion operators declared
by the classes or structs in D that
convert from a type encompassing S to
a type encompassed by T. If U is
empty, the conversion is undefined and
a compile-time error occurs.

All right, we've got two operators satisfying these conditions. The version declared in Type1 meets the requirements because Type1 is in D and it converts from Type2 (which obviously encompasses S) to Type1 (which is obviously encompassed by T). The version in Type2 also meets the requirements for exactly the same reasons. So U includes both of these operators.

Lastly, with respect to finding the most specific "source type" SX of the operators in U:

If any of the operators in U convert from S, then SX is S.

Now, both operators in U convert from S -- so this tells me that SX is S.

Doesn't this mean that the Type2 version should be used?

But wait! I'm confused!

Couldn't I have only defined Type1's version of the operator, in which case, the only remaining candidate would be Type1's version, and yet according to the spec SX would be Type2? This seems like a possible scenario in which the spec mandates something impossible (namely, that the conversion declared in Type2 should be used when in fact it does not exist).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

只为守护你 2024-09-22 16:49:43

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

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

Ultimately, it can't be prohibitted with complete success. You and I could publish two assemblies. They we could start using each other's assembles, while updating our own. Then we could each provide implicit casts between types defined in each assembly. Only when we release the next version, could this be caught, rather than at compile time.

There's an advantage in not trying to ban things that can't be banned, as it makes for clarity and consistency (and there's a lesson for legislators in that).

孤云独去闲 2024-09-22 16:49:43

我们真的不希望它成为一个编译时错误,只是为了定义可能导致歧义的转换。假设我们更改 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 转换。在我的示例中,可能的目标是 Int32UInt32,两者都不比另一个更好匹配,因此这就是转换失败的地方。编译器无法知道 float f = t 是否意味着 float f = (float)(Int32)tfloat 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。

We don't really want it to be a compile-time error just to define conversions which might cause ambiguity. Suppose that we alter Type0 to store a double, and for some reason we want to provide separate conversions to signed integer and unsigned integer.

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);
    }

}

This compiles fine, and I can use use both conversions with

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

However, it's a compile error to try float f = t because either of the implicit conversions could be used to get to an integer type which can then be converted to float.

We only want the compiler to complain about these more complex ambiguities when they're actually used, since we'd like the Type0 above to compile. For consistency, the simpler ambiguity should also cause an error at the point you use it rather than when you define it.

EDIT

Since Hans removed his answer which quoted the spec, here's a quick run through the part of the C# spec that determines whether a conversion is ambiguous, having defined U to be the set of all the conversions which could possibly do the job:

  • Find the most specific source type, SX, of the operators in U:
    • If any of the operators in U convert from S, then SX is S.
    • Otherwise, SX is the most encompassed type in the combined set of target types of the operators in U. If no most encompassed type can be found, then the conversion is ambiguous and a compile-time error occurs.

Paraphrased, we prefer a conversion which converts directly from S, otherwise we prefer the type which is "easiest" to convert S to. In both examples, we have two conversions from S available. If there were no conversions from Type2, we'd prefer a conversion from Type0 over one from object. If no one type is obviously the better choice to convert from, we fail here.

  • Find the most specific target type, TX, of the operators in U:
    • If any of the operators in U convert to T, then TX is T.
    • Otherwise, TX is the most encompassing type in the combined set of target types of the operators in U. If no most encompassing type can be found, then the conversion is ambiguous and a compile-time error occurs.

Again, we'd prefer to convert directly to T, but we'll settle for the type that's "easiest" to convert to T. In Dan's example, we have two conversions to T available. In my example, the possible targets are Int32 and UInt32, and neither is a better match than the other, so this is where the conversion fails. The compiler has no way to know whether float f = t means float f = (float)(Int32)t or float f = (float)(UInt32)t.

  • If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator. If no such operator exists, or if more than one such operator exists, then the conversion is ambiguous and a compile-time error occurs.

In Dan's example, we fail here because we have two conversions left from SX to TX. We could have no conversions from SX to TX if we chose different conversions when deciding SX and TX. For example, if we had a Type1a derived from Type1, then we might have conversions from Type2 to Type1a and from Type0 to Type1 These would still give us SX=Type2 and TX=Type1, but we don't actually have any conversion from Type2 to Type1. This is OK, because this really is ambiguous. The compiler doesn't know whether to convert Type2 to Type1a and then cast to Type1, or cast to Type0 first so that it can use that conversion to Type1.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文