泛型这个词是从generics翻译过来的,本意有一般的的意思。但是不知道被哪位惊才绝艳的前辈翻译成了泛型,初看之下让人摸不着头脑,但其实大有深意。在之前的学习中,我们已经了解了classstruct,如果不考虑struct的内存特殊性的话,其实它们都可以归于一种广义意义上的类型。也就是说,在Swift的设计哲学中,类型是无处不在的,一块具体的数据它要么是class要么是struct,总之得有具体类型

而把具体类型看作一枚银币的其中一面,那么另外一面就应该是“非具体类型”,或者说是泛化类型,简称范型

不过你也不要误会,所谓泛化类型,并不是要抹除具体类型存在的意义。而是说,对于某些数据的类型,可以从编写代码阶段的明确延迟到运行阶段再明确。

这么说可能有些抽象,我们来看下苹果官方文档给出的例子:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

这个函数的功能是交换两个Int值,同理如果想交换两个String,我们还得再写个函数:

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

以此类推,如果要交换两个Double类型还得继续写。并且你会发现,这些函数内部的逻辑就是一模一样的,唯一的区别就是接受参数的类型不同。

那么,如果我们不把参数的类型硬编码,而是也通过参数传递给函数,这样不就可以抽象出来更高级的函数代码了么?

上面这个问题的解决方案就是——泛型,我们先来看通过泛型改进过后的代码:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

其实函数内部的逻辑代码我们一行没改,变化还是发生在函数的声明部分。这里参数ab的类型,从之前我们熟悉的类型改成了T,需要注意的是T并不是一个类型,而是一种占位符,本质上来说它也是个参数。就像a是个占位符,也是个参数一样。

但是T参数又很特殊,它用来约定了参数ab的类型,可以说成是参数的参数。那么既然是参数,就得需要调用者进行参数指定吧,我们是否需要明确传递参数T呢?比如这样:

var x = 1
var y = 2

swapTwoValues<Int>(&x, &y)

遗憾的是我们会收到一行报错:

error: cannot explicitly specialize a generic function

想想也是,既然我们传递ab的值确定了,那这俩参数的Int类型也确定了,所以完全没有必要多此一举再去给T传递参数,直接简简单单这样就可以了:

swapTwoValues(&x, &y)

不过,如果当泛型作用于struct或者class时,情况就不一样了。

在计算机领域中,有一种被称作栈(Stack)的数据结构,想象一下,你桌子上有一摞书,也就是一本压着一本摆放的。显而易见,最下面那本书是你最早放置的,而最上面一本书是你最后放置的。如果你想把第一本书,也就是最下面那本书找出来,你不得不从上开始把书一本一本的拿走(假设你每次只能操纵一本书的话)。这里描述的场景就是,用代码模拟的话,可以这样写:

struct IntStack {
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

我们构造了一个IntStack用来存储Int类型的栈,它提供两个方法:push负责把数放进栈,pop负责把栈顶(也就是最上面/最后一个)取出来。

跟之前举的例子类似,如果你需要另外一个StringStack来存储String类型的栈的时候,你不得不复制大部分的代码,像下面这样:

struct StringStack {
    var items: [String] = []
    mutating func push(_ item: String) {
        items.append(item)
    }
    mutating func pop() -> String {
        return items.removeLast()
    }
}

仔细观察IntStackStringStack代码的些许不同,不难发现问题还是出在类型上,如果Stack关注内容的类型是非硬编码的,而是通过某种手段动态指定的,那么一切的问题将迎刃而解。展现泛型威力的时候又到了:

struct Stack<T> {
    var items: [T] = []
    mutating func push(_ item: T) {
        items.append(item)
    }
    mutating func pop() -> T {
        return items.removeLast()
    }
}

这样我们就拥有了一个不拘泥于一个具体类型的Stack,如果你需要它存储String,只需要像下面这样:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

这里你应该发现了,参数T需要我们在实例化Stack的时候指定,并且一旦指定之后,Stack内部就已经明确了T的类型。如果我们尝试push一个Int类型的数据的话,就会收到报错:

error: cannot convert value of type 'Int' to expected argument type 'String'


讲到这,泛型的概念你应该理解的差不多了,不论是从函数调用还是从类/结构体的使用角度来看,都可以分为调用者被调用者,而泛型的价值就是在于让调用者有更大的施展空间,也可以理解为被调用者让出了一部分权利供调用者自己发挥。

但这种权利的让渡也是有限度的,有时候被调用者虽然不再关心具体的类型,但是却希望这些类型能够局限在一个范围内,而不是完全放任自流。请看下面的例子:

func anyCommonElements<T>(_ lhs: T, _ rhs: T) -> Bool
where T:Sequence, T.Element: Equatable
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}

