Swift基础入门知识学习(25)-泛型-讲给你懂

Swift基础入门知识学习(25)-泛型-讲给你懂,第1张

高效阅读-事半功倍读书法-重点笔记-不长,都是干货

Swift基础入门知识学习(24)-协议(协定)-讲给你懂

目录 泛型能解决的问题泛型函数类别参数命名类别参数 泛型类别扩展泛型类别 类别约束类别约束语法使用类别约束 关联类别经由扩展一个已存在的类别来设置关联类型关联类别使用类型标注 where 语句使用 where 语句的扩展使用 where 语句的关联类别 泛型下标


理解难度
★★★★☆
实用程度
★★☆☆☆

泛型(generic)是 Swift 一个有意思的特性,可以让你自定义出一个适用任意类别的函数及类型。可以避免重复的代码码且清楚的表达代码码的目的。

许多 Swift 标准函数库就是经由泛型代码码建构出来的,像是数组( Array )和字典( Dictionary )都是泛型的,你可以声明一个 [Int] 数组,也可以声明一个 [String] 数组。同样地,你也可以声明任意指定类别的字典。

你可以将泛型使用在函数、枚举、结构体及类别上。

泛型能解决的问题

以下是一个可以利用泛型来简化代码码的例子:


// 定义一个将两个整数变量的值互换的函数
func swapTwoInts(_ a: inout Int, _ b: inout Int) {

    let temporaryA = a
    a = b
    b = temporaryA
    
}

// 声明两个整数变量 并当做参数传入函数
var oneInt = 10
var anotherInt = 5000
swapTwoInts(&oneInt, &anotherInt)

// 打印:互换后的 oneInt 为 5000,anotherInt 为 10
print("互换后的 oneInt 为 \(oneInt),anotherInt 为 \(anotherInt)")

// 与上面定义的函数功能相同 只是这时互换的变量类别为字符串
func swapTwoStrings(_ a: inout String, _ b: inout String) {

    let temporaryA = a
    a = b
    b = temporaryA
    
}

由上面的代码可以看出,两个函数的功能完全一样,唯一不同的只有传入参数的类别,这种情况便可以使用泛型来简化。

泛型函数

根据前面提到的两个功能完全一样的函数,以下使用泛型来定义一个适用任意类别的函数:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {

    let temporaryA = a
    a = b
    b = temporaryA
    
}

上面的代码中的函数使用了占位类别名称(placeholder type name,习惯以字母T来表示)来代替实际类别名称(像是Int、Double或String)。

可以注意到函数名称后面紧接着一组角括号 < >,且包着T。这代表角括号内的T是函数定义的一个占位类别名称,因此 Swift 不会去查找名称为T的实际类型。

定义占位类别名称时不会明确表示T是什么类型,但参数a与b都必须是这个T类型。而只有当这个函数被呼叫时,才会根据传入参数的实际类别,来决定T所代表的类型。

这时便可以使用这个泛型函数,如下:


//  首先是两个整数
var oneInt2 = 20
var anotherInt2 = 1000
swapTwoValues(&oneInt2, &anotherInt2)

// 再来是两个字符串
var oneString = "Hello"
var anotherString = "world"
swapTwoValues(&oneString, &anotherString)

类别参数

前面提到的swapTwoValues(::)中,占位类别名称T是类型参数的一个例子。

类别参数会指定并命名一个占位类型,且会紧跟在函数名称后面使用一组角括号 <> 包起来。当一个类别参数被指定后,就可以用来定义一个函数的参数类别、函数的返回值类别或是函数内的类型标注。

类别参数可以指定一个或一个以上,使用多个时以逗号 , 隔开。

命名类别参数

在一般情况下,类别参数会指定为一个有描述性的名字,像是Dictionary中的Key和Value,或是Array中的Element,用来明显表示这些类别参数与泛型函数之间的关系。而当无法有意义的描述类型参数时,通常会使用单一字母来命名,像是T、U或V。

通常会使用大驼峰式命名法(像是T或MyTypeParameter)来为类别参数命名,以表示他们是占位类型,而不是一个值。

泛型类别

除了泛型函数,你也可以定义一个泛型类别。你可以定义在枚举、结构体或类别上,类似数组(Array)和字典(Dictionary)。

以下会定义一个堆叠(Stack)的泛型集合类别来当做一个例子。堆叠的运作方式有点像数组,可以增加(push)一个元素到数组最后一员,也可以从数组中取出(pop)最后一个元素。


// 定义一个泛型结构体 Stack 其占位类别参数命名为 Element
struct Stack<Element> {

    // 将类别参数用于类型标注 设置一个类别为 [Element] 的空数组
    var items = [Element]()

