众所周知,String 是不可变的。String 不可变的原因是什么?为什么需要引入StringBuilder类来支持可变字符串?
众所周知,String 是不可变的。String 不可变的原因是什么?为什么需要引入StringBuilder类来支持可变字符串?
out
或ref
(因为这会更改引用,而不是对象)。因此程序员知道,如果string x = "abc"
在方法开头,而且在方法体中未改变,那么在方法结束时x == "abc"
。"abc" == "ab" + "c"
。虽然这不需要不可变性,但是这样一个指向字符串的引用在其生命周期内总是等于"abc"(这确实需要不可变性),使得在维护先前值的相等性关系很重要的键的使用,更容易确保正确性(字符串确实常被用作键)。Christmas.AddMonths(1)
应该生成一个新的DateTime
而不是更改可变对象。(另一个例子是,如果我作为一个可变对象改变我的名字,改变的只是我使用的名字,“Jon”仍然是不变的,其他人仍然会叫做Jon。return this
就可以创建一个克隆。由于无论如何都不能更改此副本,所以将某物视为其自身的副本是安全的。总之,对于不需要经常变更状态的对象来说,不可变性可以带来很多优点。主要的缺点在于需要额外的构建,尽管即使在这里,它也经常被夸大了(请记住,在StringBuilder变得比等效的串联序列更有效之前,您必须进行多次追加,具有其固有的构造)。
如果可变性是一个对象设计目的的一部分,那么它将是一个缺点(谁想被Employee对象建模,而该对象的薪水永远不会改变),但有时甚至在这种情况下它也可能很有用(在许多Web和其他无状态应用程序中,执行读操作的代码与执行更新操作的代码是分开的,并且使用不同的对象可能是自然的——我不会将对象变成不可变的,然后强制使用该模式,但如果我已经拥有了该模式,为了提高性能和正确性保障,我可能会将我的“读取”对象变成不可变的)。
Copy-on-write是一种折中方案。在这里,“真实”类持有对“状态”类的引用。在复制操作时,状态类是共享的,但如果更改状态,则会创建状态类的新副本。这种方法在C++中比在C#中更常用,这就是为什么std:string具有不可变类型的一些优点,但仍然保持可变性的原因。
std::string
。 - Max Truxa字符串并非真正的不可变,它们只是公开不可变。这意味着您无法通过公共接口修改它们。但在内部,它们实际上是可变的。
如果您不相信我,请查看使用反编译工具查看String.Concat
定义的最后几行代码...
int length = str0.Length;
string dest = FastAllocateString(length + str1.Length);
FillStringChecked(dest, 0, str0);
FillStringChecked(dest, length, str1);
return dest;
正如您所看到的,FastAllocateString
返回一个空但已分配的字符串,接着被FillStringChecked
修改。
实际上,FastAllocateString
是一个extern方法,而FillStringChecked
是不安全的,因此它使用指针来复制字节。
也许有更好的例子,但这是我目前找到的。
字符串管理是一项昂贵的过程。使字符串不可变可以重复使用字符串,而不是重新创建。
仅仅为了补充一下,一个经常被忽视的观点是安全性。想象一下这样的场景,如果字符串是可变的:
string dir = "C:\SomePlainFolder";
//Kick off another thread
GetDirectoryContents(dir);
void GetDirectoryContents(string directory)
{
if(HasAccess(directory) {
//Here the other thread changed the string to "C:\AllYourPasswords\"
return Contents(directory);
}
return null;
}
您永远不必防御性地复制不可变数据。尽管您需要复制它来改变它,但通常可以自由别名,并且无需担心此别名的意外后果,这可以通过缺少防御性复制来提高性能。
.NET 中的字符串被视为引用类型。
引用类型在栈上放置了一个指针,指向托管堆中实际的实例。这与值类型不同,值类型将整个实例保存在栈上。
当传递值类型作为参数时,运行时会在堆栈上创建值的副本,并将该值传递到方法中。这就是为什么必须使用'ref'关键字来传递整型以返回更新后的值。
当传递引用类型时,运行时会在堆栈上创建指针的副本。复制的指针仍然指向引用类型的原始实例。
字符串类型具有重载的=运算符,它创建自身的副本,而不是指针的副本-使其更像值类型。但是,如果只复制指针,则第二个字符串操作可能会意外覆盖另一个类的私有成员的值,从而导致一些非常不好的结果。
正如其他帖子所提到的,StringBuilder类允许创建字符串,而无需GC开销。
为了提高可读性和运行效率,字符串和其他具体对象通常被表达为不可变对象。安全性是另一个考虑因素,进程无法更改您的字符串并将代码注入到字符串中。
std::string
和std::string&
参数之间的区别)。但在C#中,一切都与引用有关,因此,如果您在各个函数之间传递可变字符串,则每个函数都可能更改它并触发意外的副作用。