03 类型与字符串

整数与浮点的边界、Character 与 String 的全部写法、正则、格式化

03 类型与字符串

数值类型全景

类型位数范围 / 精度
Int / UInt平台字长(64 位平台上 64 位)Int 是默认整数类型,能用就用它
Int8 Int16 Int32 Int648 / 16 / 32 / 64有符号,与 C 的 int8_t 等一一对应
UInt8 UInt16 UInt32 UInt64同上无符号,处理二进制协议时用
Float1616精度约 3 位十进制;只有 Apple 芯片(arm64)可用,Intel Mac 上是 unavailable 🆕
Float32精度约 6 位十进制
Double64精度约 15 位十进制,浮点默认类型
1
2
3
4
print(Int8.max, Int8.min, UInt8.max)
// prints: 127 -128 255
print(Int.max)
// prints: 9223372036854775807

⚠️ UInt 并不是"更安全的 Int"。它只是不给你负数:UInt(0) - 1 不会变成 -1,而是当场崩溃。真要用环绕写法,&- 会送你一个看起来非常体面的巨大正数,bug 就从这里开始:

1
2
3
4
5
6
7
print(Int(0) - 1)
// prints: -1
print(UInt(0) &- 1)
// prints: 18446744073709551615
print(UInt(5) &- 7)
// prints: 18446744073709551614     本来想要 -2,拿到一个天文数字
// print(UInt(0) - 1)              // 🛑 运行时崩溃:下溢

再加上 [1, 2, 3].count、index 这类 API 一律返回 Int,全程用 UInt 只会换来满屏 Int(...) 转换。除非在写二进制协议或与 C 交互,否则一律用 Int。

整数

字面量

1
2
3
4
5
6
let decimal = 42
let binary = 0b1010_1010
let octal = 0o755
let hex = 0xFF_FF
let padded = 1_000_000     // 下划线只是给人看的
let negative = -17

常用操作

操作写法
判整除n.isMultiple(of: 3) 🔥 比 n % 3 == 0 更能表达意图
同时取商和余let (q, r) = 17.quotientAndRemainder(dividingBy: 5) → (3, 2)
随机数Int.random(in: 1...100)、Bool.random()、array.randomElement()
转字符串String(42)
从字符串解析Int("42") 返回 Int?
取绝对值abs(-3)
最小值 / 最大值min(a, b)、max(a, b)、array.max()
整数转浮点Double(7) / 2 → 3.5
1
2
3
4
5
6
print(7 / 2)
// prints: 3
print(Double(7) / 2)
// prints: 3.5
print(-7 / 2, -7 % 2)
// prints: -3 -1      除法向零截断,余数符号跟着被除数

⚠️ 两个整数相除得到整数,这不是 bug 是特性。算平均值时先转 Double,或者用 11 标准库速查 里的格式化工具。

溢出报告:既不想崩,又想知道出了事

+ 溢出会崩,&+ 会静默环绕,第三条路是让它把"溢出了"当返回值告诉你。这一族方法返回一个元组 (partialValue:overflow:),partialValue 是按环绕规则算出来的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let over = Int.max.addingReportingOverflow(1)
print(over.overflow, over.partialValue)
// prints: true -9223372036854775808

print(Int.max.multipliedReportingOverflow(by: 2).overflow)
// prints: true

print(Int(17).dividedReportingOverflow(by: 5))
// prints: (partialValue: 3, overflow: false)

print(Int8(100).addingReportingOverflow(100).partialValue)
// prints: -56     128 放不进 Int8,环绕成了 -56
需求写法
加法 / 减法a.addingReportingOverflow(b)、a.subtractingReportingOverflow(b)
乘法a.multipliedReportingOverflow(by: b)
除法 / 取余a.dividedReportingOverflow(by: b)、a.remainderReportingOverflow(dividingBy: b)
“装得下才给我”Int(exactly: someValue) → Int?,装不下给 nil

⚠️ 唯一一个除法也会溢出的组合是 Int.min / -1(结果比 Int.max 还大 1)。它同样有报告版本:Int.min.dividedReportingOverflow(by: -1).overflow 是 true。但直接写常量测不到——编译器会在编译期先报 division '-9223372036854775808 / -1' results in an overflow,得让除数经过一个变量或函数调用才测得到(实测)。

