泛型在Java
,C++
等多个语言中都有,C#
把泛型发挥的淋漓尽致,OC
中也有泛型(比如OC
中的数组,你可以限制他里面装的是NSString
类型),Swift
中泛型的使用范围更加多元化。
一、泛型函数
1.1. 泛型可以将类型参数化,提高代码复用率,减少代码量
示例代码:
var n1 = 10
var n2 = 20
func swapValues(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
swapValues(&n1, &n2)
print("a=\(n1), b=\(n2)") // 输出:a=20, b=10
复制代码
如果上面示例代码中swapValues
让传入的是其他类型参数就无法使用了。这时候就要考虑使用泛型。
在函数名后面加上<T>
就可以表示该函数接收的是泛型参数,参数类型也是用T
修饰(T
不是固定写法,也可以是S
、ABC
等其他任意标识,仅仅代表不确定类型)。
泛型示例代码:
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
swapValues(&n1, &n2)
复制代码
这时候就可以传入任意类型的参数了,但参数类型必须保持一致。
调用swapValues
函数时后面不能写成swapValues<Int>(&n1, &n2)
,因为参数已经明确了要传入的是什么类型。
声明泛型函数时,函数后面的泛型标识也不能省略,否则就是一个普通函数,编译器无法识别泛型。
1.2. 泛型函数赋值给变量
普通函数是可以直接赋值给变量的:
var n1 = 10
var n2 = 20
func swapValues(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
var fn = swapValues
fn(&n1, &n2)
复制代码
但是泛型函数是不能向普通函数一样直接赋值给变量的,否则直接报错。
正确做法(变量后面明确泛型的类型):
var n1 = 10
var n2 = 20
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
var fn: (inout Int, inout Int) -> () = swapValues
fn(&n1, &n2)
复制代码
多个泛型示例代码:
func test<T1, T2>(_ a: T1, _ b: T2) {
print("a=\(a), b=\(b)")
}
test(10, 20.0)
复制代码
二、泛型类型
结构体和类也是可以增加泛型的,这种类型叫做泛型类型。
2.1. 类
示例代码(栈:先入后出):
// 栈
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
}
}
// 元素是Int类型
var intStack = Stack<Int>()
// 元素是String类型
var stringStack = Stack<String>()
// 元素是任意类型
var anyStack = Stack<Any>()
复制代码
为什么使用类的时候就可以在类名后面加上类型,函数就不可以呢?因为函数在入参的时候,参数已经明确了泛型的类型。
2.2. 继承
继承泛型类型的类,子类也必须是泛型类型。
class SubStack<E>: Stack<E> {}
复制代码
2.3. 结构体
如果结构体内函数需要修改结构体的内存(修改存储属性),则必须在函数前面加上mutating
。
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
}
}
复制代码
2.4. 枚举
enum Score<T> {
case point(T)
case grade(String)
}
// 完整写法
let score0 = Score<Int>.point(100)
// 忽略枚举后面的类型声明(参数已经明确的泛型类型)
let score1 = Score.point(100.0)
// 必须指定泛型(此时并不知道T什么类型)
let score2 = Score<Double>.grade("A")
复制代码
三、泛型本质
示例代码:
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
var i1 = 10
var i2 = 20
swapValues(&i1, &i2)
var d1 = 10.0
var d2 = 20.0
swapValues(&d1, &d2)
复制代码
猜想: 同一个函数可以传入不同类型的参数,是不是像C++
一样生成了很多不同类型参数的重载函数?如果是这样的话,不同参数类型的函数内存地址肯定是不一样的,我们通过汇编看下。
通过汇编发现,两个函数地址是一样的。
疑问: 传入的参数类型不一样(内存布局不一样),是如何做到参数交互赋值呢?
通过上面汇编可以看出,函数传入的参数除了两个外界传入的参数外,还传入另外一个元类型信息参数。函数内部就是根据这个元类型参数获取参数真正的类型是什么。
四、关联类型
关联类型(Associated Type)的作用:给协议中用到的类型定义一个占位名称。
示例代码(栈):
protocol Stackable {
associatedtype Element
mutating func push(_ element: Element)
mutating func pop() -> Element
func top() -> Element
func size() -> Int
}
复制代码
associatedtype Element
代表的意思就是定义一个名称为Element
的泛型。
使用关联类型时,有两种方法可以让编译器知道关联类型的确定类型:
- 格式:
typealias 关联类型 = 真实类型
示例:typealias Element = String
- 第二种方式就是直接把实现协议参数修改为确定类型。
使用关联类型:
class StringStack: Stackable {
// 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
}
}
复制代码
当实现协议的类也无法确定关联类型时:
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
}
}
复制代码
协议中可以拥有多个关联类型:
protocol Stackable {
associatedtype Element
associatedtype Element2
associatedtype Element3
mutating func push(_ element: Element)
mutating func pop() -> Element
func top() -> Element
func size() -> Int
}
复制代码
不能把关联类型拼接在一起声明,否则报错。
五、类型约束
可以对泛型进行限制/约束。
示例代码一:
protocol Runnable { }
class Person { }
func swapValues<T: Person & Runnable>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
复制代码
上面的示例代码中T
就被Person
和Runnable
做了约束,意思是T
只能是Person
类并且需要遵守Runnable
协议。
示例代码二:
protocol Stackable {
associatedtype Element: Equatable
}
class Stack<E: Equatable> : Stackable {
typealias Element = E
}
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2: S2) -> Bool
where S1.Element == S2.Element, S1.Element: Hashable {
return false
}
var s1 = Stack<Int>()
var s2 = Stack<Int>()
var s3 = Stack<String>()
equal(s1, s2)
equal(s2, s3)
复制代码
示例代码二就稍微有点复杂了,约束泛型后还添加了其他条件。S1
和S2
不仅要遵守Stackable
协议,而且S1
和S2
还要相等,S1
必须遵守Hashable
协议(可哈希)。
equal(s1, s2)
可以编译通过,因为泛型都是Int
类型。但是equal(s2, s3)
就会编译报错,因为s2
泛型类型是Int
类型,而s3
泛型类型是String
类型,不符合条件,所以编译报错。
扩展:
Int
和String
都是遵守Hashable
协议的。
注意点:
示例代码:
protocol Runnable { }
class Person: Runnable { }
class Car: Runnable { }
func get(_ type: Int) -> Runnable {
if type == 0 {
return Person()
}
return Car()
}
var r1 = get(0)
var r2 = get(1)
复制代码
当使用r1
的时候,编译器会认为r1
是Runnable
类型。因为返回的具体类型是程序运行中才知道的。
如果协议中有associatedtype
:
protocol Runnable {
associatedtype Speed
var speed: Speed { get }
}
class Person: Runnable {
var speed: Double { 0.0 }
}
class Car: Runnable {
var speed: Int { 0 }
}
func get(_ type: Int) -> Runnable {
if type == 0 {
return Person()
}
return Car()
}
复制代码
编译就会报错:
协议Runnable
不能用作泛型约束,因为他有Self
或关联类型约束
报错的原因就是程序在编译期间并不知道协议中的关联类型Speed
具体是什么类型。
解决方案有两种:
方案一:使用泛型
protocol Runnable {
associatedtype Speed
var speed: Speed { get }
}
class Person: Runnable {
var speed: Double { 0.0 }
}
class Car: Runnable {
var speed: Int { 0 }
}
func get<T: Runnable>(_ type: Int) -> T {
if type == 0 {
return Person() as! T
}
return Car() as! T
}
var r1: Person = get(0)
var r2: Car = get(1)
复制代码
返回值是泛型,定义变量的时候把变量类型确定好,所以函数返回值就随之确定。
方案二:不透明类型
使用some
关键字声明一个不透明类型。
即使在函数返回值前面限定了不透明类型还是报错,那是因为不透明类型限制函数只能返回一种类型。
思考一:为什么只需要限定不透明类型就可以了?
因为不透明类型限制函数只能返回一种类型,所以函数内部已经知道返回的类型是什么。
思考二:既然只能返回一种类型,为什么不直接返回具体类型?
可以避免外界知道返回值的具体类型(即屏蔽真实类型)。
应用场景: 当需要返回一个遵守某个协议的对象时,如果不希望外界知道返回的具体对象类型,仅对外公开协议中定义的接口时,可以使用不透明类型。
some
除了用在返回值类型上,一般还可以用在属性类型上。
protocol Runnable {
associatedtype Speed
}
class Dog: Runnable {
typealias Speed = Double
}
class Person {
var pet: some Runnable {
return Dog()
}
}
复制代码
上面代码中属性pet
就对外隐藏了返回值真实类型(如果没有关联类型,就可以不加some
,也能够达到隐藏真实类型的目的)。
六、可选项的本质
可选项的本质是枚举类型。
枚举定义:
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
public init(_ some: Wrapped)
}
复制代码
示例代码:
var age: Int? = 10
age = 20
age = nil
复制代码
上面示例代码的完整代码形式:
var age: Optional<Int> = Optional(10)
// var age: Optional<Int> = .some(10)
age = .some(20)
age = .none
复制代码
?
其实就是可选项的语法糖。
可选项在switch中的注意点:
var age: Int? = 10
age = 20
age = nil
switch age {
case let v?:
print("1", v)
case nil:
print("2")
}
复制代码
正常情况下,case let v
,v
一定是一个可选类型,和if
不一样(if
会自动解包)。
如果把v
后面加上?
,最终得到的v
是Int
类型:
等价代码:
if let v = age {
print("1", v)
} else {
print("2")
}
复制代码
多重可选性的示例:
// 示例一
var age_: Int? = 10
var age: Int?? = age_
age = nil
// 示例一本质
// 写法一:
var age0 = Optional.some(Optional.some(10))
age0 = .none
// 写法二:
var age1: Optional<Optional> = .some(.some(10))
age1 = .none
// 示例二
//var age: Int?? = 10
//// 示例二本质
//var age0: Optional<Optional> = 10
复制代码
更多系列文章,请关注微信公众号【1024星球】。