采取以下措施:
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) {}
}
为什么会发生上述编译时错误? ref
和out
参数都会发生这种情况。
答案 0 :(得分:165)
=============
更新:我使用此答案作为此博客条目的基础:
Why do ref and out parameters not allow type variation?
有关此问题的更多评论,请参阅博客页面。谢谢你提出的好问题。
=============
假设您有类Animal
,Mammal
,Reptile
,Giraffe
,Turtle
和Tiger
,并且有明显的子类关系。
现在假设你有一个方法void M(ref Mammal m)
。 M
可以同时读写m
。
您可以将
Animal
类型的变量传递给M
吗?
没有。该变量可能包含Turtle
,但M
会假设它只包含哺乳动物。 Turtle
不是Mammal
。
结论1 :ref
参数无法变得“更大”。 (动物比哺乳动物多,所以变量越来越大,因为它可以包含更多东西。)
您可以将
Giraffe
类型的变量传递给M
吗?
没有。 M
可以写入m
,M
可能希望将Tiger
写入m
。现在,您已将Tiger
放入实际为Giraffe
类型的变量。
结论2 :ref
参数无法变得“更小”。
现在考虑N(out Mammal n)
。
您可以将
Giraffe
类型的变量传递给N
吗?
没有。 N
可以写信给n
,N
可能会写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
。 N
将Tiger
写入n
,这是x
的别名。 Turtle
写入x
。 N
尝试阅读n
的内容,并在其认为属于Turtle
类型的变量时发现Mammal
。显然,我们希望将其视为非法。
结论4 :out
参数无法变得“更大”。
最终结论: ref
和out
参数都不会改变其类型。否则就是打破可验证的类型安全。
如果基本类型理论中的这些问题让您感兴趣,请考虑阅读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 B
,Foo2
会导致格式错误,因为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中,它检查内部的类型并执行很多逻辑
如果我的回答重复但其他人的回答太长,我深表歉意。