    // 类别参数用于方法的参数类型 方法功能是增加一个元素到数组最后一员
    mutating func push(_ item: Element) {
        items.append(item)
        
    }

    // 类别参数用于方法的返回值类型 方法功能是移除数组的最后一个元素
    mutating func pop() -> Element {
    
        return items.removeLast()
        
    }
    
}

上面定义的结构体中可以看到,指定Element为占位类别参数后,便可在结构体中作为类别标注、方法的参数类别及方法的返回值类型,而因为必须修改结构体的内容,所以方法都必须加上mutating。

接着就可以使用这个刚定义好的Stack类别,如下:


// 先声明一个空的 Stack 这时才决定其内元素的类别为 String
var stackOfStrings = Stack<String>()

// 依序放入三个字符串
stackOfStrings.push("one")
stackOfStrings.push("two")
stackOfStrings.push("three")

// 然后移除掉最后一个元素 即字符串 "three"
stackOfStrings.pop()

// 现在这个 Stack 还有两个元素 分别为 one 及 two

扩展泛型类别

当你扩展一个泛型类别时,不需要在扩展的定义中提供类别参数列表,原类别已经定义的类型参数列表(如前面提到的 Stack 定义的 Element)可以直接在扩展中使用。

以下为堆叠(Stack)扩展一个名称为 topItem 的唯读计算属性,它会返回这个堆叠的最后一个元素,且不会将其移除:


extension Stack {

    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
    
}

上面的代码可以看到,扩展中可以直接使用Element。而返回值为一个可选值,所以底下使用可选绑定来取得最后一个元素:


if let topItem = stackOfStrings.topItem {

    // 打印:最后一个元素为 two
    print("最后一个元素为 \(topItem)")
    
}

类别约束

有时在定义一个泛型函数或泛型类别时,会需要为这个泛型类别参数增加一些限制,可能是指定类别参数必须继承自指定的类型,或是符合一个特定的协定,也就是类型约束( type constraint )。

像是 Swift 内建的字典( Dictionary )便对字典的键的类别作了些限制。字典的键的类别必须是可杂凑的( hashable ),也就是必须只有唯一一种方式可以表示这个键。

而实际上为了实现这个限制,字典的键的类别符合了 Hashable 协定。 Hashable 是 Swift 标准函数库中定义的一个特定协定,所有 Swift 的基本类别(像是Int、Double、Bool和String)预设都是可杂凑的(hashable)。

类别约束语法

你可以在一个类别参数名称后面加上冒号 : 并紧接着一个类型或是协定来做为类别约束,它们会成为类别参数列表的一部分,例子如下(泛型类别也是一样方式):


func 泛型函数名称<T: 某个类别, U: 某个协定>(参数: T, 另一个参数: U) {

    函数内部的代码
    
}

上面的定义中可以看到,T类别参数必须继承自某个类型,U类别参数则必须遵循某个协定。

使用类别约束

以下会定义一个函数,两个参数分别为一个数组及一个值,函数的功能是寻找第一个参数数组中是否有另一个参数值,如果有就返回这个值在数组中的索引值,找不到则返回nil。

这个函数的类别约束会使用到另一个 Swift 标准函数库中的 Equatable 协定,这个协定要求任何遵循该协定的类别必须实作 == 及 != ,进而可以对该类型的任意两个值进行比较。(所有的 Swift 标准类别预设都符合 Equatable 协定。)


func findIndex<T: Equatable>(

  of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
    
}

// 首先找看看 [Double] 数组的值
let doubleIndex = findIndex(of: 9.2, in: [68.9, 55.66, 10.05])
// 因为 9.2 不在数组中 所以返回 nil

// 接着找 [String] 数组的值
let stringIndex = findIndex(of: "Kevin", in: ["Adam", "Kevin", "Jess"])
// Kevin 为数组中第 2 个值 所以会返回 1

上面的代码中,为这个泛型函数的泛型类别加上泛型约束,该类别必须遵循
Equatable协定才能使用这个函数,借此约束了无法彼此比较(也就是没有实作 == 及 !=)的类别来使用。

关联类别

关联类别(associated type)表示会为协定中的某个类别提供一个占位名称(placeholder name),其代表的实际类型会在协定被遵循时才会被指定。使用 associatedtype 关键字来指定一个关联类别。

底下是一个例子,定义一个协定Container,协定中定义了一个关联类别Item:


protocol Container {

    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    
}

上面的代码中可以看到,协定定义的方法append()参数的类别及下标的返回值类型都是Item,目前仍是占位名称,实际类别要等到这个协定被遵循后才会被指定。

接着我们将前面定义的堆叠(Stack)遵循这个协定Container,在实作协定Container的全部功能后,Swift 会自动推断Item的类别就是Element,如下:


struct NewStack<Element>: Container {

