【问题标题】:C# compiler bug? Why doesn't this implicit user-defined conversion compile?C#编译器错误?为什么这个隐式用户定义的转换不能编译?
【发布时间】:2009-07-30 19:29:01
【问题描述】:

给定以下结构:

public struct Foo<T>
{
   public Foo(T obj) { }

   public static implicit operator Foo<T>(T input)
   {
      return new Foo<T>(input);
   }
}

这段代码编译:

private Foo<ICloneable> MakeFoo()
{
    string c = "hello";
    return c; // Success: string is ICloneable, ICloneable implicitly converted to Foo<ICloneable>
}

但是这段代码不能编译——为什么?

private Foo<ICloneable> MakeFoo()
{
    ICloneable c = "hello";
    return c; // Error: ICloneable can't be converted to Foo<ICloneable>. WTH?
}

【问题讨论】:

    标签: c# compiler-construction


    【解决方案1】:

    显然,当其中一种类型是接口时,隐式用户定义转换不起作用。来自 C# 规范:


    6.4.1 允许的用户定义转换

    C# 只允许声明某些用户定义的转换。特别是,不可能重新定义已经存在的隐式或显式转换。 对于给定的源类型 S 和目标类型 T,如果 S 或 T 是可空类型,则令 S0 和 T0 引用它们的基础类型,否则 S0 和 T0 分别等于 S 和 T。仅当满足以下所有条件时,才允许类或结构声明从源类型 S 到目标类型 T 的转换:

    • S0 和 T0 是不同的类型。
    • S0 或 T0 是发生运算符声明的类或结构类型。
    • S0 和 T0 都不是接口类型
    • 排除用户定义的转换,不存在从 S 到 T 或从 T 到 S 的转换。

    在您的第一种方法中,两种类型都不是接口类型,因此用户定义的隐式转换有效。

    规范不是很清楚,但在我看来,如果涉及的类型之一是接口类型,编译器甚至不会尝试查找任何用户定义的隐式转换。

    【讨论】:

    • 哇。多么奇怪的要求。我想听听 Lippert 或 Skeet 或其他一些 C# 专家为什么接口类型不适用于此;这种奇怪的现象肯定有一个很好的理由。
    • 经过一些实验,我发现接口似乎是这里的关键。奇怪的是,对于编译器来说,弄清楚经常要做的事情似乎并不是一种精神上的飞跃。嗯。
    • 我对第一个示例如何编译的解释非常感兴趣。鉴于第 6.4.4 节中的规则,鉴于ICloneable 不包含string(因为ICloneable 是一个接口)和Foo&lt;string&gt; 不包含,我看不出它如何选择Foo&lt;ICloneable&gt; 转换由Foo&lt;ICloneable&gt; (因为没有从Foo&lt;string&gt;Foo&lt;ICloneable&gt; 的隐式转换)。也许 6.1.9“涉及类型参数的隐式转换”正在以某种方式发挥作用?
    • 是的,我还没有完全理解;我仍然想知道这是否是编译器中的错误。我已经联系了 Eric Lippert 并请他在这里插话,因为他是 StackOverflow 的常客。也许他可以向我们展示所有的光芒。
    【解决方案2】:

    (跟进已接受答案的 cmets。)

    是的,这是规范中非常非常令人困惑的部分。特别是关于“包含类型”的全部内容存在严重缺陷。几年来我一直在努力寻找时间将整个部分完全重写为更连贯的内容,但它从来没有成为一个足够高的优先级。

    基本上我们在这里得到的是一个矛盾;我们不存在涉及接口的用户定义隐式转换,但显然在这种情况下并非如此;有一个用户定义的从 IC 到 Foo&lt;IC&gt; 的隐式转换,一个字符串通过该转换转到 Foo&lt;IC&gt; 的事实证明了这一点。

    我们真正应该更好地强调的是您引用的这句话:

    特别是,不可能 重新定义一个已经存在的隐式 或显式转换。

    这就是整个事情的动机;当您实际上正在调用用户定义的方法时,不希望您认为您正在执行保留表示的类型测试。例如考虑这种变化:

    interface IBar {}
    interface IFoo : IBar {}
    class Foo<T> : IFoo
    {
       public static explicit operator Foo<T>(T input) { whatever }
    }
    class Blah : Foo<IBar> {}
    ...
    IBar bar = new Blah();  
    Foo<IBar> foo = (Foo<IBar>)bar;
    

    现在,是否调用了用户定义的显式转换?该对象确实是从 Foo 派生的,所以您希望它不会;这应该是一个简单的类型测试和一个引用分配,而不是对辅助方法的调用。 对接口值的强制转换总是被视为类型测试,因为几乎总是有可能该对象确实属于该类型并且确实实现了该接口。我们不想否认您进行廉价的表示保留转换的可能性。

    【讨论】:

    • 好的,关于接口转换的特殊规则,以及这些规则的原因,有助于我理解这一点。感谢您的澄清,埃里克。
    • 我应该补充一下,我的代码示例“感觉”应该可以工作。有点像协方差,它是你的大脑认为应该起作用但没有起作用的事情之一。呵呵。
    • 这是否意味着在上面的示例中 (Foo)bar 被视为“强制转换”而不是显式转换?
    • 鉴于永远不会有从结构到接口的表示转换转换,在这种情况下不允许用户定义转换的原因是什么?当List&lt;T&gt;.GetEnumerator 用于鸭子类型的foreach 时,它返回一个结构非常好,但结果结构的行为并不像IEnumerator&lt;T&gt; 应该的那样。让该结构可转换为 IEnumerator&lt;T&gt;(产生适当的类类型实现)似乎比要求它以装箱后行为异常的方式实现 IEnumerator&lt;T&gt; 更好。
    猜你喜欢
    • 2023-03-13
    • 1970-01-01
    • 2018-12-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-08
    • 1970-01-01
    相关资源
    最近更新 更多