为什么将基类设置为等于派生类型仅在设置范围内起作用?

时间:2018-09-14 17:24:55

标签: c# polymorphism covariance

在这种情况下,我发现很难提供足够描述性的标题,因此我将让代码来完成大部分讨论。

考虑协方差,您可以在其中用派生类型代替基类。

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并将该对象向下传递到堆栈中将使所做的更改保持在堆栈中。这是有道理的,因为该对象是参考对象。

什么导致对象仅在通过引用传递的情况下才能沿堆栈永久更改类型?

4 个答案:

答案 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,而yDerived。您两次分配了相同的变量;第二项任务获胜。这与您说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”类。

当您通过引用传递时,实例的地址(即实例本身)将传递给函数。因此,对该函数内部的实例所做的任何更改都是永久的。