命名空间
Swift 的命名空间是基于 module
而不是在代码中显式地指明,每个 module
代表了 Swift 中的一个命名空间。也就是说,同一个 target
里的类型名称还是不能相同的。
在使用时,如果出现可能冲突的时候,我们需要在类型名称前面加上 module
的名字 (也就是 target
的名字):MyFramework.MyClass.hello()
另一种策略是使用类型嵌套的方法来指定访问的范围。常见做法是将名字重复的类型定义到不同 的 struct
中,以此避免冲突。这样在不使用多个 module
的情况下也能取得隔离同样名字的类型的效果:
struct MyClassContainer1 {
class MyClass {
class func hello() {
print("hello from MyClassContainer1")
}
}
}
struct MyClassContainer2 {
class MyClass {
class func hello() {
print("hello from MyClassContainer2")
}
}
}
MyClassContainer1.MyClass.hello()
MyClassContainer2.MyClass.hello()
typealias
typealias
是用来为已经存在的类型重新定义名字的,通过命名,可以使代码变得更加清晰。使用的语法也很简单,使用 typealias 关键字像使用普通的赋值语句一样,可以将某个已经存在的类型赋值为新的名字。
typealias Location = CGPoint
typealias Distance = Double
typealias
是单一的,也就是说你必须指定将某个特定的类型通过 typealias
赋值为新名字,而不能将整个泛型类型进行重命名。
// 这是错误的代码
class Person<T> {}
typealias Worker = Person
typealias Worker = Person<T>
不过如果我们在别名中也引入泛型,是可以进行对应的:
// 这是 OK 的
typealias Worker<T> = Person<T>
当泛型类型的确定性得到保证后,显然别名也是可以使用的:
class Person<T> {}
typealias WorkId = String
typealias Worker = Person< WorkId >
另一个使用场景是某个类型同时实现多个协议的组合时。我们可以使用 &
符号连接几个协议,然后给它们一个新的更符合上下文的名字,来增强代码可读性:
protocol Cat { ... }
protocol Dog { ... }
typealias Pat = Cat & Dog
associatetype
在协议中除了定义属性和方法外,我们还能定义类型的占位符,让实现协议的类型来指定具体的类型。使用 associatetype
我们就可以在 Animal
协议中添加一个限定,让 tiger
来指定食物的具体类型:
protocol Food {}
struct Meat : Food {}
struct Grass : Food {}
protocol Animal {
associatedtype F //定义类型占位符
func eat(_ food: F)
}
struct Tiger: Animal {
typealias F = Meat //实现 Animal 协议, 指定具体的类型
func eat(_ food: Meat) {
print("eat meat");
}
}
let meat = Meat()
Tiger().eat(meat)
在 Tiger
通过 typealias
具体指定 F
为 Meat
之前,Animal
协议中并不关心 F
的具体类型, 只需要满足协议的类型中的 F
和 eat
参数一致即可。如此一来,我们就可以避免在 Tiger
的 eat
中进行判定和转换了。不过这里忽视了被吃的必须是 Food
这个前提。 associatedtype
声明中可以使用冒号来指定类型满足某个协议,另外,在 Tiger
中只要实现了正确类型的eat
,F
的类型就可以被推断出来,所以我们也不需要显式地写明 F
。最后这段代码看起来是这样的:
protocol Animal {
associatedtype F: Food
func eat(_ food: F)
}
struct Tiger: Animal {
func eat(_ food: Meat) {
print("eat meat");
}
}
struct Sheep: Animal {
func eat(_ food: Grass) {
print("eat \(food)")
}
}
不过在添加 associatedtype
后,Animal
协议就不能被当作独立的类型使用了。试想我们有一个函数来判断某个动物是否危险:
func isDangerous(animal: Animal) -> Bool {
if animal is Tiger {
return true
}
else {
return false
}
}
编译器会报错:
Protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements
这是因为 Swift 需要在编译时确定所有类型,这里因为 Animal
包含了一个不确定的类型,所以随着 Animal
本身类型的变化,其中的 F
将无法确定 (试想一下如果在这个函数内部调用 eat
的情形,你将无法指定 eat
参数的类型)。
注意:在一个协议加入了像是 associatedtype
或者 Self
的约束后,它将只能被用为泛型约束,而不能作为独立类型的占位使用,也失去了动态派发的特性。也就是说,这种情况下,我们需要将函数改写为泛型:
func isDangerous<T: Animal>(animal: T) -> Bool {
if animal is Tiger {
return true
}
else {
return false
}
}
print(isDangerous(animal: Tiger())) // true
print(isDangerous(animal: Sheep())) // false
Associated Object
不知道是从什么时候开始,“是否能通过Category给已有的类添加成员变量”就成为了一道Objective-C面试中的常见题目。不幸的消息是这个面试题目在Swift中可能依旧会存在。
得益于Objective-C的运行时和Key-Value Coding的特性,我们可以在运行时向一个对象添加值存储。而在使用Category扩展现有的类的功能的时候,直接添加实例变量这种行为是不被允许的,这时候一般就使用 property
配合 Associated Object
的方式,将一个对象“关联”到已有的要扩展的对象上。进行关联后,在对这个目标对象访问的时候,从外界看来,就似乎是直接在通过属性访问对象的实例变量一样,可以非常方便。
在Swift中这样的方法依旧有效,只不过在写法上可能有些不同。两个对应的运行时的 get
和 set Associated Object
的API是这样的:
func objc_setAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!, _ value: Any!, _ policy: objc_AssociationPolicy)
func objc_getAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!) -> Any!
//UnsafeRawPointer 在 Swift 3.0 以前是 UnsafePointer<Void> 也就是 void *
这两个API所接受的参数也都Swift化了,并且因为Swift的安全性,在类型检查上严格了不少, 因此我们有必要也进行一些调整。在Swift中向某个 extension
里使用Associated Object的方式将对象进行关联的写法是:
// MyClass.swift
class MyClass {
}
// MyClassExtension.swift
private var key: Void?
extension MyClass {
var title: String? {
get {
return objc_getAssociatedObject(self, &key) as? String
}
set {
objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
// 测试
func printTitle(_ input: MyClass) {
if let title = input.title {
print("title: \(title)")
} else {
print("没有设置title")
}
}
let a = MyClass()
printTitle(a) //没有设置title
a.title = "Swifter.tips"
printTitle(a) //title: Swifter.tips
key
的类型在这里声明为了 Void?
,并且通过操作符取地址并作为 UnsafeRawPointer
类型被传入。这在Swift与C协作和指针操作时是一种很常见的用法。关于C的指针操作和这些 Unsafe
开头的类型的用法,可以参看UnsafePointer一节的内容。