为什么 'ref' 和 'out' 不支持多态性?
-
05-07-2019 - |
题
采取以下措施:
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
论据。
解决方案
=============
更新:我用这个答案作为这篇博客文章的基础:
有关此问题的更多评论,请参阅博客页面。感谢您提出的好问题。
=============
假设你有课 Animal
, Mammal
, Reptile
, Giraffe
, Turtle
和 Tiger
, ,具有明显的子类关系。
现在假设你有一个方法 void M(ref Mammal m)
. M
既可以读写 m
.
可以传递一个类型的变量吗
Animal
到M
?
不。该变量可以包含一个 Turtle
, , 但 M
将假设它只包含哺乳动物。A 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
参数的类型可能有所不同。否则就会破坏可验证的类型安全性。
如果您对基本类型理论中的这些问题感兴趣,请考虑阅读 我的关于 C# 4.0 中协变和逆变如何工作的系列.
其他提示
因为在这两种情况下,您必须能够为ref / out参数赋值。
如果你尝试将b传递给Foo2方法作为参考,并且在Foo2中尝试分配a = new A(),这将无效。
你不能写的原因相同:
B b = new A();
你正在努力解决协方差(和逆变)的经典OOP问题,请参阅维基百科:就像这个事实可能违背直觉期望一样,在数学上不可能允许替换派生类来代替基础类的可变(可赋值)参数(以及其项目可分配的容器,同样的原因)同时仍然尊重 Liskov的原则。为什么会这样,在现有答案中勾勒出来,并在这些维基文章及其链接中进行更深入的探讨。
在保持传统静态类型安全的同时出现这种情况的OOP语言是“作弊”。 (插入隐藏的动态类型检查,或要求检查所有源的编译时检查);根本的选择是:要么放弃这种协方差并接受从业者的困惑(如C#在这里做的那样),要么转向动态类型化方法(作为第一个OOP语言,Smalltalk,确实如此),或者转向不可变(单一 - 赋值)数据,就像函数式语言一样(在不变性的情况下,你可以支持协方差,还可以避免其他相关的谜题,例如你在可变数据世界中不能拥有Square子类Rectangle这一事实)。考虑:
class C : A {}
class B : A {}
void Foo2(ref A a) { a = new C(); }
B b = null;
Foo2(ref b);
它会违反类型安全
虽然其他回复已经简洁地解释了这种行为背后的原因,但我认为值得一提的是,如果你真的需要做这种性质的事情,你可以通过将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 {}
}
因为给 Foo2
一个 ref B
会导致格式错误的对象,因为 Foo2
只知道如何填充 A
B
的一部分。
这不是编译器告诉你它希望你明确地转换对象,以便它可以确定你知道你的意图是什么吗?
Foo2(ref (A)b)
从安全角度来看是有道理的,但如果编译器发出警告而不是错误,我会更喜欢它,因为有合法使用的引用传递的多态对象。 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);
}
这不会编译,但会起作用吗?
如果您使用类型的实际示例,您会看到它:
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();
}
你塞了一个 OracleConnection
在 SqlConnection
之上。
在我的情况下,我的函数接受了一个对象,我无法发送任何内容,所以我只是做了
object bla = myVar;
Foo(ref bla);
这有效
My Foo在VB.NET中,它检查内部类型并执行大量逻辑
如果我的答案重复但有些人太长了,我道歉