转载

Swift4 中的泛型约束

范型可以说是 Swift 跟 OC 相比最大的优势了。通过给像集合这类东西关联泛型, 可以写出更可预测并且更安全的代码。在 Swift4 中类型约束更为强大, 它能够让我们更能够轻而易举的做很多事情。即使是通用代码, 也能充分的利用 Swift 的类型系统。

例1:

首先我们来看看一个简单的例子。比如说给一个数字数字求和。我们可能会些这样的代码:

// 在这段代码中, 我们定义了一个方法, 接受一个 Int 数组作为参数, 在方法内部使用高阶函数 reduce 最后返回这个结果。
func sum(_ numbers: [Int]) -> Int {
    return numbers.reduce(0, +)
}
let array = [1,2,3,4,5]
sum(array) // 15

使用泛型约束, 我们可以这样来实现这个需求:

// 在这段代码中, 我们给 Array 添加了类型约束的 Extension。当数组的 Element 遵守了 Numeric 协议的时候, Array 就拥有 sum 这个方法。
extension Array where Element: Numeric {
    func sum() -> Element {
        return reduce(0, +)
    }
}
array.sum() // 15

两者相比, 使用泛型约束最大的优势是使用扩展, 能够让这个功能跟调用者更紧密。比较一下:

let sum = sum(array)
let sum = array.sum()

在 OC 里面, 可能有些同学会写一个 XXXTool 之类的类, 来封装这种类型的功能。 或者是直接写成 C 的函数。但是不论怎么写, 这样貌似都不是特别的 OOP。或者OC 还可以直接给 NSArray 加一个 category, 然后再实现相似的功能。但是, 这样做不就等于所有的 array 都具有这个功能了吗?

例2

再来看这样的需求: 计算某个包含字符串集合中有多少个单词。我们可以通过给集合添加一个扩展轻松的完成这件事情。给 Collection 添加一个约束, 限制集合中的 Element 是 String类型:

extension Collection where Element == String {
    func countWords() -> Int {
        return reduce(0) {
            let components = $1.components(separatedBy: .whitespacesAndNewlines)
            return $0 + components.count
        }
    }
}
let array2 = ["sunny","cloudy","apple orange"]
array2.countWords() // 4

还有一个很酷的做法是约束集合类型中的 Element 是 Closure:

extension Sequence where Element ==  () -> Void {
    func callAll() {
        forEach { $0() }
    }
}
let closure1 = {
    print("1")
}
let closure2 = {
    print("2")
}
let array3 = [closure1, closure2]
array3.callAll() 
//1
//2

例3

还有一个很好用的特性是使用协议定义 API 的时候。这几乎是写可测试代码以及功能解耦的最佳实践了。需要注意的是, 在需要灵活使用嵌套类型的时候, 这可能会有点麻烦。

看例子吧!我们经常都想要定义一些通用的 API, 来管理程序中的各种 model。这时候肯定会想要定义一个协议:

protocol ModelManager {
    associatedtype Model
}

现在我们再来定一个查找符合某个条件的方法:传入某个查询条件, 然后返回符合这个条件的模型数组。

protocol ModelManager {
    associatedtype Model
    func models(matching query: String) -> [Model]
}

这个时候, 这个协议变成了这样。这个样子依然有问题,不够灵活, 而且还有一个很恐怖的问题: 硬编码。接下来我们试着使用范型约束来使用 Swift 的类型系统, 让这个功能更灵活, 并且使用类型系统来解决硬编码的问题。

接下来, 再给 ModelManager 关联两个类型, Query 和 Collection。Query 用来描述查询的条件。他可以是任何东西, 只要能够描述查询条件就可以。当然, 个人认为可能 enum 是最好的选择。Collection用来描述查询结果, 他用来限制返回的结果就是这个管理类的模型。现在这个协议就成这样了:

protocol ModelManager {
    associatedtype Model
    associatedtype Collection: Swift.Collection where Collection.Element == Model
    associatedtype Query
    func models(matching query: Query) -> Collection
}

有了上面的基础, 就可以很方便的实现一些具有查询功能的模型管理类了。比如说我们要用户管理类, 需要通过用户姓名和年龄段来查询符合要求的用户:

// 定义 User 模型
struct User {
    var name: String
    var age: Int
}
// 定义 User 管理类
class UserManager: ModelManager {
    typealias Model = User
    // 查询条件, 姓名或者年龄
    enum Query {
        case name(String)
        case ageRange(Range)
    }
    // 查询方法
    func models(matching query: Query) -> [User] {
        // 这里做了几个假的数据
        let user1 = User(
            name: "sunny",
            age: 25)
        let user2 = User(
            name: "lily",
            age: 18)
        let user3 = User(
            name: "michael",
            age: 30)
        
        let users = [user1, user2, user3]
        
        switch query {
        case .name(let name):
            return users.filter{ $0.name == name }
        case .ageRange(let range):
            return users.filter{ range ~= $0.age }
        }
        
    }
}
let manager = UserManager()
manager.models(matching: .name("sunny")) // [{name "sunny", age 25}]
manager.models(matching: .ageRange(10 ..< 20)) // [{name "lily", age 18}]

对有些模型来说, 使用字典来作为返回的 Collection可能是更好的方法。下面这个例子是用来通过影片名称和导演名字来筛选电影的例子。返回的结果通过电影分类来做分类。

// 定义电影分类的枚举, 因为要作为字典的 Key 所有需要 Hashable 协议。
// 使用String 类型的枚举只是为了 hashValue
enum Genre: String, Hashable {
    case cartoon = "cartoon"
    case action = "action"
    case comedy = "Comedy"
    
    var hashValue: Int {
        return self.rawValue.hashValue
    }
    static func ==(lhs: Genre, rhs: Genre) -> Bool {
        return lhs.rawValue == rhs.rawValue
    }
}
// 定义电影模型, 因为要作为字典的 Key 所有需要 Hashable 协议。
// 使用String 类型的枚举只是为了 hashValue
struct Movie: Hashable {
    var name: String
    var director: String
    var genre: Genre
    
    
    var hashValue: Int {
        return Int("/(name.hashValue)" + "/(director.hashValue)")!
    }
    static func ==(lhs: Movie, rhs: Movie) -> Bool {
        return lhs.name == rhs.name && lhs.director == rhs.director && lhs.genre == rhs.genre
    }
}
class MovieManager: ModelManager {
    typealias Model  = (key: Genre, value: Movie)
    
    enum Query {
        case name(String)
        case director(String)
    }
    
    func models(matching query: Query) -> [Genre : Movie] {
        // 方法跟上个例子差不多, 就不实现了
        return [:]
    }
}

使用泛型约束能够很容易的进行面向协议编程(POP)。

正文到此结束
Loading...