【问题标题】:Why is the C# compiler using the parent type of a value returned by an implicit operator to invoke overloaded operators为什么 C# 编译器使用隐式运算符返回的值的父类型来调用重载运算符
【发布时间】:2020-09-30 06:54:10
【问题描述】:

我有以下两个课程:

public class Parent
{
    public static Parent operator +(Parent l, Parent r)
    {
        return new Parent(); //do something meaningful
    }
}

public class Child: Parent
{
    public static Child operator +(Child l, Parent r)
    {
        return new Child(); //do something meaningful, child related
    }
}

然后我有一个包装类,它使用implicit 转换来返回包装后的值:

public class Wrapper<T>
{
    private T value;

    public T Value => value;

    public static implicit operator T(Wrapper<T> wrapper)
    {
        return wrapper.value;
    }
}

然后我将两者组合如下:

public class Usage
{
    private Parent someField;
    private Wrapper<Child> wrappedValue;

    public void UseOperatorWithImplicitConversion()
    {
        //Child sum1 = wrappedValue + someField; //<-- compilation error
        Parent sum2 = wrappedValue + someField;

        Child temp = wrappedValue; //works but defeats the purpose of reduced verbosity
        Child sum3 = temp + someField;
    }
}

我期待 sum1 线路能够正常工作。我查看了生成的 IL,似乎类型在那里:

IL_0001: ldarg.0      // this
IL_0002: ldfld        class Example.Wrapper`1<class Example.Child> Example.Usage::wrappedValue
IL_0007: call         !0/*class Example.Child*/ class Example.Wrapper`1<class Example.Child>::op_Implicit(class Example.Wrapper`1<!0/*class Example.Child*/>)
IL_000c: ldarg.0      // this
IL_000d: ldfld        class Example.Parent Example.Usage::someField
IL_0012: call         class Example.Parent Example.Parent::op_Addition(class Example.Parent, class Example.Parent)
IL_0017: stloc.0      // sum2

虽然IL_0012 是对Parentop_Addition 的调用,而不是Child

这里有什么我遗漏的吗?

我正在使用 .NET Framework 4.6.1 C# 7.2

【问题讨论】:

  • 我会说编译器首先选择一个可用的 operator + 候选者(参见语言规范的 binary operator overload resolution 部分)并使用它可以工作的第一个。
  • @GuruStron 但我希望如果我从Parent 中删除运算符重载,那么它将编译为第二好的是来自Child 的运算符。不是这样的,我仍然得到错误。
  • 是的,没错,错过了。

标签: c# generics operator-overloading implicit-conversion overload-resolution


【解决方案1】:

在尝试确定在特定情况下调用哪个运算符重载时,C# 不会考虑每个类中每个可能的用户定义运算符重载。它只考虑在操作数之一的(编译时)类型中定义的运算符重载。它不考虑任何操作数隐式转换到的每种类型中定义的运算符。

【讨论】:

    【解决方案2】:

    我认为@Servy 的回答是正确的。我只是想通过提供 C# 规范的链接并添加解释来扩展它。

    Binary operator overload resolution用于确定一组候选运算符:

    x op y 形式的操作,其中op 是可重载的二进制文件 运算符,xX 类型的表达式,y 是类型的表达式 Y,处理如下:

    • XY 提供的候选用户定义运算符集,用于 确定操作operator op(x,y)。该套装包括 X 提供的候选运算符与候选运算符的联合 Y提供的运算符,每个运算符都使用Candidate user-defined operators的规则确定。如果XY 是同一类型,或者如果XY 派生自一个共同的基本类型,然后是共享的候选运算符 只在组合集中出现一次。
    • (其他项目不重要)

    在这行代码中

    Parent sum2 = wrappedValue + someField;
    

    xWrapper&lt;Child&gt; 类型的表达式,yParent 类型的表达式。

    根据二元运算符重载决议的规则,一组候选运算符是这两种类型提供的运算符的并集。对于这些类型中的每一种,使用规则Candidate user-defined operators 确定一组候选运算符:

    给定一个类型T 和一个操作operator op(A),其中op 是一个 可重载运算符和A 是一个参数列表,候选集 由Toperator op(A) 提供的用户定义运算符确定 如下:

    • 对于T 中的所有operator op 声明以及此类运算符的所有提升形式,如果至少有一个运算符适用于参数列表A (Applicable function member),则 候选运算符由T 中的所有此类适用运算符组成。
    • (其他项目不重要)

    Applicable function member:

    一个函数成员被称为一个适用的函数成员 当以下所有条件都为真时,关于参数列表A

    • 对于A中的每个参数,参数的参数传递方式(即valuerefout)与参数传递方式相同 对应的参数,和
      • 对于值形参或形参数组,存在从实参到类型的隐式转换 (Implicit conversions) 对应的参数。
      • (其他项目不重要)

    使用这些规则,我们可以得出以下结论:

    • 对于Wrapper&lt;Child&gt; 类型,一组用于操作operator +(Wrapper&lt;Child&gt;, Parent) 的候选运算符为空。
    • 对于类型Parent,一组用于操作operator +(Wrapper&lt;Child&gt;, Parent) 的候选运算符由Parent 中定义的单个运算符组成:operator +(Parent, Parent)。此运算符适用(根据Applicable function member)作为候选运算符,因为存在从Wrapper&lt;Child&gt;Parent 的隐式转换。

    所以我们有一个候选运算符operator +(Parent, Parent),因此它适用于我们的案例。

    我们还可以得出以下结论:

    • 一组候选运算符仅针对操作中使用的操作数的实际类型定义。在此过程中不考虑操作数可以隐式转换为的类型。因此,Child 类中的 operator +(Child, Parent) 不被视为候选运算符。
    • 在定义运算符是否适用于其参数时使用隐式转换。因此operator +(Parent, Parent) 被定义为适用于operator +(Wrapper&lt;Child&gt;, Parent) 的情况。

    【讨论】:

    • 感谢您花时间详细说明。当另一个用户链接它时,我已经阅读了规范。虽然我看到op_Implicit 首先在IL_0007 中被调用,然后是op_Addition。我希望编译器知道当时的类型,不是吗?
    • 编译器知道Wrapper&lt;Child&gt;Child 的隐式转换,但是根据规范,在形成一组候选运算符时不使用这种隐式转换。编译器仅在操作中使用的Wrapper&lt;Child&gt;Parent 类中查找候选运算符,而不在操作数Wrapper&lt;Child&gt;Parent 可以隐式转换到的类中查找候选运算符。
    • 因此,当您在 Parent 类中评论或删除 operator + 时,您的代码不会编译。因为编译器不会将来自 Childoperator + 视为您的示例中的操作 Parent sum2 = wrappedValue + someField; 的候选运算符。
    • 是的,when 的最后一部分在解析过程中使用了隐式运算符,我似乎并不清楚。
    • 你是对的。隐式转换用于使operator +(Parent, Parent) 适用于操作Parent sum2 = wrappedValue + someField;
    猜你喜欢
    • 2023-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-29
    • 1970-01-01
    • 2012-04-26
    • 1970-01-01
    • 2013-01-04
    相关资源
    最近更新 更多