不可变字符串的优缺点

6

一些语言(如C#或Java)具有不可变的字符串,而其他语言(例如Ruby)具有可变的字符串。这些设计选择背后的原因是什么?


4
这里有一个类似的东西:https://dev59.com/iXA75IYBdhLWcg3wOWVd - Science_Fiction
@Science_Fiction 上面那个问题的最佳答案是关于不变性的,但是为什么是字符串?我认为这与标记清除垃圾回收器有关。 - synapse
1
https://dev59.com/7nE85IYBdhLWcg3wzWvM - Karoly Horvath
请参考 SO 上的 when-does-python-allocate-new-memory-for-identical-strings:在我看来,没有明确的答案。 - denis
4个回答

5
不可变字符串的好处之一是使Unicode支持更加容易。现代Unicode不能有效地适应固定大小的数据单元,这破坏了字符串索引和内存地址之间的一对一对应关系,而这正是可变字符串的优势所在。
过去,大多数西方应用程序使用单字节字符(各种基于ASCII的编码或EBCDIC...),因此您通常可以通过将字符串视为字节缓冲区(如传统的C应用程序中)有效地处理它们。
当Unicode比较新时,没有太多需要使用第一个16位以外内容的要求,因此Java在其String和StringBuffer中使用双字节字符。这使用了两倍的内存,并忽略了Unicode扩展超过16位可能出现的任何问题,但当时这很方便。
现在Unicode不再那么新鲜,虽然最常用的字符仍适合16位,但你不能真正假装基本多语言平面是唯一存在的。如果你想诚实地声称支持Unicode,则需要可变长度字符或更大的(32位?)字符单元。
使用可变长度字符,您不能再在O(1)时间内索引任意长度的字符串 - 除非有额外的信息,否则您需要从开头计算以确定第N个字符。这也破坏了可变字符串缓冲区的主要优势:无缝修改子字符串的能力。

幸运的是,大多数字符串操作实际上并不需要这种原地修改的能力。词法分析、语法分析和搜索都是按顺序迭代进行的,从头到尾。通用的搜索和替换从一开始就不是原地操作,因为替换字符串的长度不必与原始字符串相同。


连接大量的子字符串并不需要修改原字符串来提高效率。但是需要更加小心,因为(正如其他人指出的那样),一个天真的拼接循环可以很容易地通过为每个 N 个部分子字符串分配一个新字符串而变成 O(N^2)…
避免天真的拼接一种方法是提供一个可变的StringBuffer或ConcatBuffer对象,用于高效地进行拼接。另一种方法是包含一个不可变字符串构造函数,该构造函数将迭代器输入到要(高效)拼接的字符串序列中。
但是,更一般地,可以编写一个不可变字符串库,通过引用高效地进行拼接。这种字符串通常被称为“rope”或“cord”,以表明它至少比组成它的基本字符串更重,但对于拼接目的,它更加高效,因为它根本不需要重新复制数据!
上面的维基百科链接说,“绳索”数据结构连接的时间复杂度为O(log N),但Okasaki的经典论文“Purely Functional Data Structures”展示了如何在O(1)时间内完成连接。

只是有几点我不同意,我认为你触及了所有错误的点。首先,Unicode 是代码点到字符和字形的映射。所选择的编码方案是一个单独的话题,完全可以做到一对一 - 使直接索引访问高效。现在,即使词法分析、解析和搜索不会就地修改,但还有许多其他情况会这样做 - 您通常的 toupper()/tolower()/titlecase()/mid()/left()/right()/reverse() 只是其中一些常见情况,都可以就地完成(有一些本地特定的例外)。甚至替换也可以。 - Wiz
通常情况下,由于可变字符串对象通常保留比实际使用更多的内存(大多数使用2的幂使整体增长的平均时间复杂度为O(1)),因此字符串操作通常是原地进行的,在实际分析中它们非常高效。 - Wiz

2
在 Java 中,将字符串设为不可变的一部分原因是出于安全性和线程安全性考虑。Java非常重视运行时安全性(最初设计是为了允许机顶盒和Web浏览器下载并执行远程内容而不会危及主机系统)。为了提高安全性,字符串是不可变的且不能被子类化。这意味着Java运行时可以在用户之间传递和接收字符串,同时保证字符串的值保持不变(也就是说,攻击者无法对字符串进行子类化,向函数中传入看似有效的字符串,然后在稍后更改其值以获取错误的数据,或者利用多个线程使得字符串在某些时候看起来正确,但随后被改变)。
另外,不可变性在多线程系统中具有效率优势,因为无需对字符串进行锁定。它还使得实现子串操作变得容易,因为许多字符串可以共享相同的字符数组,但具有不同的起始点和终止点。

1

如果你仔细想一下,所有基本数据类型都是不可变的。你不能将整数10改为11,而是用11替换10。将字符串作为基本且不可变的类型允许进行池化和其他优化,否则这些操作是不可能实现的。


什么使数据类型成为基本类型? - synapse
内置于语言中的(而不是由某个库或扩展添加的) - ddyer
1
在一些编程语言中,字符是基本数据类型。字符串只是字符数组。 - Karoly Horvath

1

至于缺点,不可变字符串需要互补的可变数据结构(即字符串缓冲区)来允许经济地添加、重新排序和其他类似操作。

在不可变结构上执行此类操作将需要不合理的资源量。

Lua 编程入门对此问题有一精彩的解释


进一步思考,有些语言(如Common Lisp)既有非破坏性函数又有破坏性函数,而其他语言则同时具有不可变和可变列表(Python)。
引用《Common Lisp书籍》的话:
“如果赋值如此危险,为什么不将其从语言中省略?原因有两个:表达能力和效率。赋值是改变共享数据最清晰的方式。而且赋值比绑定更有效。绑定会创建一个新的存储位置,这会分配存储空间,消耗额外的内存(如果绑定从未超出范围),或者对垃圾收集器造成负担(如果绑定最终确实超出了范围)。 ”
然而,作为反例,许多JavaScript(具有不可变字符串)解释器在实现级别上将字符串视为可变数组。
同样,Clojure拥有瞬态(transients),看起来像是对不可变数据结构的优雅纯函数,但内部使用可变状态以提高效率。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接