在 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 表示 variable,let 表示 constant。在您的情况下, var String 将是可变的,而 let String 将是不可变的。字符也可以附加到可变字符串。对于预分配,您可以使用[Character](count: 100, repeatedValue: "0")
创建一定长度的Character
s 数组。 (并使用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 这是否意味着“不可变可变”String
与let x: String
语句一起使用?可变字符串与不可变字符串的内部表示可能非常不同,因为它们针对不同的场景(例如不可变子字符串)进行了优化。
【参考方案1】:
(此答案是根据对 Swift 2 和 3 有效的文档和源代码编写的:一旦 Swift 4 到来,可能需要更新和修改)
由于 Swift 现已开源,我们其实可以看一下 Swift 的源代码:s native String
从上面的来源,我们有以下评论
/// 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 位系统上
CoreData/SQLite 确定字段是不是以特定字符串结尾的最快方法