[英]In C#, why is String a reference type that behaves like a value type?
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
是引用类型而不是值类型,因为确保字符串可以以最有效的方式存储在非泛型集合(例如System.Collections.ArrayList
对于 Microsoft 至关重要。
将值类型存储在非泛型集合中需要对类型object
进行特殊转换,这称为装箱。 当 CLR 将值类型装箱时,它会将值包装在System.Object
并将其存储在托管堆中。
从集合中读取值需要逆操作,称为拆箱。
装箱和拆箱都有不可忽视的成本:装箱需要额外的分配,拆箱需要类型检查。
一些答案错误地声称string
永远不会被实现为值类型,因为它的大小是可变的。 实际上,很容易将字符串实现为包含两个字段的固定长度数据结构:一个表示字符串长度的整数,以及一个指向 char 数组的指针。 您还可以在此基础上使用小字符串优化策略。
如果泛型从一开始就存在,我想将字符串作为值类型可能是一个更好的解决方案,具有更简单的语义、更好的内存使用和更好的缓存位置。 仅包含小字符串的List<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 语言创建的术语。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.