💭 这族 API 适合"用户输入的两个数相乘"这种既不想崩、又必须知道结果可不可信的场景:拿到 overflow == true 就返回一条友好的错误,而不是让进程消失。

二进制与位运算常用属性

写协议解析、权限位、哈希这些东西时,下面这几个属性比手写循环好用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let mask: UInt8 = 0b1011_0000
print(mask.nonzeroBitCount, UInt8(1).leadingZeroBitCount, UInt8(0b1000_0000).trailingZeroBitCount)
// prints: 3 7 7     1011_0000 里有三个 1;后两个数是"高位 / 低位连续几个 0"

print(Int8(-5).signum(), Int8(-5).magnitude)
// prints: -1 5

print(UInt16(0x0102).byteSwapped == 0x0201)
// prints: true

print(Int8.bitWidth, Int16(1).bitWidth)
// prints: 8 16

print(String(0b1011_0000, radix: 2))
// prints: 10110000
属性 / 方法含义
x.nonzeroBitCount二进制里 1 的个数(popcount)
x.leadingZeroBitCount / x.trailingZeroBitCount高位 / 低位连续 0 的个数 🔥 求"最高位在哪"常用
x.bitWidth类型一共多少位(是类型属性,写在类型上)
x.signum()符号:-1 / 0 / 1
x.magnitude丢掉符号的绝对值,类型变成对应的无符号类型
x.byteSwapped字节序翻转,网络字节序转换用得上
x.littleEndian / x.bigEndian按指定字节序取同一段位模式
x.words拆成机器字数组(大整数实现才用得到)🝖

💭 这些能力都来自 FixedWidthInteger 协议,Int、UInt8 这些定长整数全都遵守它。泛型里想用这些 API,约束就写 T: FixedWidthInteger。

浮点

1
2
3
4
5
6
7
let a: Double = 1.0 / 3.0
print(a)
// prints: 0.3333333333333333
print(Double.pi, Double.infinity, -Double.infinity)
// prints: 3.141592653589793 inf -inf
print(1e3, 1.5e-3, 0x1p2)
// prints: 1000.0 0.0015 4.0

特殊值与安全判断

属性 / 方法作用
x.isNaN判断是不是 NaN 🔥
x.isFinite既不是 NaN 也不是 ±inf
x.isInfinite是 ±inf
x.rounded()四舍五入到最接近的整数,.5 一律远离零(2.5 → 3)
x.rounded(.toNearestOrEven)银行家舍入法:.5 舍到偶数(2.5 → 2、3.5 → 4)
x.rounded(.down) / .up / .towardZero / .toNearestOrAwayFromZero指定其他舍入规则
x.ulp相邻可表示浮点之间的距离 🝖
保留两位小数(x * 100).rounded() / 100,标准库没有 scale: 参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print(Double.nan == Double.nan)
// prints: false    ⚠️ NaN 连自己都不等于自己
print(Double.nan.isNaN)
// prints: true

print(0.1 + 0.2 == 0.3)
// prints: false    浮点数永远的经典
print(0.1 + 0.2)
// prints: 0.30000000000000004

print((2.5).rounded(), (2.5).rounded(.toNearestOrEven))
// prints: 3.0 2.0      默认不是银行家舍入,要银行家那套得自己写

⚠️ 永远不要用 == 比较两个浮点数,要比较就用误差范围:

1
2
3
4
5
func almostEqual(_ a: Double, _ b: Double, tolerance: Double = 1e-9) -> Bool {
    abs(a - b) < tolerance
}
print(almostEqual(0.1 + 0.2, 0.3))
// prints: true

💭 涉及金额的计算请用整数分单位,或者 Foundation 的 Decimal,别和二进制浮点讲道理。

浮点的邻居与余数

整数有 + %,浮点这边对应的两个工具叫法更长:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
print(7.5.truncatingRemainder(dividingBy: 2))
// prints: 1.5
print((-7.5).truncatingRemainder(dividingBy: 2))
// prints: -1.5      符号跟被除数,和整数的 % 一个规矩

print(7.5.remainder(dividingBy: 2))
// prints: -0.5      IEEE 取余:商取"最近的整数",所以 7.5 减去 8

