Swift 字符串与字符完全导读(二):Unicode 视图、索引系统与内存陷阱

Unicode 的三种编码视图

Swift 把同一个 String 暴露成 4 种迭代方式:

视图 元素类型 单位长度 典型用途
String Character 人眼"一个字符" 业务逻辑
utf8 UInt8 1~4 字节 网络/文件 UTF-8 流
utf16 UInt16 2 或 4 字节 与 Foundation / Objective-C 交互
unicodeScalars UnicodeScalar 21-bit 精确到标量,做编码分析

代码一览

swift 复制代码
let dog = "Dog‼🐶"          
// 4 个 Character,5 个标量,10 个 UTF-8 字节,6 个 UTF-16 码元

// 1. Character 视图
for ch in dog {
    print(ch, terminator: "|")
}   // D|o|g|‼|🐶|
print()

// 2. UTF-8
dog.utf8.forEach {
    print($0, terminator: " ")
}// 68 111 103 226 128 188 240 159 144 182
print()

// 3. UTF-16
dog.utf16.forEach {
    print($0, terminator: " ")
}// 68 111 103 8252 55357 56374
print()

// 4. Unicode Scalars
dog.unicodeScalars.forEach {
    print($0.value, terminator: " ")
}// 68 111 103 8252 128054
print()

扩展字形簇 vs 字符计数

swift 复制代码
var cafe = "cafe"
print(cafe.count)           // 4
cafe += "\u{301}"           // 附加组合重音
print(cafe, cafe.count)     // café 4  (仍然是 4 个 Character)

结论:

  • count 走的是"字形簇"边界,必须从头扫描,复杂度 O(n)。
  • 不要在大循环里频繁读取 str.count;缓存到局部变量。

String.Index 体系

基础 API

swift 复制代码
let str = "Swift🚀"
let start = str.startIndex
let end   = str.endIndex        // 指向最后一个字符之后
// let bad = str[7]             // ❌ Compile-time error:Index 不是 Int

let fifth = str.index(start, offsetBy: 5)
print(str[fifth])             // 🚀

往前/后偏移

swift 复制代码
let prev = str.index(before: fifth)
let next = str.index(after: start)
let far  = str.index(start, offsetBy: 4, limitedBy: end) // 安全版,返回可选值

区间与切片

swift 复制代码
let range = start...fifth
let sub = str[range]            // Substring

子串 Substring 的"零拷贝"双刃剑

swift 复制代码
let article = "Swift String 深度指南"
let intro   = article.prefix(9) // Substring
// 此时整份 article 的缓冲区仍被 intro 强引用,内存不会释放

// 正确姿势:尽快转成 String
let introString = String(intro)

内存图简述

lua 复制代码
article ┌-------------------------┐
        │ Swift String 深度指南     │
        └-------------------------┘
          ▲
          │零拷贝
       intro (Substring)

只要 Substring 活着,原 String 的缓冲区就不得释放。

最佳实践:函数返回时立刻 String(substring),避免"隐形内存泄漏"。

插入、删除、Range 替换全 API 速查

swift 复制代码
var s = "Hello Swift"

// 插入字符
s.insert("!", at: s.endIndex)
// Hello Swift!
print(s)

// 插入字符串
s.insert(contentsOf: " 2025", at: s.index(before: s.endIndex))
// Hello Swift 2025!
print(s)

// 删除单个字符
s.remove(at: s.firstIndex(of: " ")!)        // 删掉第一个空格
// HelloSwift 2025!
print(s)

// 删除子范围
let range = s.range(of: "Swift")!
s.removeSubrange(range)
// Hello 2025!
print(s)

// 直接替换
s.replaceSubrange(s.range(of: "2025")!, with: "2026")
// Hello 2026!
print(s)

实战:写一个"安全截断"函数

需求

  • 按"字符数"截断,但不能把 Emoji/组合音标劈成两半;
  • 尾部加"..."且总长度不超过 maxCount;
  • 返回 String,而非 Substring。

代码

swift 复制代码
func safeTruncate(_ text: String, maxCount: Int, suffix: String = "...") -> String {
    guard maxCount > suffix.count else { return suffix }
    let maxTextCount = maxCount - suffix.count
    var count = 0
    var idx = text.startIndex
    while idx < text.endIndex && count < maxTextCount {
        idx = text.index(after: idx)
        count += 1
    }
    // 如果原文很短,无需截断
    if idx == text.endIndex { return text }
    return String(text[..<idx]) + suffix
}

// 测试
let long = "Swift 字符串深度指南🚀🚀🚀"
print(safeTruncate(long, maxCount: 12)) // "Swift 字符..."

复杂度 O(n),只扫描一次;不依赖 count 的重复计算。

性能与内存最佳实践清单

  1. 大量拼接用 String.reserveCapacity(_:) 预分配。
  2. 遍历+修改时先复制到 var,再批量改,减少中间临时对象。
  3. 网络/文件 IO 用 utf8 视图直接写入 Data,避免先转 String
  4. 正则提取到的 [Substring] 尽快 map 成 [String] 再长期持有。
  5. 不要缓存 str.count 在多次循环外,如果字符串本身在变。

扩展场景:今天就能落地的 3 段代码

日志脱敏(掩码手机号)

swift 复制代码
func maskMobile(_ s: String) -> String {
    guard s.count == 11 else { return s }
    let start = s.index(s.startIndex, offsetBy: 3)
    let end   = s.index(s.startIndex, offsetBy: 7)
    return s.replacingCharacters(in: start..<end, with: "****")
}

语法高亮(简易关键词着色)

swift 复制代码
let keywords = ["let", "var", "func"]
var code = "let foo = 1"
for kw in keywords {
    if let range = code.range(of: kw) {
        code.replaceSubrange(range, with: "[KW]\(kw)[KW]")
    }
}

大文件分块读(UTF-8 视图直接操作)

swift 复制代码
import Foundation
func chunk(path: String, chunkSize: Int = 1<<14) -> [String] {
    guard let data = FileManager.default.contents(atPath: path) else { return [] }
    return data.split(separator: UInt8(ascii: "\n"),
                      maxSplits: .max,
                      omittingEmptySubsequences: false)
               .map { String(decoding: $0, as: UTF8.self) }
}

利用 UInt8 切片,避免先整体转成 String 的额外内存峰值。

相关推荐
非专业程序员Ping12 小时前
一文读懂字体文件
ios·swift·assembly·font
wahkim16 小时前
移动端开发工具集锦
flutter·ios·android studio·swift
非专业程序员Ping1 天前
一文读懂字符、字形、字体
ios·swift·font
东坡肘子1 天前
去 Apple Store 修手机 | 肘子的 Swift 周报 #0107
swiftui·swift·apple
非专业程序员2 天前
iOS/Swift:深入理解iOS CoreText API
ios·swift
xingxing_F2 天前
Swift Publisher for Mac 版面设计和编辑工具
开发语言·macos·swift
YGGP3 天前
【Swift】LeetCode 438. 找到字符串中所有字母异位词
swift
QWQ___qwq4 天前
Swift中.gesture的用法
服务器·microsoft·swift
QWQ___qwq4 天前
SwiftUI 布局之美:Padding 让界面呼吸感拉满
ios·swiftui·swift