一、为什么要用Subscript[下标]?
优点:
Swift的下标(Subscript)是一种强大的语言特性,它允许开发者通过类似于数组索引的语法来访问类型的元素或属性。下标提供了一种简洁、直观的方式来操作集合、列表、序列以及自定义数据结构,大大提高了代码的可读性和易用性。
缺点:
- 1:subscript重载的参数个数(>=1),参数类型,返回类型,理论上可以随意,也可以随便。
- 2:调用方不知道api提供者内部subscript重载方式,也不好做函数注释,xcode也.不出来。调用者纯粹靠吓瞎猜,查资料
- 3:一些系统内置的语法糖,subscript的实现简直是一个黑箱,可怕的还有多个重载版本,更可怕的是重载决策放在c++实现的,作者找了好久也不知道它内置的选择逻辑,并拿出c++的证据。所以作者只能从swift提案找线索。本文的一个目的就是弄明白它的重载版本选择策略。
这些缺点和优点在@dynamicMemberLookup与@propertyWrapper上体现非常明显
下标特性的提案历史与贡献人
Swift的下标机制经历了多次演进和增强,以下是几个关键的提案:
-
静态下标(Static and class subscripts) - SE-0254
- 提案人:Becca Royal-Gordon
- 实现版本:Swift 5.1
- 核心思想:允许在类型本身而不是实例上定义下标,使语言更加一致和灵活
- 提案链接
-
泛型下标(Generic Subscripts) - SE-0148
- 允许下标方法使用泛型参数,增强了下标机制的灵活性和可重用性
- 提案链接
-
动态成员查找下标扩展(Allow Additional Arguments to @dynamicMemberLookup Subscripts) - SE-0484
- 提案人:Itai Ferber
- 实现版本:较新版本的Swift
- 核心思想:允许动态成员查找下标接受额外的默认参数
- 提案链接
-
C函数作为下标导入(Import C functions as subscript methods)
- 允许将C函数通过
swift_name
属性导入为Swift下标 - 讨论链接
- 允许将C函数通过
这些提案的贡献者们通过不断完善下标机制,使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编译器会根据以下规则确定优先级:
- 参数类型完全匹配的下标优先于需要类型转换的下标
- 非泛型下标优先于泛型下标
- 具体类型参数的下标优先于协议类型参数的下标
三、静态下标类型与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 {
// 实例下标查找逻辑
// ...
}
}
// ...
}
静态下标的应用场景
- 类型工具方法:提供与类型相关的便捷计算或查找功能
- 类型级缓存:实现全局缓存机制
- 命名空间隔离:在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支持多种格式来自定义下标名称:
- 基本格式 :
subscript(<参数标签>:<参数类型>, ...)
- 带参数标签的格式 :
subscript(label:arg:)
- 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
支持以下四种下标形式:
- 字符串参数形式:最基础的动态成员查找
swift
subscript(dynamicMember member: String) -> ValueType {
// 通过字符串名称实现动态属性访问
}
- KeyPath参数形式:提供只读访问的类型安全版本
swift
subscript<U>(dynamicMember member: KeyPath<T, U>) -> U {
// 通过KeyPath实现类型安全的只读属性访问
}
- WritableKeyPath参数形式:支持读写操作的类型安全版本
swift
subscript<U>(dynamicMember member: WritableKeyPath<T, U>) -> U {
get { /* 读取实现 */ }
set { /* 写入实现 */ }
}
- 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 {
// 自定义设置逻辑
}
}
让我们详细解释这个静态下标的参数:
-
_enclosingInstance observed
:这是包装属性所属的实例,泛型参数EnclosingSelf
表示包装类型所在的封闭类型。 -
wrapped wrappedKeyPath
:这是一个键路径,指向被包装的属性本身,用于在需要时访问或修改原始属性。 -
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下标机制的强大功能。