16-泛型

88 阅读6分钟

Swift 泛型 (Generics)

什么是泛型?

泛型可以将类型参数化,提高代码复用率,减少代码量

本质来说就是类型不确定,但是对不同类型的处理可以整合到一个方法里面。

func swapValues<T>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}

这里可以看到泛型的写法,一般是在方法后面加上一个<>,然后在里面写上你想用什么表示泛型的具体类型,我们常常用T,T这里表示Type,值得是我们用T来代替后面泛型的类型。当然如果我里面有两个不同的泛型,那么可以用别的字母或者单词表示,你想用什么就用什么,但是要注意还是要见名知意。

var i1 = 10
var i2 = 20
swapValues(&i1, &i2)

var d1 = 10.0
var d2 = 20.0
swapValues(&d1, &d2)

struct Date {
    var year = 0, month = 0, day = 0
}

var dd1 = Date(year: 2011, month: 9, day: 10)
var dd2 = Date(year: 2012, month: 10, day: 11)
swapValues(&dd1, &dd2)

这里要注意的是通过调用方法改变两个参数的变量,这里要用传地址。

func test<T1, T2>(_ t1: T1, _ t2: T2) {}
var fn: (Int, Double) -> () = test

下面我们再看一个泛型 类型的使用例子

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 }
}

上面代码是用类和泛型来模拟一个栈结构,由于栈里面的存储的元素不确定,所以这里用到了泛型,这里还有一个注意点为啥 swapValues(&i1, &i2) 在调用的时间不需要声明类型,而 var stack = Stack() 一定要声明类型,是因为方法在调用的时间就能推断出来方法的类型,而类在声明的时候无法确定类型。

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

遇到继承的情况,上面是声明了一个子类,子类也必须要指明泛型

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

改结构体类型的内存,所以要加mutating。 其实栈的结构,除了使用类结构来模拟,还可使用结构体来模拟。上面是结构体和泛型来模拟的

var stack = Stack<Int>()
stack.push(11)
stack.push(22)
stack.push(33)
print(stack.top())  // 33
print(stack.pop())  // 33
print(stack.pop())  // 22
print(stack.pop())  // 11
print(stack.size()) // 0

这部分代码是使用举例子

下面再看一个使用举的例子

enum Score<T> {
    case point(T)
    case grade(String)
}

let score0 = Score<Int>.point(100)
let score1 = Score.point(99)
let score2 = Score.point(99.5)
let score3 = Score<Int>.grade("A")

这是一个重要的例子,首先是声明了一个有关联值的枚举, let score0 = Score<Int>.point(100)这句代码是在创建虽然是传的字符串100,但是我在创建的时候也需要确定point是啥类型的,要不然内存没办法确定,所以这里需要直接指明point的类型

关联类型

关联类型的作用:给协议中用到的类型定义一个占位名称

协议中可以拥有多个关联类型(就是可以用多个关联类型)

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

如果我想声明一个泛型类,并且使用该协议,可以这样写

   class Stack<E> : Stackable {
   // typealias Element = 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 }
}

如果我想声明一个具体类,并且使用该协议,可以这样写

    class StringStack: Stackable {
    // 给关联类型设定真实类型 (Set the concrete type for the associated type)
    // typealias Element = String
    var elements = [String]()
    func push(_ element: String) { elements.append(element) }
    func pop() -> String { elements.removeLast() }
    func top() -> String { elements.last! }
    func size() -> Int { elements.count }
}

类型约束

先举个例子

protocol Runnable { }
class Person { }
func swapValues<T : Person & Runnable>(_ a: inout T, _ b: inout T) {
  (a, b) = (b, a)
}
  • 上面首先定义空协议 Runnable 和空类 Person
  • swapValues 是泛型函数,类型参数 T 必须同时是 Person 的子类且遵循 Runnable
  • 使用 Person & Runnable 表示协议组合约束
  • 函数交换两个 inout 参数的值
protocol Stackable {
  associatedtype Element: Equatable
}
class Stack<E : Equatable> : Stackable { typealias Element = E }
  • Stackable 协议定义关联类型 Element,要求遵循 Equatable
  • Stack 是泛型类,E 必须遵循 Equatable,并通过 typealias Element = E 满足协议要求
  • 这样 Element 必须是可比较的类型 下面再看一个where来解决复杂约束
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2: S2) -> Bool
 where S1.Element == S2.Element, S1.Element : Hashable {
 return false
}
  • equal 是泛型函数,S1 和 S2 都需遵循 Stackable
  • where 子句添加额外约束:
  • S1.Element == S2.Element:两个栈的 Element 必须是同一类型
  • S1.Element : Hashable:Element 还需遵循 Hashable 这里有一个注意点就是S1.Element,S2.Element并不是固定写成Element,如果前面 typealias item = E,那么访问的时候要写成S1.item来访问关联类型的这样的语法

下面的代码会报类型错误

var stack1 = Stack<Int>()
var stack2 = Stack<String>()
// error: requires the types 'Int' and 'String' be equivalent
equal(stack1, stack2)

协议类型一个常见的注意点

protocol Runnable { }
class Person : Runnable { }
class Car : Runnable { }

// ✅ 正常:Runnable 可以直接作为返回值类型
func get(_ type: Int) -> Runnable {
    if type == 0 {
        return Person()
    }
    return Car()
}

var r1 = get(0)
var r2 = get(1)

上面的是定义一个协议,两个类,这两个类都遵循这个协议。上面的写法编译器不会报任何错误。

下面我们对Runnable协议,class Person,class Car进行一下调整,改成下面的样子,主要是在协议中定义了一个关联类型。如果按照下面的定义。

protocol Runnable {
    associatedtype Speed  // ⚠️ 关键点:定义了关联类型
    var speed: Speed { get }
}
class Person : Runnable {
    var speed: Double { 0.0 } // Speed 被推断为 Double
}
class Car : Runnable {
    var speed: Int { 0 }      // Speed 被推断为 Int
}

按照上面的写法,func get(_ type: Int) -> Runnable这句代码会报错,因为在get(0)调用的时候,在编译阶段无法返回的是Person()还是Car()类型,进而无法判断协议中关联类型是什么,无法对协议进行分配内存空间或者检查,所以报错。

要想解决上面的问题,有两种办法 1.使用泛型,下面是使用泛型解决的代码.泛型能解决是因为,虽然函数声明的时候不知道关联类型是什么,但是在方法调用的时候告诉了是用person类型,编译器自然能推断出关联类型是啥。所以不会报错,核心在调用的时间告诉了

   func get<T: Runnable>() -> T {  // 定义时
       return Person() as! T
   }

   var person = get<Person>()  // 调用时:你明确告诉编译器 T = Person
   // 编译器:好的,T = Person,Person.Speed = Double,完全清楚!✅

2.使用不透明类型,啥叫不透明,不透明就是不向外界公开,内部他自己很清楚,这里就是这样的。我们先看下不透明类型的代码。

func get() -> some Runnable {  // ✅
    return Person()  // 编译器知道是 Person,虽然调用者不知道
}

var r = get()  // r 是某种遵循 Runnable 的类型

不透明类型首先需要将get方法的返回改成一种类型,其次需要在返回值上加上some,不透明类型其实是编译器知道是返回的什么类型,但是不公开,不暴露给外界。

有人会说为啥get方法返回值不直接返回Person类型呢?是因为我们如果直接返回Person其实已经暴露了很多细节给外界,而用some Runnable只会将Runable协议返回。