为什么.NET String是不可变的?

Nir*_*ngh 182 .net c# string immutability

众所周知,String是不可变的.String不可变的原因是什么,StringBuilder类的引入是可变的?

Jon*_*nna 236

  1. 不可变类型的实例本质上是线程安全的,因为没有线程可以修改它,线程以干扰另一个的方式修改它的风险被删除(引用本身是另一回事).
  2. 类似地,别名不能产生变化(如果x和y都指向同一个对象,对x的更改需要更改为y)这一事实允许进行大量的编译器优化.
  3. 节省内存的优化也是可能的.实习和雾化是最明显的例子,尽管我们可以做同样原理的其他版本.我曾经通过比较不可变对象和替换重复项的引用来节省大约半GB的内存,这样它们都指向同一个实例(耗费时间,但一分钟的额外启动以节省大量内存是一个在有问题的情况下表现胜利).使用无法完成的可变对象.
  4. 将不可变类型作为方法传递给参数不会产生任何副作用,除非它是outref(因为它改变了引用,而不是对象).因此,程序员知道如果string x = "abc"在方法的开头,并且在方法的主体中没有改变,那么x == "abc"在方法的最后.
  5. 从概念上讲,语义更像是值类型; 特别是平等是基于国家而不是身份.这意味着"abc" == "ab" + "c".虽然这不需要不变性,但是对这样一个字符串的引用在其整个生命周期中总是等于"abc"(这确实需要不变性)的事实使得使用作为保持与先前值相等的关键是至关重要的,更容易确保正确性(字符串确实常用作键).
  6. 从概念上讲,它是不可改变的更有意义的.如果我们在圣诞节加一个月,我们没有改变圣诞节,我们在1月下旬创造了一个新的约会.因此,有意义的是Christmas.AddMonths(1)产生新的DateTime而不是改变可变的.(另一个例子,如果我作为一个可变对象改变我的名字,改变的是我正在使用的名字,"Jon"仍然是不可变的,其他Jons将不受影响.
  7. 复制快速而简单,只需创建一个克隆return this.由于副本无论如何都无法改变,假装某些东西是它自己的副本是安全的.
  8. [编辑,我忘记了这个].可以在对象之间安全地共享内部状态.例如,如果您实现的是由数组支持的列表,起始索引和计数,那么创建子范围最昂贵的部分就是复制对象.但是,如果它是不可变的,那么子范围对象可以引用相同的数组,只需要改变起始索引和计数,并且对构造时间进行非常大的改变.

总之,对于没有经历变化的对象作为其目的的一部分,存在不可变的许多优点.主要的缺点是需要额外的构造,尽管在这里它经常被夸大(记住,在StringBuilder变得比等效的连接系列更有效之前,你必须做几个附加,具有它们固有的结构).

如果可变性是对象的目的的一部分(谁想要通过其工资永远不会改变的Employee对象建模),那将是一个缺点,尽管有时甚至它可能是有用的(在许多web和其他无状态中)应用程序,执行读取操作的代码与执行更新的代码是分开的,使用不同的对象可能是很自然的 - 我不会使对象成为不可变的然后强制该模式,但如果我已经拥有该模式,我可能会使我的"读取"对象性能和正确性 - 保证收益是不可变的).

写时复制是一个中间立场.这里的"真实"类包含对"状态"类的引用.状态类在复制操作上共享,但如果更改状态,则会创建状态类的新副本.这通常与C++一起使用而不是C#,这就是为什么它的std:string享有不可变类型的一些但不是全部优点,同时保持可变性.

  • @IanBoyd是的,但是它是一个好的中间地带还是一个更糟糕的两个世界的中间地带是另一个问题.这里不是一个详细讨论,但是http://drdobbs.com/cpp/184403779对如何在STL字符串类型中使用COW进行了有趣的批评.有趣的是,结论是,拥有单独的可变和不可变类型可能会更好,这当然正是我们在这里所讨论的. (2认同)
  • 字符串是不可变的,因为框架设计者决定应该这样做。**可能是**,因为此答案中列出了这些内容。 (2认同)

Ree*_*sey 74

使字符串不可变具有许多优点.它提供自动线程安全性,并使字符串以简单有效的方式表现为内在类型.它还允许在运行时提高效率(例如允许有效的字符串实习以减少资源使用),并且具有巨大的安全优势,因为第三方API调用不可能更改字符串.

添加StringBuilder是为了解决不可变字符串的一个主要缺点 - 不可变类型的运行时构造会导致很多GC压力并且本质上很慢.通过创建一个显式的可变类来处理这个问题,解决了这个问题,而没有在字符串类中添加不必要的复杂性.

  • @Juliet:但你也在那里交易 - 你得到O(1)concat和lg n子串,但你失去了恒定的时间元素访问,你失去了缓存局部性.有一个原因字符串通常不像绳索那样实现. (8认同)
  • 必须在这里说明,即使字符串类的特定实现是不可变的,但不变性本身并不慢.字符串不必实现为字符数组,它完全可以将字符串实现为不可变的绳索,它具有O(1)concats和O(lg n)子串的有趣属性. (6认同)

