MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift下标语法与集合定制方法

2023-01-087.3k 阅读

Swift 下标语法基础

在 Swift 中,下标(subscript)是一种访问对象、集合或序列中元素的快捷方式。通过下标语法,我们可以使用类似数组访问元素的方括号 [] 来访问自定义类型的元素。下标可以被定义在类、结构体和枚举中,是一种便捷的访问器,它可以有不同的参数列表和返回值类型。

定义简单下标

以下是一个简单的 TimesTable 结构体,它表示一个乘法表,通过下标来获取乘法表中指定行和列的结果:

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}
let threeTimesTable = TimesTable(multiplier: 3)
print("3 的 6 倍是 \(threeTimesTable[6])")

在上述代码中,TimesTable 结构体有一个属性 multiplier 用于表示乘法表的倍数。subscript 定义了一个下标,接受一个 Int 类型的参数 index,返回值类型也是 Int,即 multiplierindex 的乘积。

下标重载

与函数重载类似,一个类型可以定义多个下标,只要它们的参数列表不同。例如,我们可以为 TimesTable 结构体再添加一个接受两个参数的下标,用于获取不同倍数乘法表交叉位置的值:

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
    subscript(row: Int, column: Int) -> Int {
        let rowTable = TimesTable(multiplier: row)
        let columnTable = TimesTable(multiplier: column)
        return rowTable[column]
    }
}
let threeTimesTable = TimesTable(multiplier: 3)
print("3 的 6 倍是 \(threeTimesTable[6])")
print("3 倍表与 4 倍表交叉位置(3 行 4 列)的值是 \(threeTimesTable[row: 3, column: 4])")

这里新定义的下标接受两个 Int 类型的参数 rowcolumn,通过构建不同倍数的乘法表来返回交叉位置的值。

读写下标

前面的例子中,下标都是只读的。实际上,下标也可以是读写的。我们来看一个表示矩阵的结构体,它可以通过下标来读取和设置矩阵中的元素:

struct Matrix {
    let rows: Int
    let columns: Int
    var grid: [Double]
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }
    func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < 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
        }
    }
}

var matrix = Matrix(rows: 3, columns: 3)
matrix[0, 0] = 1.0
matrix[0, 1] = 2.0
matrix[1, 0] = 3.0
print(matrix[0, 0])
print(matrix[0, 1])
print(matrix[1, 0])

Matrix 结构体中,grid 数组用于存储矩阵的所有元素。subscript 定义了一个读写下标,get 部分用于获取指定位置的元素,set 部分用于设置指定位置的元素。indexIsValid 方法用于检查下标是否越界,在 getset 中都使用 assert 来确保下标在有效范围内。

集合定制中的下标应用

Swift 中的集合类型如 ArrayDictionarySet 都广泛使用了下标语法。当我们自定义集合类型时,合理地运用下标语法可以大大提高代码的易用性和可读性。

自定义数组-like 集合

假设我们要创建一个自定义的循环数组,它的下标可以循环访问数组元素。例如,对于一个长度为 5 的循环数组,当下标为 5 时,它应该返回下标为 0 的元素;当下标为 6 时,返回下标为 1 的元素,以此类推。

struct CircularArray<T> {
    private var array: [T]
    init(_ elements: [T]) {
        array = elements
    }
    subscript(index: Int) -> T {
        let adjustedIndex = index % array.count
        return array[adjustedIndex]
    }
}

let circularArray = CircularArray([1, 2, 3, 4, 5])
print(circularArray[0])
print(circularArray[5])
print(circularArray[10])

CircularArray 结构体中,array 存储实际的数组元素。下标 subscript 通过取模运算 index % array.count 来调整下标,使其在数组有效范围内循环访问元素。

自定义字典-like 集合

我们可以创建一个自定义的字典类型,它支持通过一个复合键来访问值。比如,我们要创建一个用于存储学生成绩的字典,复合键由学生姓名和课程名称组成。

struct StudentGrade {
    let studentName: String
    let courseName: String
    var grade: Double
    init(studentName: String, courseName: String, grade: Double) {
        self.studentName = studentName
        self.courseName = courseName
        self.grade = grade
    }
}

class StudentGradeBook {
    private var grades: [StudentGrade] = []
    subscript(student: String, course: String) -> Double? {
        for grade in grades {
            if grade.studentName == student && grade.courseName == course {
                return grade.grade
            }
        }
        return nil
    }
    func addGrade(_ grade: StudentGrade) {
        grades.append(grade)
    }
}

let gradeBook = StudentGradeBook()
gradeBook.addGrade(StudentGrade(studentName: "Alice", courseName: "Math", grade: 85.0))
print(gradeBook["Alice", "Math"]?? "No grade found")

