在 Swift 中追加字符以形成字符串的最快、最精简的方法

Posted

技术标签:

【中文标题】在 Swift 中追加字符以形成字符串的最快、最精简的方法【英文标题】:Fastest, leanest way to append characters to form a string in Swift 【发布时间】:2016-04-04 06:07:31 【问题描述】:

我来自 C# 背景,其中System.String 是不可变的,字符串连接相对昂贵(因为它需要重新分配字符串)我们知道使用StringBuilder 类型,因为它预先分配了一个更大的缓冲区,其中单个字符(@ 987654325@,一种 16 位值类型)和短字符串可以很便宜地连接起来,无需额外分配。

我正在将一些 C# 代码移植到 Swift,它从字符长度小于 8 位的子八位字节索引处读取位数组 ([Bool])(这是一种非常注重空间的文件格式)。

我的 C# 代码是这样的:

 StringBuilder sb = new StringBuilder( expectedCharacterCount );
 int idxInBits = 0;
 Boolean[] bits = ...;
 for(int i = 0; i < someLength; i++) 
     Char c = ReadNextCharacter( ref idxInBits, 6 ); // each character is 6 bits in this example
     sb.Append( c );
 

在 Swift 中,我假设 NSMutableString 相当于 .NET 的 StringBuilder,并且我发现了这个关于附加单个字符 (How to append a character to string in Swift?) 的 QA,所以在 Swift 中我有这个:

var buffer: NSMutableString
for i in 0..<charCount 
    let charValue: Character = readNextCharacter( ... )
    buffer.AppendWithFormat("%c", charValue)

return String(buffer)

但我不知道为什么它首先通过格式字符串,这似乎效率低下(在每次迭代中重新解析格式字符串)并且由于我的代码在 ios 设备上运行,我希望对我的程序非常保守CPU 和内存使用情况。

在我写这篇文章的时候,我了解到我的代码真的应该使用UnicodeScalar 而不是Character,问题是NSMutableString 不允许你附加UnicodeScalar 值,你必须使用Swift 自己的可变@ 987654335@ 类型,所以现在我的代码如下所示:

var buffer: String
for i in 0..<charCount 
    let x: UnicodeScalar = readNextCharacter( ... )
    buffer.append(x)

return buffer

我认为String 是不可变的,但我注意到它的append 方法返回Void

这样做我仍然感到不舒服,因为我不知道 Swift 的 String 类型是如何在内部实现的,而且我不知道如何预先分配一个大缓冲区以避免重新分配(假设 Swift 的 String 使用增长算法)。

【问题讨论】:

在 Swift 中,var 表示 variablelet 表示 constant。在您的情况下, var String 将是可变的,而 let String 将是不可变的。字符也可以附加到可变字符串。对于预分配,您可以使用[Character](count: 100, repeatedValue: "0") 创建一定长度的Characters 数组。 (并使用String(charArray) 将其转换回字符串)。我会说没有这个必要。在 Swift 中追加非常快。 对于它的价值,GitHub 上有一个 Swift StringBuilder 要点:gist.github.com/kristopherjohnson/1fc55e811d944a430289 看起来它旨在实现 C# StringBuilder 类的子集,并且在手动将 C# 程序转换为 Swift 时可能很有用. (至少,如果你不担心让 Swift 纯粹主义者不高兴,他们更愿意重写代码以“以 Swift 方式”完成。)但不幸的是,它是为 Swift 3 之前的 Swift 版本编写的,需要大约 10小改动被接受为有效的 Swift 3。 @J.Wang 这是否意味着“不可变可变”Stringlet x: String 语句一起使用?可变字符串与不可变字符串的内部表示可能非常不同,因为它们针对不同的场景(例如不可变子字符串)进行了优化。 【参考方案1】:

(此答案是根据对 Swift 2 和 3 有效的文档和源代码编写的:一旦 Swift 4 到来,可能需要更新和修改)

由于 Swift 现已开源,我们其实可以看一下 Swift 的源代码:s native String

swift/stdlib/public/core/String.swift

从上面的来源,我们有以下评论

/// Growth and Capacity
/// ===================
///
/// When a string's contiguous storage fills up, new storage must be
/// allocated and characters must be moved to the new storage.
/// `String` uses an exponential growth strategy that makes `append` a
/// constant time operation *when amortized over many invocations*.

鉴于上述情况,您不必担心在 Swift 中附加字符的性能(无论是通过 append(_: Character)append(_: UniodeScalar) 还是 appendContentsOf(_: String)),因为为某个 @987654332 重新分配了连续存储@instance 不应该非常频繁地w.r.t。要进行此重新分配,需要附加的单个字符数。

另请注意,NSMutableString 不是“纯原生”Swift,而是属于桥接 Obj-C 类家族(可通过 Foundation 访问)。


对您的评论的注释

“我认为String 是不可变的,但我注意到它的append 方法返回Void。”

String 只是一个(值)类型,可变属性和不可变属性都可以使用它

var foo = "foo" // mutable 
let bar = "bar" // immutable
    /* (both the above inferred to be of type 'String') */

mutating void-return 实例方法 append(_: Character)append(_: UniodeScalar) 可被可变和不可变的 String 实例访问,但与后者一起使用它们自然会产生编译时错误

let chars : [Character]  = ["b","a","r"]
foo.append(chars[0]) // "foob"
bar.append(chars[0]) // error: cannot use mutating member on immutable value ...

【讨论】:

那么+append 在性能方面是否相同? s+= "a"s = s + "a"s.append("a") 做同样的工作吗? @DanM。我们可以访问标准库的(开放)源代码来回答这个问题:+= operator 调用lhs._core.append(rhs._core)+ operator 创建一个新的String 实例来保存结果(命名为lhs),此后也调用lhs._core.append(rhs._core)。最后append(...) method直接调用_core.append(other._core) 由于String 有一个内部可变缓冲区(通过StringCore,我理解) - 是否可以预分配该缓冲区?我看不到任何接受容量或保留大小参数的init 函数。 这个答案只保证对 Swift 3 是正确的,因为在 Swift 4 中将对 String 进行重大更改。要分配 String 的内部缓冲区,请使用 s.characters.reserveCapacity(capacity)。这是RangeReplaceableCollection 的协议要求,String.CharacterView 符合。参照。 developer.apple.com/reference/swift/string.characterview 和 developer.apple.com/reference/swift/string.characterview/… @Dai as BallpointBen:s cmets 下面,您可以为给定数量的字符(扩展字形簇)保留容量,但访问给定 String 实例的 CharacterView 并在查看。

以上是关于在 Swift 中追加字符以形成字符串的最快、最精简的方法的主要内容,如果未能解决你的问题,请参考以下文章

Swift 追加到数组给了我 EXC_BAD_INSTRUCTION EXC_l386_INVOP 在 32 位系统上

以最快的速度获取字符串中图像的所有像素数据

PHP 以最快的方式判断字符串是否以某给定字符串开始

CoreData/SQLite 确定字段是不是以特定字符串结尾的最快方法

在 JavaScript 中测试两个字符串是不是完全匹配的最快方法

算法题2486. 追加字符以获得子序列