print((1.0).nextUp > 1.0, (1.0).nextDown < 1.0)
// prints: true true
写法含义
x.truncatingRemainder(dividingBy: y)商向零截断取余,等价于 C 的 fmod 🔥
x.remainder(dividingBy: y)IEEE 取余,商取最近的整数,结果可能和被除数异号 🝖
x.nextUp / x.nextDown相邻的下一个 / 上一个可表示浮点数
x.ulp当前量级下"一格"有多大
x.exponent / x.significand拆成指数与尾数,写数值分析才用得到 🝖

⚠️ Swift 的 % 只给整数。 写 7.5 % 2 会报 '%' is unavailable: For floating point numbers use truncatingRemainder instead(实测),照它说的改就行。

算钱请用 Decimal

Double 的 0.1 + 0.2 != 0.3 不是 bug,是二进制浮点的本性。要按人习惯的十进制算,就用 Foundation 的 Decimal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import Foundation

let a = Decimal(string: "0.1")!
let b = Decimal(string: "0.2")!
print(a + b)
// prints: 0.3
print(a + b == Decimal(string: "0.3")!)
// prints: true          同样是十进制,这里就没有 0.30000000000000004

print(Decimal(1) / Decimal(3))
// prints: 0.33333333333333333333333333333333333333

⚠️ Decimal 也不是万能的:它是十进制浮点,有效数字大约 38 位,除法照样会除不尽;而且它和 Double 不能混算,得显式转。🝖

💭 金额三条路,按推荐顺序:整数分单位 → Decimal → 先想清楚再碰 Double。展示时再用 formatted(.currency(code:)) 把整数分或 Decimal 变好看。

常用数学函数

这些来自标准库或 Foundation,多数需要 import Foundation:

函数说明
x.squareRoot()平方根,标准库
abs(x)绝对值,标准库
min(a, b) / max(a, b)取小 / 取大,标准库
pow(a, b)幂,需要 Foundation
round(x) / floor(x) / ceil(x)取整(返回浮点),需要 Foundation
sin cos tan log exp三角与指数,需要 Foundation
1
2
3
4
5
6
7
8
import Foundation

print((2.0).squareRoot())
// prints: 1.4142135623730951
print(pow(2, 10))
// prints: 1024        ⚠️ 两个整数字面量会选中 Foundation 的 Decimal 重载
print(pow(Double(2), 10))
// prints: 1024.0      要 Double 就得把类型说清楚

⚠️ pow 有 (Double, Double) 和 (Decimal, Int) 两个重载,而 2、2.0 这种字面量两边都能去,于是 pow(2.0, 10) 直接报 ambiguous use of 'pow'。老老实实写 pow(Double(2), 10),或者先声明成 let base: Double = 2.0。

数值与字符串之间的转换

Swift 不做隐式转换:Int 和 Double 之间、数字和字符串之间,都要显式说一句。规矩就一条——凡是可能失败的,返回可选值;凡是可能丢信息的,名字里带 exactly。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
print(Double(7) / 2)
// prints: 3.5

print(Int(3.9), Int(-3.9))          // 直接截断,负数也朝零截
// prints: 3 -3
print(Int((3.9).rounded()))         // 要四舍五入就自己先 rounded
// prints: 4

print(Int(exactly: 3.0) ?? -1)
// prints: 3
print(Int(exactly: 3.5) as Any)     // 不精确就不给,绝不悄悄砍一刀
// prints: nil
print(Float(exactly: 16_777_217) as Any)   // Float 放不下这个整数
// prints: nil
print(Float(16_777_217))            // 普通转换只会悄悄取整
// prints: 16777216.0

print(Double(Int.max) == Double(Int.max - 1))
// prints: true

💭 最后那个 true 就是精度丢失的证据:Int.max 和 Int.max - 1 是两个不同的整数,塞进 53 位尾数的 Double 之后变成了同一个值。整数运算永远不要借道 Double,尤其是 ID、时间戳、金额这几种大数。

