String 是一种引用类型,尽管它具有值类型的大部分特征,例如不可变和重载 == 以比较文本,而不是确保它们引用同一个对象。
那么为什么字符串不只是一个值类型呢?
String 是一种引用类型,尽管它具有值类型的大部分特征,例如不可变和重载 == 以比较文本,而不是确保它们引用同一个对象。
那么为什么字符串不只是一个值类型呢?
字符串不是值类型,因为它们可能很大,并且需要存储在堆上。值类型(在迄今为止的所有 CLR 实现中)存储在堆栈中。堆栈分配字符串会破坏各种事情:堆栈对于 32 位只有 1MB,对于 64 位只有 4MB,你必须对每个字符串进行装箱,会产生复制惩罚,你不能实习字符串和内存使用会气球等...
(编辑:添加了关于值类型存储作为实现细节的说明,这导致我们有一个类型的值语义不是从 System.ValueType 继承的。谢谢 Ben。)
它不是一个值类型,因为如果它是一个值类型,性能(空间和时间!)会很糟糕,并且每次传递给方法和从方法返回时都必须复制它的值,等等。
它具有保持世界理智的价值语义。你能想象如果编码会有多困难吗?
string s = "hello";
string t = "hello";
bool b = (s == t);
设置b
为false
?想象一下,几乎任何应用程序的编码都会有多么困难。
字符串是具有值语义的引用类型。这种设计是一种折衷,它允许某些性能优化。
引用类型和值类型之间的区别基本上是语言设计中的性能权衡。引用类型在构造和销毁以及垃圾收集方面有一些开销,因为它们是在堆上创建的。另一方面,值类型在赋值和方法调用上有开销(如果数据大小大于指针),因为整个对象被复制到内存中,而不仅仅是指针。因为字符串可以(并且通常)比指针的大小大得多,所以它们被设计为引用类型。此外,在编译时必须知道值类型的大小,而字符串并非总是如此。
但是字符串具有值语义,这意味着它们是不可变的并且通过值进行比较(即字符串的逐个字符),而不是通过比较引用。这允许某些优化:
实习意味着如果已知多个字符串相等,编译器可以只使用单个字符串,从而节省内存。此优化仅在字符串不可变时有效,否则更改一个字符串会对其他字符串产生不可预测的结果。
字符串文字(在编译时已知)可以被编译器实习并存储在内存的特殊静态区域中。这节省了运行时的时间,因为它们不需要被分配和垃圾收集。
不可变字符串确实增加了某些操作的成本。例如,您不能就地替换单个字符,您必须为任何更改分配一个新字符串。但与优化的好处相比,这是一个很小的成本。
值语义有效地为用户隐藏了引用类型和值类型之间的区别。如果一个类型具有值语义,那么对于用户来说该类型是值类型还是引用类型并不重要——它可以被认为是一个实现细节。
这是对一个老问题的迟到答案,但所有其他答案都没有抓住重点,即 .NET 直到 2005 年的 .NET 2.0 才具有泛型。
String
是引用类型而不是值类型,因为对于 Microsoft 而言,确保字符串可以以最有效的方式存储在非泛型集合中至关重要,例如System.Collections.ArrayList
.
将值类型存储在非泛型集合中需要对object
称为装箱的类型进行特殊转换。当 CLR 将值类型装箱时,它会将值包装在 a 中System.Object
并将其存储在托管堆中。
从集合中读取值需要反向操作,称为拆箱。
装箱和拆箱都有不可忽略的成本:装箱需要额外的分配,拆箱需要类型检查。
一些答案错误地声称string
永远不会被实现为值类型,因为它的大小是可变的。实际上,将字符串实现为包含两个字段的固定长度数据结构很容易:字符串长度的整数和指向 char 数组的指针。您还可以在此基础上使用小字符串优化策略。
如果泛型从一开始就存在,我想将字符串作为值类型可能是一个更好的解决方案,它具有更简单的语义、更好的内存使用和更好的缓存局部性。仅包含小字符串的 AList<string>
可能是单个连续的内存块。
不仅字符串是不可变的引用类型。 多播代表也是如此。 这就是为什么写作是安全的
protected void OnMyEventHandler()
{
delegate handler = this.MyEventHandler;
if (null != handler)
{
handler(this, new EventArgs());
}
}
我认为字符串是不可变的,因为这是使用它们并分配内存的最安全方法。为什么它们不是值类型?以前的作者对堆栈大小等的看法是正确的。我还要补充一点,当您在程序中使用相同的常量字符串时,使字符串成为引用类型可以节省程序集的大小。如果你定义
string s1 = "my string";
//some code here
string s2 = "my string";
很有可能“我的字符串”常量的两个实例都只会在您的程序集中分配一次。
如果您想像通常的引用类型一样管理字符串,请将字符串放在新的 StringBuilder(string s) 中。或者使用 MemoryStreams。
如果您要创建一个库,您希望在函数中传递大量字符串,请将参数定义为 StringBuilder 或 Stream。
简而言之,任何具有确定大小的值都可以被视为值类型。
此外,字符串的实现方式(每个平台不同)以及何时开始将它们拼接在一起。就像使用StringBuilder
. 它为你分配一个缓冲区供你复制,一旦你到达终点,它会为你分配更多的内存,希望如果你做一个大的连接性能不会受到阻碍。
也许 Jon Skeet 可以在这里帮忙?
你怎么知道string
是引用类型?我不确定它的实施方式是否重要。C# 中的字符串是不可变的,因此您不必担心这个问题。
实际上,字符串与值类型几乎没有相似之处。对于初学者,并非所有值类型都是不可变的,您可以随意更改 Int32 的值,它仍然是堆栈上的相同地址。
字符串是不可变的有一个很好的理由,它与引用类型无关,但与内存管理有很大关系。当字符串大小发生变化时创建一个新对象比在托管堆上转移东西更有效。我认为您将值/引用类型和不可变对象概念混合在一起。
至于“==”:就像你说的“==”是一个运算符重载,再次实现它是为了让框架在处理字符串时更有用。
不仅仅是由字符数组组成的字符串那么简单。我将字符串视为字符数组 []。因此它们位于堆上,因为引用内存位置存储在堆栈上,并指向堆上数组内存位置的开头。字符串大小在分配之前是未知的……非常适合堆。
这就是为什么一个字符串真的是不可变的,因为当你改变它时,即使它的大小相同,编译器也不知道,并且必须分配一个新数组并将字符分配给数组中的位置。如果您将字符串视为语言保护您不必动态分配内存的一种方式,这是有道理的(像编程一样阅读C)
冒着又一次神秘的否决票的风险......许多人提到堆栈和内存与值类型和原始类型有关的事实是因为它们必须适合微处理器中的寄存器。如果它占用的位多于寄存器的位,则您不能将其推入或弹出堆栈。...指令是,例如“pop eax”——因为 eax 在 32 位系统上是 32 位宽的。
浮点基本类型由 80 位宽的 FPU 处理。
这一切早在有一种 OOP 语言来混淆原始类型的定义之前就已经决定了,我假设值类型是专门为 OOP 语言创建的术语。