语法糖和其他大多数修饰性代码更改是否真的增加了价值,或者它们是否会引起混乱和复杂性,这是开发人员之间争论的一个常见来源。虽然以更简洁的方式编写代码可以提高生产率,但有时也会使结果更难阅读和维护。

我们理想的想法是能够在低冗长和清晰之间取得一个良好的平衡, 并充分利用Swift的类型系统提供的各种功能来实现这一点。 其中一个特性就是类型aliases,本周,让我们看看几种不同的方法,它们可以用来以一种非常轻量级的方式创建新的、有重点的类型。

Semantic types

一般来说,将语义嵌入到我们的类型中可以使代码更直观和更容易处理。 在处理原始类型(如数字和字符串)时尤其如此,因为它们几乎可以用于任何事情。 当在函数签名中看到Int、Double或String类型时-我们通常必须依赖于函数的名称和它的参数,以便理解该值将用于什么。

以Foundation的TimeInterval类型为例。 当我们看到时间间隔时,我们知道我们处理的是时间(更具体地说,是秒),如果使用的是像Double这样的“原始”数字类型,这就不正确了。然而,事实证明TimeInterval实际上不是一个全新的类型,实际上只是Double的类型别名:

typealias TimeInterval = Double

使用这样的类型别名有利有弊。 因为TimeInterval实际上不是它自己的类型,而只是一个别名——这意味着所有的Double值都是有效的TimeInterval值,反之亦然。缺点是我们没有额外的编译时安全性如果我们创建一个全新的类型,但另一方面,这样做的好处是任何Double方法(包括操作符)也可以用于TimeInterval——减少了代码重复和样板的需要。

那么,如果不是针对我们自己的类型,我们怎么能做同样的事情呢? 就像我们在“编写自文档的Swift代码”中看到的那样,类型别名可以是使我们的代码变得更加自文档化的好方法。例如,受TimeInterval启发,我们可以定义一个Kilograms类型别名 - 这样我们就可以很容易地表达出某一重量值使用的是什么单位——像这样:

typealias Kilograms = Double

struct Package {
    var weight: Kilograms
}

做上面的事情可能看起来无关紧要,但是可以使我们的属性和它们的值特别清楚-无需依赖文档,或通过为我们的属性使用非常详细的名称, 像weightInKilograms。

Specializing generics

类型别名还提供了一种简单的方法来专门化泛型,特别是那些在我们的代码库中使用相同泛型类型的泛型。假设我们已经创建了一个用于本地或远程文件系统的文件存储类型-如iCloud Drive或Dropbox:

class FileStorage<Key: Hashable, Location: FileStorageLocation> {
    ...    
}

使用这种类型非常方便,因为它允许我们在一个地方包含处理任何类型的文件系统所需的所有代码,同时仍然允许在调用站点专门化。例如,一个NoteSyncController可能使用上述类的两个实例-一个用来记录存储在用户本地设备上的所有笔记,一个用于上传文件到云,如下所示:

class NoteSyncController {
    init(localStorage: FileStorage<Note.StorageKey, LocalFileStorageLocation>,
         cloudStorage: FileStorage<Note.StorageKey, CloudStorageLocation>) {
        ...
    }
}

然而,每次使用这些长文件存储专门化时都必须输入它们,这很快就会变得非常繁琐,并使代码更难阅读-特别是当许多这样的专门化被用在相同的地方,像上面那样。

这是类型别名变得非常有用的另一种情况,因为它们本质上让我们只进行一次专门化——并为每个用例创建专用的轻量级类型。在这个场景中,我们可以扩展我们的Note模型,以包含两个这样的类型别名,一个用于LocalStorage,一个用于CloudStorage:

extension Note {
    typealias LocalStorage = FileStorage<StorageKey, LocalFileStorageLocation>
    typealias CloudStorage = FileStorage<StorageKey, CloudStorageLocation>
}

有了上面的内容,我们现在可以大量清理之前的notesyncontroller初始化器,让它更容易阅读:

class NoteSyncController {
    init(localStorage: Note.LocalStorage,
         cloudStorage: Note.CloudStorage) {
        ...
    }
}

进行上述更改还在某种程度上隐藏了实现细节。尽管这些细节在需要时仍然可以访问,但我们不再需要将函数签名或属性类型与存储类型使用的键类型等细节混杂在一起。 更干净了

Type-driven logic

就像我们在“Swift中的专门化协议”中看到的,使用关联类型允许我们创建通用协议,然后可以专门化—通过使用带有约束的协议扩展,或通过使各种具体类型符合它们。

例如,为了在整个代码库中以统一的方式使用索引,我们可以创建一个通用的Index类型——然后可以通过Indexed协议专门化它。因为Index只能用于符合indexed的类型 ,我们可以使用它的RawIndex类型来确定索引的基础值是由什么组成的:

protocol Indexed {
    associatedtype RawIndex
    var index: Index<Self> { get }
}

struct Index<Object: Indexed> {
    typealias RawValue = Object.RawIndex
    let rawValue: RawValue
}

通过上述设置,我们现在可以使用类型别名来声明我们希望如何为每种类型建立索引。 例如,用户可能根据其标识符建立索引,而一张专辑(如果我们正在创建一个音乐应用程序)可以根据它所属的音乐类型进行索引:

extension User: Indexed {
    typealias RawIndex = Identifier<User>
}

extension Album: Indexed {
    typealias RawIndex = Genre
}

上面最酷的事情是,我们现在可以创建完全类型安全的索引, 使用上述类型别名提供的信息来确保使用正确的原始值:

let albumIndex = Index<Album>(rawValue: .rock)

如果上面的例子看起来很熟悉,那是因为它与“Swift中的类型安全标识符”中实现更健壮标识符的技术非常相似。

Generic closure aliases

最后,让我们看看如何使用类型别名为闭包创建通用缩写。例如,如果我们的代码库大量使用结果类型来建模异步操作的各种结果-我们可能想要为接受这样一个结果的闭包定义一个简写,这是我们在很多完成处理程序中最有可能用到的:

typealias Handler<T> = (Result<T>) -> Void

现在,每当我们的某个函数接受了一个完成处理程序时,我们可以简单地使用上面的处理程序类型,并将其专门化为我们将传递给该处理程序的任何结果——像这样:

func searchForNotes(matching query: String,
                    then handler: @escaping Handler<[Note]>) {
    ...
}

同样,上面的更改可能看起来纯粹是修饰性的,但它可能对代码的读和写的流畅性产生很大的影响- 特别是随着我们的代码库的增长,我们最终会得到大量使用完成处理程序的函数声明。

Conclusion

类型别名是一种快速的功能,乍一看可能很简单,但一旦我们深入研究,就会发现它们在很多情况下都很有用。 虽然过度使用它们会使我们的代码库更难导航(以防我们经常需要查找每个别名背后的具体类型),但有选择地使用它们可以导致更优雅和简化的代码。

原文链接