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协议返回。