10、泛型与集合

154 阅读6分钟

一、泛型

来个例子:

func multiNumInt(_ x: Int, _ y: Int) -> Int{
    return x * y
}

func multiNumDouble(_ x: Double, _ y: Double) -> Double{
    return x * y
}

以上两个方法的函数体是一样的,区别在于入参的类型不同,泛型的使用可以让我们无视不同类型带来的冗余代码。

例子:

func multiNum<T:FloatingPoint>(_ x:T, _y:T) -> T{
    return x * y
}

泛型的基本写法 ,首先指定一个占位符 T,紧挨着写在函数名后面的一对尖括号(表示这个T要遵循某个协议),这时可以使用T来替换任意定义的函数形式参数。

结构体中也可以使用泛型

class Stack<E> {
    var elements = [E]()
    func push(_ element:E) {
        elements.append(element)
    }
    func pop() -> E{
        elements.removeLast()
    }
    func top() -> E{
        elements.last!
    }
    func size() -> Int {
        elements.count
    }
}

使用:

var stack = Stack<Int>()
stack.push(10)
stack.pop(5)

var stack2 = Stack<Float>()
stack2.push(50.1)
stack2.pop(10.1)

可以在class中默认给个初始化方法,直接传入值就好,无需指定类型

init(firstElement:E){
  elements.append(firstElement)
}

使用:

var stack3 = Stack(firstElement: 99)

子类继承自带有泛型的父类时,也需要加上泛型

class subStack<E>:Stack<E>{
    
}

二、关联类型 associatedtype

在swift的协议中,是不支持泛型写法的,需要配合 associatedtype 关键字才能实现。

关联类型的作用是给协议中用到的类型定义一个占位名称,并且协议中可以拥有多个关联类型

protocol Stackable {
    associatedtype Element //关联类型
    mutating func push(_ element: Element)
    mutating func pop() -> Element
    func top() -> Element
    func size() -> Int
}

注意 pushpop 在前面用 mutating 修饰,这是为了给值类型也可以遵守这份协议,因为这两个方法都是要修改 Stack<E> 的内存。

接下来我们可以用一个泛型类型来遵守 Stackable,通过 typealias 给关联类型设定了一个类型,这个类型就是泛型E

class Stack<E>: Stackable {
    typealias Element = E  //typealias 指定 Element 的类型, 这句话也可以注释掉
    var elements = [E]()
    func push(_ element: E) { elements.append(element) }
    func pop() -> E{ elements.removeLast() }
    func top() -> E{ elements.last! }
    func size() -> Int { elements.count }
}

三、类型约束

1、可以在associatedtype后面指定必须遵循的协议

protocol Stackable {
    associatedtype Element<FixedWidthInteger> //关联类型 指定遵循某个协议
    mutating func push(_ element: Element)
    mutating func pop() -> Element
    func top() -> Element
    func size() -> Int
}

2、直接在协议中使用约束

protocol EvenProtocol: Stackable{
    associatedtype Even: EvenProtocol where Even.Element == Element //约束
    func pushEven(_ item: Int) -> Even
}

//使用
extension LGStack: EvenProtocol{ 
  func pushEven(_ item: Int) -> LGStack { 
    var result = LGStack() 
    if item % 2 == 0{ 
      result.push(item) 
    } 
    return result 
  } 
}

在这个协议里,Even 是一个关联类型,Even 拥有两个约束:

  1. 它必须遵循 EvenProtocol 协议(就是当前定义的协议)。
  2. 以及它的 Element 类型必须是和容器里的 Element 类型相同。

3、在函数中使用约束

func compare<T1: Stackable, T2: Stackable>(_ stack1: T1, _ stack2: T2) -> BOOL where T1.Element == T2.Element, T1.Element:Equatable {
  guard stack1.size == stack2.size else {
    return false
  }
  return true
}

这个函数有两个形式参数:stack1stack2,stack1T1类型,stack2T2类型,T1T2的具体类型在函数调用时决定。

约束要求:

  1. T1T2 都必须遵守 Stackable 协议。
  2. T1ElementT2Element 相同。
  3. T1Element 遵守 Equatable 协议。

这些约束意味着:

  • stack1 是一个 T1 类型的容器。
  • stack2 是一个 T2 类型的容器。
  • stack1stack2 中的元素类型相同。

四、 类型擦除

通过两个案例来说明白这个类型擦除是什么玩意儿

案例一: 声明一份协议DataFetch,协议里有一个关联类型DataType,这份协议是用来提取数据的。

// 数据提取器
protocol DataFetch {
    /// 关联类型
    associatedtype DataType

    /// 提取数据的方法
    func fetch(completion: Body<DataType>?)
}

fetch()方法接收一个函数 Body,这个函数的定义如下:

