在这种情况下,我发现很难提供足够描述性的标题,因此我将让代码来完成大部分讨论。
考虑协方差,您可以在其中用派生类型代替基类。
class Base
{
}
class Derived : Base
{
}
可以将typeof(Base)
传递给此方法并将该变量设置为派生类型。
private void TryChangeType(Base instance)
{
var d = new Derived();
instance = d;
Console.WriteLine(instance.GetType().ToString());
}
但是,当从上述函数的调用方检查类型时,实例仍为Base
类型
private void CallChangeType()
{
var b = new Base();
TryChangeType(b);
Console.WriteLine(b.GetType().ToString());
}
我会假设由于对象本质上是固有引用,因此调用方变量现在将为Derived
类型。使呼叫者成为Derived
类型的唯一方法是像这样
ref
传递引用对象
private void CallChangeTypeByReference()
{
var b = new Base();
TryChangeTypeByReference(ref b);
Console.WriteLine(b.GetType().ToString());
}
private void TryChangeTypeByReference(ref Base instance)
{
var d = new Derived();
instance = d;
}
此外,我觉得这是一个常识,即将对象传递给方法,编辑props并将该对象向下传递到堆栈中将使所做的更改保持在堆栈中。这是有道理的,因为该对象是参考对象。
答案 0 :(得分:11)
您有很多困惑和错误的信念。让我们解决这个问题。
考虑协方差,您可以在其中用派生类型代替基类。
那不是协方差。这就是分配兼容性。苹果与水果类型的变量赋值兼容,因为您可以将苹果分配给这样的变量。同样,那不是协方差。协方差是类型上的变换保留分配兼容性关系的事实。苹果的序列可以用于需要水果的序列的地方,因为苹果是一种水果。 那是协方差。映射“苹果->苹果序列,水果->水果序列”是协变映射。
继续前进。
可以将
typeof(Base)
传递给此方法并将该变量设置为派生类型。
您将类型与实例混淆。您不将typeof(Base)
传递给此方法;您将对此实例的引用Base
传递给该实例。 typeof(Base)
的类型为System.Type
。
请注意,形式参数是变量。形式参数是一个新变量,它被初始化为实际参数或 argument 。
但是,当从上述函数的调用者检查类型时,实例仍将是Base类型。
正确。 参数的类型为Base
。将其复制到变量,然后重新分配变量。这与说没什么不同
Base x = new Base();
Base y = x;
y = new Derived();
现在x
仍然是Base
,而y
是Derived
。您两次分配了相同的变量;第二项任务获胜。这与您说a = 1; b = a; b = 2;
没什么不同,您不会期望a
之后就是2
,因为您过去说过b = a
。
我会假设由于对象本质上是固有引用,因此调用方变量现在将是Derived类型的。
这个假设是错误的。同样,您对同一个变量进行了两次分配,并且您拥有两个变量,一个在调用方中,一个在被调用方中。 变量包含值;对对象的引用是值。
使调用方成为派生类型的唯一方法是像这样通过ref传递引用对象
现在我们要解决问题的症结所在。
考虑这一点的正确方法是 ref为变量创建别名。正常的形式参数是新变量。 ref
形式参数使形式参数中的变量成为调用站点中变量的别名。因此,现在您有了一个变量,但是却有了两个名称,因为形式参数的名称是调用时该变量的 alias 。这与:
Base x = new Base();
ref Base y = ref x; // x and y are now two names for the same variable
y = new Derived(); // this assigns both x and y because there is only one variable, with two names
此外,我觉得这是一个常识,即将对象传递给方法,编辑props并将该对象向下传递到堆栈中将使所做的更改保持在堆栈中。这是有道理的,因为该对象是参考对象。
正确。
您在这里犯的错误很常见。对于C#设计团队来说,将变量别名功能命名为“ ref”是一个坏主意,因为这会引起混乱。对变量的引用是别名;它给变量起了另一个名字。对对象的引用是表示具有特定标识的特定对象的令牌。当您将两者混合使用时,会感到困惑。
通常要做的是不用ref
传递变量,尤其是当它们包含引用时。
什么导致对象永久改变堆栈的类型,仅当其通过引用传递时才如此?
现在,我们有最根本的困惑。 您将对象与变量混淆了。对象从不永远更改其类型!苹果是一个对象,苹果现在永远是苹果。苹果从不不会变成任何其他种类的水果。
立即停止认为变量是对象。您的生活会变得更好。内化这些规则:
ref
为现有变量赋予新名称现在,如果我们再次使用正确的术语问您一个问题,混乱立即消失:
什么使变量的值仅在
ref
传递的情况下才能沿堆栈更改其类型?
答案非常明确:
ref
传递的变量是另一个变量的别名,因此更改参数的值与更改调用站点上的变量的值相同如果我们不通过ref而是通过正常通过:
如果仍然不清楚,请在白板上开始绘制框,圆和箭头,其中对象是圆形,变量是框,对象引用是从变量到对象的箭头。通过ref
进行别名可以为现有的圈子添加一个新名称;不使用ref
进行呼叫会绕第二圈并复制箭头。一切都会有道理。
答案 1 :(得分:1)
这不是继承和多态性的问题,您看到的是按值传递和按引用传递之间的区别。
private void TryChangeType(Base instance)
先前方法的实例参数将是调用方的基本引用的副本。您可以更改所引用的对象,并且这些更改对调用者是可见的,因为这两个调用者都被调用者都引用了同一对象。但是,对引用本身的任何更改(例如将其指向新对象)都不会影响调用者的引用。这就是为什么当您通过引用传递时,它可以按预期工作的原因。
答案 2 :(得分:1)
当您调用TryChangeType()时,您会将引用的副本传递给“ b”到“实例”中。对“实例”的成员的任何更改都在您的调用方法中仍由“ b”引用的同一存储空间中进行。但是,命令“ instance = d” 重新分配了由“ instance”寻址的内存值。 “ b”和“实例”不再指向同一内存。当您返回CallChangeType时,“ b”仍然引用原始空间,因此引用了Type。 TryChangeTypeByReference将a引用传递到实际存储“ b”的指针值的位置。现在,重新分配“实例”会更改“ b”实际上指向的地址。
答案 3 :(得分:0)
当不通过引用传递时,基类对象的副本将在函数内部传递,并且此副本将在TryChangeType函数内部进行更改。当您打印基类实例的类型时,它仍然是“ Base”类型的,因为实例的副本已更改为“ Derived”类。
当您通过引用传递时,实例的地址(即实例本身)将传递给函数。因此,对该函数内部的实例所做的任何更改都是永久的。