在c#中,为什么字符串是一个引用类型,行为像一个值类型?

String是一种引用类型,尽管它具有值类型的大多数特征,例如不可变和重载==以比较文本,而不是确保它们引用同一个对象。

为什么字符串不只是一个值类型呢?

208300 次浏览

字符串不是值类型,因为它们可能很大,需要存储在堆上。值类型(在CLR的所有实现中)存储在堆栈上。堆栈分配字符串会破坏各种各样的事情:32位的堆栈只有1MB, 64位的堆栈只有4MB,你必须装箱每个字符串,导致复制惩罚,你不能实习字符串,内存使用会膨胀,等等……

(编辑:增加了关于值类型存储是实现细节的说明,这导致了这种情况,即我们有一个具有值语义的类型没有从System.ValueType继承。由于本。)

它不是一个值类型,因为如果它是一个值类型,并且它的值每次传递给方法或从方法返回时都必须复制,那么性能(空间和时间!)会很糟糕。

它有价值语义来保持世界的理智。你能想象编码有多困难吗

string s = "hello";
string t = "hello";
bool b = (s == t);

b设置为false?想象一下编写任何应用程序是多么困难。

此外,字符串的实现方式(每个平台都不同)以及何时开始将它们拼接在一起。比如使用StringBuilder。它为你分配了一个缓冲区供你复制,一旦你到达终点,它会为你分配更多的内存,希望如果你做一个大的连接性能不会受到阻碍。

也许乔恩·斯基特可以过来帮忙?

实际上,字符串与值类型几乎没有相似之处。对于初学者来说,并不是所有的值类型都是不可变的,你可以随心所欲地改变Int32的值,而它在堆栈上的地址仍然是相同的。

字符串是不可变的有一个很好的理由,这与它是引用类型无关,但与内存管理有很大关系。当字符串大小发生变化时,创建一个新对象比在托管堆上移动对象更有效。我认为你把值/引用类型和不可变对象的概念混在一起了。

至于“==”:就像你说的,“==”是一个操作符重载,它的实现有一个很好的理由,使框架在处理字符串时更有用。

这主要是性能问题。

在编写代码时,让字符串的行为像值类型一样有帮助,但如果它是值类型,则会对性能造成巨大影响。

要深入了解,请查看。net框架中字符串的不错的文章

如何判断string是引用类型?我不确定它是如何实施的。c#中的字符串是不可变的,所以你不必担心这个问题。

不仅字符串是不可变的引用类型。 还有多类型转换委托。 这就是为什么写入

是安全的
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";

“my string”常量的两个实例很可能只在程序集中分配一次。

如果你想像一般引用类型一样管理字符串,把字符串放在一个新的StringBuilder(string s)中。或者使用MemoryStreams。

如果您要创建一个库,并希望在函数中传递一个巨大的字符串,则可以将参数定义为StringBuilder或Stream。

并不像字符串由字符数组组成那么简单。我把字符串看作字符数组[]。因此,它们位于堆上,因为引用内存位置存储在堆栈上,并且指向数组在堆上内存位置的开始。字符串大小在分配之前是不知道的…非常适合堆。

这就是为什么字符串是不可变的,因为当你改变它时,即使它的大小相同,编译器也不知道,它必须分配一个新的数组,并将字符分配到数组中的位置。如果你认为字符串是语言保护你不需要动态分配内存的一种方式,这是有道理的(像编程一样阅读C语言)

字符串是具有值语义的引用类型。这种设计是一种折衷,允许某些性能优化。

引用类型和值类型之间的区别基本上是语言设计中的性能权衡。引用类型在构造、销毁和垃圾收集方面有一些开销,因为它们是在堆上创建的。另一方面,值类型有赋值和方法调用的开销(如果数据大小大于指针),因为整个对象被复制到内存中,而不仅仅是一个指针。因为字符串可以(通常)比指针的大小大得多,所以它们被设计为引用类型。此外,值类型的大小必须在编译时知道,但字符串并不总是这样。

但是字符串有值语义,这意味着它们是不可变的,并通过值(即字符串的字符逐个字符)进行比较,而不是通过比较引用。这允许某些优化:

实习意味着如果已知多个字符串相等,编译器可以只使用一个字符串,从而节省内存。这种优化只在字符串不可变的情况下有效,否则更改一个字符串将对其他字符串产生不可预测的结果。

字符串字面值(在编译时已知)可以被编译器存储在内存的一个特殊静态区域中。这节省了运行时的时间,因为它们不需要分配和垃圾收集。

不可变字符串确实会增加某些操作的成本。例如,您不能就地替换单个字符,必须为任何更改分配一个新字符串。但与优化的好处相比,这是一个很小的成本。

值语义有效地为用户隐藏了引用类型和值类型之间的区别。如果一个类型具有值语义,那么该类型是值类型还是引用类型对用户来说并不重要——它可以被视为一个实现细节。

事实上,许多人提到的堆栈和内存是关于值类型和基本类型的,因为它们必须适合微处理器中的寄存器。如果它占用的比特数超过了一个寄存器....的比特数,那么您就不能向堆栈中插入或从堆栈中取出某个东西说明是,例如“pop eax"——因为eax在32位系统上是32位宽的。

浮点基元类型由80位宽的FPU处理。

这早在OOP语言混淆原始类型定义之前就已经确定了,我认为值类型是专门为OOP语言创建的术语。

简单地说,任何具有一定大小的值都可以被视为值类型。

这是对一个老问题的迟来的回答,但所有其他的回答都忽略了一点,那就是。net直到2005年的。net 2.0才有泛型。

String是一个引用类型,而不是一个值类型,因为对于微软来说,确保字符串能够以最有效的方式存储在非泛型集合中是至关重要的,例如System.Collections.ArrayList

在非泛型集合中存储值类型需要对类型object进行特殊转换,称为装箱。当CLR装箱一个值类型时,它将该值包装在System.Object中并将其存储在托管堆中。

从集合中读取值需要反向操作,称为开箱操作。

装箱和拆箱都有不可忽略的成本:装箱需要额外的分配,拆箱需要类型检查。

一些答案错误地声称string永远不可能被实现为值类型,因为它的大小是可变的。实际上,将字符串实现为包含两个字段的固定长度数据结构是很容易的:字符串长度的整数和指向char数组的指针。你也可以在此基础上使用小字符串优化策略。

如果泛型从第一天就存在,我想将字符串作为值类型可能是一个更好的解决方案,具有更简单的语义,更好的内存使用和更好的缓存位置。只包含小字符串的List<string>可能是单个连续的内存块。