0%

在Swift里的自定义模式匹配(译)

原文地址
模式匹配在 swift 里是随处可见的,虽然switch case是匹配模式最常见的用法,但是 Swift 有多种类型的模式,这些模式可以混合甚至在switches外部使用,从而写出真正酷而短的代码。我特别感兴趣的事模式匹配可以用作于各种各样的事情。

乍一看下面的代码,很容易把模式匹配看做是简单的等值检查

1
switch 80 {
2
case 100:
3
    //
4
case 80:
5
    //匹配, 因为 80 == 80
6
default:
7
    break
8
}

鉴于下面的代码这种情况,你可能会认为像 case "eighty" 这样的将不会被编译,毕竟”eighty”和 80 不是相同的类型,如果你现在尝试一下的确会编译不通过

1
if case "eighty" = 80 {
2
    //error: expression pattern of type 'String' cannot match values of type 'Int'
3
}

但事实并非如此,在项目开发中,你可能已经注意到某些类型之间存在着特殊的交互,例如Ranges它们及其相关类型

1
switch 80 {
2
case 0...20:
3
	break
4
case 21...50:
5
    break
6
case 51...100:
7
    //匹配, 因为 80 在 51...100 之间
8
default:
9
    break
10
}

究其原因是~=这个匹配模式运算符,这个操作符看起来在常规项目里没什么用(你之前可能已经看到过这个确切的数字范围内的例子),但是他在 swift 内部使用很多,而且他是用于确认 case 语句
很多情况下,这个运算符是一个简单的等值检查的包装器(例如 Int),但是 Range 有一个特殊的实现,当针对其自己的关联类型使用时,允许他在模式匹配时具有自定义的行为

1
extension RangeExpression {
2
    @inlinable
3
    public static func ~= (pattern: Self, value: Bound) -> Bool {
4
      return pattern.contains(value)
5
    }
6
}

并且由于~=的作用域是全局的,为了编写自己的模式匹配的逻辑,你可以重载它。

例如,要让”eighty”匹配 80,你需要做的是,重载一个~=,使它将 String 模式和 Int 值匹配

1
func ~= (pattern: String, value: Int) -> Bool {
2
    if pattern == "eighty" {
3
        return value == 80
4
    } else if pattern == "not eighty" {
5
        return value != 80
6
    } else {
7
        return false
8
    }
9
}
10
11
switch 80 {
12
case "eighty":
13
    //编译通过并且匹配
14
case "not eighty":
15
    //
16
default:
17
   break
18
}

现在假设我的 APP 收到了字符串形式的深层链接,我需要知道这个深层链接属于哪一个控制器

1
enum AppTab: String {
2
    case home
3
    case orderHistory
4
    case profile
5
}
6
7
let deepLink = DeepLink(path: "home", parameters: [:])

这里有很多种方法来做这到这个功能,包括向 DeepLink 添加属性,如 correspondingTab 或者对 DeepLink 进行子类化,也可以使用自定义模式匹配,只需要一行代码而且无需修改 DeepLink。

1
func ~= (pattern: AppTab, value: DeepLink) -> Bool {
2
    return value.path.hasPrefix(pattern.rawValue)
3
}
4
5
switch deepLink {
6
case .home:
7
    homeViewController.handle(deepLink: deepLink)
8
case .orderHistory:
9
    historyViewController.handle(deepLink: deepLink)
10
case .profile:
11
    profileViewController.handle(deepLink: deepLink)
12
default:
13
    break
14
}

这允许你可以绕过将更广泛的类型映射到更具体的类型,例如如何将 Int 映射到工作日枚举。在这种情况下,您的后端将返回一周作为一个 Int 或一个 String,并使用自定义模式匹配,您可以使用这种更广泛的类型,同时仍然将其视为映射到更特定的枚举类型。当您想尝试使用而不想引用某些方法或类型时,这可能是有用的:

1
enum WeekDay: Int {
2
    case sunday
3
    case monday
4
    case tuesday
5
    case wednesday
6
    case thursday
7
    case friday
8
    case saturday
9
}
10
11
func ~= (pattern: WeekDay, value: Int) -> Bool {
12
    return pattern.rawValue == value
13
}
14
15
// Server returns:
16
// { nextHoliday: { weekDay: 5 } }
17
18
if case .friday? = nextHoliday?.weekDay {
19
    print("Woohoo!")
20
}

创建自定义模式是编写更干净代码的一种简单方法,不需要太多的努力,因为您可以通过 cases 直接跳到这一点而无需为类型添加其他属性 - 同时确保您的代码不会变得难以理解。

我很想知道你是否做过类似重载过~=这样的事情,如果你有任何建议,意见和反馈,请随时与我联系,@rockthebruno

参考和推荐阅读

Apple Docs:模式