StudentGradeBook 类中,grades 数组存储所有学生的成绩信息。下标 subscript 接受学生姓名和课程名称作为参数,通过遍历 grades 数组来查找对应的成绩。addGrade 方法用于向成绩簿中添加新的成绩记录。

下标语法的高级特性

下标与泛型

当下标与泛型结合时,可以实现更加通用的集合定制。例如,我们可以创建一个通用的缓存结构,它可以缓存不同类型的值,并通过键来访问缓存的值。

struct Cache<K, V> {
    private var storage: [K: V] = [:]
    subscript(key: K) -> V? {
        get {
            return storage[key]
        }
        set {
            if let newValue = newValue {
                storage[key] = newValue
            } else {
                storage.removeValue(forKey: key)
            }
        }
    }
}

let cache = Cache<String, Int>()
cache["one"] = 1
print(cache["one"]?? "Not in cache")
cache["one"] = nil
print(cache["one"]?? "Not in cache")

Cache 结构体中,K 表示键的类型,V 表示值的类型。storage 是一个字典,用于存储缓存的数据。下标 subscript 实现了对缓存数据的读写操作,当设置值为 nil 时,会从缓存中移除对应的键值对。

关联类型与下标

在使用协议和关联类型时,下标也可以发挥重要作用。例如,我们定义一个表示可索引集合的协议,该协议要求实现一个下标来访问集合中的元素。

protocol IndexableCollection {
    associatedtype Element
    subscript(index: Int) -> Element { get }
    var count: Int { get }
}

struct MyCollection: IndexableCollection {
    typealias Element = Int
    private var data: [Int] = [1, 2, 3, 4, 5]
    subscript(index: Int) -> Int {
        return data[index]
    }
    var count: Int {
        return data.count
    }
}

let myCollection = MyCollection()
for i in 0..<myCollection.count {
    print(myCollection[i])
}

在上述代码中,IndexableCollection 协议定义了一个关联类型 Element,表示集合中元素的类型。协议还要求实现一个只读下标 subscript 和一个 count 属性。MyCollection 结构体遵循该协议,实现了相应的下标和属性,从而可以像其他可索引集合一样使用。

下标语法与内存管理

在自定义集合类型并使用下标语法时,需要注意内存管理的问题。特别是在处理引用类型时,不正确的下标使用可能导致内存泄漏或悬空指针等问题。

引用类型的下标处理

假设我们有一个自定义的图片缓存集合,用于缓存 UIImage(这里假设在 iOS 开发环境中,UIImage 是一个引用类型)对象。

import UIKit

class ImageCache {
    private var images: [String: UIImage] = [:]
    subscript(key: String) -> UIImage? {
        get {
            return images[key]
        }
        set {
            if let newValue = newValue {
                images[key] = newValue
            } else {
                images.removeValue(forKey: key)
            }
        }
    }
}

let cache = ImageCache()
let image = UIImage(named: "exampleImage")
cache["example"] = image
// 使用完后释放引用
cache["example"] = nil

ImageCache 类中,当下标设置为 nil 时,通过 images.removeValue(forKey: key) 从字典中移除对应的键值对,这样可以确保 UIImage 对象在没有其他强引用时被释放,避免内存泄漏。

避免循环引用

在自定义集合中,如果集合中的元素之间存在相互引用,并且通过下标访问可能导致循环引用。例如,我们有两个自定义类 PersonGroupGroup 类中有一个 Person 数组,Person 类中有一个指向所属 Group 的引用。

class Person {
    let name: String
    weak var group: Group?
    init(name: String) {
        self.name = name
    }
}

class Group {
    private var members: [Person] = []
    subscript(index: Int) -> Person {
        return members[index]
    }
    func addMember(_ person: Person) {
        members.append(person)
        person.group = self
    }
}

let group = Group()
let person1 = Person(name: "Alice")
group.addMember(person1)
// 这里不会产生循环引用,因为 person1.group 是弱引用

在上述代码中,Person 类中的 group 属性被声明为 weak,这样当 Group 持有 Person 对象,而 Person 对象又指向 Group 时,不会形成强引用循环,从而避免了内存泄漏。

下标语法的性能考量

在自定义集合类型并使用下标语法时,性能是一个重要的考量因素。不同的实现方式可能会对下标访问的性能产生显著影响。

线性查找下标的性能

以之前的 StudentGradeBook 类为例,它通过线性查找来获取指定学生和课程的成绩。当下标访问频繁且集合元素数量较多时,这种线性查找的方式性能会比较差。

