【问题标题】:Why doesn't 'ref' and 'out' support polymorphism?为什么'ref'和'out'不支持多态性?
【发布时间】:2010-11-15 11:34:02
【问题描述】:

采取以下措施:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= compile-time error: 
                     // "The 'ref' argument doesn't match the parameter type"
    }

    void Foo(A a) {}

    void Foo2(ref A a) {}  
}

为什么会出现上述编译时错误? refout 参数都会发生这种情况。

【问题讨论】:

    标签: c# polymorphism out-parameters ref-parameters


    【解决方案1】:

    =============

    更新:我将此答案用作此博客条目的基础:

    Why do ref and out parameters not allow type variation?

    有关此问题的更多评论,请参阅博客页面。谢谢你的好问题。

    =============

    假设您有类AnimalMammalReptileGiraffeTurtleTiger,具有明显的子类关系。

    现在假设你有一个方法void M(ref Mammal m)M 可以读写m


    你能把Animal 类型的变量传递给M 吗?

    没有。该变量可能包含Turtle,但M 将假定它仅包含哺乳动物。 Turtle 不是 Mammal

    结论1ref参数不能“变大”。 (动物比哺乳动物多,因此变量变得“更大”,因为它可以包含更多的东西。)


    您可以将Giraffe 类型的变量传递给M 吗?

    没有。 M 可以写信给m,而M 可能想写一个Tigerm。现在您已将Tiger 放入一个实际上是Giraffe 类型的变量中。

    结论2ref参数不能“更小”。


    现在考虑N(out Mammal n)

    您可以将Giraffe 类型的变量传递给N 吗?

    没有。 N 可以写信给n,而N 可能想写一个Tiger

    结论3out参数不能“变小”。


    你能把Animal类型的变量传递给N吗?

    嗯。

    好吧,为什么不呢? N 无法从 n 读取,只能写入,对吧?您将Tiger 写入Animal 类型的变量,然后一切就绪,对吧?

    错了。规则不是“N 只能写到n”。

    简单来说,规则是:

    1) N 必须在N 正常返回之前写入n。 (如果N 抛出,所有赌注都关闭。)

    2) N 必须先向 n 写入内容,然后才能从 n 读取内容。

    这允许这样的事件序列:

    • 声明x 类型的字段Animal
    • x 作为out 参数传递给N
    • NTiger 写入n,这是x 的别名。
    • 在另一个线程上,有人将Turtle 写入x
    • N 尝试读取n 的内容,并在它认为是Mammal 类型的变量中发现Turtle

    显然我们想让它成为非法的。

    结论4out参数不能“变大”。


    最终结论refout 参数都不能改变它们的类型。否则会破坏可验证的类型安全性。

    如果您对基本类型理论中的这些问题感兴趣,请考虑阅读my series on how covariance and contravariance work in C# 4.0

    【讨论】:

    • +1。使用清楚地展示问题的真实世界类示例进行很好的解释(即 - 用 A、B 和 C 进行解释使得更难证明它为什么不起作用)。
    • 阅读这个思考过程让我感到谦卑。我想我最好回到书本上!
    • 在这种情况下,我们真的不能使用抽象类变量作为参数并传递其派生类对象!
    • 仍然,为什么out 参数不能“更大”?您描述的序列可以应用于任何变量,而不仅仅是out 参数变量。而且读者在尝试以Mammal 访问它之前,需要将参数值转换为Mammal,当然如果他不体贴,它可能会失败
    • 这里没有涉及的一种情况是在类型上使用隐式运算符。例如,我使用隐式运算符声明MyDouble,以便编译器知道如何进行从System.DoubleMyDouble 的转换。但是,即使分配 MyDouble result2 = result; 在下一行是合法的,我仍然不能调用 double.TryParse("0.1", out MyDouble result)
    【解决方案2】:

    因为在这两种情况下,您都必须能够为 ref/out 参数赋值。

    如果您尝试将 b 作为引用传递给 Foo2 方法,而在 Foo2 中您尝试设置 a = new A(),这将是无效的。
    同样的原因你不能写:

    B b = new A();
    

    【讨论】:

    • +1 开门见山,很好地解释了原因。
    【解决方案3】:

    您正在为协变(和逆变)的经典 OOP 问题苦苦挣扎,请参阅 wikipedia:尽管这一事实可能违背直觉预期,但在数学上不可能允许替换派生类代替可变(可分配)参数的基本参数(以及出于相同原因其项目可分配的容器),同时仍然尊重Liskov's principle。为什么会这样在现有答案中进行了概述,并在这些 wiki 文章及其链接中进行了更深入的探讨。

    OOP 语言在保持传统的静态类型安全的同时似乎这样做是“作弊”(插入隐藏的动态类型检查,或需要在编译时检查所有源代码以进行检查);基本的选择是:要么放弃这种协方差并接受从业者的困惑(就像 C# 在这里所做的那样),要么转向动态类型方法(就像第一个 OOP 语言 Smalltalk 所做的那样),或者转向不可变(单赋值)数据,就像函数式语言一样(在不变性下,您可以支持协方差,并且还可以避免其他相关难题,例如在可变数据世界中您不能拥有 Square 子类 Rectangle)。

    【讨论】:

      【解决方案4】:

      考虑:

      class C : A {}
      class B : A {}
      
      void Foo2(ref A a) { a = new C(); } 
      
      B b = null;
      Foo2(ref b);
      

      这会违反类型安全

      【讨论】:

      • 由于 var 存在问题,更多的是“b”的不明确推断类型。
      • 我猜第 6 行你的意思是 => B b = null;
      • @amiralles - 是的,var 完全错误。固定。
      【解决方案5】:

      虽然其他回复已经简洁地解释了这种行为背后的原因,但我认为值得一提的是,如果你真的需要做这种性质的事情,你可以通过将 Foo2 变成一个泛型方法来完成类似的功能,如下所示:

      class A {}
      
      class B : A {}
      
      class C
      {
          C()
          {
              var b = new B();
              Foo(b);
              Foo2(ref b); // <= no compile error!
          }
      
          void Foo(A a) {}
      
          void Foo2<AType> (ref AType a) where AType: A {}  
      }
      

      【讨论】:

        【解决方案6】:

        因为给Foo2 一个ref B 会导致对象格式错误,因为Foo2 只知道如何填充A 的一部分B

        【讨论】:

          【解决方案7】:

          编译器不是告诉你它希望你显式地转换对象,以便它可以确定你知道你的意图是什么吗?

          Foo2(ref (A)b)
          

          【讨论】:

          • 不能那样做,“一个 ref 或 out 参数必须是一个可赋值的变量”
          【解决方案8】:

          从安全的角度来看是有道理的,但如果编译器给出警告而不是错误,我会更喜欢它,因为通过引用传递的多态对象是合法的用途。例如

          class Derp : interfaceX
          {
             int somevalue=0; //specified that this class contains somevalue by interfaceX
             public Derp(int val)
              {
              somevalue = val;
              }
          
          }
          
          
          void Foo(ref object obj){
              int result = (interfaceX)obj.somevalue;
              //do stuff to result variable... in my case data access
              obj = Activator.CreateInstance(obj.GetType(), result);
          }
          
          main()
          {
             Derp x = new Derp();
             Foo(ref Derp);
          }
          

          这不会编译,但它会工作吗?

          【讨论】:

            【解决方案9】:

            如果您为您的类型使用实际示例,您会看到它:

            SqlConnection connection = new SqlConnection();
            Foo(ref connection);
            

            现在您的函数采用了祖先Object):

            void Foo2(ref Object connection) { }
            

            这可能有什么问题?

            void Foo2(ref Object connection)
            {
               connection = new Bitmap();
            }
            

            您刚刚设法将Bitmap 分配给您的SqlConnection

            这样不好。


            与其他人再试一次:

            SqlConnection conn = new SqlConnection();
            Foo2(ref conn);
            
            void Foo2(ref DbConnection connection)
            {
                conn = new OracleConnection();
            }
            

            你在SqlConnection 上塞了一个OracleConnection

            【讨论】:

              【解决方案10】:

              在我的情况下,我的函数接受了一个对象,但我无法发送任何东西,所以我只是做了

              object bla = myVar;
              Foo(ref bla);
              

              这行得通

              我的 Foo 在 VB.NET 中,它会检查里面的类型并执行很多逻辑

              如果我的答案重复但其他答案太长,我深表歉意

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2014-01-11
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-02-25
                • 1970-01-01
                相关资源
                最近更新 更多