需求写法失败 / 丢精度时
整数 → 浮点Double(i)、Float(i)大整数会丢精度,不报错
浮点 → 整数Int(d)小数被截断;NaN、±inf、超范围直接崩
浮点 → 整数(安全版)Int(exactly: d)给 Int?,不精确就是 nil
浮点 → 浮点Double(f)、Float(d)可能有舍入,用 exactly: 问清楚
整数 → 字符串String(i)、"\(i)"不会失败
字符串 → 整数Int("42")返回 Int?,非法输入给 nil
任意进制 → 整数Int("ff", radix: 16)同上
整数 → 任意进制字符串String(255, radix: 16)给 "ff"
字符串 → 浮点Double("1.5")返回 Double?
字符 → 数字 / ASCIIc.wholeNumberValue、c.asciiValue都是可选值
任意值 → 字符串String(describing:)、String(reflecting:)见 15 章
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
print(Int("42") ?? -1)
// prints: 42
print(Int(" 42 ") as Any)
// prints: nil       ⚠️ 前后空白不认,先 trimming 再转
print(Int("ff", radix: 16) ?? -1)
// prints: 255
print(String(255, radix: 16), Double("1.5") ?? -1)
// prints: ff 1.5
print("3.14".first!.wholeNumberValue ?? -1)
// prints: 3

⚠️ Int(d) 和 Int(exactly: d) 的区别值得记一辈子:前者"能转就转,转不了就崩",后者"不精确就返回 nil"。处理外部数据用后者,处理自己刚算出来的、确定在范围内的值用前者。

Bool

1
2
3
4
5
6
7
let ok = true
let done = !ok          // done == false

print(ok && !done || false)
// prints: true       && 比 || 紧,所以先算 (ok && !done)
print(ok && (done || false))
// prints: false      括号一加,结果就翻了

Bool 只有 true / false 两个值,没有 0/1 的隐式转换。要转数字自己写:ok ? 1 : 0。

Character

Character 表示一个扩展字形簇,不是"一个字节",也不是"一个 Unicode 标量"。

1
2
3
4
5
let letter: Character = "A"
let flag: Character = "🇨🇳"
let family: Character = "👨‍👩‍👧‍👦"
print(flag, family)
// prints: 🇨🇳 👨‍👩‍👧‍👦

常用判断:

属性说明
isLetter isNumber isWhitespace isPunctuation字符类别
isUppercase isLowercase大小写
isASCII是否落在 ASCII 范围内
isHexDigit十六进制数字
wholeNumberValue数字的数值,支持 "Ⅷ" 这类字符
asciiValueASCII 码,非 ASCII 返回 nil
1
2
3
4
5
6
print("7".first!.wholeNumberValue ?? -1)
// prints: 7
print("Ⅷ".first!.wholeNumberValue ?? -1)
// prints: 8
print("A".first!.isUppercase)
// prints: true

⚠️ Character 与 String 不能混用。let c: Character = "ab" 是编译错误,而 "a" + "b" 里两个都是 String。要给 Character 拼字符串,用 String(c) 或者 "" + String(c)。

String

创建方式

写法结果
"hello"普通字符串
""" 多行 """多行字符串,自动去掉公共缩进
#"C:\path"#原始字符串,反斜杠不转义
#"\#(value)"#原始字符串里的插值要写成 \#(...)
String(repeating: "ab", count: 3)ababab
String(42) / String(describing:)从其他值构造
String(decoding: bytes, as: UTF8.self)从字节构造,非法序列会被替换
String(validating: bytes, as: UTF8.self)同上,但非法序列返回 nil

多行字符串

1
2
3
4
5
6
7
let poem = """
    第一行
    第二行
    """
print(poem)
// prints: 第一行
//         第二行

规则只有两条:结束的 """ 决定缩进基准,每行多出来的缩进会被剥掉;想保住前导空格就在结束符号左侧多留空格。

1
2
3
4
5
let indented = """
        前面有四个空格
    """
print("[\(indented)]")
// prints: [    前面有四个空格]

上面这段里,内容行缩进 8 格、结束符号缩进 4 格,于是每行剥掉 4 格,结果保留了 4 个空格。缩进基准由结束符号决定——这也是为什么结束的 """ 不能顶格写在左边。

原始字符串

处理正则、路径、JSON 时非常省事:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let path = #"C:\Users\dev\file.txt"#
print(path)
// prints: C:\Users\dev\file.txt

let name = "Swift"
let greeting = #"你好,\#(name)"#
print(greeting)
// prints: 你好,Swift

let pattern = #"\\d+\.\\d+"#
print(pattern)
// prints: \\d+\.\\d+

插值与转义