typealias Body<T> = (Result<T, Error>) -> ()

这个函数接收一个 Result 类型的参数,并且它支持是一个泛型参数 TT 可以是任意类型。

这么做的目的在于:首先这是一份协议,意味着它会被多个类型遵守,每个实现 fetch(completion:) 的代码是不一样的,也就是提取数据的方式不一样。

当一个类中包含了一个遵守 DataFetch 协议类型的变量,但这个变量的类型并不是单一的,而希望它支持遵守了 DataFetch 协议的其它类型,我们不需要关心数据是如何提取的,只关心提取数据后的结构。此时我们把这个变量当作该类的一个属性或者一个方法中的参数,第一时间想到的是用 DataFetch 作为类型,代码如下:

class SomeViewController {
    let dataFetch: DataFetch

    init(_ dataFetch: DataFetch){
        self.dataFetch = dataFetch
    }
}

但此时编译器会报错: Snipaste_2022-08-30_17-16-09.png 也就是说,协议DataFetch只能用作通用约束,当协议中有用到关联类型Self 的时候,协议就不能当作类型来使用。因为编译器不知道协议中的这个关联类型的具体类型是什么。

那这个时候就可以用到类型擦除的技术了

类型擦除是一种非常有用的技术,用来阻止泛型对代码的侵入,也可用来保证接口简单明了。通过将底层类型包装起来,将API与具体的功能进行拆分。

  • 定义一个中间层类型,该类型实现了协议DataFetch中的所有方法。
  • 在中间层类型中实现的具体协议方法里转发给实现协议的抽象类型。
  • 在中间层类型的初始化方法中把抽象类型当参数传入(依赖注入)。
struct AnyDataFetch<T>: DataFetch {

    private let _fetch: (Body<T>?)-> ()

    init<U: DataFetch>(_ fetchable: U) where U.DataType == T {
        _fetch = fetchable.fetch
    }

    func fetch(completion: Body<T>?) {
        _fetch(completion)
    }
}

这个中间层命名为 AnyDataFetch,它遵守 DataFetch 协议,并且实现了协议中的所有方法。这个方法里面就只干一件事,调用协议的抽象类型的 fetch(completion:) 方法。协议的抽象类型的 fetch(completion:) 我们用一个属性去接收。

因为要接收协议的抽象类型的 fetch(completion:) 方法,所以我们要在初始化的时候把协议的抽象类型的实例传进来,然后用 _fetch 去接收。需要注意的是这里要求协议的抽象类型中的 DataType 必须和泛型 T 相同。

此时这个中间层的结构就完成了,接下来是这个抽象类型的定义,我把它命名为 UserData,它用来提取用户的数据,代码如下:

struct UserData: DataFetch {
    func fetch(completion: Body<Model>?) {
        completion?(.success(Model(id: 1001, other: "其他")))
    }
}

协议的抽象类型实现了 fetch(completion:) 方法后,我们在里面简单的写了一个提取成功的回调,这个可以是任意类型,为了方便我就把它写成了一个 Model,Model 的代码如下:

struct Model {
    var id: Int
    var other: Any
}

最后 controller 就可以这么去用了:

typealias Body<T> = (Result<T, Error>) -> ()

protocol DataFetch {

    associatedtype DataType

    func fetch(completion: Body<DataType>?)
}

struct AnyDataFetch<T>: DataFetch {

    private let _fetch: (Body<T>?)-> ()

    init<U: DataFetch>(_ fetchable: U) where U.DataType == T {
        _fetch = fetchable.fetch
    }

    func fetch(completion: Body<T>?) {
        _fetch(completion)
    }
}

struct Model {
    var id: Int
    var other: Any
}


//user的数据
struct UserData: DataFetch {
    func fetch(completion: Body<Model>?) {
        completion?(.success(Model(id: 1001, other: "其他")))
    }
}

//teacher的数据
struct TeacherData: DataFetch {
    func fetch(completion: Body<Model>?) {
        completion?(.success(Model(id: 1002, other: "llllll")))
    }
}

class SomeViewController {
    let dataFetch: AnyDataFetch<Model>

    init(_ dataFetch: AnyDataFetch<Model>){
        self.dataFetch = dataFetch
        loadData()
    }

    func loadData() {
        self.dataFetch.fetch { body in
            switch body {
            case .success(let model):
                print("success:\(model)" )
            case .failure(let error):
                print("failure:\(error)" )
            }
        }
    }
}

let userData = UserData()
let teacherData = TeacherData()

let userDataFetch = AnyDataFetch<Model>.init(userData)
let teacherDataFetch = AnyDataFetch<Model>.init(teacherData)

let someVC1 = SomeViewController(userDataFetch)
let someVC2 = SomeViewController(teacherDataFetch)