深入探索Swift的Subscript机制和最佳实践

一、为什么要用Subscript[下标]?

优点:

Swift的下标(Subscript)是一种强大的语言特性,它允许开发者通过类似于数组索引的语法来访问类型的元素或属性。下标提供了一种简洁、直观的方式来操作集合、列表、序列以及自定义数据结构,大大提高了代码的可读性和易用性。

缺点:

  • 1:subscript重载的参数个数(>=1),参数类型,返回类型,理论上可以随意,也可以随便。
  • 2:调用方不知道api提供者内部subscript重载方式,也不好做函数注释,xcode也.不出来。调用者纯粹靠吓瞎猜,查资料
  • 3:一些系统内置的语法糖,subscript的实现简直是一个黑箱,可怕的还有多个重载版本,更可怕的是重载决策放在c++实现的,作者找了好久也不知道它内置的选择逻辑,并拿出c++的证据。所以作者只能从swift提案找线索。本文的一个目的就是弄明白它的重载版本选择策略。

这些缺点和优点在@dynamicMemberLookup与@propertyWrapper上体现非常明显

下标特性的提案历史与贡献人

Swift的下标机制经历了多次演进和增强,以下是几个关键的提案:

  1. 静态下标(Static and class subscripts) - SE-0254

    • 提案人:Becca Royal-Gordon
    • 实现版本:Swift 5.1
    • 核心思想:允许在类型本身而不是实例上定义下标,使语言更加一致和灵活
    • 提案链接
  2. 泛型下标(Generic Subscripts) - SE-0148

    • 允许下标方法使用泛型参数,增强了下标机制的灵活性和可重用性
    • 提案链接
  3. 动态成员查找下标扩展(Allow Additional Arguments to @dynamicMemberLookup Subscripts) - SE-0484

    • 提案人:Itai Ferber
    • 实现版本:较新版本的Swift
    • 核心思想:允许动态成员查找下标接受额外的默认参数
    • 提案链接
  4. C函数作为下标导入(Import C functions as subscript methods)

    • 允许将C函数通过swift_name属性导入为Swift下标
    • 讨论链接

这些提案的贡献者们通过不断完善下标机制,使Swift语言在保持简洁性的同时,提供了更强大的表达能力。

二、Subscripts的语法特征与机制

基本语法

Swift下标使用subscript关键字定义,可以包含getter和setter方法:

swift 复制代码
subscript(index: Int) -> Element {
    get {
        // 返回与index对应的元素
    }
    set(newValue) {
        // 设置与index对应的元素为newValue
    }
}

下标重载机制

Swift允许为一个类型定义多个下标方法,通过参数类型和标签的不同来区分,这就是下标重载:

swift 复制代码
struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        self.grid = Array(repeating: 0.0, count: rows * columns)
    }
    
    // 基本下标:通过行和列访问
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
    
    // 重载下标:通过单个索引访问
    subscript(index: Int) -> Double {
        get {
            assert(index >= 0 && index < grid.count, "Index out of range")
            return grid[index]
        }
        set {
            assert(index >= 0 && index < grid.count, "Index out of range")
            grid[index] = newValue
        }
    }
    
    private func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }
}

KeyPath版本下标

Swift支持使用KeyPath作为下标参数,这提供了一种类型安全的方式来动态访问属性:

swift 复制代码
struct Container<T> {
    private var value: T
    
    init(_ value: T) {
        self.value = value
    }
    
    // KeyPath版本下标
    subscript<U>(keyPath: KeyPath<T, U>) -> U {
        return value[keyPath: keyPath]
    }
    
    // WritableKeyPath版本下标,支持修改
    subscript<U>(keyPath: WritableKeyPath<T, U>) -> U {
        get {
            return value[keyPath: keyPath]
        }
        set {
            value[keyPath: keyPath] = newValue
        }
    }
}

// 使用示例
struct Person {
    var name: String
    var age: Int
}

var person = Person(name: "Alice", age: 30)
var container = Container(person)

print(container[\.name])  // 输出: Alice
container[\.age] = 31     // 修改age属性
print(container[\.age])   // 输出: 31

下标优先级

当多个下标方法都可以匹配一个下标表达式时,Swift编译器会根据以下规则确定优先级:

  1. 参数类型完全匹配的下标优先于需要类型转换的下标
  2. 非泛型下标优先于泛型下标
  3. 具体类型参数的下标优先于协议类型参数的下标

