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

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

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


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

泛型(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>中的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<Element> 原实作的内容
    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)-访问控制(存取控制)-讲给你懂

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MillVA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值