126

采取以下措施:

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

4

10 回答 10

172

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

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

为什么 ref 和 out 参数不允许类型变化?

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

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

假设您有类Animal, Mammal, Reptile,和Giraffe,具有明显的子类关系。TurtleTiger

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


您可以将类型的变量传递AnimalM吗?

不,该变量可以包含 a Turtle,但M会假定它仅包含哺乳动物。ATurtle不是Mammal.

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


您可以将类型的变量传递GiraffeM吗?

M可以写到,m并且M可能要写到。现在您已将 a放入一个实际上是 type 的变量中。TigermTigerGiraffe

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


现在考虑N(out Mammal n)

您可以将类型的变量传递GiraffeN吗?

N可以写n,并且N可能想写一个Tiger

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


您可以将类型的变量传递AnimalN吗?

唔。

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

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

规则是,简要地说:

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

2)N必须先写一些东西才能nn.

这允许以下事件序列:

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

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

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


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

如果您对基本类型理论中的这些问题感兴趣,请考虑阅读我关于 C# 4.0 中协变和逆变如何工作的系列文章

于 2009-07-30T15:18:13.783 回答
31

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

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

B b = new A();
于 2009-07-30T14:57:39.857 回答
10

您正在努力解决协变(和逆变)的经典 OOP 问题,请参阅wikipedia:尽管这一事实可能违背直觉的期望,但在数学上不可能允许用派生类代替基类来代替可变(可分配)参数(和也出于同样的原因,其物品可分配的容器)同时仍然尊重Liskov 的原则。现有答案中概述了为什么会这样,并在这些 wiki 文章和其中的链接中进行了更深入的探讨。

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

于 2009-07-30T15:21:18.443 回答
4

考虑:

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

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

B b = null;
Foo2(ref b);

它会违反类型安全

于 2009-07-30T15:20:42.517 回答
4

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

因为给出Foo2aref B会导致对象格式错误,因为只Foo2知道如何填充A.B

于 2009-07-30T14:57:44.583 回答
0

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

Foo2(ref (A)b)
于 2009-07-30T19:16:52.810 回答
0

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

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

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

于 2012-03-14T10:58:22.197 回答
0

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

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

现在你有了你的函数来接受祖先ie Object):

void Foo2(ref Object connection) { }

这可能有什么问题?

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

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

那不好。


与其他人再试一次:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

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

你塞满OracleConnection了你的SqlConnection.

于 2017-08-02T19:54:50.483 回答
0

就我而言,我的函数接受了一个对象,但我无法发送任何东西,所以我只是做了

object bla = myVar;
Foo(ref bla);

那行得通

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

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

于 2018-06-20T11:47:45.620 回答