为什么字符串是引用类型?

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

4
为什么在C#中字符串是一个行为类似值类型的引用类型?详情请见:https://dev59.com/ZnRB5IYBdhLWcg3wZmdz - davidtbernal
@Pandiay chendur:你想要说什么? - selvaraj
1
请更改您的问题标题,因为它并没有说明任何相关内容。 - Carlos Muñoz
3个回答

19

除了Dan提到的原因外:

按照定义,值类型是将其值存储在自身而不是引用其他值的类型。这就是为什么值类型被称为"值类型",引用类型被称为"引用类型"的原因。因此,你的问题实际上是"为什么字符串引用其内容而不仅仅是包含其内容?"

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

那又怎样呢?这个属性有什么好处?好吧,假设字符串是可以任意大小的值类型,请考虑以下情况:

string[] mystrings = new string[3];

这个由三个字符串组成的数组最初的内容是什么?因为值类型不存在"null",所以唯一合理的做法是创建一个由三个空字符串组成的数组。那么它在内存中的布局是怎样的呢?请思考一下,你会如何实现它?

现在假设您这样说:

string[] mystrings = new string[3];
mystrings[1] = "hello";
现在我们的数组中有"", "hello"和""。 “hello”存储在内存的哪个位置?分配给mystrings [1]的占用空间有多大?这个数组及其元素的内存必须放置在某个地方

这就让CLR面临以下选择:

  • 每次更改其元素就调整数组的大小,复制整个数组,其大小可能为几兆字节
  • 禁止创建未知大小的值类型数组
  • 禁止创建未知大小的值类型

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


显而易见的方法是在声明时分配字符串允许的最大长度,并准备好处理可能出现的OutOfMemoryExceptions。 - Anthony Pegram
2
很遗憾我不能收藏答案。 - Arcturus
@Eric Lippert:我同意你的观点,但如果“string”是一个值类型,其唯一字段是引用内部化字符数组的“char []”怎么办?由于数组是引用类型,“string”的大小将是常量sizeof(IntPtr)+任何填充,不是吗?那么就没有问题可以有一个字符串数组了。或者我完全错了吗? - Ani
2
@Ani:正确。这就是引用类型的定义。一个唯一字段为引用的值类型和一个引用之间没有实质性的区别!显然它们具有完全相同的位,因为值类型的位只是其成员的位,如果它的成员是引用类型,则它只有引用的位。如果位完全相同,那么为什么还要使用结构体?直接使用引用就可以了。 - Eric Lippert
@Eric Lippert:好的,那很有道理。谢谢。 - Ani
@Eric,@Ani:Eric说得对,一个值类型有一个引用类型字段和一个纯引用类型之间实际上没有区别。但是我在我的回答中要解决的问题是:如果真的没有区别,为什么要选择其中之一?当然,选择引用类型似乎更合乎逻辑,这个简单的事实很难争辩。但是,在这种情况下选择值类型也有实际的缺点:例如,它的行为就像引用类型一样,但是当转换为“object”时,它会被装箱。引用类型不会有这个弱点。我讲得通吗? - Dan Tao

11

哎呀,这个答案被采纳了,然后我又改了。我应该在底部包含原始答案,因为那是被提问者所采纳的。

新答案

更新:事情是这样的。string 绝对需要像引用类型一样表现出来。迄今为止,所有答案都已经涉及到这些原因: string 类型没有恒定的大小,从一个方法复制整个字符串的内容是没有意义的,否则 string[] 数组将不得不调整自己的大小 - 只是举几个例子。

但是你仍然可以将 string 定义为一个 struct,它内部指向一个 char[] 数组或甚至一个 char* 指针和一个 int 表示其长度,使其成为不可变的,并且,voila!,你将拥有一个 表现出 像引用类型但实际上是值类型的类型。

这似乎相当愚蠢,老实说。正如 Eric Lippert 在其他答案的评论中指出的那样,像这样定义值类型基本上与定义引用类型相同。在几乎所有情况下,它将与以相同方式定义的引用类型无法区分。

因此,“为什么 string 是引用类型?”的答案基本上是:“让它成为值类型只是愚蠢的。”但是,如果这是唯一的原因,那么实际上,逻辑上的结论是,string 实际上可以被定义为上面描述的一个 struct,而没有任何特别好的反对该选择的理由。

然而,有比纯粹的智力更好的原因使 string 成为 class 而不是 struct。以下是我能想到的一些:

防止装箱

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

