命名空间
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一节的内容。