首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》4.9 泛型

关灯直达底部

泛型是一种类型占位符,实际的类型会在稍后进行填充。由于Swift有严格的类型,所以泛型是非常有用的一个特性。在不牺牲严格类型的情况下,有时你不能或是不想在代码中的某处精确指定类型。

重要的是要理解泛型并没有放松Swift严格的类型。特别地,泛型并未将类型解析推迟到运行期。在使用泛型时,代码依然要指定真实的类型;这个真实的类型在编译期就会完全指定好!代码中如果需要某个类型,那么可以使用泛型,这样就不必完全指定好类型了,不过当其他代码使用这部分代码时就需要指定好类型。占位符就是泛型,不过在使用泛型时,它会被解析为实际的特定类型。

Optional就是个很好的示例。任何类型的值都可以包装到Optional中,不过你永远不必担心某个Optional中包装的是什么类型,这是怎么做到的呢?因为Optional是个泛型类型,这正是Optional的工作原理。

我之前已经说过Optional是个枚举,它有两个Case:.None与.Some。如果Optional的Case是.Some,那么它就会有一个关联值,即被该Optional所包装的值。不过这个关联值的类型是什么呢?一方面,我们会说它可以是任何类型;毕竟,任何东西都可以被包装到Optional中。另一方面,包装某个值的任何Optional都会包装某个特定类型的值。在展开Optional时,被展开的值需要转换为它原本的类型,这样才能向其发送恰当的消息。

该问题的解决方案就是Swift泛型。Swift头文件中Optional枚举声明的开头如下代码所示:


