在C#中,为什么String是一个像值类型一样行为的引用类型?
在C#中,为什么String是一个行为类似值类型的引用类型?
String是一个具有值语义的引用类型。这种设计是为了进行某些性能优化的一种权衡。
引用类型和值类型之间的区别基本上是语言设计中的一种性能权衡。引用类型在构造和销毁以及垃圾回收方面有一些开销,因为它们在堆上创建。另一方面,值类型在赋值和方法调用方面有开销(如果数据大小大于指针),因为整个对象在内存中被复制,而不仅仅是一个指针。因为字符串可以(通常)比指针的大小大得多,所以它们被设计为引用类型。此外,值类型的大小必须在编译时知道,而对于字符串来说,这并不总是正确的。
但是,字符串具有值语义,这意味着它们是不可变的,并且通过值比较(即按字符比较字符串),而不是通过比较引用。这允许进行某些优化:
- 字符串的内部化(Interning)意味着如果多个字符串被认为是相等的,编译器可以只使用一个字符串,从而节省内存。这种优化仅在字符串是不可变的情况下才有效,否则更改一个字符串将对其他字符串产生不可预测的结果。
- 字符串字面量(在编译时已知)可以被内部化并存储在编译器的特殊静态内存区域中。这样可以节省运行时的时间,因为它们不需要分配和进行垃圾回收。
不可变字符串确实增加了某些操作的成本。例如,您无法原地替换单个字符,您必须为任何更改分配一个新的字符串。但是与优化的好处相比,这只是一个小的开销。
对于用户来说,值语义有效地隐藏了引用类型和值类型之间的区别。如果一个类型具有值语义,那么对于用户来说,类型是值类型还是引用类型并不重要-它可以被视为实现细节。
值类型和引用类型之间的区别实际上与性能无关。它是关于变量是否包含实际对象还是对对象的引用。字符串永远不可能是值类型,因为字符串的大小是可变的;它需要是常量才能成为值类型;性能几乎与此无关。引用类型的创建也不会很昂贵。
- 字符串的大小是常量的。
因为它只包含对字符数组的引用,而字符数组的大小是可变的。如果一个值类型实际上只有一个引用类型,那将更加混乱,因为它对于所有目的来说仍然具有引用语义。
- 数组的大小是常量的。
对数组的引用的大小是常量的。数组本身的大小取决于数组中的项目数量和数组保存的类型的大小。
- 一旦创建了一个数组,它的大小是常量的,但是整个世界上的数组并不都具有完全相同的大小。这就是我的观点。为了使字符串成为值类型,存在的所有字符串都需要具有完全相同的大小,因为这是.NET中值类型的设计方式。它需要能够在实际拥有值之前为这种值类型保留存储空间,因此大小必须在编译时知道。这样的字符串类型需要具有某个固定大小的字符缓冲区,这将是限制性的和高度低效的。
- 哦,现在我明白你的意思了。是的,字符串的大小不一定在编译时知道。而且.NET不支持在堆栈上创建动态类型的数组。
在C#中,String是一个引用类型,但具有值类型的行为。这种设计是为了在性能和效率上做出权衡。引用类型是在堆上创建的,而值类型则在栈上创建。这两种类型都有各自的优缺点,但对于字符串这种可能很大的数据类型来说,使用引用类型可以更好地处理。
不可变性是字符串的一个重要特点。这意味着一旦创建了一个字符串对象,它就不能被修改。任何对字符串的修改都会创建一个新的字符串对象。这个设计决策有助于字符串的内部化和比较,从而提高了性能。
字符串的内部化是指如果多个字符串被认为是相等的,那么编译器可以共享它们的内存空间,从而节省内存。这种优化在字符串是不可变的情况下才有效,因为如果一个字符串被修改,其他字符串的值会变得不可预测。
字符串字面量是在编译时已知的字符串,它们可以被编译器存储在一个特殊的静态内存区域中。这样,在运行时,不需要为这些字符串分配内存或进行垃圾回收,从而提高了性能。
尽管不可变性会增加某些操作的成本,例如无法原地替换单个字符,但这个开销相对于优化带来的好处来说是微不足道的。
总之,C#中的String是一个引用类型,但具有值类型的行为,这是为了在性能和效率上做出权衡。字符串的不可变性和引用类型的优化使得字符串的处理更加高效和可靠。
在C#中,为什么String是一个表现像值类型的引用类型?
String被设计为一个引用类型,而不是值类型,是因为如果它是一个值类型并且每次传递给方法或从方法返回时都需要复制它的值,那么性能(空间和时间!)将会非常糟糕。
String具有值语义以保持世界的稳定性。想象一下,如果我们需要在代码中这样比较字符串:
string s = "hello";
string t = "hello";
bool b = (s == t);
将b设置为false,你能想象编写任何应用程序将会有多么困难吗?
Java不以简洁著称。
正是因为这个原因,当我切换到C#时,我感到有些困惑,因为我总是使用(有时仍然使用).equals(..)来比较字符串,而我的团队成员只是使用"=="。我从未理解为什么他们不将"=="留给比较引用,尽管如果你思考一下,你很可能90%的时间都希望比较内容而不是引用。
实际上,我认为从不希望检查引用,因为有时new String("foo");
和另一个new String("foo")
可能会得到相同的引用,这并不是你期望一个new
操作符做的事情。(或者你能告诉我一个你想要比较引用的情况吗?)
嗯,你必须在所有比较中包括引用比较,以捕获与null的比较。另一个比较引用和字符串的好地方是在比较时而不是等式比较。两个相等的字符串在比较时应该返回0。不过,检查这种情况所花费的时间与运行整个比较所花费的时间相同,所以它并不是一个有用的快捷方式。检查ReferenceEquals(x, y)
是一个快速的测试,你可以立即返回0,当与你的null测试混合在一起时,甚至不会增加任何额外的工作量。
如果String被实现为一个值类型,其只有一个char[]或PrivateStringData(后者是一个对定义该结构的模块是私有的类类型)字段,那么大多数情况下它将像现在一样工作;不同之处在于,除非字符串有特殊的装箱规则,否则装箱的字符串将是可变的(注意,所有装箱的结构体,甚至是被认为是“不可变”的结构体,都是可空的*),尽管改变装箱的字符串将导致它内部引用一个不同的堆对象,而不是改变堆对象本身)。
...将字符串作为这种类型的值类型而不是类类型意味着字符串的默认值可以像在.net之前的系统中一样表现为空字符串,而不是作为null引用。实际上,我个人更喜欢有一个包含引用类型NullableString的值类型String,前者的默认值等同于String.Empty,后者的默认值为null,并且有特殊的装箱/拆箱规则(这样,装箱一个默认值的NullableString将产生对String.Empty的引用)。
Hanna:引用比较加速了字符串相等且恰好是同一个对象的情况(所以这是一种改进)。我期望.net团队足够聪明,在实现字符串的“==”运算符时使用了这种方法。
它确实如此,字符串通常是受益于这种方法的情况。更一般地说,这种好处取决于自身比较的可能性有多大,尽管大多数更详细的比较在null上失败,这意味着必须在某个地方检查x == null && y == null
的可能性,如果尚未进行对ReferenceEquals(x, y)
的测试,那么对于所有这些类型来说,进行引用相等的测试没有任何不利的一面。我在讨论这个的时候是在说这个的泛化,那就是对于有序比较(.CompareTo()和.Compare()...
...那么快捷方式if(ReferenceEquals(x, y)) return 0;
也总是有效的,有时也很有用,因为不仅身份等于相等(为什么if(ReferenceEquals(x, y)) return true;
对.Equals()
有效),而且相等对大多数排序都意味着等价,而身份对所有排序都意味着等价。内置的字符串比较有时会使用这个快捷方式,但不是所有情况。
你的意思是b的值是false吗?b是true,因为两个变量的引用相同。
字面量是interned的,所以应该是true。
在C#中,为什么String是一个行为类似值类型的引用类型?
String不是值类型,因为它们可以很大,需要存储在堆上。截至目前,堆上的值类型(在CLR的所有实现中)都存储在堆上。在堆上分配字符串将破坏所有的事情:32位系统的堆栈仅为1MB,64位系统的堆栈为4MB,你必须对每个字符串进行装箱,产生复制开销,你不能对字符串进行内部化,内存使用量会膨胀,等等...
我在这里挑剔,只是因为这给了我一个机会,链接到一个与这个问题相关的博客文章:值类型并不一定存储在堆栈上。在ms.net中通常是正确的,但在CLI规范中并没有指定。值类型和引用类型的主要区别是引用类型遵循按值复制的语义。请参见learn.microsoft.com/en-us/archive/blogs/ericlippert/...和learn.microsoft.com/en-us/archive/blogs/ericlippert/...
更不用说,字符串是可变大小的,所以它们不能是值类型(因为值类型是直接存储在声明它们的位置)。当你在类内部声明一个字符串时,由于可以随时将字符串更改为不同长度的字符串,类如何直接保存字符串?不,必须有一个对字符串的引用,因为它是可变大小的。
String不是可变大小的。当你向其添加内容时,实际上是创建另一个字符串对象,为其分配新的内存。
话虽如此,理论上,字符串可以是一个值类型(结构体),但这个"值"只是一个对字符串的引用。.NET设计者自然决定去掉中间人(.NET 1.0中的结构体处理效率低下,按照Java的方式定义字符串已经是一个引用类型,而不是原始类型。此外,如果字符串是一个值类型,那么将其转换为对象将需要对其进行装箱,这是一种不必要的低效性)。
字符串变量是可变的,因此是可变大小的。
一个变量没有大小(除非你谈论引用的大小,但即使是这样,它总是相同的)。实际占用内存的是对象。
Qwertie是对的,但我认为措辞有些令人困惑。一个字符串的大小可能与另一个字符串的大小不同,因此,与真正的值类型不同,编译器无法预先知道要分配多少空间来存储字符串值。例如,Int32始终是4字节,因此当你定义一个字符串变量时,编译器会分配4个字节。当编译器遇到一个int变量时(如果它是一个值类型),应该分配多少内存?请理解,在那个时候,值还没有被分配。
抱歉,在我的评论中有一个我现在无法更正的拼写错误;应该是...例如,Int32始终是4字节,因此当你定义一个int变量时,编译器会分配4个字节。当编译器遇到一个string变量时(如果它是一个值类型),应该分配多少内存?请理解,在那个时候,值还没有被分配。
- 首先,编译器不管理堆栈空间,运行时管理。这意味着你可以动态地分配堆栈空间。例如,你可以动态地分配数组的堆栈空间,其大小直到运行时才知道。鉴于字符串实例是不可变的,并且大小在分配后不会改变(即使大小在运行时可能不知道),可以想象字符串可以在堆栈上分配,并因此成为值类型。
据我所知,你只能在不安全的上下文中这样做,并且堆栈分配的数组是固定大小的。是的,运行时可以选择重新排列事物,但是编译器定义了在堆栈上分配的大小,因此值类型的字符串必须具有已知的大小或使用不安全的指针管理(用于动态大小的堆栈分配)。那么你是不是建议字符串应该是不安全的?
当然,编译器可以对这样的字符串进行特殊处理,然后就不需要是不安全的了,但是这样你就提出了一种特殊类型的值类型字符串(不是当前的struct定义),这将是将字符串定义为类的另一个原因-它可以使用语言中已定义的两种主要类型(struct或class),而无需更多的"魔法"(对于现有的字符串类,编译器中已经有了足够的魔法)。
- 我的观点是,语言甚至不需要在我们的假设情况下暴露工作原理,这似乎是你在第二篇帖子中意识到的。编译器一直在处理特殊类型的特殊事情,如果字符串是值类型,那么它们将只是另一种情况。然而,如果它们是值类型,运行时将需要发生根本性的变化,正如我上面的回答所列举的那样。我辩论的目的是要表明Qwertie关于字符串的陈述在你的解释下仍然不准确。
值类型变量的堆栈分配与它们的赋值无关。在方法开始时为堆栈帧分配空间,而不是在方法实际运行时分配空间,所以我不确定这个动态分配的想法会如何实现。
你说:引用类型遵循按值复制的语义。你确定吗,还是你是说值类型遵循按值复制的语义?
根据你链接的文章:值类型最重要的事实当然不是它们的分配方式的实现细节,而是“值类型”的设计语义意义,即它们始终通过“按值”进行复制。