写进字符串得到
\(表达式)任意表达式的字符串形式 🔥
\n \t \r \" \\换行、制表、回车、引号、反斜杠
\0空字符
\u{1F600}指定码位,😀
\(a, b)⚠️ 不是合法语法,Swift 5 起已移除元组插值
1
2
3
4
5
6
7
8
9
let x = 3, y = 4
print("\(x) + \(y) = \(x + y)")
// prints: 3 + 4 = 7
print("坐标为 (\(x), \(y))")
// prints: 坐标为 (3, 4)
print("坐标为 \(String(describing: (x, y)))")
// prints: 坐标为 (3, 4)
print("\u{1F600}")
// prints: 😀

⚠️ 一次插多个值必须并排写:"\(x), \(y)"。老教程里的 "\(x, y)" 在 Swift 5 就被移除了,现在它不是"能跑但不推荐",而是编译失败——编译器会把它解析成标准库里那个 appendInterpolation(_:default:),于是报 missing argument label 'default:'(值不是 String 时报的是"要求遵守 StringProtocol")。真想插一整个元组,就 String(describing:) 包一层。

⚠️ 插值会调用 description。可选值插进去会带上 Optional(...) 包装:

1
2
3
4
5
let maybe: Int? = 5
print("值是 \(maybe)")
// prints: 值是 Optional(5)
print("值是 \(maybe ?? 0)")
// prints: 值是 5

自定义插值:给自己的类型造语法

插值不是内建魔法,它只是 String.StringInterpolation 上的一组 appendInterpolation 方法。给这个类型写扩展,就能往 \( ) 里塞任何你想得到的写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: Int, repeated times: Int) {
        appendLiteral(String(repeating: String(value), count: times))
    }
    mutating func appendInterpolation(hex value: Int) {
        appendLiteral("0x" + String(value, radix: 16))
    }
}

print("\(7, repeated: 3)")
// prints: 777
print("\(hex: 255)")
// prints: 0xff

索引与切片(最容易踩的地方)

String 的索引不是整数。原因很简单:UTF-8 字节、Unicode 标量、扩展字形簇是三套不同的计数单位,没有一个整数能同时对上。所以 Swift 给你一个 String.Index。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let s = "Hello, Swift"

print(s[s.startIndex])
// prints: H
print(s[s.index(s.startIndex, offsetBy: 7)])
// prints: S
print(s[s.index(before: s.endIndex)])
// prints: t

print(s.prefix(5), s.suffix(5))
// prints: Hello Swift

let comma = s.firstIndex(of: ",")!
print(String(s[..<comma]))
// prints: Hello
print(String(s[s.index(after: comma)...]))   // 注意结果前面那个空格也是内容
// prints:  Swift

⚠️ 三条纪律:

  1. s[s.index(...)] 返回的是 Character,要变回字符串得用 String(...)。
  2. s.index(_:offsetBy:) 是 O(n) 的,在循环里按索引逐个取字符会变成 O(n²)。需要频繁按位取字符时先转 Array(s)。
  3. s[0] 永远不合法,这不是 Swift 在为难你。

Substring:切片的免费与代价

切片返回 Substring 而不是 String,它和原字符串共享同一块内存,所以切片本身不复制数据。

1
2
3
4
5
6
let original = "Hello, Swift"
let piece = original.prefix(5)
print(type(of: piece))
// prints: Substring
print(piece.uppercased())
// prints: HELLO

⚠️ 只要 piece 活着,整个 original 就不会被释放。如果切片要长期持有,记得 String(piece) 拷一份——这是内存泄漏的高发区。

索引的算术:distance 与 limitedBy

索引拿到手之后,常见的四种"算一算":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let s = "Hello, 世界"
let i = s.index(s.startIndex, offsetBy: 7)
print(s[i], s.distance(from: s.startIndex, to: i))
// prints: 世 7

print(s.index(s.startIndex, offsetBy: 100, limitedBy: s.endIndex) as Any)
// prints: nil      越界时给 nil,而不是崩

print(s.indices.count, s[s.index(before: s.endIndex)])
// prints: 9 界
需求写法
前后挪 n 位s.index(i, offsetBy: n)
挪,但别越界s.index(i, offsetBy: n, limitedBy: s.endIndex) → String.Index?
两个索引差多远s.distance(from: a, to: b) 🔥 数的是 Character 个数
上一个 / 下一个s.index(before: i)、s.index(after: i)
走遍每个下标for i in s.indices { }、s.indices.count

⚠️ 这些操作都是 O(n),因为它们得按字形簇一个个数过去。s.indices.count 和 s.count 一样慢,别在循环条件里反复调。

