【问题标题】:Why is string a reference type?为什么字符串是引用类型?
【发布时间】:2010-09-08 07:19:54
【问题描述】:

为什么字符串是引用类型,即使它通常是原始数据类型,例如 int、float 或 double。

【问题讨论】:

标签: c# string primitive-types reference-type


【解决方案1】:

除了Dan贴出来的原因:

值类型,根据定义是那些将值存储在自身中的类型,而不是引用其他地方的值。这就是为什么值类型被称为“值类型”而引用类型被称为“引用类型”的原因。所以你的问题真的是“为什么一个字符串引用它的内容而不是简单地包含它的内容?”

这是因为值类型有一个很好的属性,即给定值类型的每个实例在内存中的大小相同。

那又怎样?为什么这是一个不错的属性?好吧,假设字符串是可以是任意大小的值类型,并考虑以下内容:

string[] mystrings = new string[3];

这个由三个字符串组成的数组的初始内容是什么?值类型没有“null”,因此唯一明智的做法是创建一个包含三个空字符串的数组。那将如何在内存中布局?想一想。你会怎么做?

现在假设你说

string[] mystrings = new string[3];
mystrings[1] = "hello";

现在我们在数组中有“”、“hello”和“”。 “hello”在内存中的什么位置? 到底分配给 mystrings[1] 的插槽有多大?数组及其元素的内存必须放在某处

这让 CLR 有以下选择:

  • resize 每次更改数组的一个元素时,都会复制整个数组,其大小可能是兆字节
  • 不允许创建大小未知的值类型数组
  • 不允许创建未知大小的值类型

CLR 团队选择了后者。 将字符串变成引用类型意味着您可以高效地创建它们的数组。

【讨论】:

  • 显而易见的方法是在声明时为字符串分配允许的最大长度,并为涌入的 OutOfMemoryExceptions 做好准备。
  • 很遗憾我不能收藏答案。
  • @Eric Lippert:我同意你的观点,但如果 string 是一个值类型,其唯一字段是引用了一个内部字符数组的 char[] 呢?由于数组是引用类型,string 的大小将是一个常量sizeof(IntPtr) + 任何填充,不是吗?那么有一个字符串数组就没有问题了。还是我搞错了?
  • @Ani:正确。 这就是引用类型。唯一字段是引用的值类型和引用之间没有有效的区别!显然它们具有完全相同的位,因为值类型的位只是其成员的位,如果它具有引用类型作为其成员,那么它就只有引用的位。如果位完全相同,那么为什么要使用结构呢?只要有参考就可以了。
  • @Eric Lippert:好的,这完全有道理。谢谢。
【解决方案2】:

哎呀,这个答案被接受了,然后我改变了它。我可能应该在底部包含原始答案,因为这是 OP 所接受的。

新答案

更新:事情是这样的。 string 绝对需要表现得像引用类型。到目前为止,所有答案都提到了造成这种情况的原因:string 类型没有固定大小,将字符串的全部内容从一种方法复制到另一种方法是没有意义的,string[] 数组否则会必须调整主题的大小——仅举几例。

但您仍然可以定义 stringstruct 内部指向 char[] 数组甚至是 char* 指针和 int 的长度,让它不可变,并且瞧!,您将拥有一个行为类似于引用类型但技术上是值类型的类型。

老实说,这看起来很愚蠢。正如 Eric Lippert 在其他答案的一些 cmets 中指出的那样,定义这样的值类型与定义引用类型基本相同。在几乎所有意义上,它都与以相同方式定义的引用类型没有区别。

所以“为什么string 是引用类型?”这个问题的答案是什么?基本上是:“让它成为一个值类型只是愚蠢的。”但如果这是唯一的原因,那么实际上,合乎逻辑的结论是string 实际上可以被定义为如上所述的struct,并且没有特别好的论据反对这种选择。

但是,有 个原因表明,将 string 制作成 class 比制作 struct 更好,这不仅仅是纯粹的智力。以下是我能想到的几个:

为了防止拳击

如果string 是一个值类型,那么每次你将它传递给某个期望object 的方法时,它都必须被装箱,这将创建一个新的object,这会使堆膨胀并导致毫无意义气相色谱压力。由于字符串基本上无处不在,让它们一直导致装箱将是一个大问题。

用于直观的相等比较

是的,string 可以覆盖 Equals,无论它是引用类型还是值类型。但是如果它是一个值类型,那么ReferenceEquals("a", "a") 会返回false这是因为两个参数都会被装箱,而装箱的参数从不有相同的引用(据我所知)。