Car*_*ñoz 21

字符串并不是真正不可改变的.它们只是公开不变的.这意味着您无法从其公共界面修改它们.但在内部实际上是可变的.

如果你不相信我String.Concat反射器来看待定义.最后一行是......

int length = str0.Length;
string dest = FastAllocateString(length + str1.Length);
FillStringChecked(dest, 0, str0);
FillStringChecked(dest, length, str1);
return dest;
Run Code Online (Sandbox Code Playgroud)

正如您所看到的那样,FastAllocateString返回一个空的但已分配的字符串,然后将其修改为FillStringChecked

实际上这FastAllocateString是一个extern方法,并且FillStringChecked是不安全的,所以它使用指针来复制字节.

也许有更好的例子,但这是我到目前为止找到的.

  • 在此博客文章中查看有关此内容的更多信息https://blog.getpaint.net/2015/07/21/net-strings-are-immutable-except-that-theyre-not/ (2认同)

kol*_*osy 14

字符串管理是一个昂贵的过程.保持字符串不可变允许重复使用重复的字符串,而不是重新创建.

  • @up:你觉得传递~100 MB(甚至更大)的字符串叠加会好吗? (4认同)
  • 啊..所以这就是为什么字符串是引用类型或者值类型..实际上对我来说这是一个很大的问题**如果字符串是不可变的,为什么不使用值类型..?**感谢无论如何. (2认同)

Neb*_*oft 13

为什么字符串类型在C#中是不可变的

String是一种引用类型,因此它永远不会被复制,而是通过引用传递.将其与C++ std :: string对象(不是不可变的)进行比较,该对象由value传递.这意味着如果你想在Hashtable中使用String作为键,你在C++中就可以了,因为C++会复制字符串以将键存储在哈希表中(实际上是std :: hash_map,但仍然存在)以供以后比较.所以即使你以后修改了std :: string实例,你也没关系.但在.Net中,当您在Hashtable中使用String时,它将存储对该实例的引用.现在假设字符串不是不可变的,看看会发生什么:1.有人将一个带有键"hello"的值x插入到Hashtable中.2. Hashtable计算String的哈希值,并将对字符串的引用和值x放在适当的存储桶中.3.用户将String实例修改为"bye".4.现在有人想要哈希表中与"hello"关联的值.它最终会查找正确的存储桶,但在比较字符串时会显示"bye"!="hello",因此不返回任何值.也许有人想要"再见"的价值?"bye"可能有不同的哈希值,因此哈希表会在不同的桶中查找.该存储桶中没有"再见"键,因此仍未找到我们的输入.

使字符串不可变意味着步骤3是不可能的.如果有人修改了字符串,他正在创建一个新的字符串对象,只留下旧的字符串对象.这意味着哈希表中的键仍然是"你好",因此仍然是正确的.

因此,可能除其他外,不可变字符串是一种方法,可以使通过引用传递的字符串用作哈希表或类似字典对象中的键.


Nic*_*ver 6

只是把它扔进去,一个经常被遗忘的视图是安全的,如果字符串是可变的,请记录这个场景:

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;
}
Run Code Online (Sandbox Code Playgroud)

如果你被允许在传递完成后改变它们,你会看到它是如何非常非常糟糕的.

  • @ktutnik - 在一个多线程场景中,您可以更改该字符串的内容,*在*它通过访问检查后,有效地绕过它并访问您想要的任何内容.这是*许多*安全例子中的一个.这个答案没有解决"你会做什么*如果*他们是可变的?"......这是一个不同的问题,问题是"为什么他们现在不可变?". (2认同)

dsi*_*cha 5

您永远不必防御性地复制不可变数据.尽管您需要复制它以使其变异,但通常能够自由别名并且永远不必担心这种混叠的意外后果可以导致更好的性能,因为缺乏防御性复制.


Kev*_*vin 5

字符串作为.NET中的引用类型传递。

引用类型在堆栈上放置一个指向托管堆上实际实例的指针。这与值类型不同,后者将整个实例保存在堆栈中。

当将值类型作为参数传递时,运行时将在堆栈上创建该值的副本,并将该值传递给方法。这就是为什么必须使用'ref'关键字传递整数以返回更新的值的原因。

传递引用类型后,运行时将在堆栈上创建指针的副本。那个复制的指针仍然指向引用类型的原始实例。

字符串类型具有重载的=运算符,该运算符将创建自身的副本,而不是指针的副本-使其行为更像值类型。但是,如果仅复制了指针,则第二个字符串操作可能会意外覆盖另一个类的私有成员的值,从而导致一些令人讨厌的结果。

正如其他文章所提到的,StringBuilder类允许创建字符串而没有GC开销。

  • 实际上,string没有重载=运算符,如果您对a = b进行字符串运算,则ReferenceEquals(a,b)甚至是ReferenceEquals(a,a.Clone())。关键在于,由于它的不可变性,即使不是,我们也可以将其视为=复制。我们不必担心更改b会影响a,因为不可能更改b。 (3认同)