enum Optional< Wrapped> {    // ...}  

上述语法表示:“在声明中,我使用了一个假的类型(类型占位符),叫作Wrapped。”它是个真实且单一的类型,不过现在不想过多地表示它的信息。你需要知道的是,当我说Wrapped时,我指的是一个特定的类型。在创建实际的Optional时,类型Wrapped的含义就一目了然了,接下来我再说Wrapped时,你应该将其替换为它所表示的类型。

下面再来看看Optional声明:


enum Optional<Wrapped> {    case None    case Some(Wrapped)    init(_ some: Wrapped)    // ...}>  

我们已经将Wrapped声明成了一个占位符,接下来就可以使用它了。有一个Case为.None,还有一个Case为.Some,它有一个关联值,类型为Wrapped。我们还有一个初始化器,它接收一个类型为Wrapped的参数。因此,初始化时所使用的类型就是Wrapped,它也是关联到.Some Case的值的类型。

正是由于初始化器参数的类型与.Some关联值的类型之间的这种同一性才使得后者能够被解析出来。在Optional枚举的声明中,Wrapped是个占位符。不过在实际情况下,当创建实际的Optional时,它会被某个确定的类型值所初始化。很多时候,我们会使用问号语法糖(String?类型),初始化器则会在背后得到调用。出于清晰的目的,下面来显式调用初始化器:


let s = Optional("howdy")  

上述代码会针对这个特定的Optional实例对Wrapped类型进行解析。显然,"howdy"是个String,因此编译器知道,对于这个特定的Optional<Wrapped>来说,Wrapped是个String。在底层Optional枚举声明中凡是出现Wrapped的地方,编译器都会将其替换为String。因此,从编译器的角度来看,变量s所引用的这个特定Optional的声明如下所示:


enum Optional <String>{    case None    case Some(String)    init(_ some: String)    // ...}  

这是Optional声明的伪代码,其中Wrapped占位符已经被String类型所替换。我们可以说s是个Optional<String>。事实上,这是合法的语法!我们可以像下面这样创建相同的Optional:


let s : Optional<String> = "howdy"  

大量内建的Swift类型都涉及泛型。事实上,该语言特性在设计时就充分考虑了Swift类型;正是由于泛型的存在,Swift类型才能实现自己的目的。

4.9.1 泛型声明

下面列出了在什么地方可以声明Swift泛型:

使用Self的泛型协议

在协议中,关键字Self(注意首字母大写)会将协议转换为泛型。Self是个占位符,表示使用者的类型。比如,下面这个Flier协议声明了一个接收Self参数的方法:


protocol Flier {    func flockTogetherWith(f:Self)}  

这表示,如果Bird对象类型使用了Flier协议,那么flockTogetherWith的实现就需要将其f参数声明为Bird。

使用空类型别名的泛型协议

协议可以声明类型别名,不必定义类型别名表示什么。也就是说,typealias语句并不会包含等号。这会将协议转换为泛型;别名的名字(也叫作关联类型)是个占位符。比如:


protocol Flier {    typealias Other    func flockTogetherWith(f:Other)    func mateWith(f:Other)}  

使用者会在泛型使用类型别名的地方声明特定的类型,从而解析出占位符。如果Bird结构体使用了Flier协议,并将flockTogetherWith的f参数声明为Bird,那么该声明就会针对这个特定的使用者将Other解析为Bird,现在Bird也需要将mateWith的f参数声明为Bird类型:


struct Bird : Flier {    func flockTogetherWith(f:Bird) {}    func mateWith(f:Bird) {}}   

这种形式的泛型协议从根本上来说与前一种形式一样;如果写成f:Other,那么Swift就会知道它表示f:Self.Other,实际上这么写是合法的(也更加清晰)。

泛型函数

函数声明可以对其参数、返回类型以及在函数体中使用泛型占位符。请在函数名后的尖括号中声明占位符的名字:


func takeAndReturnSameThing<T> (t:T) -> T {    return t}  

调用者会在函数声明中占位符出现的地方使用特定的类型,从而解析出占位符:


let thing = takeAndReturnSameThing("howdy")  

调用中所用的实参"howdy"的类型会将T解析为String;因此,对takeAndReturn-SameThing的调用也会返回一个String,捕获结果的变量thing也会被推断为String。

泛型对象类型

对象类型声明可以在花括号中使用泛型占位符类型。请在对象类型名后面的尖括号中声明占位符名字:


struct HolderOfTwoSameThings<T> {    var firstThing : T    var secondThing : T    init(thingOne:T, thingTwo:T) {        self.firstThing = thingOne        self.secondThing = thingTwo    }}  

该对象类型的使用者会在对象类型声明中占位符出现的地方使用特定的类型,从而解析出占位符:


let holder = HolderOfTwoSameThings(thingOne:"howdy", thingTwo:"getLost")  

初始化器调用中所使用的thingOne实参"howdy"的类型会将T解析为String;因此,thingTwo也一定是个String,属性firstThing与secondThing都是String。

对于使用了尖括号语法的泛型函数与对象类型,尖括号中可以包含多个占位符名,中间通过逗号分隔,比如:


func flockTwoTogether<T, U>(f1:T, _ f2:U) {}  

现在,flockTwoTogether的两个参数可以被解析为两个不同的类型(不过也可以相同)。

4.9.2 类型约束

到目前为止,所有示例都可以使用任何类型替代占位符。此外,你可以限制用于解析特定占位符的类型,这叫作类型限制。最简单的类型限制形式是其首次出现时,在占位符名后面加上一个冒号和一个类型名。冒号后面的类型名可以是类名或是协议名。

回到Flier及其flockTogetherWith函数。假设flockTogetherWith的参数类型需要被使用者声明为使用了Flier的类型。你不能在协议中将参数类型声明为Flier:


protocol Flier {    func flockTogetherWith(f:Flier)}  

上述代码表示:只有声明的函数flockTogetherWith的f参数是Flier类型,你才能使用该协议:


struct Bird : Flier {    func flockTogetherWith(f:Flier) {}}  

这并不是我们想要的!我们需要的是,Bird应该可以使用Flier协议,同时将f声明为某个Flier使用者类型,如Bird。方式就是将占位符限制为Flier。比如,我们可以这样做:


protocol Flier {    typealias Other : Flier    func flockTogetherWith(f:Other)}  

遗憾的是,这么做是不合法的:协议不能将自身作为类型约束。解决办法就是再声明一个协议,然后让Flier使用这个协议,并且将Other约束到这个协议上:


protocol Superflier {}protocol Flier : Superflier {    typealias Other : Superflier    func flockTogetherWith(f:Other)}  

现在,Bird就是个合法的使用者了:


struct Bird : Flier {    func flockTogetherWith(f:Bird) {}}  

在泛型函数或泛型对象类型中,类型限制位于尖括号中。比如:


func flockTwoTogether<T:Flier>(f1:T, _ f2:T) {}  

现在不能使用两个String参数调用flockTwoTogether了,因为String并不是Flier。此外,如果Bird与Insect都使用了Flier,那么flockTwoTogether可以通过两个Bird参数或两个Insect参数调用,但不能一个是Bird,另一个是Insect,因为T仅仅是一个占位符而已,表示Flier使用者类型。

对于占位符的类型限制通常用于告诉编译器,某个消息可以发送给占位符类型的实例。比如,假设我们要实现一个函数myMin,它会从相同类型的一个列表中返回最小值。下面是一个看起来还不错的泛型函数实现,不过有个问题,即它无法编译通过:


func myMin<T>(things:T ...) -> T {    var minimum = things[0]    for ix in 1..<things.count {        if things[ix] < minimum { // compile error            minimum = things[ix]        }    }    return minimum}  

问题在于比较things[ix]<minimum。编译器怎么知道类型T(things[ix]与minimum的类型)所解析出的类型能够使用小于运算符进行比较呢?它不知道,这也是上述代码无法编译通过的原因所在。解决方案就是向编译器承诺,T解析出的类型能够使用小于运算符。方式就是将T限制为Swift内建的Comparable协议;使用Comparable协议可以确保使用者能够使用小于运算符:


func myMin<T:Comparable>(things:T ...) -> T {  

现在的myMin可以编译通过,因为只有将T解析为使用了Comparable的对象类型它才能被调用,因此它也可以使用小于运算符进行比较。自然地,你觉得可以进行比较的内建对象类型(如Int、Double、String及Character等)实际上都使用了Comparable协议!查阅Swift头文件,你会发现内建的min全局函数就是按照这种方式声明的,原因与此相同。

泛型协议(声明中使用了Self或拥有关联类型的协议)只能用在泛型类型中,并且作为类型限制。如下代码无法编译通过:


protocol Flier {    typealias Other    func fly}func flockTwoTogether(f1:Flier, _ f2:Flier) { // compile error    f1.fly    f2.fly}  

要想将泛型Flier协议作为类型,你需要编写一个泛型并将Flier作为类型限制,如下代码所示:


protocol Flier {    typealias Other    func fly}func flockTwoTogether<T1:Flier, T2:Flier>(f1:T1, f2:T2) {    f1.fly    f2.fly}  

4.9.3 显式特化

到目前为止,所有示例中泛型的使用者都是通过推断来解析占位符类型的。不过,还有一种解析方式:使用者可以手工解析类型,这叫作显式特化。在某些情况下,显式特化是强制的,即如果占位符类型无法通过推断得出,那就需要使用显式特化。有两种形式的显式特化:

拥有关联类型的泛型协议

协议使用者可以通过typealias声明手工解析出协议的关联类型,方式是使用协议别名与显式类型赋值。比如:


protocol Flier {    typealias Other}struct Bird : Flier {    typealias Other = String}  

泛型对象类型

泛型对象类型的使用者可以通过相同的尖括号语法手工解析出对象的占位符类型,尖括号用于声明泛型,里面的是实际的类型名。比如:


class Dog<T> {    var name : T?}let d = Dog<String>  

(这解释了本章之前与第3章介绍的Optional<String>类型。)

不能显式特化泛型函数。不过,你可以使用非泛型函数(使用了泛型类型的占位符)来声明泛型类型;泛型类型的显式特化会解析出占位符,因此也能解析出函数:


protocol Flier {    init}struct Bird : Flier {    init {}}struct FlierMaker<T:Flier> {    static func makeFlier -> T {        return T    }}let f = FlierMaker<Bird>.makeFlier // returns a Bird  

如果类是泛型的,那么你可以对其子类化,前提是可以解析出泛型(这是Swift 2.0的新特性)。可以通过匹配的泛型子类或显式解析出父类泛型来做到这一点。比如,下面是个泛型Dog:


class Dog<T> {   var name : T?}  

你可以将其子类化为泛型,其占位符与父类占位符相匹配:


class NoisyDog<T> : Dog<T> {}  

这么做是合法的,因为对NoisyDog占位符T的解析也会解析Dog占位符T。另一种方式是子类化一个明确指定的Dog:


class NoisyDog : Dog<String> {}  

4.9.4 关联类型链

如果具有关联类型的泛型协议使用了泛型占位符,那么我们可以通过对占位符名使用点符号将关联类型名链接起来,从而指定其类型。

来看下面这个示例。假设有一个游戏程序,士兵与弓箭手彼此为敌。我通过将Soldier结构体与Archer结构体纳入拥有Enemy关联类型的Fighter协议中来表示这一点,Enemy本身又被限制为是一个Fighter(这里还是需要另外一个协议,Fighter会使用该协议):


protocol Superfighter {}protocol Fighter : Superfighter {    typealias Enemy : Superfighter}  

下面手工为这两个结构体解析这个关联类型:


struct Soldier : Fighter {    typealias Enemy = Archer}struct Archer : Fighter {    typealias Enemy = Soldier}  

现在来创建一个泛型结构体,表示这些战士对面的营地:


struct Camp<T:Fighter> {}  

假设一个营地可以容纳来自对方阵营的一个间谍。那么间谍的类型应该是什么呢?如果是Soldier营地,那么它就是Archer;如果是Archer营地,那么它就是Soldier。更为一般地,由于T是个Fighter,那么它应该是Fighter的Enemy类型。我可以通过将关联类型名链接到占位符名来清楚地表达这一点:


struct Camp<T:Fighter> {    var spy : T.Enemy?}  

结果就是,针对某个特定的Camp,如果T被解析为Soldier,那么T.Enemy就表示Fighter,反之亦然。我们为Capm的spy类型创建了正确的规则。如下代码无法编译通过:


var c = Camp<Soldier>c.spy = Soldier // compile error  

我们尝试将错误类型的对象赋给这个Camp的spy属性。但如下代码可以编译通过:


var c = Camp<Soldier>c.spy = Archer  

使用更长的关联类型名链也是可以的,特别是当泛型协议有一个关联类型,这个关联类型本身又被强制约束为一个拥有关联类型的泛型协议时更是如此。

比如,下面为每一类Fighter赋予一个有特色的武器:士兵有剑,弓箭手有弓。创建一个Sword结构体和一个Bow结构体,并将它们置于Wieldable协议之下:


protocol Wieldable {}struct Sword : Wieldable {}struct Bow : Wieldable {}  

向Fighter添加一个Weapon关联类型,Weapon被强制约束为Wieldable,这次还是手工解析每一种Fighter类型的Weapon:


protocol Superfighter {    typealias Weapon : Wieldable}protocol Fighter : Superfighter {    typealias Enemy : Superfighter}struct Soldier : Fighter {    typealias Weapon = Sword    typealias Enemy = Archer}struct Archer : Fighter {    typealias Weapon = Bow    typealias Enemy = Soldier}  

假设每个Fighter都可以窃取敌人的武器,我为Fighter泛型协议添加一个steal(weapon:from:)方法。Fighter泛型协议该如何表示参数类型才能让其使用者通过恰当的类型来声明这个方法呢?

from:参数类型是该Fighter的Enemy。我们已经知道该如何表示它了:它是由占位符、点符号以及关联类型名构成的。这里的占位符就是该协议的使用者,即Self。因此,from:参数类型就是Self.Enemy。那么weapon:参数类型又是什么呢?它是Enemy的Weapon!因此,weapon:参数类型就是Self.Enemy.Weapon:


protocol Fighter : Superfighter {    typealias Enemy : Superfighter    func steal(weapon:Self.Enemy.Weapon, from:Self.Enemy)}  

(上述代码可以编译通过,省略Self表达的也是相同的含义。不过,Self依然是整个链条的隐式起始点,我觉得加上Self会让代码的含义变得更加清晰。)

如下Soldier与Archer的声明正确地使用了Fighter协议,代码会编译通过:


struct Soldier : Fighter {    typealias Weapon = Sword    typealias Enemy = Archer    func steal(weapon:Bow, from:Archer) {    }}struct Archer : Fighter {    typealias Weapon = Bow    typealias Enemy = Soldier    func steal (weapon:Sword, from:Soldier) {    }}  

这个示例是假想出来的(但我希望能说明问题),不过其表示的概念却不是。Swift头文件大量使用了关联类型链,关联类型链Generator.Element使用得非常多,因为它表示了序列元素的类型。SequenceType泛型协议有一个关联类型Generator,它被约束为泛型GeneratorType协议的使用者,反过来它会有一个关联类型Element。

4.9.5 附加约束

简单的类型约束会对类型进行限制,使其能够将占位符解析为单个类型。有时,你需要对可解析的类型做进一步的限制:这就需要附加约束了。

在泛型协议中,类型别名约束中的冒号与类型声明中的冒号是一个意思。这样,其后面可以跟着多个协议,或是后跟一个父类再加上多个协议:


class Dog {}class FlyingDog : Dog, Flier {}protocol Flier {}protocol Walker {}protocol Generic {    typealias T : Flier, Walker    typealias U : Dog, Flier}  

在Generic协议中,关联类型T只能被解析为使用了Flier协议与Walker协议的类型,关联类型U只能被解析为Dog(或Dog子类)并使用了Flier协议的类型。

在泛型函数或对象类型的尖括号中,这种语法是非法的;相反,你可以附加一个where字句,其中包含一个或多个逗号分隔的对所声明的占位符的附加约束:


func flyAndWalk<T where T:Flier, T:Walker> (f:T) {}func flyAndWalk2<T where T:Flier, T:Dog> (f:T) {}  

Where子句还可以对已经包含了占位符的泛型协议的关联类型进行附加限制,方式是使用关联类型链(参见4.9.4节的介绍)。如下伪代码表明了我的意图:我省略了where子句的内容,将注意力放在where子句所限制的内容上:


protocol Flier {    typealias Other}func flockTogether<T:Flier where T.Other /*???*/ > (f:T) {}  

如你所见,占位符T已经被限制为了一个Flier。Flier本身是个泛型协议,并且有一个关联类型Other。这样,无论什么类型解析T,它都会解析Other。Where子句进一步限制了到底什么类型可以解析T,这是通过限制可解析Other的类型来做到的。

我们可以对关联类型链施加什么限制呢?一种可能是与上述示例相同的限制,一个冒号,后跟它需要使用的协议;或是通过它必须继承的类来做到这一点。如下示例使用了协议:


protocol Flier {    typealias Other}struct Bird : Flier {    typealias Other = String}struct Insect : Flier {    typealias Other = Bird}func flockTogether<T:Flier where T.Other:Equatable> (f:T) {}  

Bird与Insect都使用了Flier,不过这并不是说它们都可以作为flockTogether函数调用的参数。flockTogether函数可以通过Bird实参调用,因为Bird的Other关联类型会被解析为String,而String使用了内建的Equatable协议。不过,flockTogether却不能通过Insect实参调用,因为Insect的Other关联类型会被解析为Bird,而Bird并没有使用Equatable协议:


flockTogether(Bird) // okayflockTogether(Insect) // compile error  

如下示例使用了类:


protocol Flier {    typealias Other}class Dog {}class NoisyDog : Dog {}struct Pig : Flier {    typealias Other = NoisyDog // or Dog}func flockTogether<T:Flier where T.Other:Dog> (f:T) {}  

flockTogether函数可以通过Pig实参调用,因为Pig使用了Flier,并且会将Other解析为Dog或Dog的子类:


flockTogether(Pig) // okay  

除了冒号,我们还可以使用等号==并且后跟一个类型。关联类型链最后的类型必须是这个精确的类型,而不能仅仅是协议使用者或子类。比如:


protocol Flier {    typealias Other}protocol Walker {}struct Kiwi : Walker {}struct Bird : Flier {    typealias Other = Kiwi}struct Insect : Flier {    typealias Other = Walker}func flockTogether<T:Flier where T.Other == Walker> (f:T) {}  

flockTogether函数可以通过Insect实参调用,因为Insect使用了Flier并且会将Other解析为Walker。不过,它不能通过Bird实参调用。Bird使用了Flier,并且会将Other解析为Walker的使用者,即Kiwi;不过,这并不满足==限制。

在上一个示例中使用==Dog也会得到同样的结果。如果Pig将Other解析为NoisyDog,那么Pig实参就不再是可接受的了;Pig必须要将Other解析为Dog本身,这样才能成为可接受的实参。

==运算符右侧的类型本身可以是个关联类型链。两个链中末尾被解析出的类型必须要相同。比如:


protocol Flier {    typealias Other}struct Bird : Flier {    typealias Other = String}struct Insect : Flier {    typealias Other = Int}func flockTwoTogether<T:Flier, U:Flier where T.Other == U.Other>(f1:T, _ f2:U) {}  

flockTwoTogether函数可以通过Bird与Bird调用,也可以通过Insect与Insect调用;不过,不能一个是Insect,另一个是Bird,因为它们不会将Other关联类型解析为相同的类型。

Swift头文件大量使用了带有==运算符的where子句,特别是用它来限制序列类型。比如,String的appendContentsOf方法声明了两次,如下代码所示:


mutating func appendContentsOf(other: String)mutating func appendContentsOf<S : SequenceTypewhere S.Generator.Element == Character>(newElements: S)  

第3章介绍过appendContentsOf可以将一个String连接到另一个String上。不过appendContentsOf并不仅仅可以将String连接到String上!字符序列也可以:


var s = "hello"s.appendContentsOf(" world".characters) // "hello world"  

Character数组也可以:


s.appendContentsOf(["!" as Character])  

它们都是字符序列,第2个appendContentsOf方法声明中的泛型指定了这一点。它是个序列,因为其类型使用了SequenceType协议。不过,并不是任何序列都可以;其Generator.Element关联类型链必须要解析为Character。如前所述,Generator.Element链是Swift用于表示序列元素类型概念的一种方式。

Array结构体也有一个appendContentsOf方法,不过其声明有些不同:


mutating func appendContentsOf<S : SequenceType    where S.Generator.Element == Element>(newElements: S)  

序列只能是一种类型。如果序列包含了String元素,那么你可以向其添加更多的元素,但只能是String元素;你不能向String元素序列添加Int元素序列。数组是序列;它是个泛型,其Element占位符是其元素的类型。因此,Array结构体在其appendContentsOf方法声明中通过==运算符来强制使用这个规则:实参序列的元素类型必须要与现有数组的元素类型相同。