为什么'ref'和'out'不支持多态?

时间:2009-07-30 14:54:52

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

采取以下措施:

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参数都会发生这种情况。

10 个答案:

答案 0 :(得分:165)

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

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

Why do ref and out parameters not allow type variation?

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

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

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

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


  

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

没有。该变量可能包含Turtle,但M会假设它只包含哺乳动物。 Turtle不是Mammal

结论1 ref参数无法变得“更大”。 (动物比哺乳动物多,所以变量越来越大,因为它可以包含更多东西。)


  

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

没有。 M可以写入mM可能希望将Tiger写入m。现在,您已将Tiger放入实际为Giraffe类型的变量。

结论2 ref参数无法变得“更小”。


现在考虑N(out Mammal n)

  

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

没有。 N可以写信给nN可能会写Tiger

结论3 out参数无法变得“更小”。


  

您可以将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的内容,并在其认为属于Turtle类型的变量时发现Mammal

显然,我们希望将其视为非法。

结论4 out参数无法变得“更大”。


最终结论 refout参数都不会改变其类型。否则就是打破可验证的类型安全。

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

答案 1 :(得分:29)

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

如果你尝试将b传递给Foo2方法作为参考,并且在Foo2中你试图给a = new A(),这将是无效的。
你不能写的原因相同:

B b = new A();

答案 2 :(得分:10)

你正在努力解决协方差(和逆变)的经典OOP问题,请参阅wikipedia:正如这个事实可能违背直觉预期一样,在数学上不可能允许替换派生类似于替换可变(可赋值)参数的基类(以及其项目可分配的容器,出于同样的原因),同时仍然尊重Liskov's principle。为什么会这样,在现有答案中勾勒出来,并在这些维基文章及其链接中进行更深入的探讨。

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

答案 3 :(得分:4)

考虑:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

它会违反类型安全

答案 4 :(得分:3)

虽然其他回复已经简洁地解释了这种行为背后的原因,但我认为值得一提的是,如果你真的需要做这种性质的事情,你可以通过将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 {}  
}

答案 5 :(得分:2)

由于Foo2 ref BFoo2会导致格式错误,因为A只知道如何填充B的{​​{1}}部分。

答案 6 :(得分:0)

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

Foo2(ref (A)b)

答案 7 :(得分:0)

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

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

这不会编译,但会起作用吗?

答案 8 :(得分:0)

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

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的{​​{3}}。

答案 9 :(得分:0)

在我的情况下,我的函数接受了一个对象,我什么也不能发送,所以我简单地做到了

object bla = myVar;
Foo(ref bla);

那行得通

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

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