class StudentGradeBook {
    private var grades: [StudentGrade] = []
    subscript(student: String, course: String) -> Double? {
        for grade in grades {
            if grade.studentName == student && grade.courseName == course {
                return grade.grade
            }
        }
        return nil
    }
    func addGrade(_ grade: StudentGrade) {
        grades.append(grade)
    }
}

在这个实现中,每次下标访问都需要遍历整个 grades 数组,时间复杂度为 O(n),n 是 grades 数组的长度。如果集合中元素数量非常大,这种方式会导致下标访问变得很慢。

优化下标性能

为了优化性能,我们可以使用字典来存储成绩信息,这样可以将下标访问的时间复杂度降低到 O(1)。

class StudentGradeBook {
    private var grades: [String: [String: Double]] = [:]
    subscript(student: String, course: String) -> Double? {
        return grades[student]?[course]
    }
    func addGrade(_ grade: StudentGrade) {
        if grades[grade.studentName] == nil {
            grades[grade.studentName] = [:]
        }
        grades[grade.studentName]![grade.courseName] = grade.grade
    }
}

在这个优化后的实现中,grades 字典的键是学生姓名,值是另一个字典,其键是课程名称,值是成绩。这样通过两次字典查找就可以获取指定学生和课程的成绩,大大提高了下标访问的性能。

结合协议扩展的下标定制

协议扩展可以为遵循协议的类型提供默认的下标实现,这在定制集合类型时非常有用。

为协议提供默认下标实现

假设我们有一个 NumericCollection 协议,用于表示包含数字的集合,我们可以通过协议扩展为其提供一个默认的计算集合元素总和的下标。

protocol NumericCollection {
    associatedtype Element: Numeric
    subscript(index: Int) -> Element { get }
    var count: Int { get }
}

extension NumericCollection {
    subscript(sum: String) -> Element {
        var total: Element = 0
        for i in 0..<count {
            total += self[i]
        }
        return total
    }
}

struct IntCollection: NumericCollection {
    typealias Element = Int
    private var data: [Int] = [1, 2, 3, 4, 5]
    subscript(index: Int) -> Int {
        return data[index]
    }
    var count: Int {
        return data.count
    }
}

let intCollection = IntCollection()
print(intCollection["sum"])

在上述代码中,NumericCollection 协议定义了 Element 关联类型和基本的下标及 count 属性。通过协议扩展,为遵循该协议的类型提供了一个新的下标,当传入字符串 "sum" 时,会计算集合中所有元素的总和。IntCollection 结构体遵循 NumericCollection 协议,自动获得了这个默认的下标实现。

利用协议扩展实现更复杂的下标逻辑

我们还可以利用协议扩展来实现更复杂的下标逻辑,比如实现一个分页的功能。

protocol PaginatableCollection {
    associatedtype Element
    subscript(index: Int) -> Element { get }
    var count: Int { get }
}

extension PaginatableCollection {
    subscript(page: Int, pageSize: Int) -> [Element] {
        let startIndex = page * pageSize
        let endIndex = min((page + 1) * pageSize, count)
        var result: [Element] = []
        for i in startIndex..<endIndex {
            result.append(self[i])
        }
        return result
    }
}

struct StringCollection: PaginatableCollection {
    typealias Element = String
    private var data: [String] = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
    subscript(index: Int) -> String {
        return data[index]
    }
    var count: Int {
        return data.count
    }
}

let stringCollection = StringCollection()
print(stringCollection[page: 1, pageSize: 3])

在这个例子中,PaginatableCollection 协议定义了基本的集合接口。通过协议扩展,添加了一个新的下标,该下标接受页码 page 和每页大小 pageSize,返回对应页的元素数组。StringCollection 结构体遵循该协议,从而可以使用这个分页的下标功能。

与其他 Swift 特性结合的下标语法

下标与可选链

可选链在结合下标语法时,可以提供更安全的访问方式。例如,我们有一个可能为空的字典,并且通过下标访问其值可能返回 nil

var optionalDictionary: [String: Int]? = ["one": 1]
let value = optionalDictionary?["one"]
print(value?? "Value not found")

在上述代码中,optionalDictionary 是一个可选的字典。通过可选链 optionalDictionary?["one"],当下 optionalDictionarynil 时,不会导致运行时错误,而是返回 nil,这样可以更安全地访问字典中的值。

下标与高阶函数

高阶函数可以与下标语法结合,实现更强大的集合操作。例如,我们可以使用 map 函数结合下标来对集合中的元素进行转换。

let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { numbers[$0] * numbers[$0] }
print(squaredNumbers)

在上述代码中,map 函数接受一个闭包,闭包中的 $0 表示数组的下标。通过 numbers[$0] 获取数组中的元素,并对其进行平方运算,从而得到一个新的包含平方值的数组。