三、静态下标类型与swift_name attribute风格下标

静态下标类型

静态下标是Swift 5.1中引入的特性(SE-0254),允许开发者在类型级别定义下标,而不是在实例级别。

静态下标的基本语法

swift 复制代码
struct Math {
    // 静态下标:计算平方数
    static subscript(n: Int) -> Int {
        return n * n
    }
    
    // 静态下标:计算阶乘
    static subscript(factorial n: Int) -> Int {
        guard n >= 0 else { return 0 }
        return n <= 1 ? 1 : n * Math[factorial: n - 1]
    }
}

// 使用静态下标
let squareOfFive = Math[5]           // 结果: 25
let factorialOfFive = Math[factorial: 5]  // 结果: 120

静态下标的C++内部实现

在Swift编译器源码中,静态下标的处理机制与实例下标有明显区别。以下是关键代码片段:

cpp 复制代码
// 检查下标是否为静态成员
bool SubscriptDecl::isStatic() const {
  return getStorageKind() == StorageKind::Static;
}

// 处理静态下标的查找逻辑
void TypeChecker::lookupDirect(/* 参数 */) {
  // ...
  if (auto subscript = dyn_cast<SubscriptDecl>(member)) {
    if (subscript->isStatic()) {
      // 静态下标查找逻辑
      // ...
    } else {
      // 实例下标查找逻辑
      // ...
    }
  }
  // ...
}

静态下标的应用场景

  1. 类型工具方法:提供与类型相关的便捷计算或查找功能
  2. 类型级缓存:实现全局缓存机制
  3. 命名空间隔离:在enum中实现命名空间隔离的功能

swift_name attribute风格下标

swift_name attribute允许开发者自定义C/Objective-C函数在Swift中的导入名称,包括将C函数导入为Swift下标。

基本用法

objc 复制代码
// Objective-C代码
@interface NSString (SubscriptAdditions)
// 使用swift_name自定义下标名称
- (unichar)characterAtIndex:(NSUInteger)index 
    __attribute__((swift_name("subscript(_:)")));
@end

在Swift中,这将被导入为:

swift 复制代码
// 在Swift中使用
let str = "Hello"
let char = str[1]  // 相当于调用characterAtIndex:1

自定义下标名称格式

swift_name attribute支持多种格式来自定义下标名称:

  1. 基本格式subscript(<参数标签>:<参数类型>, ...)
  2. 带参数标签的格式subscript(label:arg:)
  3. getter/setter格式getter:TypeName.subscript(...)setter:TypeName.subscript(...:newValue:)

C函数转换为Swift下标示例

c 复制代码
// C代码
void* getElement(void* array, size_t index) {
    // 实现细节
}

void setElement(void* array, size_t index, void* element) {
    // 实现细节
}

// 使用swift_name转换为下标
void* getElement(void* array, size_t index) 
    __attribute__((swift_name("getter:Array.subscript(_:)")));

void setElement(void* array, size_t index, void* element) 
    __attribute__((swift_name("setter:Array.subscript(_:)")));

在Swift中,这些函数将作为下标使用:

swift 复制代码
// 在Swift中使用
let element = array[5]  // 调用getElement
array[5] = newValue     // 调用setElement

四、@dynamicMemberLookup与@propertyWrapper的下标实现机制

@dynamicMemberLookup的下标实现

@dynamicMemberLookup是Swift中一个强大的特性,允许类型通过下标方法动态响应属性访问。这一特性在Swift 5.1版本中通过SE-0252提案进一步增强,支持了基于KeyPath的动态成员查找功能。

动态成员查找下标形式

根据Keypath Dynamic Member Lookup提案@dynamicMemberLookup支持以下四种下标形式:

  1. 字符串参数形式:最基础的动态成员查找
swift 复制代码
subscript(dynamicMember member: String) -> ValueType {
    // 通过字符串名称实现动态属性访问
}
  1. KeyPath参数形式:提供只读访问的类型安全版本
swift 复制代码
subscript<U>(dynamicMember member: KeyPath<T, U>) -> U {
    // 通过KeyPath实现类型安全的只读属性访问
}
  1. WritableKeyPath参数形式:支持读写操作的类型安全版本
swift 复制代码
subscript<U>(dynamicMember member: WritableKeyPath<T, U>) -> U {
    get { /* 读取实现 */ }
    set { /* 写入实现 */ }
}
  1. ReferenceWritableKeyPath参数形式:针对类实例的引用类型安全版本
