为什么将基 class 设置为等于派生类型仅在设置它的范围内有效?
Why does setting a base class equal to a derived type only work within scope of where it is set?
我发现很难为这个场景想出一个足够描述性的标题,所以我让代码来做大部分的讨论。
考虑协变性,您可以在其中用派生类型替换基数 class。
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());
}
我假设因为 object 本质上是引用,所以调用者变量现在是 Derived
类型。让调用者成为类型 Derived
的唯一方法是通过 ref
传递引用 object 像这样
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;
}
此外,我觉得将 object 传递给方法、编辑 props 并将该 object 传递到堆栈中将保留所做的更改是众所周知的常识。这是有道理的,因为 object 是一个引用 object。
是什么导致 object 永久更改堆栈中的类型,仅当它通过引用传递时?
我们知道 class 是引用类型,所以通常当我们传递一个类型时,我们传递的是一个引用,但是只传递 b
和 ref b
是有区别的,可以理解为:
- 在第一种情况下,它通过值引用传递,这意味着在内部创建一个指向内存位置的单独指针,现在当基础class对象被分配给派生的 class 对象,它开始指向内存中的另一个对象,当该方法 returns 时,只保留原始指针,它提供与 Base class 相同的实例,当新的为垃圾回收创建的指针已关闭
- 然而,当对象作为 ref 传递时,这是将 reference 传递给内存中的引用 ,这就像指向指针的指针,就像 C 或 C++ 中的双指针一样,当发生变化时,它实际上改变了原始内存分配,因此您会看到差异
对于第一个显示相同结果值的方法,必须从方法返回,旧对象应开始指向新的派生对象
以下是对您的程序的修改,以获得案例 1 中的预期结果:
private Base TryChangeType(Base instance)
{
var d = new Derived();
instance = d;
Console.WriteLine(instance.GetType().ToString());
return instance;
}
private void CallChangeType()
{
var b = new Base();
b = TryChangeType(b);
Console.WriteLine(b.GetType().ToString());
}
以下是两种情况的图片参考:
当您不通过引用传递时,基础 class 对象的副本会在函数内部传递,并且此副本会在 TryChangeType 函数内部更改。当您打印基础 class 实例的类型时,它仍然是类型 "Base" 的类型,因为实例的副本已更改为 "Derived" class.
当你通过引用传递时,实例的地址即实例本身将被传递给函数。因此,对函数内的实例所做的任何更改都是永久性的。
这不是继承和多态性的问题,您看到的是按值传递和按引用传递之间的区别。
private void TryChangeType(Base instance)
上述方法的实例参数将是调用者 Base 引用的 副本。您可以更改引用的对象,并且这些更改对调用者可见,因为调用者和被调用者都引用同一个对象。但是,对引用本身的任何更改(例如将其指向一个新对象)都不会影响调用者的引用。这就是当您通过引用传递时它按预期工作的原因。
你有很多混乱和错误的信念。让我们解决这个问题。
Consider covariance where you can substitute a derived type for a base class.
那不是协方差。即分配兼容性。 Apple 与 Fruit 类型的变量赋值兼容,因为您可以将 Apple 赋值给此类变量。同样,这不是协方差。协方差是这样一个事实,即类型 上的 转换 保留 分配兼容性关系。 苹果序列可以用在需要水果序列的地方因为苹果是一种水果. 即协方差。映射 "apple --> sequence of apples, fruit --> sequence of fruit" 是一个 协变映射 .
继续前进。
Passing in typeof(Base)
to this method and setting that variable to the derived type is possible.
您混淆了类型和实例。您没有将 typeof(Base)
传递给此方法;您将 Base
的引用传递给此实例。 typeof(Base)
属于 System.Type
.
类型
正如您正确注意到的那样,形式参数是变量。形式参数是一个新变量,它被初始化为实际参数 aka argument.
However, when checking the type from the caller of the above function, the instance will still be of type 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
。
I would assume since objects are inherently reference by nature that the caller variable would now be of type Derived.
这个假设是错误的。同样,您对同一变量进行了两次赋值,并且您有两个变量,一个在调用者中,一个在被调用者中。 变量包含值;对对象的引用是值.
The only way to get the caller to be type Derived is to pass a reference object by ref like so
现在我们进入了问题的关键。
考虑这一点的正确方法是 ref 为变量创建别名。一个正常的形式参数是一个新变量。 ref
形式参数使形式参数中的变量成为调用站点变量的别名。所以现在你有一个变量但是它有两个名字,因为形式参数的名字是一个别名 用于调用时的变量。这等同于:
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
Further more, I feel like it's common knowledge that passing in an object to a method, editing props, and passing that object down the stack will keep the changes made down the stack. This makes sense as the object is a reference object.
正确。
你在这里犯的错误很常见。 C# 设计团队将变量别名功能命名为 "ref" 是一个坏主意,因为这会引起混淆。对 变量 的引用构成 别名 ;它为变量提供了另一个名称。对 object 的引用是一个标记,表示具有特定标识的特定对象。当您将两者混合时,它会变得混乱。
通常的做法是 不 通过 ref
传递变量,尤其是当它们包含引用时。
What causes an object to permanently change type down the stack, only if it's passed in by reference?
现在我们有了最根本的困惑。 您混淆了对象和变量。一个对象 永远不会 改变它的类型,永远!一个苹果是一个对象,一个苹果现在永远是一个苹果。苹果 永远不会 变成任何其他种类的水果。
现在不要再认为变量是对象了。你的生活会变得更好。内化这些规则:
- 变量是存储值的存储位置
- 对对象的引用是值
- 对象有一个永不改变的类型
ref
为现有变量赋予新名称
- 分配给一个变量会改变它的值
现在,如果我们使用正确的术语再次问您的问题,混淆会立即消失:
What causes the value of a variable to change its type down the stack, only if it's passed in by ref
?
现在答案很明确了:
ref
传递的一个变量是另一个变量的别名,所以改变参数的值和调用处改变变量的值是一样的
- 将对象引用分配给变量会更改该变量的值
- 对象具有特定类型
如果我们不通过 ref 而是正常通过:
- 一个正常传递的值被复制到一个新的变量,形参
- 我们现在有两个没有联系的变量;改变其中之一不会改变另一个。
如果还是不清楚,开始在白板上画框、圆圈和箭头,其中对象是圆圈,变量是框,对象引用是从变量到对象的箭头。通过 ref
为现有圈子取一个新名称;没有 ref
的调用会生成第二个圆圈并复制箭头。到时候就明白了。
当您调用 TryChangeType() 时,您将 reference 的副本传递给 "b" 到 "instance"。对 "instance" 的 members 的任何更改都在您的调用方法中仍由 "b" 引用的同一内存 space 中进行。但是,命令 "instance = d" 重新分配了由 "instance" 寻址的内存 的值。 "b" 和“实例不再指向相同的内存。当您 return 到 CallChangeType 时,"b" 仍然引用原始的 space,因此引用类型。
TryChangeTypeByReference 将引用传递给实际存储 "b" 的指针值的位置。重新分配 "instance" 现在会更改 "b" 实际指向的地址。
我发现很难为这个场景想出一个足够描述性的标题,所以我让代码来做大部分的讨论。
考虑协变性,您可以在其中用派生类型替换基数 class。
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());
}
我假设因为 object 本质上是引用,所以调用者变量现在是 Derived
类型。让调用者成为类型 Derived
的唯一方法是通过 ref
传递引用 object 像这样
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;
}
此外,我觉得将 object 传递给方法、编辑 props 并将该 object 传递到堆栈中将保留所做的更改是众所周知的常识。这是有道理的,因为 object 是一个引用 object。
是什么导致 object 永久更改堆栈中的类型,仅当它通过引用传递时?
我们知道 class 是引用类型,所以通常当我们传递一个类型时,我们传递的是一个引用,但是只传递 b
和 ref b
是有区别的,可以理解为:
- 在第一种情况下,它通过值引用传递,这意味着在内部创建一个指向内存位置的单独指针,现在当基础class对象被分配给派生的 class 对象,它开始指向内存中的另一个对象,当该方法 returns 时,只保留原始指针,它提供与 Base class 相同的实例,当新的为垃圾回收创建的指针已关闭
- 然而,当对象作为 ref 传递时,这是将 reference 传递给内存中的引用 ,这就像指向指针的指针,就像 C 或 C++ 中的双指针一样,当发生变化时,它实际上改变了原始内存分配,因此您会看到差异
对于第一个显示相同结果值的方法,必须从方法返回,旧对象应开始指向新的派生对象
以下是对您的程序的修改,以获得案例 1 中的预期结果:
private Base TryChangeType(Base instance)
{
var d = new Derived();
instance = d;
Console.WriteLine(instance.GetType().ToString());
return instance;
}
private void CallChangeType()
{
var b = new Base();
b = TryChangeType(b);
Console.WriteLine(b.GetType().ToString());
}
以下是两种情况的图片参考:
当您不通过引用传递时,基础 class 对象的副本会在函数内部传递,并且此副本会在 TryChangeType 函数内部更改。当您打印基础 class 实例的类型时,它仍然是类型 "Base" 的类型,因为实例的副本已更改为 "Derived" class.
当你通过引用传递时,实例的地址即实例本身将被传递给函数。因此,对函数内的实例所做的任何更改都是永久性的。
这不是继承和多态性的问题,您看到的是按值传递和按引用传递之间的区别。
private void TryChangeType(Base instance)
上述方法的实例参数将是调用者 Base 引用的 副本。您可以更改引用的对象,并且这些更改对调用者可见,因为调用者和被调用者都引用同一个对象。但是,对引用本身的任何更改(例如将其指向一个新对象)都不会影响调用者的引用。这就是当您通过引用传递时它按预期工作的原因。
你有很多混乱和错误的信念。让我们解决这个问题。
Consider covariance where you can substitute a derived type for a base class.
那不是协方差。即分配兼容性。 Apple 与 Fruit 类型的变量赋值兼容,因为您可以将 Apple 赋值给此类变量。同样,这不是协方差。协方差是这样一个事实,即类型 上的 转换 保留 分配兼容性关系。 苹果序列可以用在需要水果序列的地方因为苹果是一种水果. 即协方差。映射 "apple --> sequence of apples, fruit --> sequence of fruit" 是一个 协变映射 .
继续前进。
Passing in
typeof(Base)
to this method and setting that variable to the derived type is possible.
您混淆了类型和实例。您没有将 typeof(Base)
传递给此方法;您将 Base
的引用传递给此实例。 typeof(Base)
属于 System.Type
.
正如您正确注意到的那样,形式参数是变量。形式参数是一个新变量,它被初始化为实际参数 aka argument.
However, when checking the type from the caller of the above function, the instance will still be of type 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
。
I would assume since objects are inherently reference by nature that the caller variable would now be of type Derived.
这个假设是错误的。同样,您对同一变量进行了两次赋值,并且您有两个变量,一个在调用者中,一个在被调用者中。 变量包含值;对对象的引用是值.
The only way to get the caller to be type Derived is to pass a reference object by ref like so
现在我们进入了问题的关键。
考虑这一点的正确方法是 ref 为变量创建别名。一个正常的形式参数是一个新变量。 ref
形式参数使形式参数中的变量成为调用站点变量的别名。所以现在你有一个变量但是它有两个名字,因为形式参数的名字是一个别名 用于调用时的变量。这等同于:
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
Further more, I feel like it's common knowledge that passing in an object to a method, editing props, and passing that object down the stack will keep the changes made down the stack. This makes sense as the object is a reference object.
正确。
你在这里犯的错误很常见。 C# 设计团队将变量别名功能命名为 "ref" 是一个坏主意,因为这会引起混淆。对 变量 的引用构成 别名 ;它为变量提供了另一个名称。对 object 的引用是一个标记,表示具有特定标识的特定对象。当您将两者混合时,它会变得混乱。
通常的做法是 不 通过 ref
传递变量,尤其是当它们包含引用时。
What causes an object to permanently change type down the stack, only if it's passed in by reference?
现在我们有了最根本的困惑。 您混淆了对象和变量。一个对象 永远不会 改变它的类型,永远!一个苹果是一个对象,一个苹果现在永远是一个苹果。苹果 永远不会 变成任何其他种类的水果。
现在不要再认为变量是对象了。你的生活会变得更好。内化这些规则:
- 变量是存储值的存储位置
- 对对象的引用是值
- 对象有一个永不改变的类型
ref
为现有变量赋予新名称- 分配给一个变量会改变它的值
现在,如果我们使用正确的术语再次问您的问题,混淆会立即消失:
What causes the value of a variable to change its type down the stack, only if it's passed in by
ref
?
现在答案很明确了:
ref
传递的一个变量是另一个变量的别名,所以改变参数的值和调用处改变变量的值是一样的- 将对象引用分配给变量会更改该变量的值
- 对象具有特定类型
如果我们不通过 ref 而是正常通过:
- 一个正常传递的值被复制到一个新的变量,形参
- 我们现在有两个没有联系的变量;改变其中之一不会改变另一个。
如果还是不清楚,开始在白板上画框、圆圈和箭头,其中对象是圆圈,变量是框,对象引用是从变量到对象的箭头。通过 ref
为现有圈子取一个新名称;没有 ref
的调用会生成第二个圆圈并复制箭头。到时候就明白了。
当您调用 TryChangeType() 时,您将 reference 的副本传递给 "b" 到 "instance"。对 "instance" 的 members 的任何更改都在您的调用方法中仍由 "b" 引用的同一内存 space 中进行。但是,命令 "instance = d" 重新分配了由 "instance" 寻址的内存 的值。 "b" 和“实例不再指向相同的内存。当您 return 到 CallChangeType 时,"b" 仍然引用原始的 space,因此引用类型。 TryChangeTypeByReference 将引用传递给实际存储 "b" 的指针值的位置。重新分配 "instance" 现在会更改 "b" 实际指向的地址。