多参数下标与函数式编程

多参数下标在函数式风格中的应用

在函数式编程中,多参数下标可以用于实现更灵活的操作。例如,我们可以创建一个自定义的集合类型,它的下标接受多个参数来进行复杂的计算。

struct MathCollection {
    private var data: [Int] = [1, 2, 3, 4, 5]
    subscript(operation: (Int, Int) -> Int, index1: Int, index2: Int) -> Int {
        return operation(data[index1], data[index2])
    }
}

let mathCollection = MathCollection()
let sum = mathCollection[+ /* 加法操作 */, 0, 1]
let product = mathCollection[* /* 乘法操作 */, 2, 3]
print(sum)
print(product)

MathCollection 结构体中,下标接受一个闭包 operation 和两个下标参数 index1index2。闭包定义了对 data 数组中两个指定位置元素的操作,这样可以通过下标实现不同的数学运算,体现了函数式编程的灵活性。

多参数下标与柯里化

柯里化是函数式编程中的一个重要概念,多参数下标也可以与柯里化相结合。例如,我们可以将前面的 MathCollection 下标进行柯里化处理。

struct MathCollection {
    private var data: [Int] = [1, 2, 3, 4, 5]
    func operate(_ operation: @escaping (Int, Int) -> Int) -> (Int, Int) -> Int {
        return { index1, index2 in
            return operation(self.data[index1], self.data[index2])
        }
    }
}

let mathCollection = MathCollection()
let addFunction = mathCollection.operate(+)
let sum = addFunction(0, 1)
let multiplyFunction = mathCollection.operate(*)
let product = multiplyFunction(2, 3)
print(sum)
print(product)

在这个实现中,operate 方法接受一个闭包 operation,并返回一个新的闭包,这个新闭包接受两个下标参数。通过这种方式,实现了类似于多参数下标柯里化的效果,使得代码更加灵活和可组合。

下标语法在不同应用场景中的实践

游戏开发中的应用

在游戏开发中,下标语法可以用于访问游戏地图中的元素。例如,我们有一个二维数组表示游戏地图的地形,通过下标可以快速获取指定位置的地形类型。

enum TerrainType {
    case grass, water, mountain
}

struct GameMap {
    private var terrain: [[TerrainType]] = [
        [.grass, .grass, .water],
        [.mountain, .grass, .grass],
        [.grass, .water, .grass]
    ]
    subscript(row: Int, column: Int) -> TerrainType {
        return terrain[row][column]
    }
}

let gameMap = GameMap()
print(gameMap[row: 0, column: 2])

GameMap 结构体中,terrain 是一个二维数组,存储游戏地图的地形信息。通过下标 subscript(row:column:) 可以方便地获取指定行和列位置的地形类型。

数据处理与分析中的应用

在数据处理和分析场景中,我们可能需要自定义集合来存储和处理数据。例如,我们要处理学生的考试成绩数据,计算每个学生的平均成绩。

struct Student {
    let name: String
    var scores: [Double]
    init(name: String, scores: [Double]) {
        self.name = name
        self.scores = scores
    }
    var averageScore: Double {
        let total = scores.reduce(0, +)
        return total / Double(scores.count)
    }
}

class StudentData {
    private var students: [String: Student] = [:]
    subscript(studentName: String) -> Student? {
        return students[studentName]
    }
    func addStudent(_ student: Student) {
        students[student.studentName] = student
    }
    func averageScoreOfAllStudents() -> Double {
        let totalScores = students.values.map { $0.averageScore }.reduce(0, +)
        return totalScores / Double(students.count)
    }
}

let studentData = StudentData()
studentData.addStudent(Student(name: "Alice", scores: [85, 90, 95]))
studentData.addStudent(Student(name: "Bob", scores: [75, 80, 85]))
if let alice = studentData["Alice"] {
    print("Alice 的平均成绩: \(alice.averageScore)")
}
print("所有学生的平均成绩: \(studentData.averageScoreOfAllStudents())")

在上述代码中,StudentData 类使用字典存储学生信息,通过下标可以方便地获取指定学生的信息。同时,还提供了计算所有学生平均成绩的方法,展示了在数据处理场景中自定义集合和下标语法的应用。

通过以上对 Swift 下标语法与集合定制方法的详细介绍,我们可以看到下标语法在 Swift 编程中是一个非常强大和灵活的特性,它能够大大提高代码的可读性和易用性,无论是在简单的自定义类型,还是复杂的集合定制和各种应用场景中,都发挥着重要作用。在实际编程中,合理运用下标语法与集合定制方法,可以使我们的代码更加优雅和高效。