swift 复制代码
subscript<U>(dynamicMember member: ReferenceWritableKeyPath<T, U>) -> U {
    get { /* 读取实现 */ }
    set { /* 写入实现 */ }
}

以下是一个结合了KeyPath和WritableKeyPath的实际示例,展示了如何创建一个类似Lens的结构来实现属性访问和修改:

swift 复制代码
struct Point {
  let x: Int
  var y: Int
}

@dynamicMemberLookup 
struct Lens<T> {
  var obj: T

  init(_ obj: T) {
    self.obj = obj
  }

  subscript<U>(dynamicMember member: KeyPath<T, U>) -> Lens<U> {
    get { return Lens<U>(obj[keyPath: member]) }
  }

  subscript<U>(dynamicMember member: WritableKeyPath<T, U>) -> Lens<U> {
    get { return Lens<U>(obj[keyPath: member]) }
    set { obj[keyPath: member] = newValue.obj }
  }
}

// 使用示例
var lens = Lens(Point(x: 0, y: 0))
_ = lens.x // 调用 KeyPath 版本的下标
lens.y = Lens(10) // 调用 WritableKeyPath 版本的下标

现在让我们深入了解Swift编译器内部是如何实现这些特性的:

1. 验证逻辑

cpp 复制代码
// TypeCheckAttr.cpp中的关键验证逻辑
void AttributeChecker::visitDynamicMemberLookupAttr(DynamicMemberLookupAttr *attr) {
  // 仅允许用于标称类型
  auto decl = cast<NominalTypeDecl>(D);
  auto type = decl->getDeclaredType();
  auto &ctx = decl->getASTContext();
  
  // 查找符合条件的subscript(dynamicMember:)方法
  auto subscriptName = DeclName(ctx, DeclBaseName::createSubscript(), ctx.Id_dynamicMember);
  auto candidates = TypeChecker::lookupMember(decl, type, subscriptName);
  
  // 验证候选方法是否有效
  // ...
}

2. 字符串动态成员查找验证

cpp 复制代码
// 验证字符串动态成员查找的有效性
bool swift::isValidStringDynamicMemberLookup(SubscriptDecl *decl, DeclContext *DC, bool ignoreLabel) {
  auto &ctx = decl->getASTContext();
  // 要求:
  // 1. 下标方法只有一个非可变参数
  // 2. 参数类型符合ExpressibleByStringLiteral协议
  if (!hasSingleNonVariadicParam(decl, ctx.Id_dynamicMember, ignoreLabel))
    return false;

  const auto *param = decl->getIndices()->get(0);
  auto paramType = param->getType();

  auto stringLitProto = ctx.getProtocol(KnownProtocolKind::ExpressibleByStringLiteral);

  // 检查参数类型是否符合ExpressibleByStringLiteral协议
  return (bool)TypeChecker::conformsToProtocol(paramType, stringLitProto, DC, ConformanceCheckOptions());
}

3. 键路径动态成员查找验证

cpp 复制代码
// 验证键路径动态成员查找的有效性
bool swift::isValidKeyPathDynamicMemberLookup(SubscriptDecl *decl, bool ignoreLabel) {
  auto &ctx = decl->getASTContext();
  if (!hasSingleNonVariadicParam(decl, ctx.Id_dynamicMember, ignoreLabel))
    return false;

  const auto *param = decl->getIndices()->get(0);
  if (auto NTD = param->getInterfaceType()->getAnyNominal()) {
    // 参数类型必须是KeyPath、WritableKeyPath或ReferenceWritableKeyPath
    return NTD == ctx.getKeyPathDecl() ||
           NTD == ctx.getWritableKeyPathDecl() ||
           NTD == ctx.getReferenceWritableKeyPathDecl();
  }
  return false;
}

@propertyWrapper的下标实现

@propertyWrapper是Swift 5.1中引入的一个重要特性(SE-0258提案),它允许开发者定义自定义的属性访问行为。除了基本的wrappedValue实现外,还支持通过特殊的静态下标提供更灵活的访问方式。

属性包装器内置静态下标

根据Property Wrappers提案,属性包装器可以定义一个特殊的静态下标来控制属性的访问行为。这个静态下标具有以下形式:

swift 复制代码
static subscript<EnclosingSelf>(
  _enclosingInstance observed: EnclosingSelf,
  wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
  storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value {
  get {
    // 自定义获取逻辑
  }
  set {
    // 自定义设置逻辑
  }
}

让我们详细解释这个静态下标的参数:

  1. _enclosingInstance observed :这是包装属性所属的实例,泛型参数EnclosingSelf表示包装类型所在的封闭类型。

  2. wrapped wrappedKeyPath:这是一个键路径,指向被包装的属性本身,用于在需要时访问或修改原始属性。

  3. storage storageKeyPath:这是一个键路径,指向存储包装器实例的属性,用于访问或修改包装器的状态。

这种静态下标的调用方式是隐式的,当访问被@propertyWrapper标记的属性时,编译器会自动转换为对这个静态下标的调用。

以下是一个实际示例,展示如何使用这种静态下标来实现自定义访问行为:

swift 复制代码
@propertyWrapper
struct Delayed<Value> {
    private var _value: Value?
    private let defaultValue: Value
    
    init(wrappedValue: Value) {
        self.defaultValue = wrappedValue
        self._value = nil
    }
    
    var wrappedValue: Value {
        get { _value ?? defaultValue }
        set { _value = newValue }
    }
    
    // 静态下标实现
    static subscript<EnclosingSelf>(
        _enclosingInstance observed: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> Value {
        get {
            let wrapper = observed[keyPath: storageKeyPath]
            return wrapper._value ?? wrapper.defaultValue
        }
        set {
            //wrappedKeyPath和storageKeyPath路径不一样
            //wrappedKeyPath是User的2个包装对象name,age,如:ReferenceWritableKeyPath<User, Int>
            //storageKeyPath是Delayed的是_value 如: WritableKeyPath<User,  Delayed<Int>>
            var wrapped_obj = observed[keyPath: wrappedKeyPath]
            print("wrapped_obj-->\r\n\(wrapped_obj)")
            withUnsafePointer(to: &wrapped_obj) { pointer in
                   print("wrapped_obj的内存地址: \(pointer)")
            }
            var storage_obj = observed[keyPath: storageKeyPath]
            print("storage_obj-->\r\n\(storage_obj)")
            withUnsafePointer(to: &storage_obj) { pointer in
                   print("storage_obj的内存地址: \(pointer)")
              }
            observed[keyPath: storageKeyPath]._value = newValue
        }
    }
}


// 使用示例
class User {
    @Delayed var name: String = "Anonymous"
    @Delayed var age: Int = 0
}

let user = User() 
print(user.name) // 会调用静态下标getter,输出: Accessing property 和 Anonymous
user.age = 30    // 会调用静态下标setter,输出: Modifying property to: 30

现在,让我们深入了解Swift编译器内部是如何实现这些特性的:

1. 属性包装器验证逻辑

cpp 复制代码
// PropertyWrapperTypeInfoRequest的求值逻辑
llvm::Expected<PropertyWrapperTypeInfo>
PropertyWrapperTypeInfoRequest::evaluate(Evaluator &eval, NominalTypeDecl *nominal) const {
  // 必须有@propertyWrapper属性
  if (!nominal->getAttrs().hasAttribute<PropertyWrapperAttr>()) {
    return PropertyWrapperTypeInfo();
  }

  // 查找名为"wrappedValue"的非静态属性
  ASTContext &ctx = nominal->getASTContext();
  auto valueVar = findValueProperty(ctx, nominal, ctx.Id_wrappedValue, /*allowMissing=*/false);
  if (!valueVar)
    return PropertyWrapperTypeInfo();
  
  // ...其他验证和初始化逻辑
}

2. 静态下标查找逻辑

cpp 复制代码
// 查找用于访问封闭实例的静态下标
static SubscriptDecl *findEnclosingSelfSubscript(ASTContext &ctx,
                                               NominalTypeDecl *nominal,
                                               Identifier propertyName) {
  Identifier argNames[] = {
    ctx.Id_enclosingInstance,
    propertyName,
    ctx.Id_storage
  };
  DeclName subscriptName(ctx, DeclBaseName::createSubscript(), argNames);

  SmallVector<SubscriptDecl *, 2> subscripts;
  for (auto member : nominal->lookupDirect(subscriptName)) {
    auto subscript = dyn_cast<SubscriptDecl>(member);
    if (!subscript)
      continue;

    if (subscript->isInstanceMember())  // 必须是静态成员
      continue;

    if (subscript->getDeclContext() != nominal)
      continue;

    subscripts.push_back(subscript);
  }
  
  // ...验证和返回逻辑
}

3. PropertyWrapperTypeInfo结构体

cpp 复制代码
struct PropertyWrapperTypeInfo {
  // ...其他成员
  
  /// 用于访问类实例属性的静态下标(替代wrappedValue)
  SubscriptDecl *enclosingInstanceWrappedSubscript = nullptr;

  /// 用于访问类实例属性的静态下标(替代projectedValue)
  SubscriptDecl *enclosingInstanceProjectedSubscript = nullptr;
  
  // ...
};

五、自定义实现下标的最佳实践

1. 尽可能严格约束类型

为下标参数和返回值添加明确的类型约束,提高代码的类型安全性和可读性:

swift 复制代码
// 不好的实现
subscript(_ index: Any) -> Any? {
    // ...
}

// 更好的实现
subscript<T: Collection>(_ indices: T) -> [Element] where T.Element == Int {
    // 确保indices中的元素都是Int类型
    return indices.compactMap { self[$0] }
}

2. 尽可能使用KeyPath

当实现动态成员访问时,优先考虑使用KeyPath而非字符串,以获得更好的类型安全性和编译时检查:

swift 复制代码
// 基于字符串的实现(类型不安全)
@dynamicMemberLookup
struct StringBasedContainer {
    subscript(dynamicMember name: String) -> Any? {
        // 通过字符串查找值
        // ...
    }
}

// 基于KeyPath的实现(类型安全)
@dynamicMemberLookup
struct KeyPathBasedContainer<T> {
    private let value: T
    
    init(_ value: T) {
        self.value = value
    }
    
    subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
        return value[keyPath: keyPath]
    }
}

3. 为下标实现设置有意义的别名

使用参数标签为下标提供更具描述性的名称,使代码意图更加清晰:

swift 复制代码
extension Array {
    // 没有参数标签的下标
    subscript(_ indices: [Int]) -> [Element] {
        return indices.compactMap { self[$0] }
    }
    
    // 有描述性参数标签的下标(更好)
    subscript(elementsAt indices: [Int]) -> [Element] {
        return indices.compactMap { self[$0] }
    }
    
    // 带默认参数的下标
    subscript(safe index: Int, defaultValue: Element? = nil) -> Element? {
        return indices.contains(index) ? self[index] : defaultValue
    }
}

4. 考虑性能优化

对于频繁调用的下标,考虑添加适当的缓存机制或优化访问路径:

swift 复制代码
struct ExpensiveComputation {
    private var cache: [Int: ResultType] = [:]
    
    subscript(index: Int) -> ResultType {
        // 检查缓存
        if let cachedResult = cache[index] {
            return cachedResult
        }
        
        // 执行昂贵的计算
        let result = performExpensiveComputation(for: index)
        
        // 缓存结果
        cache[index] = result
        
        return result
    }
    
    private func performExpensiveComputation(for index: Int) -> ResultType {
        // 复杂计算逻辑
        // ...
    }
}

5. 处理边界情况

确保下标实现能够优雅地处理边界情况和非法输入:

swift 复制代码
extension Collection {
    subscript(safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

// 使用示例
let array = [1, 2, 3]
let validElement = array[safe: 1]  // 返回2
let invalidElement = array[safe: 10]  // 返回nil,避免越界崩溃

通过遵循这些最佳实践,开发者可以创建更加安全、高效、易用的下标实现,充分发挥Swift下标机制的强大功能。

相关推荐
stayong5 分钟前
市面主流跨端开发框架对比
前端
庞囧18 分钟前
大白话讲 React 原理:Scheduler 任务调度器
前端
东华帝君30 分钟前
react 虚拟滚动列表的实现 —— 动态高度
前端
CptW32 分钟前
手撕 Promise 一文搞定
前端·面试
温宇飞33 分钟前
Web 异步编程
前端
腹黑天蝎座33 分钟前
浅谈React19的破坏性更新
前端·react.js
东华帝君34 分钟前
react组件常见的性能优化
前端
第七种黄昏34 分钟前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
angelQ34 分钟前
前端fetch手动解析SSE消息体,字符串双引号去除不掉的问题定位
前端·javascript
Huangyi34 分钟前
第一节:Flow的基础知识
android·前端·kotlin