三个视角:字形簇、UTF-8、Unicode 标量

同一个 String,按不同单位数出来的长度完全不一样。这不是 Swift 在找麻烦,而是"长度"这个词在 Unicode 世界里本来就有歧义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let s = "Hello, 世界"
print(s.count, s.utf8.count, s.unicodeScalars.count, s.utf16.count)
// prints: 9 13 9 9      "世界"各占 3 个 UTF-8 字节

print(Array(s.utf8).prefix(5))
// prints: [72, 101, 108, 108, 111]
print(String(decoding: Array(s.utf8), as: UTF8.self))
// prints: Hello, 世界
print(s.unicodeScalars.filter { $0.value > 127 }.map(String.init).joined())
// prints: 世界
视角拿到什么单位
s.count、for c in s扩展字形簇,用户眼里的"一个字"Character
s.utf8字节序列,写文件 / 算哈希 / 走网络用UInt8
s.utf16UTF-16 码元,和 NSString、NSRange 打交道时用UInt16
s.unicodeScalarsUnicode 标量,处理编码细节时用Unicode.Scalar
String(decoding:as:)把字节按指定编码还原成字符串—

⚠️ s.utf8.count 和 s.count 不一样,所以不能用"字符数"去分配字节缓冲区。要把字符串交给 C 或写进文件,先 Array(s.utf8);反过来还原就用 String(decoding:as:),它比 String(bytes:encoding:) 宽容(非法字节会变成替换字符,而不是给 nil)。

常用 API

需求写法
长度s.count(数的是字形簇,O(n))
是否为空s.isEmpty
拼接"a" + "b"、var s = ""; s += "x"、["a","b"].joined()
查找子串s.contains("abc")(标准库)、s.range(of: "abc")(Foundation)
前后缀s.hasPrefix("He")、s.hasSuffix("ft")
位置s.firstIndex(of: "l")、s.lastIndex(of: "l")
替换s.replacingOccurrences(of: "l", with: "L")(Foundation)
替换区间var t = s; t.replaceSubrange(t.startIndex...t.index(t.startIndex, offsetBy: 4), with: "Howdy")
分隔s.split(separator: ",") 返回 [Substring](标准库);s.components(separatedBy: ",") 返回 [String](Foundation)
去空白s.trimmingCharacters(in: .whitespacesAndNewlines)(Foundation)
大小写s.uppercased()、s.lowercased()(标准库)、s.capitalized(Foundation)
反转String(s.reversed())
多次重复String(repeating: s, count: 3)
逐字符处理for c in s、s.map { ... }、s.filter(\.isNumber)
全角 / 半角s.applyingTransform(.fullwidthToHalfwidth, reverse: false)(Foundation)🝖
1
2
3
4
print("a,b,,c".split(separator: ",").count)
// prints: 3      默认丢弃空子串
print("a,b,,c".split(separator: ",", omittingEmptySubsequences: false).count)
// prints: 4

比较

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let a = "café"              // é 是单个码位
let b = "cafe\u{301}"       // e + 组合重音
print(a == b)
// prints: true            Swift 按"规范化等价"比较,这点非常正确
print(a.count, a.unicodeScalars.count)
// prints: 4 4

print("Z" < "a")
// prints: true            大写字母排在前面
print("apple" < "banana")
// prints: true

需要更聪明的比较时用 compare(_:options:):

1
2
3
4
5
6
import Foundation

print("file10".compare("file9", options: .numeric) == .orderedDescending)
// prints: true            按数字大小比,"file10" > "file9"
print("Hello".compare("hello", options: .caseInsensitive) == .orderedSame)
// prints: true

正则表达式

Swift 5.7 起正则成为语言的一部分,可以用 /.../ 字面量写,并做编译期检查。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let log = "2026-09-16 ERROR disk full"
let pattern = /(\d{4})-(\d{2})-(\d{2})\s+(\w+)/

if let m = log.firstMatch(of: pattern) {
    print(m.0)
    // prints: 2026-09-16 ERROR
    print(m.1, m.2, m.3, m.4)
    // prints: 2026 09 16 ERROR
}

print(log.contains(/ERROR/))
// prints: true

let rewritten = log.replacing(pattern) { match in "\(match.4)@\(match.1)" }
print(rewritten)
// prints: ERROR@2026 disk full

