Answers:
字符串不是值类型,因为它们可能很大,并且需要存储在堆中。值类型(到目前为止,在CLR的所有实现中)都存储在堆栈中。堆栈分配字符串会破坏各种情况:32位堆栈只有1MB,64位堆栈只有4MB,您必须将每个字符串装箱,这会产生复制损失,您不能内在字符串和内存使用情况会气球等
(编辑:添加了关于值类型存储是实现细节的说明,这导致这种情况,即我们的类型具有不从System.ValueType继承的值语义。感谢Ben。)
String
大小不可变。当添加到它时,实际上是在创建另一个String
对象,为其分配新的内存。
Int32
始终为4个字节,因此,每次您定义字符串变量时,编译器都会分配4个字节。编译器在遇到int
变量(如果是值类型)时应分配多少内存?了解当时该值尚未分配。
Int32
总是为4个字节,因此,每次您定义int
变量时,编译器都会分配4个字节。遇到string
变量(如果是值类型)时,编译器应分配多少内存?了解当时尚未分配该值。
它不是值类型,因为如果它是一个值类型,性能(空间和时间!)会很糟糕,并且每次将其值传递给方法或从方法返回时都必须复制其值,等等。
它具有使世界保持理智的有价值的语义。你能想象如果
string s = "hello";
string t = "hello";
bool b = (s == t);
设置b
为false
?想象一下,几乎对任何应用程序进行编码都是多么困难。
new String("foo");
其他人new String("foo")
可以在相同的引用中进行评估,这不是您期望new
操作员执行的操作。(或者你能告诉我一个我想比较参考文献的情况吗?)
ReferenceEquals(x, y)
是一种快速的测试,您可以立即返回0,而将其与null测试混合甚至不会增加任何工作。
string
可以表现为空字符串(如在.net之前的系统中)而不是空引用。实际上,我自己的偏好是拥有一个String
包含引用类型的值类型,其中NullableString
前者的默认值等于,String.Empty
后者的默认值为null
,并且具有特殊的装箱/拆箱规则(例如,将默认的装箱值NullableString
将产生对的引用String.Empty
。
引用类型和值类型之间的区别基本上是语言设计中的性能折衷。引用类型是在堆上创建的,因此它们在构造,销毁和垃圾回收上有一些开销。另一方面,值类型会增加方法调用的开销(如果数据大小大于指针),因为整个对象而不是指针被复制。因为字符串可以(通常是)比指针的大小大得多,所以它们被设计为引用类型。而且,正如Servy指出的那样,必须在编译时就知道值类型的大小,而字符串并非总是如此。
可变性问题是一个单独的问题。引用类型和值类型都可以是可变的或不可变的。但是,值类型通常是不可变的,因为可变值类型的语义可能会造成混淆。
引用类型通常是可变的,但如果有意义,可以将其设计为不可变的。字符串被定义为不可变的,因为它使某些优化成为可能。例如,如果同一字符串文字在同一程序中多次出现(这很常见),则编译器可以重用同一对象。
那么,为什么“ ==”重载以按文本比较字符串?因为它是最有用的语义。如果两个字符串在文本上相等,则由于优化,它们可能不是同一对象引用。因此,比较引用几乎没有用,而比较文本几乎总是您想要的。
更笼统地说,字符串具有所谓的值语义。这是比值类型更笼统的概念,值类型是C#特定的实现细节。值类型具有值语义,但是引用类型也可能具有值语义。当类型具有值语义时,您不能真正判断基础实现是引用类型还是值类型,因此可以考虑实现细节。
string
类型将需要具有某个固定大小的char缓冲区,这既是限制性的又是非常低效的。
这是对一个老问题的较晚答案,但是所有其他答案都没有抓住重点,那就是.NET在2005年的.NET 2.0之前没有泛型。
String
是引用类型而不是值类型,因为对于Microsoft而言,确保以最有效的方式将字符串存储在非通用集合(例如)中至关重要System.Collections.ArrayList
。
在非通用集合中存储值类型需要对类型进行特殊转换,object
即装箱。当CLR装箱值类型时,它将值包装在a内System.Object
并将其存储在托管堆中。
从集合中读取值需要反向操作,这称为拆箱。
装箱和拆箱的成本都是不可忽略的:装箱需要额外的分配,装箱需要类型检查。
一些答案错误地声称 string
由于其大小是可变的因此永远不可能实现为值类型。实际上,使用“小型字符串优化”策略将字符串实现为固定长度的数据结构很容易:字符串将以Unicode字符序列的形式直接存储在内存中,除了大型字符串会作为指向外部缓冲区的指针存储之外。两种表示形式都可以设计为具有相同的固定长度,即指针的大小。
如果从一开始就存在泛型,我想将字符串作为值类型可能是更好的解决方案,它具有更简单的语义,更好的内存使用率和更好的缓存局部性。List<string>
仅包含小字符串的A 可能是一个连续的内存块。
string
仅包含其大小和指向char
数组的指针,因此它不会是“巨大的值类型”。但这是此设计决策的简单且相关的原因。谢谢!
不仅字符串是不可变的引用类型。 多播代表。 这就是为什么写安全
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。
您怎么知道string
引用类型?我不确定它的实现方式是否重要。C#中的字符串精确地是不可变的,因此您不必担心此问题。
实际上,字符串与值类型几乎没有相似之处。对于初学者来说,并不是所有的值类型都是不可变的,您可以随意更改Int32的值,并且它仍然是堆栈上的相同地址。
字符串是不可变的,这有很好的理由,它与作为引用类型无关,但与内存管理有很大关系。当字符串大小改变时,创建新对象比在托管堆上转移内容更有效。我认为您正在将值/引用类型和不可变对象概念混合在一起。
就“ ==”而言:就像您说的那样,“ ==”是运算符重载,并且再次实现它是有很好的理由的,目的是使框架在处理字符串时更有用。
不仅仅是字符串是由字符数组组成的。我将字符串视为字符数组[]。因此它们在堆上,因为参考内存位置存储在堆栈上,并指向堆上数组内存位置的开头。在为堆分配完美的字符串大小之前未知。
这就是字符串实际上是不可变的原因,因为即使更改了相同大小的字符串,编译器也不知道该字符串,因此必须分配一个新数组并将字符分配给该数组中的位置。如果您认为字符串是语言保护您免于动态分配内存的一种方式,那么这是有道理的(类似于编程,请参阅C)
is
测试),因此答案可能是“出于历史原因”。由于无需物理复制不可变对象,因此复制性能不能成为原因。现在,如果不破坏实际使用is
检查(或类似约束)的代码,就无法进行更改。