直观的相等比较

是的,无论是引用类型还是值类型,string都可以覆盖Equals。但是如果它是值类型,那么ReferenceEquals("a", "a")将返回false这是因为两个参数都会被装箱,而装箱的参数永远没有相等的引用(据我所知)。
因此,尽管确实可以通过使其由单个引用类型字段组成来定义值类型以像引用类型一样运行,但它仍然不是完全相同的。因此,我认为这是更完整的原因,为什么string是引用类型:你可以将其定义为值类型,但这只会给它带来不必要的弱点。

原始答案

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

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

由于它是引用类型,因此不是像"Hello world!"这样的字符串值被传递 - "Hello world!"是12个字符,这意味着它需要(至少)24个字节的存储空间 - 只有对这些字符串的引用被传递。传递引用比传递字符串中的每个字符要便宜得多。

而且,它真的不是普通的基本数据类型。谁告诉你了?

*实际上,这并不是严格正确的。如果字符串内部持有一个char[]数组,并且只要数组类型是引用类型,则实际上不会按值传递字符串的内容 - 只会传递对数组的引用。尽管如此,我仍然认为这基本上是正确的答案。


2
实际上,引用类型也是按值传递的,但复制的是引用本身而不是对象。 - Brian Rasmussen
@Brian:好好好,哎呀...我知道总有人会过来纠正我;)我会更新答案,让它更加技术上准确... - Dan Tao
@Dan tao:我在等你的回答。 - selvaraj
@Strilanc:针对你的第二点,绝对不是这样!每次将一个“string”传递给期望“object”的方法时,引用字符串被传递而无需进行任何新的内存分配。另一方面,每次将“int”传递给这样的方法时,例如,为了装箱该“int”(因为您没有传递到任何引用;您传递了一个),会创建一个新的“object”。返回引用与返回值也是如此。 - Dan Tao
String作为一个值类型,它包装了一个私有的Char[],并且不会暴露给任何可能改变它的东西,这样做的一个效果是default(string)可以始终像一个空字符串一样一致地工作,而不是作为一个空引用。我认为这可能是一个有价值的优势。更好的方法是定义两种字符串类型——一个结构体和一个类——系统的装箱和拆箱方法将识别,因此String结构体将被装箱为StringObject。然后default(StringObject)可以是null,而default(String)将是一个空字符串。 - supercat
显示剩余2条评论

1

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

string s = Console.ReadLine();

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

要了解更多,请阅读:Petzold的“.net zero”。

阅读CLR Via C#中的垃圾回收以获取有关堆栈分配详细信息。

编辑:将Console.WriteLine()更改为Console.ReadLine();


我不确定我理解这个解释。正如我在我的答案中指出的那样,无论 string 是否是引用类型,只要它将其 内容 以引用类型 内部 的形式储存(例如,一个 char[] 数组),它就可以基本上像当前一样运作。这将包括堆上的动态重新分配,就像你描述的情况一样。我认为我在我的答案中提供的原因提供了一个不太明显但仍然更直接的解释,即为什么 string 是一个引用类型。 - Dan Tao
2
我猜你的例子应该是 Console.ReadLine()。 - Emile
这根本就没有意义。如果内容完全存储在结构体中,那么除非进行某种特殊处理,不同大小的字符串将必须是不同类型;一个容纳1个字符的1-char类型,一个容纳2个字符的2-char类型,等等。或者,如果结构体包含对堆中数组的引用,则无论大小,在运行时都可以工作。后一种方法完全可行。 - Jon Hanna
1
@丹:什么是一个字符串值类型和char[]引用及其长度的区别,以及一个字符串引用类型?听我说,我将挥动我的魔法棒和,好了,一个字符串现在是一个值类型,它是对包含长度字符数组堆分配数据结构的句柄。但这就是字符串作为引用类型的本质;我只是描述了字符串实际上如何被实现的。只是一个对堆内存的句柄的值被我们称为.NET中的“引用类型”。 - Eric Lippert
@Eric: 如果String是一个结构体,其唯一的内容是一个数组,它可以使用一个Array作为其主要存储元素,并且定义和重载方法和属性,仅需要为每个字符串创建一个对象。否则,它将必须是包含数组的类(每个字符串需要两个堆分配),从Array继承(通常不允许),或者具有特殊支持作为可变大小对象而不是数组。 .Net的实现者决定包括对String的特殊处理。如果没有这样做,结构体似乎是一个好选择。 - supercat
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接