    // Stack 原实作的内容
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
        
    }

    // 原本应该要写 typealias 
    // 但因为 Swift 会自动推断类别 所以下面这行可以省略
    // typealias Item = Element

    // 协定 Container 实作的内容
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

经由扩展一个已存在的类别来设置关联类型

前面章节有提过,可以利用扩展来让一个已存在的类别符合协定,使用了关联类别的协定也一样可以。

Swift 内建的数组(Array)类别恰恰好已经有前面提过的协定 Container 需要实作的功能(分别是方法 append()、属性 count 及下标返回一个依索引值取得的元素)。所以现在可以很简单的利用一个空的扩展来让Array遵循这个协定,如下:


extension Array: Container {}

关联类别使用类型标注

你可以为协定中的关联类别增加一个类型标注,让这个关联类别也必须遵循这个条件,下面的例子定义了一个 Item 必须遵循 Equatable 的 OtherContainer 协定:


protocol OtherContainer {

    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    
}

where 语句

有时候你也可能需要对关联类别定义更多的限制,这时可以经由在参数列表加上一个where语句,并紧接着限制条件来定义。你可以限制一个关联类别要遵循某个协定,或是某个类别参数和关联类型必须相同类别。

底下定义一个泛型函数allItemsMatch(),功能为检查两个容器是否包含相同顺序的相同元素,如果条件都符合会返回true,否则返回false:


func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

    // 检查两个容器含有相同数量的元素
    if someContainer.count != anotherContainer.count {
        return false
    }

    // 检查每一对元素是否相等
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }

    // 所有条件都符合 返回 true
    return true
}

从上面的定义可以看到,这个函数的类别参数列表还定义了对两个类型参数的要求:

C1 必须符合协定Container(即C1: Container)。C2 必须符合协定Container(即C2: Container)。C1 的Item必须与 C2 的Item类别相同(即C1.Item == C2.Item)。C1 的Item必须符合协定Equatable(即C1.Item: Equatable)。

接着可以实际使用这个函数,如下:


// 声明一个类别为 NewStack 的变量 并依序放入三个字符串
var newStackOfStrings = NewStack<String>()
newStackOfStrings.push("one")
newStackOfStrings.push("two")
newStackOfStrings.push("three")

// 声明一个数组 也放置了三个字符串
var arrayOfStrings = ["one", "two", "three"]

// 虽然 NewStack 跟 Array 不是相同类别
// 但先前已将两者都遵循了协定 Container
// 且都包含相同类别的值
// 所以可以把这两个容器当做参数传入函数
if allItemsMatch(newStackOfStrings, arrayOfStrings) {
    print("所有元素都符合")
} else {
    print("不符合")
}
// 打印:所有元素都符合

使用 where 语句的扩展

你也可以在扩展中使用 where 语句,以下是一个为泛型结构体Stack增加一个isTop(_:)方法的例子:


extension Stack where Element: Equatable {

    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
    
}

这个 isTop(😃 方法会先检查这个 Stack 是不是空的,接着再比对传入的元素是否与最顶端的元素相同。这个方法使用了 == 运算子来比对元素,但一开始定义泛型结构体 Stack 时并未定义它的元素要遵循 Equatable 协定,所以必须在扩展中使用 where 语句来增加新的条件,以规范传入的元素要遵循 Equatable 协定,也才能正常使用这个新的方法 isTop(😃。

如果尝试在一个元素没有遵循Equatable协定的 Stack 使用方法 isTop(_😃,则会发生编译时错误,如下:


// 定义一个空的结构体
struct NotEquatable { }

// 声明一个元素类别为 NotEquatable 的 Stack
var notEquatableStack = Stack<NotEquatable>()

// 声明一个类别为 NotEquatable 的值 并加入这个 Stack 中
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)

// 因为类别 NotEquatable 没有遵循 Equatable 协定
notEquatableStack.isTop(notEquatableValue) // 这行会报错误

除了遵循协定,也可以限制元素必须为特定的类别,以下的例子为元素的类别必须是Double:


extension Container where Item == Double {

    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
     
       sum += self[index]
        }
        return sum / Double(count)
    }
    
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// 打印:648.9

与先前提到泛型 where 语句一样,如果这个扩展的 where 语句有多个条件,则是使用逗号,来分隔各条件。

使用 where 语句的关联类别

你可以在关联类别后面加上 where 语句来增加条件,如下:


protocol AnotherContainer {

    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
    
}

泛型下标

下标(subscript)可以使用泛型,也能使用泛型 where 语句,以下是一个例子:


extension Container {

    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
    
}

Swift基础入门知识学习(26)-访问控制(存取控制)-讲给你懂

高效阅读-事半功倍读书法-重点笔记-不长,都是干货

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996723.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存