anyCommonElements([1, 2, 3], [3])

这应该算是一个比较复杂的例子了,anyCommonElements可以实现对两个序列是否相交的比较。核心逻辑就是对两个序列进行嵌套循环遍历,一旦发现元素相同,立刻return true进行返回。这个逻辑倒是不难理解,但是其针对泛型的应用,还是值得分析一下。

首先,我们是要运算的参数类型只能是支持遍历的序列,而不能是其他。所以我们要限制T泛型可用类型的范围,必须是遵循Sequence协议的。同时,既然是要比较两个序列,那么序列中的元素必须是可比较的,也就是T.Element: Equatable要求的T中元素必须满足协议Equatable

你可能会好奇,我是怎么知道T里面有个Element的。其实查看下Sequence的文档就会发现,Sequence的声明也用到了泛型,就是下面这句:

protocol Sequence<Element>

所以Element的出处不是别的神秘物体,正是我们今天学的泛型的一种官方应用。

此时回头看上面的where T:Sequence, T.Element: Equatable语句应该清晰了,通过where我们可以对函数声明的泛型追加一些限制条件。对于T:Sequence这样直接作用于T的条件,除了写在where处,也是可以在声明T的地方设置的。所以上面方法的声明等价于下面这种写法:

func anyCommonElements<T:Sequence>(_ lhs: T, _ rhs: T) -> Bool
where T.Element: Equatable

至此,anyCommonElements可以很好的比较同一种类型的两个序列是否有交集了,像下面这样的调用都会返回true

anyCommonElements([1,2,3], [2])
anyCommonElements(1...3, 2...2)

但是有个问题,如果我们把上面的参数交叉一下,比如像下面这样:

anyCommonElements(1...3, [2])

就会得到一个报错:

error: cannot convert value of type '[Int]' to expected argument type 'ClosedRange'

可以看出,此时函数接收到的两个参数不是同一个类型,因而报错。一个好的改进思路是,我们不再要求两个参数同属一个类型,而是允许它们各自不同,但是只需要满足Sequence即可。同时,即使两个序列的类型不同,也要保证它们内部存储的元素类型必须是相同的,不然无法直接比较。改进后的代码如下:

func anyCommonElements<T:Sequence, U:Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Element: Equatable, T.Element == U.Element
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
   return false
}
anyCommonElements(1...3, [3])

至此关于泛型的基本用法就讲完了。作为本系列教程的第十讲,这里我有一个好消息和一个坏消息要告诉大家:

  • 好消息是有了这十讲的技术铺垫,从语言语法层面上来说你已经具备动手进行App开发的基础了。
  • 坏消息是短短十期所能阐述的知识也只能算是九牛一毛,还有很多概念我们尚未提到,留待你继续探索喽。

如果你想了解更多关于App开发的相关知识,可以关注我整理这篇帖子:从0打造一款App并上架苹果App Store我的经历与总结

好了,关于Swift语法方面的介绍到这里就结束了,也是时候给大家说声青山不改,绿水长流,有缘江湖再见了。期待下次的重逢,也期待能在App Store里看到你提交的应用😃。


参考资料:

往期回顾: