是什么使代码 “Swifty”? —— Safe

2020-03-20 16:16:40 浏览数 (1)

尽管编程语言是由其语法正式定义的,但实际上在实践中使用它们的方式还是可以由它们当前的约定来确定的。毕竟,就语法而言,大多数受“ C影响 ” 的语言看起来都非常相似,以至于您可以用几乎使它看起来像JavaScript,C#或C本身的方式编写Swift。

在Swift社区中,短语 "Swifty code" 通常用于描述遵循当前最流行的约定的代码。但是,尽管Swift的核心语法自最初引入以来并没有太大变化,但其约定随着时间的推移发生了巨大变化。

例如,许多Swift开发人员都记得从Swift 2到Swift 3的转换是语法方面的重大更改,但是这些更改中的大多数并不是真正的语法更改——它们是基于新集合对标准库API的更改命名约定。加上Swift 4对关键路径和Codable的介绍,Swift 5.1的函数生成器,属性包装器和不透明的返回类型,以及多年来引入的更多API和功能,并且开始变得很清楚,是什么使代码 “swifty” 是一个不断变化的目标。

本周,让我们仔细研究一下Swift的核心约定,以试图回答是什么真正使代码“ Swifty ” 的问题。

Swifty Code —— Safe

一致的目标(Aligned Goals)

在某种程度上,上述问题的简单答案可能是“与Swift的核心目标完全吻合的代码”。毕竟,尽管Swift的各种API,约定和语言功能会随着时间而变化,但它的基本目标基本保持不变——因此,如果我们能够以符合这些目标的方式编写自己的代码,那么我们将有更好的机会在任何给定的Swift上下文中使我们的代码看起来自然而清晰。

那么,这些目标到底是什么?Swift的官方网站上的About页面列出了三个关键字:

  • 安全(Safe):为了最大限度地减少开发人员的错误;
  • 迅速(Fast):执行的速度要快;
  • 表现力(Expressive):因为Swift的目标是尽可能清晰易懂。

是什么使代码 “Swifty”? —— Fast 介绍了如何利用系统的一些内置方法来提示性能

是什么使代码 “Swifty”? —— Expressive 介绍了如何使用表达性命名和API设计传达我们的代码意图

让我们来看看一些不同的事情,这些事情可能要牢记在心,以便使我们自己的代码遵循这些原则。

通过强大的类型安全保持清晰(Clarity through strong type safety)

让我们从第一个关键字开始——安全(Safe)。Swift非常重视类型安全性这一事实不容忽视——它具有静态类型检查,强大的泛型系统,以及编译时需要执行诸如类型擦除之类的操作才能使编译器能够验证我们的代码结构。

但是,遇到不是很明显可以改善我们代码的类型安全或使代码更加“Swifty”的情况是很常见的,例如,这里我们根据笔记所属的组的名称存储笔记的集合:

代码语言:javascript复制
struct NoteCollection {
    var notesByGroup: [String : [Note]]
    ...
}

乍一看,上面的代码似乎很完美。但是,在查看上面的声明时,一个细节一点都不明显,那就是我们如何处理未分组的值,以及如何处理包含用户最近打开的所有便笺的特殊组——当前是通过传递一个空字符串或使用“recents”字符串来完成的:

代码语言:javascript复制
let groupedNotes = collection.notesByGroup["MyGroup"]
let ungroupedNotes = collection.notesByGroup[""]
let recentNotes = collection.notesByGroup["recent"]

尽管上述设计可能有完全合理的理由(例如,我们使用的结构可能是通过网络加载笔记时如何组织笔记的结构),但这确实导致我们的某些调用变得非常隐秘——绕弯子会增加开发人员犯错的机会。很容易忘记,一个空字符串意味着应该检索所有未分组的笔记,如果用户将其自定义组之一命名为“recent”会怎样?

让我们看看是否可以使上面的代码更加安全,并使其更加“Swifty”。由于我们的notesByGroup字典具有三种不同的用例,因此,我们用一个自定义枚举替换其基于字符串的键,该枚举将这三种变体建模为不同的情形,如下所示:

代码语言:javascript复制
enum Group: Hashable {
    case none
    case recent
    case named(String)
}

struct NoteCollection {
    var notesByGroup: [Group : [Note]]
    ...
}

上面的内容看似微小的变化,但是它使我们的调用更加清晰,因为我们现在利用类型系统来区分三种独立的组类型——所有这些都不会使我们的API变得更加复杂:

代码语言:javascript复制
let groupedNotes = collection.notesByGroup[.named("MyGroup")]
let ungroupedNotes = collection.notesByGroup[.none]
let recentNotes = collection.notesByGroup[.recent]

这也许就是使代码在类型安全方面“Swifty”的本质。虽然有很多方法可以使API真正变得复杂以使其更加类型安全,但窍门是使用Swift的语言功能找到一种增加该类型安全性的方法,而又不会使我们的代码难以理解或使用。

虽然通常使用类型安全性来防止将类型B的值错误地传递给接受A的API,但是强类型化通常也提供了一种改善我们代码的语义和逻辑的方法。在下面的示例中,我们的代码在技术上是类型安全的——因为我们正在使用Swift的泛型功能来实现LoadingOperation,该LoadOperation可以加载符合Loadable协议的任何资源:

代码语言:javascript复制
class LoadingOperation<Resource: Loadable> {
    private let resource: Resource

    init(resource: Resource) {
        self.resource = resource

        if let preloadable = resource as? Preloadable {
            preloadable.preload()
        }
    }
    
    ...
}

但是,我们有条件地强制转换资源以查看其是否也符合Preloadable(如果是,则预加载该资源)这一事实可以说有点奇怪。上面的实现不仅使我们很难理解如何进行资源预加载(因为类型系统没有给我们任何暗示我们应该遵循Preloadable的提示,以使这种情况发生),而且这样做非常不直观预加载是初始化操作的副作用。

作为替代,让我们预加载一个明确的API,该API仅在操作的Resource符合Preloadable时才可用,如下所示:

代码语言:javascript复制
extension LoadingOperation where Resource: Preloadable {
    func preload() {
        resource.preload()
    }
}

上面的更改都使我们更加清楚了预加载资源的条件,并且现在我们可以从初始化程序中消除类型转换的副作用——大赢家!需要注意的重要一点是,从安全角度出发编写“ Swifty”代码绝对不是尽可能多地使用泛型。而是要有选择地使用类型系统的各个方面和功能,以使我们的代码更易于理解和使用(更难于滥用)。

文章来自 John Sundell的What makes code “Swifty”?中关于Safe的内容

是什么使代码 “Swifty”? —— Fast 介绍了如何利用系统的一些内置方法来提示性能

是什么使代码 “Swifty”? —— Expressive 介绍了如何使用表达性命名和API设计传达我们的代码意图

0 人点赞