如果string在.NET中是不可变的,那么为什么子string需要O(n)次?

鉴于string在.NET中是不可变的,我想知道为什么他们被devise为这样的string.Substring()需要O( substring.Length )时间,而不是O(1)

即什么是权衡,如果有的话?

更新:我非常喜欢这个问题,我只是博客。 请参阅string,不变性和持久性


简短的回答是: 如果n不增长,则O(n)是O(1)。 大多数人从细小的string中提取细小的子串,所以复杂性如何渐进地渐进式地增长是完全不相关的

长的答案是:

构build一个不可变数据结构,使得实例上的操作允许只有less量(通常为O(1)或O(lg n))复制或新分配的原始内存的重用被称为“持久”不可变的数据结构。 .NET中的string是不可变的。 你的问题基本上是“为什么他们不坚持”?

因为当你查看.NET程序中通常在string上执行的操作时,只是简单地创build一个全新的string, 几乎没有任何相关的方式。 构build一个复杂的持久数据结构的开销和难度并不为此付出代价。

人们通常使用“子串”来提取一个短的string – 比如说十或二十个字符 – 从一个稍长的string中提取出来 – 也许是几百个字符。 你在逗号分隔的文件中有一行文本,你想提取第三个字段,这是一个姓氏。 该行可能会长达几百个字符,名字将会是几十个字符。 在现代硬件上,五十个字节的string分配和存储器复制速度惊人地快 。 这使得一个新的数据结构,由一个指向现有string中间的指针加上一个长度, 也是惊人的快速是无关紧要的; “足够快”从定义上来说足够快。

提取的子带通常体积小,寿命短。 垃圾收集者很快就要收回他们,他们一开始就没有占用太多的空间。 所以使用鼓励大部分内存重用的一贯策略也不是赢。 你所做的只是让你的垃圾收集器变慢,因为现在它不得不担心处理内部指针。

如果人们通常在string上做的子string操作是完全不同的,那么采取一个持久的方法是有意义的。 如果人们通常具有百万字符的string,并且抽取了数十个大小在十万个字符范围内的重叠的子string,并且这些子string在堆中居住了很长时间,那么使用持久子string办法; 这不是浪费和愚蠢的。 但是大多数业务线程编程人员甚至不会像这些事情那样做任何事情 。 .NET不是一个专门为人类基因组计划devise的平台; DNA分析程序员必须每天解决这些string使用特征的问题; 几率是好的,你不。 less数谁build立自己的持久性数据结构,密切配合他们的使用场景。

例如,我的团队编写的程序可以在您inputC#和VB代码时进行即时分析。 其中一些代码文件是巨大的 ,因此我们不能做O(n)string操作来提取子string或插入或删除字符。 我们已经构build了一堆持久不变的数据结构,用于表示对文本缓冲区的编辑,从而使我们能够快速高效地重用大量现有string数据并在典型编辑时重用现有的词法和语法分析。 这是一个难以解决的问题,其解决scheme是针对C#和VB代码编辑的特定领域进行的。 期望内置的stringtypes为我们解决这个问题是不现实的。

正是因为string是不可变的, .Substring必须复制至less一部分原始string。 复制n个字节应该花费O(n)次。

你如何看待你会在一段时间内复制一堆字节?


编辑:Mehrdadbuild议不要复制string,但保留一个参考。

考虑在.Net中,一个多字节的string,有人在其上调用.SubString(n, n+3) (对于string中间的任何n)。

现在,整个string不能被垃圾收集只是因为一个引用持有4个字符? 这似乎是一个荒谬的浪费空间。

此外,跟踪对子string(甚至可能在子string内)的引用,并试图在最佳时间进行复制以避免击败GC(如上所述),使得该概念成为一个噩梦。 复制.SubString并维护简单的不可变模型要简单得多,也更可靠。


编辑:这里有一个很好的一点阅读有关在较大的string内保持对子string的引用的危险。

Java(与.NET相对)提供了两种执行Substring() ,您可以考虑是仅仅保留一个引用还是将整个子string复制到新的内存位置。

简单的.substring(...)共享内部使用的char数组和原始String对象,然后如果需要,您可以使用new String(...)将其复制到新的数组(如果需要)(以避免妨碍垃圾回收一)。

我认为这种灵活性对开发者来说是最好的select。

Java用于引用较大的string,但是:

Java将其行为改为复制 ,以避免内存泄露。

我觉得可以改进:为什么不只是有条件的复制呢?

如果子string至less是父代的一半,则可以引用父代。 否则,只能复制一份。 这样可以避免大量内存泄露,同时还可以提供显着的优势。