在C#中,为什么String是一个行为类似于值类型的引用类型?

时间:2009-03-12 00:26:59

标签: c# string clr value-type reference-type

String是一种引用类型,即使它具有值类型的大部分特性,例如不可变,并且==重载以比较文本而不是确保它们引用相同的对象。

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

12 个答案:

答案 0 :(得分:304)

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

(编辑:添加了关于值类型存储作为实现细节的说明,这导致了这种情况,我们有一个类型,其中值sematics不是从System.ValueType继承的。感谢Ben。)

答案 1 :(得分:53)

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

它具有保持世界理智的价值语义。你能想象如果编码是多么困难

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

b设为false?想象一下,编写任何应用程序的难度都很大。

答案 2 :(得分:23)

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

可变性问题是一个单独的问题。引用类型和值类型都可以是可变的或不可变的。值类型通常是不可变的,因为可变值类型的语义可能会令人困惑。

引用类型通常是可变的,但如果有意义,可以设计为不可变的。字符串被定义为不可变的,因为它使某些优化成为可能。例如,如果在同一程序中多次出现相同的字符串文字(这很常见),编译器可以重用相同的对象。

那么为什么“==”重载以按文本比较字符串?因为它是最有用的语义。如果两个字符串相等,则由于优化,它们可能是也可能不是同一个对象引用。所以比较参考文献是没有用的,而比较文本几乎总是你想要的。

更一般地说,字符串具有所谓的值语义。这是一个比值类型更通用的概念,它是C#特定的实现细节。值类型具有值语义,但引用类型也可能具有值语义。当类型具有值语义时,您无法确定底层实现是引用类型还是值类型,因此您可以考虑实现细节。

答案 3 :(得分:11)

对于一个老问题,这是一个迟到的答案,但所有其他答案都忽略了这一点,即.NET在2005年的.NET 2.0之前没有泛型。

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

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

从集合中读取值需要进行逆操作,称为取消装箱。

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

有些答案错误地声称string永远不会被实现为值类型,因为它的大小是可变的。实际上,使用小字符串优化策略将字符串实现为固定长度的数据结构很容易:字符串将作为Unicode字符序列直接存储在内存中,除了将作为指向外部缓冲区的指针存储的大字符串。两种表示都可以设计成具有相同的固定长度,即指针的大小。

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

答案 4 :(得分:8)

不仅字符串是不可变的引用类型。 也可以使用多播代表。 这就是为什么写

是安全的
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(字符串s)中。或者使用MemoryStreams。

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

答案 5 :(得分:6)

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

也许Jon Skeet可以在这里帮忙吗?

答案 6 :(得分:5)

主要是性能问题。

让字符串表现为LIKE值类型有助于编写代码,但将其作为值类型会对性能产生巨大影响。

要深入了解一下,请查看.net框架中的nice article字符串。

答案 7 :(得分:2)

实际上字符串与值类型的相似性很少。对于初学者而言,并非所有值类型都是不可变的,您可以根据需要更改Int32的值,它仍然是堆栈中的相同地址。

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

就“==”而言:就像你说的那样“==”是一个运算符重载,并且它的实现也是为了让框架在使用字符串时更有用。

答案 8 :(得分:2)

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

答案 9 :(得分:2)

用一个非常简单的词来说,任何具有确定大小的值都可以被视为值类型。

答案 10 :(得分:1)

不仅仅是字符串是由字符数组组成的。我将字符串视为字符数组[]。因此它们位于堆上,因为引用内存位置存储在堆栈上并指向堆上数组内存位置的开头。字符串大小在分配之前是未知的...对于堆来说是完美的。

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

答案 11 :(得分:-1)

冒着又一次神秘的向下投票的风险......很多人提到关于值类型和原始类型的堆栈和内存是因为它们必须适合微处理器中的寄存器。如果它比寄存器需要更多的位,你就不能向堆栈推送或弹出一些东西....指令是,例如“pop eax” - 因为eax在32位系统上是32位宽。 / p>

浮点基本类型由FPU处理,它是80位宽。

在使用OOP语言来混淆基本类型的定义之前很久就已经决定了这一点,我假设值类型是专为OOP语言创建的术语。