作者:Olivier Halligon,原文链接,原文日期:2015-08-23
译者:靛青K;校对:numbbbbb;定稿:shanks

今天的目标是用代码处理链接 http://mywebsite.org/customers/:cid/orders/:oid,从中提取出订单号(#oid)和顾客 ID(#cid) 。

我们将要使用一种简单快捷的方式来实现 – 模式匹配和变量绑定。

思考过程

首先想到的解决方法是把 URL 以”/“分割成若干路径部分,然后使用 switch 语句进行模式匹配 – 匹配每一个路径部分存入一个元组中,这样我们就可以使用变量绑定获取 URL 中的变量部分。

代码的大致架构如下:

// path 是 [String] 类型,包含 URL 中分割的各个部分
// 比如 ["customer","5","order","12"]
switch (path[0], path[1], path[2], path[3]) {
  case ("customer", let cid, "order", let oid):
    print("Customer #\(cid), Order #\(oid)")
  default:
    print("Invalid request")
}

问题

最初我们有一个 NSURL,要把它转换成 Array (稍后介绍实现方式)。但是不能同时使用模式匹配和变量绑定来提取顾客和订单 ID(switch path { case ["customer", let cid]: … }这种写法不能执行)。因此我们需要使用元组(case ("customer", let cid))。

然而,我们不能把大小不定的可变数组转换成一个元组,因为元组的类型是由内部元素的数量和类型决定的。当然我们可以创建不同的 switch 语句,匹配不同长度的元组…但是这样会使代码看起来非常混乱:

func parse(path: [String]) -> String? {
    switch path.count {
    case 1:
        switch path[0] {
        case "products":  return "List of products"
        case "customers": return "List of customers"
        default: return nil
        }
    case 2:
        switch (path[0], path[1]) {
        case ("products", let pid):  return "Product #\(pid)"
        case ("customers", let cid): return "Customer #\(cid)"
        default: return nil
        }
    case 3:
        switch (path[0], path[1], path[2]) {
        case ("customers", let cid, "orders"):
            return "List of orders for customer #\(cid)"
        default: return nil
        }
        // ...
    default: return nil
    }
}

这样的写法十分恶心并且十分冗长。没错,我们不想选择这种方法。那么到底该怎么提取呢?

使用固定长度的元组

我们是否可以使用固定长度的元组,然后用nil来填充空位?当然,用这种方法来存储应用的数据一点都不优雅,但只在 switch 中使用是可行的,这样就可以使用固定长度的元组方便地处理各种情况。

但如何构造这样一个元组呢?当然,你可以使用 switch

switch path.count {
    case 0: return (nil, nil, nil)
    case 1: return (path[0], nil, nil)
    case 2: return (path[0], path[1], nil)
    default: return (path[0], path[1], path[2])
}

还可以全部写在一行,不过这样很难阅读:

return (path.count <= 0 ? nil : path[0], path.count <= 1 ? nil : path[1], path.count <= 2 ? nil : path[2], …)

如果使用这种方式,当有7到8个分割路径数时,代码就会变得很长…而且这还只是构造元组,我们还没有进行任何模式匹配!

这种方式很不优雅,而且根本无法满足我们的需求。

使用生成器(Generator)

下面介绍另一种技巧:使用生成器(Generator)。

如果你不知道 Swift 标准库中的 Generator 是什么,没关系,非常简单。Generator 基本和 C++ 中的迭代方法一样。它是一个对象,next() 方法会返回被迭代序列中的下一个值,当迭代到结尾时返回 nil

那么如何用它来建立我们的元组?很简单!每个 SequenceType(特别是array)都有一个生成器,我们只需要对每个值调用 next() 方法就可以建立元组。如果这个数组比较短,它将用 nil 填充最后几个值:

let path : [String] = …
// 获取遍历数组用的生成器
var g = path.generate() // 注意:由于我们要使用 g.next(),必须声明为变量。(译者注:因为每次调用 g 都会绑定到下一个值)
let tuple = (g.next(), g.next(), g.next(), g.next())

妥妥的!如果path只有两个值,比如["a","b"],那 tuple(元组)就将是("a","b",nil,nil)switch 再也不需要依赖 path.count

完整的解决方案!

现在,无论路径中有多少个组件,我们都可以使用一个非常简单的 switch 来进行解析 URL。

我们使用一个枚举来表示所有可能出现的请求,并用关联值来保存变量参数:

