某些语言(C#或Java)具有不可变字符串,而其他语言(例如Ruby)具有可变字符串。这些设计选择背后的原因是什么?
答案 0 :(得分:5)
不可变字符串好的一个原因是它使Unicode支持更容易。现代Unicode无法再有效地适应固定大小的数据单元,这会破坏字符串索引和内存地址之间的一对一对应关系,从而为可变字符串提供优势。
过去,大多数西方应用程序使用单字节字符(各种基于ASCII的编码,或EBCDIC ......),因此您通常可以通过将字符串视为字节缓冲区(如传统的C应用程序)来有效地处理它们。
当Unicode相当新时,对前16位以外的任何内容都没有太多要求,因此Java为其String
s(和StringBuffer
s使用了双字节字符。这使用了两倍的内存,并忽略了超出16位的Unicode扩展可能出现的任何问题,但当时很方便。
现在Unicode并不是那么新,虽然最常用的字符仍然适合16位,但你无法真正摆脱假装基本多语言平面的存在。如果你想诚实地声称支持Unicode,你需要可变长度的字符或更大的(32位?)字符单元。
对于可变长度字符,您无法再在O(1)时间内索引到任意长度的字符串 - 除非有其他信息,您需要从头开始计算以确定第N个字符是什么。这也破坏了可变字符串缓冲区的主要优点:能够无缝地修改子字符串。
幸运的是,大多数字符串操作实际上并不需要这种就地修改功能。词汇表,解析和搜索都是从头到尾按顺序迭代进行的。一般的搜索和替换从未就位,因为替换字符串的长度不必与原始字符串相同。
连接大量的子串实际上并不需要就地修改以提高效率。但是,你确实需要更加小心,因为(正如其他人已经指出的那样)通过为每个N个部分子串分配一个新字符串,一个简单的连接循环很容易就是O(N ^ 2)...
避免幼稚连接的一种方法是提供可变的StringBuffer
或ConcatBuffer
对象,旨在有效地进行连接。另一种方法是包含一个不可变的字符串构造函数,它将迭代器转换为一系列字符串(高效地)连接。
但是,更一般地说,可以编写一个通过引用有效连接的不可变字符串库。这种字符串通常被称为“rope”或“cord”,表明它至少比它所组成的基本字符串更重要,但是为了连接,它更多 more < / em>高效,因为它根本不需要重新复制数据!
上面的维基百科链接说“绳索”数据结构是连接的O(log N),但是Okasaki的开创性论文“Purely Functional Data Structures”显示了如何在O(1)时间内进行连接。
答案 1 :(得分:2)
至少在Java的情况下,使字符串不可变的部分原因是安全性和线程安全性。 Java非常重视运行时安全性(它最初设计用于允许机顶盒和Web浏览器下载和执行远程内容,而不会影响主机系统)。为了提高安全性,字符串是不可变的,不能被子类化。这意味着Java运行时可以传递并接收来自用户的字符串,同时保证字符串的值将保持不变(即,攻击者不能对字符串进行子类化,将看似有效的字符串传递给函数,但是然后稍后更改该值以获取对错误数据的访问权,或者使用多个线程使得字符串在某一点看起来正确,但随后会在之后发生变异。
此外,不变性在多线程系统中具有效率优势,因为不必对字符串进行锁定。它还可以轻松实现子字符串操作,因为许多字符串可以共享相同的底层字符数组,但具有不同的起点和终点。
答案 2 :(得分:1)
如果你考虑一下,所有基本数据类型都是不可变的。您不会将整数10更改为11,而将10替换为11.使字符串基本且不可变,允许池化和其他无法实现的优化。
答案 3 :(得分:1)
至于缺点,不可变字符串需要互补的可变数据结构(即字符串缓冲区),以允许经济的附加,重新排序和其他类似的操作。
在不可变结构上执行的此类操作将需要不合理的资源量。
Programming in Lua有brilliant explanation的问题。
为了进一步思考,一些语言(如Common Lisp)同时具有非破坏性和破坏性功能,其他语言 - 包括不可变列表和可变列表(Python)。
如果任务如此充满危险,那么为什么不从中省略它 语言?有两个原因:表现力和效率。 分配是更改共享数据的最明确方式。并且任务是 比绑定更有效。绑定创建一个新的存储位置, 它分配存储,消耗额外的内存(如果 绑定永远不会超出范围)或对垃圾收集器征税(如果 绑定最终会超出范围)。
但是,作为反例,许多JavaScript(具有不可变字符串)解释器在实现级别将字符串视为可变数组。
同样地,Clojure has transients,它看起来像优雅的纯函数而不是不可变的数据结构,但内部使用可变状态来提高效率。