【问题标题】:What exactly is a reference in C#C# 中的引用到底是什么
【发布时间】:2016-11-18 22:14:55
【问题描述】:

根据我现在的理解,我可以说 C# 中的引用是一种指向具有引用计数并知道类型兼容性的对象的指针。我的问题不是关于值类型与引用类型有何不同,而是更多关于如何实现引用。

我已经阅读this post 了解引用和指针之间的区别,但这并没有涵盖太多关于什么是引用,但与 C++ 中的指针相比,它更多地描述了它的属性.我也理解按引用传递和按值传递之间的区别(因为在 C# 中,对象默认按值传递,甚至是引用),但是当我试图向我的解释时,我很难理解什么是真正的引用同事们为什么不能像 Eric Lippert blog entry 中关于堆栈作为实现细节的那样将通过引用发送的参数存储在闭包内。

有人可以为我提供一个完整但希望简单的解释,说明 C# 中的引用到底是什么,以及它们是如何实现的?

编辑:这不是重复的,因为在Reference type in C# 中解释了引用如何工作以及它与值有何不同,但我要问的是如何在低级别定义引用。

【问题讨论】:

  • Reference type in C#的可能重复
  • 如果你认为引用计数有什么问题,你不明白
  • 关于“当我试图向我的同事解释为什么引用发送的参数不能存储在闭包中时”引用类型的变量和 byref 参数之间的重要区别是第一个一个会影响它所指向的对象的生命周期,而第二个则不会。它们有很多相似之处(它们拥有一个地址,如果 GC 执行堆压缩,它们将自动调整),但这些对于您的特定点来说并不那么重要。
  • @BenVoigt ref 参数可以引用一个装箱的值类型,不是吗?如果发生这种情况,ref 参数可能是唯一剩余的引用,如果这不会导致生命周期延长,那么事情就会变得非常糟糕。
  • 我会小心不要将ref 变量和对对象的引用混为一谈。它们在概念上完全不同。在 C# 中,对对象的引用指的是作为一个整体的对象ref 变量是另一个变量的别名。你可以看出它们在概念上是不同的,因为 C# 允许对它们进行不同的操作。您可以获取两个对object 的引用并在它们上调用ReferenceEquals 以确定它们是指相同的对象还是不同的对象。但是在 C# 中没有办法确定两个 ref 参数是否引用同一个变量。

标签: c# pointers reference


【解决方案1】:

根据我现在的理解,我可以说C#中的引用是一种指向对象的指针

如果“种类”是指“在概念上类似于”,是的。如果您的意思是“可以由”实施,是的。如果您的意思是“与 is-a-kind-of 有关系”,例如“字符串是一种对象”,那么没有。 C# 类型系统在引用类型和指针类型之间没有子类型关系。

有引用计数

CLR 的实现允许使用引用计数语义,但不是必需这样做,而且大多数都不需要。

并且知道类型兼容性。

我不确定这意味着什么。对象知道自己的实际类型。引用具有与可验证代码中的实际类型兼容的静态类型。兼容性检查由运行时的验证器在分析 IL 时实现。

我的问题不在于值类型与 引用类型,但更多关于如何实现引用。

毫无疑问,如何实现引用是一个实现细节。

有人可以为我提供一个完整但希望简单的解释,说明 C# 中真正的引用是什么

引用是C# 语言规范指定充当引用的事物。那就是:

  • 对象(引用类型)具有独立于其字段值的身份
  • 任何对象都可能有一个引用
  • 这样的引用是一个,它可以像任何其他值一样被传递
  • 平等比较为这些值实现
  • 两个引用相等当且仅当它们引用同一个对象;也就是说,引用具体化了对象身份
  • 有一个唯一的空引用,它不引用任何对象,并且不等于对对象的任何有效引用
  • 静态类型对于任何引用值总是已知的,包括空引用
  • 如果引用不为空,则引用的静态类型始终与所指对象的实际类型兼容。因此,例如,如果我们有一个对字符串的引用,那么引用的静态类型可以是字符串或对象或 IEnumerable,但不能是 Giraffe。 (显然,如果引用为 null,则没有引用对象具有类型。)

我可能遗漏了一些规则,但这些规则传达了我的想法。 引用是任何行为类似于引用的东西。这才是你应该专注的。引用是一种有用的抽象,因为它们是使对象身份独立于对象值的抽象

还有一点关于它们是如何实现的?

在实践中,C# 中引用类型的对象被实现为内存块,以包含对象信息的小标题开头,引用被实现为指向该块的指针。这个简单的方案由于我们有一个多代的标记和清除压缩收集器这一事实而变得更加复杂。它必须以某种方式知道引用图,以便在压缩堆时可以在内存中移动对象,而不会丢失引用标识。

作为练习,您可以考虑如何实施这样的方案。它构建角色以尝试弄清楚您将如何构建一个系统,其中引用是指针对象可以在内存中移动。你会怎么做?

当我试图向我的同事解释为什么引用发送的参数不能存储在闭包中时,我很难理解真正的引用是什么

这很棘手。重要的是要理解,在概念上,对变量的引用——C# 中的ref 参数——和对引用类型对象的引用在概念上是相似的但实际上是不同的东西。

在 C# 中,您可以将变量的引用视为 别名。也就是说,当你说

void M() 
{
  int x = 123;
  N(ref x);
}
void N(ref int y)
{ 
    y = 456;

基本上我们所说的是xy 是同一个变量的不同名称。 ref 是一个不幸的语法选择,因为它强调实现细节——在幕后,y 是一种特殊的“对变量的引用”类型——而不是操作的语义,这在逻辑上是 @ 987654327@ 现在只是x 的另一个名称;同一个变量有两个名字。

在 C# 中对变量的引用和对对象的引用不是一回事;您可以从它们具有不同语义的事实中看出这一点。您可以比较对对象的两个引用是否相等。但是C#中没有办法说:

static bool EqualAliases(ref int y, ref int z)
{
  return true iff y and z are both aliases for the same variable
}

你可以参考的方式:

static bool EqualReferences(object x, object y)
{
  return x == y;
}

在幕后,对变量的引用和对对象的引用都是由指针实现的。不同之处在于对变量的引用可能是指短期存储池(也称为“堆栈”)上的变量,而对对象的引用是指向堆分配对象头的指针。这就是 CLR 限制您将变量的引用存储到长期存储中的原因。它不知道你是否长期引用即将死去的东西。

要了解这两种引用是如何实现为指针的,最好的办法是从 C# 类型系统降级到作为其基础的 CLI 类型系统。 CLI 规范的第 8 章应该是有趣的阅读;它描述了不同类型的托管指针及其用途。

【讨论】:

  • 旁注:CLR 垃圾收集器能够更新指向(或进入)被移动的 gc-heap 对象的引用和指针。 C# 语言不支持跟踪指针(因此必须固定目标),但其他 .NET 语言支持。
【解决方案2】:

C# 中的引用与 C++ 引用非常相似。是的,确实,在下面有垃圾收集魔法,但我想说的是它是如何工作的,这是一个不同的、更大的话题。

C# 引用类似于 C++ 引用/不可变指针:没有指针算术等 - 但您可以重新分配它们(感谢 Ben!)。

我想说的是,在实践中,一个区别是由于指针在 C# 中通常不可用(unsafe 关键字及其关联的指针又是一个不同且更大的主题),您会发现自己使用“out”关键字来做指针对指针所做的事情。

此外,您断言引用携带类型信息是正确的。 C# 中的所有引用都来自 Object 类,该类本身具有 GetType() 方法。

但是,请注意,结构(通常被视为值而非引用)也具有 GetType()。

【讨论】:

  • C# 引用不是不可变的! (与 C++ 引用有很大不同)而且它们可以是 null 而不会违反任何不变量(与 C++ 的另一个很大不同)。
  • 谢谢本!更新了我的回复以反映您的更正
  • @BenVoigt:这有点令人困惑,因为很明显,C++ 在引用和内存管理方面在很多方面都与 C# 不同。我认为正确的类比是,C++ 引用不像 C# 中对对象的引用,而更像是 C# 中的 ref 变量。而那些在C#中是不可变的;当你说M(ref x) 而我们有void M(ref int y) 然后y 成为x 的别名,并且没有办法在M 中更改它,使y 成为其他东西的别名。跨度>
  • @EricLippert:就像 C# 7 中的 ref 变量一样,是的。在添加返回类型和本地变量之前,没有那么多。尽管请注意,在 C++ 中,引用(如指针)是位置的别名,而不是任何特定对象。在同一位置用相同类型的不同对象替换引用对象是完全合法的(在某些条件下)。
  • @EricLippert,您可能已经知道这一点,但我们的读者可能不知道:您的简单陈述暗示了正确的结论,但在 C++ 中,初始化和赋值之间存在区别,对象生命周期是急切的和确定的,得出这个结论的旅程要长得多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-06
  • 2017-10-12
  • 1970-01-01
  • 2023-03-16
  • 2020-05-03
  • 1970-01-01
相关资源
最近更新 更多