除了Dan提到的原因外:
按照定义,值类型是将其值存储在自身而不是引用其他值的类型。这就是为什么值类型被称为"值类型",引用类型被称为"引用类型"的原因。因此,你的问题实际上是"为什么字符串引用其内容而不仅仅是包含其内容?"
这是因为值类型具有良好的属性,即每个给定值类型的实例在内存中的大小都相同。
那又怎样呢?这个属性有什么好处?好吧,假设字符串是可以任意大小的值类型,请考虑以下情况:
string[] mystrings = new string[3];
这个由三个字符串组成的数组最初的内容是什么?因为值类型不存在"null",所以唯一合理的做法是创建一个由三个空字符串组成的数组。那么它在内存中的布局是怎样的呢?请思考一下,你会如何实现它?
现在假设您这样说:
string[] mystrings = new string[3];
mystrings[1] = "hello";
现在我们的数组中有"", "hello"和""。 “hello”存储在内存的哪个位置?分配给mystrings [1]的占用空间有多大?这个数组及其元素的内存必须放置在某个地方。
这就让CLR面临以下选择:
CLR团队选择了后者。将字符串变成引用类型意味着可以高效地创建它们的数组。
sizeof(IntPtr)
+任何填充,不是吗?那么就没有问题可以有一个字符串数组了。或者我完全错了吗? - Ani哎呀,这个答案被采纳了,然后我又改了。我应该在底部包含原始答案,因为那是被提问者所采纳的。
更新:事情是这样的。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[]
数组,并且只要数组类型是引用类型,则实际上不会按值传递字符串的内容 - 只会传递对数组的引用。尽管如此,我仍然认为这基本上是正确的答案。
String
作为一个值类型,它包装了一个私有的Char[]
,并且不会暴露给任何可能改变它的东西,这样做的一个效果是default(string)
可以始终像一个空字符串一样一致地工作,而不是作为一个空引用。我认为这可能是一个有价值的优势。更好的方法是定义两种字符串类型——一个结构体和一个类——系统的装箱和拆箱方法将识别,因此String
结构体将被装箱为StringObject
。然后default(StringObject)
可以是null
,而default(String)
将是一个空字符串。 - supercat字符串是引用类型而不是值类型。在许多情况下,您知道字符串的长度和内容,在这种情况下,很容易为字符串分配内存。但考虑这样的情况。
string s = Console.ReadLine();
在编译时不可能知道“s”的分配细节。用户输入值后,所有输入的字符串/行都存储在“s”中。因此,字符串存储在堆上,以便重新分配内存以适应字符串“s”的内容。对此字符串的引用存储在堆栈上。
要了解更多,请阅读:Petzold的“.net zero”。
阅读CLR Via C#中的垃圾回收以获取有关堆栈分配详细信息。
编辑:将Console.WriteLine()更改为Console.ReadLine();
string
是否是引用类型,只要它将其 内容 以引用类型 内部 的形式储存(例如,一个 char[]
数组),它就可以基本上像当前一样运作。这将包括堆上的动态重新分配,就像你描述的情况一样。我认为我在我的答案中提供的原因提供了一个不太明显但仍然更直接的解释,即为什么 string
是一个引用类型。 - Dan Tao