因此,即使您确实可以定义一个值类型,使其就像一个引用类型,通过让它包含一个引用类型字段,但它仍然不是完全 一样。因此,我认为这是 string 是引用类型的更完整原因:您可以将其设为值类型,但这只会给它带来不必要的弱点。


原答案

这是一个引用类型,因为只有对它的引用会被传递。

如果它是一个值类型,那么每次你将一个字符串从一个方法传递到另一个方法时,整个字符串都会被复制*。

因为它是一个引用类型,而不是像“Hello world!”这样的字符串值被传递——“世界你好!”顺便说一下,它是 12 个字符,这意味着它需要(至少)24 个字节的存储空间——只有对这些字符串的 references 被传递。传递引用比传递字符串中的每个字符要便宜得多。

而且,它真的不是一个普通的原始数据类型。谁告诉你的?

*实际上,这并不完全正确。如果字符串内部包含一个char[] 数组,那么只要数组类型是引用类型,字符串的内容实际上不会按值传递——只有对数组的引用将会。不过,我仍然认为这基本上是正确的答案。

【讨论】:

  • 其实引用类型也是按值传递的,但复制的是引用本身而不是对象。
  • @Brian:好的好的,天哪...我知道会有一些顽固的人会过来纠正我;)我会更新答案以在技术上更准确...
  • @丹涛:我在等你的回答
  • 很抱歉,您在拐弯抹角并没有解释为什么字符串是引用类型。你猜的不对我感觉。并查看 pedzold 的 .net zero 以了解有关字符串的更多信息。谢谢。
  • 您错过了值类型 String 的最大优势:除非您要求,否则不会出现 null。此外,String 类与盒装 String 结构不完全相同吗?我们不是已经支付了拳击开销吗?
【解决方案3】:

String 是引用类型,而不是值类型。在很多情况下,您知道字符串的长度和字符串的内容,在这种情况下,很容易为字符串分配内存。但是考虑一下这样的事情。

string s = Console.ReadLine();

在编译时不可能知道“s”的分配细节吗?用户输入值,所有输入的字符串/行都存储在 s.因此,字符串存储在堆上,以便重新分配内存以适应字符串 s 的内容。并且对该字符串的引用存储在堆栈中。

要了解更多信息,请阅读:petzold 的 .net zero

阅读:通过 C# 从 CLR 收集垃圾,了解堆栈上的分配详细信息。

编辑:Console.WriteLine();到 Console.ReadLine();

【讨论】:

  • 我不确定我是否理解这个解释。正如我在回答中指出的那样,string 是否是引用类型,只要它以引用类型 internally 的形式存储其 内容(例如,一个char[] 数组),它的行为基本上与当前相同。这将包括在您描述的情况下堆上的动态重新分配。我认为我在回答中提供的原因对为什么 string 是引用类型提供了一个不太明显但更中肯的解释。
  • 我假设你的例子应该是 Console.ReadLine()
  • 这根本没有意义。如果内容完全存储在结构中,那么除非进行某种特殊处理,否则不同大小的字符串必须是不同的类型; 1-char 类型,1char 空间,2-char 类型,2chars 空间,等等。或者,如果结构包含对堆中数组的引用,那么无论大小如何,它都可以在运行时工作。后一种方法完全可以做到。
  • @Dan:作为包含对 char[] 及其长度的引用的值类型的字符串与作为引用类型的字符串之间有什么区别?告诉你什么,我会挥动我的魔杖和poof,好的,字符串现在是一个值类型,它是堆分配数据的句柄结构,包含一个长度和一个字符数组。但这就是字符串引用类型 ;我刚刚描述了字符串实际上是如何 实现的。一个只是堆内存句柄的值就是我们在 .NET 中所说的“引用类型”。
  • @Eric:我想我开始认为这个问题是:“如果你从头开始编写 string 类型,你为什么要选择将它设为 classstruct ?”将内容放在char[] 中似乎很明显。当然,由于这是堆分配的,因此将其设为class 似乎是合乎逻辑的。但我只是说,您真的可以 将其设为struct 设为class,这并不重要。那么,除了纯粹的哲学原因之外,为什么要选择其中一个呢?我觉得我的回答(避免装箱,启用引用相等)至少给出了几个实际原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-07
  • 1970-01-01
  • 2021-08-20
  • 1970-01-01
  • 2012-12-29
  • 2019-11-21
  • 2011-01-27
相关资源
最近更新 更多