什么是下标
官方一句话:"类、结构体、枚举可以用下标(subscripts)快速访问集合、列表、序列中的元素,而无需再写专门的存取方法。"
换句话说:someArray[index]、someDictionary[key] 这种"中括号"语法糖,就是下标。
你自己写的类型也能拥有这种"中括号"魔法。
下标语法 101
swift
subscript(index: Int) -> ReturnType {
get {
// 返回与 index 对应的值
}
set(newValue) { // 可省略 (newValue)
// 用 newValue 保存
}
}
要点速记:
- 用关键字
subscript开头,不是func。 - 参数列表可以是任意个数、任意类型。
- 返回值也可以是任意类型。
- 可以是只读(省略
set),也可以是读写。 - 不支持
inout参数。
只读下标:最简单的入门示例
需求:做一个"n 乘法表"结构体,通过 table[6] 直接得到 n * 6。
swift
struct TimesTable {
let multiplier: Int
// 只读下标:省略了 get 关键字
subscript(index: Int) -> Int {
multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print(threeTimesTable[6]) // 输出 18
注意:
- 没有
set,外界只能读不能写。 - 如果写成
threeTimesTable[6] = 100会直接编译报错。
可读写下标:让下标也能"赋值"
需求:自己封装一个"固定长度"的数组,禁止越界。
swift
struct SafeArray<Element> {
private var storage: [Element]
init(repeating: Element, count: Int) {
storage = Array(repeating: repeating, count: count)
}
// 可读可写下标
subscript(index: Int) -> Element {
get {
// 越界直接崩溃,提前暴露问题
precondition(index >= 0 && index < storage.count,
"Index \(index) out of range 0..<\(storage.count)")
return storage[index]
}
set {
precondition(index >= 0 && index < storage.count,
"Index \(index) out of range 0..<\(storage.count)")
storage[index] = newValue
}
}
}
var sa = SafeArray(repeating: 0, count: 5)
sa[2] = 10
print(sa[2]) // 10
// sa[7] = 1 // 运行时触发 precondition 失败
多参数、多维度:二维矩阵实战
swift
struct Matrix {
let rows: Int, cols: Int
private var grid: [Double]
init(rows: Int, cols: Int) {
self.rows = rows
self.cols = cols
grid = Array(repeating: 0.0, count: rows * cols)
}
// 二维下标
subscript(row: Int, col: Int) -> Double {
get {
precondition(indexIsValid(row: row, col: col))
return grid[row * cols + col]
}
set {
precondition(indexIsValid(row: row, col: col))
grid[row * cols + col] = newValue
}
}
func indexIsValid(row: Int, col: Int) -> Bool {
row >= 0 && row < rows && col >= 0 && col < cols
}
// 调试打印:方便看扁平化结果
func debug() {
print("grid = \(grid)")
}
}
var m = Matrix(rows: 2, cols: 2)
m[0, 1] = 1.5
m[1, 0] = 3.2
m.debug() // grid = [0.0, 1.5, 3.2, 0.0]
下标重载:一个类型多个"中括号"
下标也能像函数一样"重载":参数类型或数量不同即可。
swift
struct MultiSub {
// 1. 通过 Int 索引
subscript(i: Int) -> String {
"Int 下标:\(i)"
}
// 2. 通过 String 索引
subscript(s: String) -> String {
"String 下标:\(s)"
}
// 3. 两个参数
subscript(x: Int, y: Int) -> String {
"二维:(\(x), \(y))"
}
}
let box = MultiSub()
print(box[5]) // Int 下标:5
print(box["hello"]) // String 下标:hello
print(box[2, 3]) // 二维:(2, 3)
类型下标(static / class):不依赖实例也能用[]
实例下标必须"先有一个对象";类型下标直接挂在类型上,用法类似 Type[key]。
swift
enum AppTheme {
case light, dark
// 类型下标:根据字符串返回颜色
static subscript(name: String) -> UInt32? {
switch name {
case "background":
return 0xFFFFFF
case "text":
return 0x000000
default:
return nil
}
}
}
// 无需实例
let bgColor = AppTheme["background"] // 0xFFFFFF
- 结构体/枚举用
static subscript - 类如果想让子类可覆写,用
class subscript
常见陷阱与调试技巧
-
越界问题
Matrix用precondition在运行时断言,开发阶段建议全开,生产环境可换成guard+ 抛出错误。 -
性能陷阱
下标语法糖容易隐藏复杂逻辑。若
get里做大量计算,会让"一行代码"拖慢整体。必要时加缓存或改用方法。 -
与函数歧义
下标不支持
inout,也不能throws。若需要这些能力,请定义成方法。 -
可读性
滥用多参数下标会降低可读性。建议保持"语义直观",例如
matrix[row, col]很直观,但foo[a, b, c, d]就要谨慎。
总结
核心 3 句话:
- 下标 = "中括号"语法糖,语法像计算属性。
- 参数、返回值随便定,可重载,不支持 inout。
- 实例下标最常用,类型下标适合"全局字典"式场景。
扩展场景:自定义 JSON、缓存、稀疏矩阵
(1)"SwiftyJSON"式下标
swift
struct JSON {
private var raw: Any
init(_ raw: Any) { self.raw = raw }
// 允许 json["user"]["name"] 一路点下去
subscript(key: String) -> JSON {
guard let dict = raw as? [String: Any],
let value = dict[key] else { return JSON(NSNull()) }
return JSON(value)
}
// 支持数组
subscript(index: Int) -> JSON {
guard let arr = raw as? [Any], index < arr.count else { return JSON(NSNull()) }
return JSON(arr[index])
}
var stringValue: String? { raw as? String }
}
let json = JSON(["user": ["name": "Alice"]])
if let name = json["user"]["name"].stringValue {
print(name) // Alice
}
(2)LRU 缓存下标
swift
final class Cache<Key: Hashable, Value> {
private var lru = NSCache<NSString, AnyObject>()
subscript(key: Key) -> Value? {
get { lru.object(forKey: "\(key)" as NSString) as? Value }
set {
if let v = newValue { lru.setObject(v as AnyObject, forKey: "\(key)" as NSString) }
else { lru.removeObject(forKey: "\(key)" as NSString) }
}
}
}
(3)稀疏矩阵
当 99% 都是 0 时,可用字典存"非零下标":
swift
struct SparseMatrix {
private var storage: [String: Double] = [:]
subscript(row: Int, col: Int) -> Double {
get { storage["\(row),\(col)"] ?? 0.0 }
set {
if newValue == 0.0 {
storage.removeValue(forKey: "\(row),\(col)")
} else {
storage["\(row),\(col)"] = newValue
}
}
}
}
下标 × 属性包装器:让"语法糖"再甜一点
从 Swift 5.1 开始,Property Wrapper 把"存储逻辑"抽成了可复用的注解;
把"下标"与"属性包装器"放在一起,可以做出带访问钩子的数组/字典,而调用端仍然只用一对中括号。
场景:线程安全数组
需求:
- 对外像普通数组一样用
[]读写; - 内部用
DispatchQueue做同步锁; - 编译期自动注入,无需手写
queue.async {}。
实现思路:
- 用
@propertyWrapper做一个泛型ThreadSafeBox; - 在
wrappedValue里用queue.sync { ... }实现读写; - 再给它一个下标投影(
projectedValue返回自身),让外部能用$语法拿到"带下标的实例"。
代码:
swift
import Foundation
@propertyWrapper
class ThreadSafeArray<Element> {
private var storage: [Element] = []
private let queue = DispatchQueue(label: "sync.array", attributes: .concurrent)
init(wrappedValue: [Element]) {
self.wrappedValue = wrappedValue
}
var wrappedValue: [Element] {
get { queue.sync { storage } }
set { queue.async(flags: .barrier) { self.storage = newValue } }
}
// 对外暴露"自身"做下标
var projectedValue: ThreadSafeArray { self }
// 下标:读,写
subscript(index: Int) -> Element {
get { queue.sync { storage[index] } }
set { queue.async(flags: .barrier) { self.storage[index] = newValue } }
}
// 方便扩展 append、count 等
func append(_ element: Element) {
queue.async(flags: .barrier) { self.storage.append(element) }
}
var count: Int {
queue.sync { storage.count }
}
}
/* ============ 使用端 ============== */
class ViewModel {
// 看似普通数组,实则线程安全
@ThreadSafeArray var numbers: [Int] = []
func demo() {
// 1. 直接读写(走 wrappedValue)
numbers = [1, 2, 3]
// 2. 用下标(走 projectedValue)
$numbers[0] = 99
print($numbers[0]) // 99
// 3. 并发安全
DispatchQueue.concurrentPerform(iterations: 1000) { i in
$numbers.append(i)
}
print("final count:", $numbers.count) // 1003
}
}
let vm = ViewModel()
vm.demo()
关键点:
projectedValue返回self,于是$numbers就能继续用[]。- 读写分离:读用
sync,写用async(flags: .barrier),多读单写模型。 - 对调用者完全透明,看起来像普通数组,却自带锁。
场景:@UserDefault 下标版
系统自带的 UserDefaults 标准写法:
swift
UserDefaults.standard.set(true, forKey: "darkMode")
我们可以把"键"做成下标,并且用属性包装器自动同步:
swift
@propertyWrapper
class UserDefault<T> {
let key: String
let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
// 允许 $ 语法直接取下标
var projectedValue: UserDefault { self }
// 下标:支持动态 key
subscript(_ suffix: String) -> T {
get {
let fullKey = "\(key)_\(suffix)"
return UserDefaults.standard.object(forKey: fullKey) as? T ?? defaultValue
}
set {
let fullKey = "\(key)_\(suffix)"
UserDefaults.standard.set(newValue, forKey: fullKey)
}
}
}
/* ============ 使用端 ============== */
@MainActor
struct Settings {
@UserDefault(key: "darkMode", defaultValue: false)
static var darkMode: Bool
// 投影后支持 $darkMode["iPhone"] 这种动态 key
static func demo() {
darkMode = true
print(darkMode) // true
$darkMode["iPad"] = false
print($darkMode["iPad"]) // false
}
}
Settings.demo()
下标与 Result Builder 的化学反应
SwiftUI 的 ViewBuilder 让大家见识到"DSL"之美;
其实我们自己也能用 Result Builder + 下标 做出"声明式语法"。
示例:做一个极简版 JSON DSL,支持这种写法:
swift
let obj = JSON {
"user" {
"name" <<< "Alice"
"age" <<< 25
}
}
实现要点:
- 用
@resultBuilder做JSONBuilder; - 用
<<<运算符把key ~ value塞进字典; - 让
JSON支持subscript(key: String) -> JSONNode实现嵌套。
swift
@resultBuilder
enum JSONBuilder {
static func buildBlock(_ components: JSONNode...) -> JSONNode {
JSONNode(children: components)
}
}
struct JSONNode {
enum Value {
case string(String)
case number(Double)
case object([String: JSONNode])
}
var value: Value?
var children: [JSONNode] = []
}
struct JSON {
private var root: JSONNode = JSONNode()
init(@JSONBuilder content: () -> JSONNode) {
root = content()
}
// 关键下标:支持链式嵌套
subscript(key: String) -> JSONNode {
get {
guard case .object(let dict) = root.value,
let node = dict[key] else { return JSONNode() }
return node
}
set {
if case .object(var dict) = root.value {
dict[key] = newValue
root.value = .object(dict)
} else {
root.value = .object([key: newValue])
}
}
}
}
infix operator <<< : AssignmentPrecedence
func <<< (lhs: JSONNode, rhs: Any) {
// 简化版:把 rhs 转成 JSONNode 并赋值
}
性能深度剖析:下标真的"零成本"吗?
-
纯"转发"下标(直接读数组)
- 经编译器优化后,与手动裸数组访问无差别;
- 在
-O -whole-module-optimization下会内联。
-
带
precondition或sync锁的下标-
运行时多了一次函数调用 + 条件判断;
-
若放在热点循环,建议:
a. 把"裸指针"提取到循环外;
b. 或者用
withUnsafeBufferPointer一次性处理。
-
-
多参数下标
- 调用约定与多参函数相同,不会额外装箱;
- 但泛型参数过多时可能触发 specialization 爆炸,注意模块划分。