解剖泛型
泛型提供了,使用一组类型定义一组新类型的机制。
在示例中,可以为Keeper定义泛型类型:
class Keeper<Animal> {}
这个定义立即定义了所有相应的Keeper类型:
你可以通过创建这些类型的值来验证这些类型是否真实,并在初始化器中指定整个类型:
var aCatKeeper = Keeper<Cat>()
首先,Keeper是泛型类型的名称。
但是你可能会说,泛型类型实际上根本不是类型。它更像是制作真正类型或具体类型的方法。这其中的一个标志是,如果你尝试在孤立状态下实例化它,你会得到错误:
var aKeeper = Keeper() // compile-time error!
编译器在这里报错,因为它不知道你想要什么样的Keeper。尖括号中的动物是类型参数,它指定你所保存的动物的类型。
一旦你提供了所需的类型参数(如Keeper<Cat>),泛型Keeper就会变成一个新的具体类型。Keeper<Cat>与Keeper<Dog>不同,尽管它们是相同的泛型。这些生成的具体类型称为泛型类型的特殊化。
为了总结机制,为了定义一个泛型类型,如Keeper<Animal>,你只需选择泛型类型的名称和类型参数的名称。类型参数应该澄清类型参数和泛型类型之间的关系。有时你会遇到像T(类型的缩写)这样的名称,但是当类型参数具有明确的角色(如Animal)时,应该避免使用这些名称。
请注意,Keeper类型目前不存储任何东西,甚至不使用任何类型的Animal。从本质上讲,泛型是一种系统定义类型集的方法。
使用类型参数
但是,通常你需要对类型参数进行处理。
假设你想要更好地表达个体。首先,类型定义要包含有标识性的属性(如名称)。这让每个值都代表单个动物或饲养员的身份:
class Cat {
var name: String
init(name: String) {
self.name = name
}
}
class Dog {
var name: String
init(name: String) {
self.name = name
}
}
class Keeper<Animal> {
var name: String
init(name: String) {
self.name = name
}
}
你还需要跟踪哪个饲养员照料哪些动物。假设每个饲养员早上负责一只,下午负责另一只。你可以为泛型添加上午的动物和下午的动物属性来表达这一点。但是还需要什么属性呢?
很明显,如果一个饲养员只管理狗,那么它的属性只能是狗。如果是猫,那就是猫。一般来说,如果它是动物的饲养者,那么早上和下午的动物属性应该是Animal这样的类型。
要表达这一点,你只需使用类型参数,也就是之前用于区分你是哪种饲养员的属性:
class Keeper<Animal> {
var name: String
var morningCare: Animal
var afternoonCare: Animal
init(name: String, morningCare: Animal, afternoonCare: Animal) {
self.name = name
self.morningCare = morningCare
self.afternoonCare = afternoonCare
}
}
通过在上面的泛型定义中使用Animal,你可以表达出早晨和下午的动物一定是管理员最了解的动物。
正如函数参数成为函数体中要使用的常量一样,你可以在整个类型定义中使用类型参数,比如Animal。在Keeper<Animal>的定义中,你可以在任何地方使用类型参数,用于存储属性以及计算属性、方法签名或嵌套类型。
现在,当你实例化一个Keeper时,Swift将确保编译时,上午和下午的类型是相同的:
let jason = Keeper(name: "Jason", morningCare: Cat(name: "Whiskers"),afternoonCare: Cat(name: "Sleepy"))
在这里,饲养员Jason在早上管理着Whiskers,而在下午管理Sleepy。jason的类型是Keeper <Cat>。注意,你不必为类型参数指定值。因为你使用了Cat实例作为morningCare和afternoonCare的值,swift知道jason的类型应该是Keeper <Cat>。
类型约束
在你对Keeper的定义中,作为标识符的Animal作为类型参数。
这就像func feed(cat: cat) {/ * open can等简单函数中的参数cat。* / }。但在定义函数时,不能简单地将任何参数传递给函数。只能传递类型为Cat的值。
目前,你可以使用任何类型表示Animal,甚至一些荒谬的类型,如字符串或Int。
这不是好的。你想要的是类似于函数的东西,你可以限制允许填充类型参数的类型。在Swift中,你可以使用类型约束。
有两种约束。最简单的类型约束是这样的:
class Keeper<Animal: Pet> {
/* definition body as before */
}
标识符Pet必须是命名类型或协议。在类型参数括号“Animal: Pet”中的新表达式指定了这样的要求:无论提供给Animal的类型是什么,都必须满足作为Pet的子类或实现Pet协议的要求。
例如,你可以使用上面修改过的Keeper定义来执行这些限制,还可以重新定义Cat和其他动物,以便使用扩展实现Pet或追溯模型遵从协议。
protocol Pet {
var name: String { get } // all pets respond to a name
}
extension Cat: Pet {}
extension Dog: Pet {}
因为Cat和Dog已经实现了一个name存储属性。接下来,创建
一些猫和狗的数组:
let cats = ["Miss Gray", "Whiskers"].map { Cat(name: $0) }
let dogs = ["Sparky", "Rusty", "Astro"].map { Dog(name: $0) }
let pets: [Pet] = [Cat(name: "Mittens"), Dog(name: "Yeller")]
注意,cats、dogs和pets都有不同的类型。如果你点击每一个,你可以看到类型是[Cat], [Dog], [Pet]
cats和dogs的数组是同质的,因为元素具有相同的类型。pets是异质的,因为它是dog和car类型的混合体。
通过限制类型,你可以对其进行操作。例如,既然你现在知道Pet有name,你就可以写一套适合Pet的通用功能。
写四种不同参数的herd函数:
// 1
func herd(_ pets: [Pet]) {
pets.forEach {
print("Come \($0.name)!")
}
}
// 2
func herd<Animal: Pet>(_ pets: [Animal]) {
pets.forEach {
print("Here \($0.name)!")
}
}
// 3
func herd<Animal: Dog>(_ dogs: [Animal]) {
dogs.forEach {
print("Here \($0.name)! Come here!")
}
}
1 此方法可以处理将Dog和Cat元素混合在一起的Pet类型数组。
2 此方法处理任何类型的Pet数组,但它们都需要是单一类型。函数名后面的尖括号允许你指定要在函数中使用的泛型类型。
3 只处理Dog或Dog的子类。
函数调用会找到最适合的函数。所以:
herd(pets) // Calls 1
herd(cats) // Calls 2
herd(dogs) // Calls3
第二种类型约束涉及明确表示类型参数或其关联类型必须等于另一个参数或其一致类型之一。通过组合许多类型参数和相关类型的需求集,可以在泛型类型之上定义复杂的关系。
为了演示这一点,假设你想为cat数组添加一个扩展,并添加meow()。元素是数组的关联类型。你可以要求该元素为类型(或子类型)Cat:
extension Array where Element: Cat {
func meow() {
forEach { print("\($0.name) says meow!") }
}
}
调用meow方法
cats.meow()
但是dog元素的数组就不能使用
dogs.meow() // error: 'Dog' is not a subtype of 'Cat'
这些错误在这里看起来很明显,但是类型约束可以防止你在编译时犯一些小的错误。