⚠️ /.../ 字面量在包含 / 的文本里会很难看,此时用原始字面量 #/.../#。另外,正则字面量需要运行时支持,部署目标低于 macOS 13 / iOS 16 时请改用 NSRegularExpression。

动态构造

1
2
3
let dynamic = try! Regex(#"(\d{4})"#)
print("year 2026".contains(dynamic))
// prints: true

RegexBuilder:让复杂正则能读

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import RegexBuilder

let word = OneOrMore(.word)
let digits = OneOrMore(.digit)

let rule = Regex {
    Capture { digits }
    "@"
    Capture { word }
}

if let m = "u42@example".firstMatch(of: rule) {
    print(m.1, m.2)
    // prints: 42 example
}

🔥 正则超过一行、或者需要嵌套分组时,RegexBuilder 的可维护性比一行"符号汤"高一个量级。它还能配合 TryCapture 直接产出强类型值 🝖。

格式化

Swift 5.7 引入的 FormatStyle 是现在推荐的做法,它按当前区域设置输出,还能在 SwiftUI 里以实时刷新的方式使用。

两代写法先摆在一起看:

1
2
3
4
5
6
7
8
import Foundation

print(1234.5.formatted(.number.precision(.fractionLength(2))))
// prints: 1,234.50
print(0.256.formatted(.percent.precision(.fractionLength(1))))
// prints: 25.6%
print(1234.5.formatted(.currency(code: "CNY")))
// prints: ¥1,234.50

类型安全、可组合、可自定义,SwiftUI 里还能直接当参数用。

⚠️ FormatStyle 和 .formatted(...) 住在 Foundation 里,不是标准库。忘了 import Foundation 时的报错很有迷惑性:value of type 'Double' has no member 'formatted'——看上去像"这个方法不存在",其实只是没导入。

1
2
3
4
5
6
import Foundation

print(String(format: "%.2f", 1234.5))
// prints: 1234.50
print(String(format: "%.1f%%", 25.6))
// prints: 25.6%

方便复制 C 的格式串,但不做类型检查,参数与占位符对不上会在运行时炸。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import Foundation

print(1234567.formatted())
// prints: 1,234,567

print(1234.5.formatted(.number.precision(.fractionLength(2))))
// prints: 1,234.50

print(0.256.formatted(.percent.precision(.fractionLength(1))))
// prints: 25.6%

print(1234.5.formatted(.currency(code: "CNY")))
// prints: ¥1,234.50
用途写法
千分位.formatted() 或 .formatted(.number)
固定小数位.formatted(.number.precision(.fractionLength(2)))
百分比.formatted(.percent)
货币.formatted(.currency(code: "CNY"))
科学计数.formatted(.number.notation(.scientific))
日期date.formatted(date: .abbreviated, time: .shortened)
相对时间date.formatted(.relative(presentation: .named))
时长Duration.seconds(90).formatted(.units(allowed: [.minutes, .seconds])) 🆕
列表["a", "b", "c"].formatted(.list(type: .and))
字节数123456.formatted(.byteCount(style: .file))

⚠️ 格式化结果依赖区域设置。 上面所有输出都是在本机 zh_CN 环境下跑出来的;如果在别的区域跑,千分位和货币符号会变。写测试时要么固定 locale,要么别断言字符串。

老写法:String(format:)

需要 C 风格格式串时还能用,但要 import Foundation,而且不做类型检查——参数与占位符对不上会在运行时出问题。

1
2
3
4
5
6
7
8
import Foundation

print(String(format: "%.2f", 3.14159))
// prints: 3.14
print(String(format: "%05d", 42))
// prints: 00042
print(String(format: "%@", "hi"))
// prints: hi

陷阱速查

陷阱正确做法
整数除法截断先转 Double,Double(a) / Double(b)
浮点用 == 比较用容差函数,或者比较 rounded() 后的整数
NaN 判等用 isNaN,别用 ==
s[0] 取字符用 s.first 或 s.startIndex
循环里按索引取字符先 Array(s),否则 O(n²)
切片长期持有String(slice) 复制一份
可选值写进插值用 ?? 兜底,否则会打印 Optional(...)
靠 count 判断"字节长度"s.utf8.count 才是字节数
split 丢掉空段加 omittingEmptySubsequences: false
格式化结果写进断言固定 locale,或者比较数值本身
最后修改 September 19, 2026: 更新 (3489033b1)