命名空间

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 具体指定 FMeat 之前,Animal 协议中并不关心 F 的具体类型, 只需要满足协议的类型中的 Feat 参数一致即可。如此一来,我们就可以避免在 Tigereat 中进行判定和转换了。不过这里忽视了被吃的必须是 Food 这个前提。 associatedtype 声明中可以使用冒号来指定类型满足某个协议,另外,在 Tiger 中只要实现了正确类型的eatF 的类型就可以被推断出来,所以我们也不需要显式地写明 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中这样的方法依旧有效,只不过在写法上可能有些不同。两个对应的运行时的 getset 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一节的内容。

results matching ""

    No results matching ""