Swift 下标(Subscripts)详解:从基础到进阶的完整指南

什么是下标

官方一句话:"类、结构体、枚举可以用下标(subscripts)快速访问集合、列表、序列中的元素,而无需再写专门的存取方法。"

换句话说:someArray[index]someDictionary[key] 这种"中括号"语法糖,就是下标。

你自己写的类型也能拥有这种"中括号"魔法。

下标语法 101

swift 复制代码
subscript(index: Int) -> ReturnType {
    get {
        // 返回与 index 对应的值
    }
    set(newValue) {   // 可省略 (newValue)
        // 用 newValue 保存
    }
}

要点速记:

  1. 用关键字 subscript 开头,不是 func
  2. 参数列表可以是任意个数、任意类型。
  3. 返回值也可以是任意类型。
  4. 可以是只读(省略 set),也可以是读写。
  5. 不支持 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

常见陷阱与调试技巧

  1. 越界问题

    Matrixprecondition 在运行时断言,开发阶段建议全开,生产环境可换成 guard + 抛出错误。

  2. 性能陷阱

    下标语法糖容易隐藏复杂逻辑。若 get 里做大量计算,会让"一行代码"拖慢整体。必要时加缓存或改用方法。

  3. 与函数歧义

    下标不支持 inout,也不能 throws。若需要这些能力,请定义成方法。

  4. 可读性

    滥用多参数下标会降低可读性。建议保持"语义直观",例如 matrix[row, col] 很直观,但 foo[a, b, c, d] 就要谨慎。

总结

核心 3 句话:

  1. 下标 = "中括号"语法糖,语法像计算属性。
  2. 参数、返回值随便定,可重载,不支持 inout。
  3. 实例下标最常用,类型下标适合"全局字典"式场景。

扩展场景:自定义 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 {}

实现思路:

  1. @propertyWrapper 做一个泛型 ThreadSafeBox
  2. wrappedValue 里用 queue.sync { ... } 实现读写;
  3. 再给它一个下标投影(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
    }
}

实现要点:

  1. @resultBuilderJSONBuilder
  2. <<< 运算符把 key ~ value 塞进字典;
  3. 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 并赋值
}

性能深度剖析:下标真的"零成本"吗?

  1. 纯"转发"下标(直接读数组)

    • 经编译器优化后,与手动裸数组访问无差别;
    • -O -whole-module-optimization 下会内联。
  2. preconditionsync 锁的下标

    • 运行时多了一次函数调用 + 条件判断;

    • 若放在热点循环,建议:

      a. 把"裸指针"提取到循环外;

      b. 或者用 withUnsafeBufferPointer 一次性处理。

  3. 多参数下标

    • 调用约定与多参函数相同,不会额外装箱;
    • 但泛型参数过多时可能触发 specialization 爆炸,注意模块划分。
相关推荐
YGGP6 小时前
【Swift】LeetCode 189. 轮转数组
swift
JZXStudio15 小时前
5.A.swift 使用指南
框架·swift·app开发
非专业程序员Ping1 天前
HarfBuzz概览
android·ios·swift·font
Daniel_Coder1 天前
iOS Widget 开发-8:手动刷新 Widget:WidgetCenter 与刷新控制实践
ios·swift·widget·1024程序员节·widgetcenter
HarderCoder2 天前
Swift 中基础概念:「函数」与「方法」
swift
西西弗Sisyphus2 天前
将用于 Swift 微调模型的 JSON Lines(JSONL)格式数据集,转换为适用于 Qwen VL 模型微调的 JSON 格式
swift·qwen3
songgeb2 天前
🧩 iOS DiffableDataSource 死锁问题记录
ios·swift
大熊猫侯佩3 天前
【大话码游之 Observation 传说】上集:月光宝盒里的计数玄机
swiftui·swift·weak·observable·self·引用循环·observations
HarderCoder3 天前
Swift 方法全解:实例方法、mutating 方法与类型方法一本通
swift