enum Request {
    case ProductsList                         // "/products"
    case Product(productID: Int)              // "/products/:pid"
    case CustomersList                        // "/customers"
    case Customer(customerID: Int)            // "/customers/:cid"
    case OrdersList(customerID: Int)          // "/customers/:cid/orders"
    case Order(customerID: Int, orderID: Int) // "/customers/:cid/orders/:oid"
}

使用之前的技巧,我们就可以用一个代表路径的 [String] 和一个单独的switch语句来创建一个Request实例。当然,初始化是最佳方案,但它有可能失败,因为分割的路径可能无法和期望的路径相匹配,比如 ID 的值无法转换成 Int 值(我们可以用 guard 语句来捕捉这些潜在的转换错误,正常情况下不会出现这种错误)。

这样我们就得到了初始化代码[1] [2] [3]

extension Request {
    init?(path: [String]) {
        var g = path.generate() // use a generator to build our tuple
        switch (g.next(), g.next(), g.next(), g.next(), g.next()) {
        case ("products"?, nil, _, _, _):
            self = .ProductsList
        case ("products"?, let spid?, nil, _, _):
            guard let pid = Int(spid) else { return nil }
            self = .Product(productID: pid)
        case ("customers"?, nil, _, _, _):
            self = .CustomersList
        case ("customers"?, let scid?, nil, _, _):
            guard let cid = Int(scid) else { return nil }
            self = .Customer(customerID: cid)
        case ("customers"?, let scid?, "orders"?, nil, _):
            guard let cid = Int(scid) else { return nil }
            self = .OrdersList(customerID: cid)
        case ("customers"?, let scid?, "orders"?, let soid?, nil):
            guard let cid = Int(scid), oid = Int(soid) else { return nil }
            self = .Order(customerID: cid, orderID: oid)
        default: return nil
        }
    }
}

收尾

如果我们想完整的完成这个练习,最后需要做的一件事就是从 URL
中解析出组件数组并传入 Request(path:…) 初始化方法。

我们用 NSURLComponentsURL 分成 hostpath 等等。然后用 NSString.pathComponents 将路径分割成目录数组。另外:

  • 我们想去掉开头的 / 。它肯定会出现在绝对路径中,我们不想在 switch 中的每个 case 里都对它进行匹配。
  • 如果结尾有 /,我们也要把它去掉。在本例中,我们希望类似/customers/5/customers/5/的 URL 都解析成.Customer(customerID: 5)
import Foundation

func parse(url: NSURL) -> Request? {
    if let comps = NSURLComponents(URL: url, resolvingAgainstBaseURL: false),
        let path = comps.path where comps.host == "mywebsite.org"
    {
        let pathComps = (path as NSString).pathComponents
        if pathComps.first == "/" {
            var canonicalComps = pathComps.dropFirst()
            if canonicalComps.last == "/" {
                // 如果有url以 "/" 结尾,那么就需要丢掉它
                canonicalComps = canonicalComps.dropLast()
            }
            return Request(path: Array(canonicalComps))
        }
    }
    return nil
}

if let url = NSURL(string: "http://mywebsite.org/customers/12/orders"),
    let req = parse(url) {
    print(req) // 输出: OrdersList(12)
}

完成!

  1. 你可以看到代码中我用了问号符号,比如模式匹配的 cases 中的"product"?。那是因为我们的元组中包含可选的String?元素,并且模式匹配将会匹配元组相同类型,所以我们的case中的参数必须是可选的。我也会使用let spid?来保证spid变量不会绑定nil。提醒一点,x?是 Swift 2.0 的语法糖,相当于 .Some(x)
  2. 我的元组(有五个值)的长度比我要处理的路径最大长度大 1 。这样通过确保let soid?的非空值接下来都是nil,从而保证了 /customers/:cid/orders/:oid/foo/bar不会 return .Order(customerID: …, orderID: …),这就代表了路径的终点。
  3. 我使用 _ 填充第一个 nil 后面的那些组件,因为我一点也不关心他们的值:考虑到我建立的元组的方法,我知道他们只能是 nil ,所以为什么还要管他们呢?当然在这里,你可以使用 nil 作为值代替这里的 _,但我觉得现在这样的代码更简单整洁。

    本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

文章目录
  1. 1. 思考过程
  2. 2. 问题
    1. 2.1. 使用固定长度的元组
  3. 3. 使用生成器(Generator)
  4. 4. 完整的解决方案!
  5. 5. 收尾