Swift3-函数式编程-全-

Swift3 函数式编程(全)

原文:zh.annas-archive.org/md5/dc1d84aebf095124cfefb6f71edf4ef9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

函数式编程(FP)因其简化了面向对象编程(OOP)中许多困难而受到广泛关注,例如可测试性、可维护性、可扩展性和并发性。Swift 拥有许多易于使用的函数式编程特性,但大多数 Objective-C 和 Swift 程序员对这些工具并不熟悉。

本书旨在简化函数式编程范式,使其对 Swift 程序员易于使用,展示如何使用流行的函数式编程技术来解决日常开发中的许多问题。无论你是函数式编程和 Swift 的新手还是有经验的开发者,本书都将为你提供设计和发展高质量、易于维护、可扩展和高效应用的技能,适用于 iOS、macOS、tvOS 和 watchOS。通过本书,你将学会使用函数式编程构建无 bug、易于维护的代码。

本书涵盖内容

第一章, Swift 中的函数式编程入门,介绍了函数式编程范式,如不可变性、无状态编程、纯函数、一等函数和高级函数。本章将介绍 Swift 编程语言及其在 Swift 中的函数式编程范式。

第二章, 函数和闭包,从函数的定义开始,继续讨论其他相关主题,如函数类型和元组,最后以更高级的主题结束,例如一等函数、高级函数、函数组合、闭包、柯里化和递归以及记忆化。

第三章, 类型与类型转换,概述了类型的一般概念,并详细探讨了引用类型与值类型的区别。我们将涵盖值类型和引用类型常量、值类型和引用类型的混合以及复制等内容。然后,我们将讨论值类型的特性。我们还将涵盖值类型和引用类型之间的关键区别,以及我们应该如何决定使用哪一种类型。接下来,我们将继续探讨等价性、身份、类型检查和转换等主题。

第四章, 枚举和模式匹配,解释了枚举的定义和用法。我们将涵盖关联值和原始值,并介绍代数数据类型的概念。我们将通过一些示例来涵盖求和、乘积和递归类型。此外,在本章中,我们将探讨通配符、值绑定、标识符、元组、枚举情况、可选类型、类型转换和表达式等模式,以及相关的模式匹配示例。

第五章, 泛型和关联类型协议,教我们如何定义和使用泛型。我们还将了解泛型解决的问题的类型。接下来,我们将通过示例探索类型约束、泛型数据结构和关联类型协议。

第六章,Map, Filter, 和 Reduce,通过适当的示例介绍了 Swift 编程语言中的 map、filter 和 reduce 方法。这些方法用于数组,可以替代几乎所有的 for-in 循环的使用,并且更加清晰和简洁。

第七章, 处理可选值,使我们熟悉处理可选值的不同技术。我们将讨论处理可选值的内置技术,如可选绑定、guard、合并和可选链。然后我们将探索处理可选值的函数式编程技术。

第八章, 函数式数据结构,向您介绍函数式数据结构的概念,并探讨了以函数式方式实现的数据结构示例,如 Semigroup、Monoid、BST、LinkedList、Stack 和 LazyList。

第九章, 不可变性的重要性,探讨了不可变性的概念。我们将通过示例查看其重要性和好处。然后我们将探讨可变性的案例,并通过一个示例来比较可变性和不可变性对我们代码的影响。

第十章, 两者之最佳 – 结合 FP 模式与 OOP,涵盖了面向对象编程的原则和模式。然后我们将介绍基于协议的编程。接下来,我们将介绍函数式响应式编程,并探讨如何混合 FP 与 OOP 模式。

第十一章, 案例研究 – 使用 FP 和 OOP 模式开发 iOS 应用,教我们如何使用我们迄今为止讨论的概念来开发 Todo 后端和 iOS 应用。我们将使用函数式编程技术来解析和映射数据,我们将使用函数式响应式编程来反应性地管理应用中的事件。我们还将使用基于协议的编程和面向对象的编程技术。

您需要为这本书准备的内容

要跟随本书中的示例,您需要一台装有 macOS 10.10 或更高版本的 Apple 计算机。您还需要安装 Xcode 版本 8 测试版 1 和 Swift 3.0 预览版 1。

这本书面向的对象

本书面向具有 Swift 编程基础知识的 iOS 和 macOS 开发者。假设读者具备面向对象编程的先验知识。

习惯用法

在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"VerboseClass.h 文件定义了一个作为 NSObject 类子类的接口。"

代码块设置如下:

let numbers = [9, 29, 19, 79]

// Imperative example
var tripledNumbers:[Int] = []
for number in numbers {
  tripledNumbers.append(number * 3)
}
print(tripledNumbers)

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"接下来,我们将在 Xcode 中创建一个单视图应用程序项目。"

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们读者反馈总是受欢迎的。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。要发送一般反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书的标题。如果您在某个主题上有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持选项卡上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从您购买此书的下拉菜单中选择。

  7. 点击代码下载

文件下载完成后,请确保您使用最新版本的以下软件解压缩或提取文件夹:

  • Windows 上的 WinRAR / 7-Zip

  • Mac 上的 Zipeg / iZip / UnRarX

  • Linux 上的 7-Zip / PeaZip

本书代码包也托管在 GitHub 上github.com/PacktPublishing/Swift-3-Functional-Programming。我们还有其他来自我们丰富图书和视频目录的代码包可供选择,请访问github.com/PacktPublishing/。查看它们!

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出中的变化。您可以从 www.packtpub.com/sites/default/files/downloads/Swift3FunctionalProgramming_ColorImages.pdf 下载此文件。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在错误清单部分显示。

盗版

网络上版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现了疑似盗版材料,请通过 copyright@packtpub.com 联系我们,并附上链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面所提供的帮助。

询问

如果您对本书的任何方面有问题,您可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。

第一章:Swift 中函数式编程入门

在本章中,我们将介绍函数式编程范式,例如不可变性、无状态编程、纯函数、一等函数和高阶函数。本章将介绍 Swift 编程语言和 Swift 中的函数式编程范式。以下主题将涵盖,并附带示例:

  • 为什么函数式编程很重要?

  • 什么是函数式编程?

  • Swift 语言基础

  • 不可变性

  • 一等、高阶和纯函数

  • 可选值和模式匹配

  • 闭包

  • 类型别名

为什么函数式编程很重要?

软件解决方案正变得越来越复杂,为了未来的维护和扩展,有必要对它们进行很好的结构化。软件工程师试图将软件模块化成更小的部分,并在不同的部分和层中抽象出复杂性。将代码分成更小的部分使得可以单独处理每个问题。这种方法提高了协作性,因为不同的工程师可以负责不同的部分。此外,他们可以在不关心其他部分的情况下专注于软件的特定部分。

将软件划分为更小的部分并不是大多数项目和编程语言中的最大挑战。例如,在面向对象编程OOP)中,软件被划分为更小的部分,如包、类、接口和方法。工程师倾向于通过领域、逻辑和层来划分软件为这些构建块。类是创建实例和对象的配方。正如其名所示,面向对象编程中最重要的构建块是对象。工程师处理对象,它们的作用和责任应该是清晰和可理解的。

在面向对象编程(OOP)中,将构建块连接起来并不像将它们分开那样容易。不同对象之间的连接可能会导致它们之间产生强烈的耦合。耦合是面向对象编程中复杂性的最大来源。模块或类中的任何变化都可能迫使所有耦合的模块和类发生变化。此外,由于耦合的模块或类,特定的模块或类可能更难重用和测试。

软件工程师试图通过合理地构建软件结构和应用不同的原则和设计模式来放松耦合。例如,当正确应用时,单一职责、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则SOLID)通常会使软件易于维护和扩展。

尽管有可能减少耦合并简化软件结构,但管理内存、引用实例和测试不同对象仍然很困难,因为在面向对象编程(OOP)中,对象是开放的,容易发生变化和突变。

在函数式编程中,纯函数是最重要的构建块。纯函数不依赖于自身之外的数据,并且它们不会改变自身之外存在的数据。纯函数易于测试,因为它们总是会提供相同的结果。

纯函数可以在不同的线程或核心上执行,而不需要任何处理多线程和多进程的机制。这是函数式编程相对于 OOP 的一个重要优势,因为在 OOP 中处理多核编程机制非常复杂。此外,为多核计算机编程正变得越来越重要,因为硬件工程师最终达到了光速的限制。计算机时钟在不久的将来不会更快,因此,为了每秒有更多的周期,硬件工程师正在将更多的处理器添加到芯片上。我们似乎没有尽头,不知道我们的计算机中会有多少处理器。一个程序要使用的处理器数量越多,处理多线程和多核机制的复杂性就越大。

函数式编程消除了对复杂的多核编程机制的需求,并且由于纯函数不依赖于任何实例或自身之外的数据,因此很容易在不改变其他部分的情况下更改它们。

什么是函数式编程?

我们知道函数式编程很重要,但它到底是什么?与函数式编程相关的炒作很多,关于它的定义也有很多,但简单来说,它是一种将计算建模为表达式的评估的编程风格。函数式编程是一种声明式编程风格,与被归类为命令式编程的面向对象编程(OOP)相对。

从理论上讲,函数式编程采用了范畴论的概念,范畴论是数学的一个分支。了解范畴论对于能够以函数式编程并不必要,但学习它将帮助我们掌握一些更高级的概念,例如函子应用函子单子。我们将在后面的章节中深入探讨范畴论及其与函数式编程的关系,所以现在我们不会谈论数学,而是从实用主义的角度来探讨函数式编程。

让我们从例子开始,了解函数式编程和 OOP 风格之间的差异。以下示例给出了两种不同的数组元素乘法方法:

let numbers = [9, 29, 19, 79]

// Imperative example
var tripledNumbers:[Int] = []
for number in numbers {
    tripledNumbers.append(number * 3)
}
print(tripledNumbers)

// Declarative example
let tripledIntNumbers = numbers.map({ number in 3 * number })
print(tripledIntNumbers)

在命令式示例中,我们给出一个命令遍历数组中的每个元素,将每个元素乘以3,并将其添加到新的数组中。在声明式示例中,我们声明了数字应该如何映射。我们将在接下来的章节中提供更多关于声明式编程的示例。

在函数式编程中,函数是基本的构建块。在面向对象编程(OOP)中,程序由类和语句组成,当执行时,这些语句会改变类的状态。

函数式编程避免使用可变状态。避免可变状态使得代码更容易测试、阅读和理解,尽管在某些情况下(如文件和数据库操作)避免可变状态并不容易。

函数式编程要求函数是一等公民。一等函数被当作任何其他值一样对待,可以被传递给其他函数或作为函数的结果返回。

函数可以形成高阶函数,它们接受其他函数作为参数。高阶函数用于重构代码和减少重复。高阶函数可以用来实现领域特定语言DSL)。

函数是纯函数,它们不依赖于自身之外的数据,也不改变自身之外的数据。纯函数每次执行都会提供相同的结果。纯函数的这个特性被称为引用透明性,使得在代码上可以进行等式推理

在函数式编程中,表达式可以懒加载。例如,在下面的代码示例中,只有数组中的第一个元素被评估:

let oneToFour = [1, 2, 3, 4]
let firstNumber = oneToFour.lazy.map({ $0 * 3}).first!
print(firstNumber) // The result is going to be 3

在本例中,使用lazy关键字来获取集合的懒加载版本,因此只有数组中的第一个元素被乘以3,其余元素不会被映射。

Swift 编程语言

Swift 是由 Apple 开发的开源混合语言,它结合了面向对象编程和协议导向编程,以及函数式编程范式。Swift 可以与 Objective-C 一起用于开发 macOS、iOS、tvOS 和 watchOS 应用程序。Swift 也可以在 Ubuntu Linux 上用于开发 Web 应用程序。本书解释了 Swift 3.0 Preview 1,并使用 Xcode 8.0 beta。GitHub 仓库中的源代码将频繁更新,以跟上 Swift 的变化。

Swift 特性

Swift 借鉴了许多其他编程语言的概念,如 Scala、Haskell、C#、Rust 和 Objective-C,并具有以下特性。

现代语法

Swift 拥有现代语法,消除了像 Objective-C 这样的编程语言的冗余。例如,以下代码示例展示了具有属性和方法的 Objective-C 类。Objective-C 类在两个独立的文件中定义(接口和实现)。VerboseClass.h文件定义了一个接口,作为NSObject类的子类。它定义了一个属性ourArray和一个方法aMethod

实现文件导入头文件类,并为aMethod提供实现:

// VerboseClass.h
@interface VerboseClass: NSObject
@property (nonatomic, strong) NSArray *ourArray;
- (void)aMethod:(NSArray *)anArray;
@end

// VerboseClass.m
#import "VerboseClass.h"

@implementation VerboseClass

- (void)aMethod:(NSArray *)anArray {
    self.ourArray = [[NSArray alloc] initWithArray:anArray];
}

@end

// TestVerboseClass.m
#import "VerboseClass.h"

@interface TestVerboseClass : NSObject

@end

@implementation TestVerboseClass

- (void)aMethod {
    VerboseClass *ourOBJCClass = [[VerboseClass alloc] init];
    [ourOBJCClass aMethod: @[@"One", @"Two", @"Three"]];
    NSLog(@"%@", ourOBJCClass.ourArray);
}

@end

在 Swift 中,可以通过以下方式实现类似的功能:

class ASwiftClass {
    var ourArray: [String] = []

    func aMethod(anArray: [String]) {
        self.ourArray = anArray
    }
}

let aSwiftClassInstance = ASwiftClass()
aSwiftClassInstance.aMethod(anArray: ["one", "Two", "Three"])
print(aSwiftClassInstance.ourArray)

从这个例子中可以看出,Swift 消除了许多不必要的语法,使代码非常简洁易读。

类型安全和类型推断

Swift 是一种类型安全的语言,与 Ruby 和 JavaScript 等语言不同。与 Objective-C 中的类型可变集合相反,Swift 提供了类型安全的集合。Swift 通过类型推断机制自动推导类型,这种机制存在于 C# 和 C++ 11 等语言中。例如,在以下示例中,constString 在编译时被推断为 String,因此不需要注释类型:

let constString = "This is a string constant" 

不可变性

Swift 使得定义不可变值(即常量)变得容易,从而增强了函数式编程,因为不可变性是函数式编程中的关键概念之一。一旦常量被初始化,它们就不能被更改。虽然在 Java 等语言中也可以实现不可变性,但并不像 Swift 那样容易。要在 Swift 中定义任何不可变类型,可以使用 let 关键字,无论它是自定义类型、集合类型还是 Struct 类型。

无状态编程

Swift 提供了非常强大的结构和枚举,它们通过值传递,并且可以是无状态的;因此,它们非常高效。无状态编程简化了并发和多线程。

一等函数

函数在 Swift 中是一等类型,就像 Ruby、JavaScript 和 Go 等语言一样,因此它们可以被存储、传递和返回。一等函数使 Swift 能够实现函数式编程风格。

高阶函数

高阶函数可以将其他函数作为其参数。Swift 提供了 mapfilterreduce 等高阶函数。此外,在 Swift 中,我们还可以开发自己的高阶函数和 DSL。

模式匹配

模式匹配是解构值并根据正确的值匹配来匹配不同的情况的能力。模式匹配能力存在于 Scala、Erlang 和 Haskell 等语言中。Swift 提供了带有 where 子句的强大 switch-cases 和 if-cases。

泛型

Swift 提供了泛型,这使得编写不特定于类型的代码成为可能,并且可以用于不同类型。

闭包

闭包是代码块,可以被传递。闭包捕获它们定义的上下文中的常量和变量。与 Objective-C 中的块相比,Swift 提供了更简单的语法。

下标

Swift 提供了下标,这些下标是访问集合、列表、序列或自定义类型成员的快捷方式。下标可以用来通过索引设置和获取值,而无需为设置和获取分别使用单独的方法。

可选链

Swift 提供了可选类型,它可以有某些或没有值。Swift 还提供了可选链,以安全有效地使用可选类型。可选链使我们能够查询和调用可能为 nil 的可选类型的属性、方法和下标。

扩展

Swift 提供了类似于 Objective-C 中的分类的扩展(Extensions)。扩展可以向现有的类、结构体、枚举或协议类型添加新功能,即使它是封闭源代码的。

Objective-C 和 Swift 桥接头

桥接头(Bridging headers)使我们能够在项目中混合使用 Swift 和 Objective-C。这种功能使得我们能够在 Swift 项目中使用之前编写的 Objective-C 代码,反之亦然。

自动引用计数(Automatic Reference Counting,ARC)

Swift 通过 自动引用计数ARC)处理内存管理,类似于 Objective-C,而不像 Java 和 C# 等使用垃圾回收的语言。ARC 用于初始化和解除初始化资源,从而在不再需要时释放类实例的内存分配。ARC 跟踪代码实例中的保留和释放,以有效地管理内存资源。

REPL 和游乐场

Xcode 提供了 读取-评估-打印循环REPL)命令行环境,用于在无需编写程序的情况下实验 Swift 编程语言。此外,Swift 还提供了游乐场(Playgrounds),使我们能够快速测试 Swift 代码片段,并通过可视化界面实时查看结果。本书中将广泛使用游乐场。此外,大多数章节中的代码示例都可以在 Swift Playgrounds App 中进行实验(developer.apple.com/swift/playgrounds/)。

语言基础

本节将简要介绍 Swift 编程语言的基础知识。本章后续小节中的主题将在后面的章节中详细解释。

类型安全和类型推断

Swift 是一种类型安全的语言。这意味着一旦我们定义了常量、变量或表达式,就不能更改其类型。此外,Swift 的类型安全特性使我们能够在编译时找到类型不匹配。

Swift 提供了类型推断。Swift 会自动推断变量、常量或表达式的类型,因此我们不需要在定义时指定类型。让我们检查以下表达式:

let pi = 3.14159 
var primeNumber = 691 
let name = "my name" 

在这些表达式中,Swift 推断 piDouble 类型,primeNumberInt 类型,以及 nameString 类型。如果我们需要特殊类型如 Int64,我们需要对类型进行注解。

类型注解

在 Swift 中,我们可以注解类型,换句话说,明确指定变量或表达式的类型。让我们看看以下示例:

let pi: Double = 3.14159 
let piAndPhi: (Double, Double) = (3.14159, 1.618) 
func ourFunction(a: Int) { /* ... */ } 

在这个示例中,我们定义了一个注解为 Double 的常量(pi),一个名为 piAndPhi 的元组注解为 (Double, Double),以及 ourFunction 函数的参数注解为 Int

小贴士

下载示例代码本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Swift-3-Functional-Programming。我们还有其他丰富的图书和视频的代码包,可在 github.com/PacktPublishing/ 找到。查看它们吧!

类型别名

类型别名定义了现有类型的替代名称。我们使用 typealias 关键字定义类型别名。类型别名在需要通过更合适的上下文名称引用现有类型时很有用,例如在处理来自外部源的具体大小数据时。例如,在以下示例中,我们为无符号 32 位整数提供了一个别名,该别名可以在我们稍后的代码中使用:

typealias UnsignedInteger = UInt32 

typealias 定义可以用作简化闭包和函数定义。

不可变性

Swift 使得定义可变和不可变变量成为可能。let 关键字用于不可变声明,而 var 关键字用于可变声明。任何使用 let 关键字声明的变量将不会开放进行更改。在以下示例中,我们使用 var 关键字定义 aMutableString 以便我们稍后能够修改它;相比之下,我们无法修改使用 let 关键字定义的 aConstString

var aMutableString = "This is a variable String" 
let aConstString = "This is a constant String" 

在函数式编程中,建议尽可能使用 let 将属性定义为常量或不可变。不可变变量更容易跟踪且错误更少。在某些情况下,例如 CoreData 编程,SDK 要求可变属性;然而,在这些情况下,建议使用可变变量。

元组

Swift 提供元组,以便可以将多个值组合成一个单一的复合值。让我们考虑以下示例:

// Tuples 
let http400Error = (400, "Bad Request")
// http400Error is of type (Int, String), and equals (400, "Bad Request")

// Decompose a Tuple's content 
let (requestStatusCode, requestStatusMessage) = http400Error 

元组可以用作函数的返回类型,以实现多返回值函数。

可选类型

Swift 提供可选类型,以便在可能不存在值的情况下使用。可选类型将具有一些或没有值。? 符号用于将变量定义为可选。让我们考虑以下示例:

// Optional value either contains a value or contains nil
var optionalString: String? = "A String literal"
optionalString = nil

! 符号可以用来强制展开可选类型中的值。例如,以下示例强制展开 optionalString 变量:

optionalString = "An optional String"
print(optionalString!)

强制展开可选类型可能会导致错误,如果可选类型没有值,因此不建议使用这种方法,因为它很难确保在不同情况下可选类型中是否有值。更好的方法是使用可选绑定技术来找出可选类型是否包含值。让我们考虑以下示例:

let nilName:String? = nil
if let familyName = nilName {
    let greetingfamilyName = "Hello, Mr. \(familyName)"
} else {
    // Optional does not have a value
}

基本运算符

Swift 提供以下基本操作:

  • 与许多不同的编程语言一样,= 运算符用于赋值。

  • 加法运算符 + 用于加法,减法运算符 - 用于减法,乘法运算符 * 用于乘法,除法运算符 / 用于除法,取余运算符 % 用于求余。这些运算符是函数,可以被传递给其他函数。

  • 一元减法运算符 -i,一元加法运算符 +i

  • 复合赋值运算符 +=-=*=

  • 等于运算符 a == b,不等式运算符 a != b,以及大于比较 a > b、小于比较 a < b 和小于等于比较 a <= b

  • 三元条件运算符,question ? answer1: answer2

  • 隐式展开 a ?? b 如果可选 a 有值,则展开可选 a 并返回默认值 b;如果 anil

  • 范围运算符:

    • 闭区间 (a...b) 包含值 ab

    • 半开区间 (a..<b) 包含 a 但不包含 b

  • 逻辑运算符:

    • !a 运算符不是 NOT a

    • a && b 运算符是逻辑与

    • a || b 运算符是逻辑或

字符串和字符

在 Swift 中,String 是字符的有序集合。String 是一个结构体,而不是类。结构体是 Swift 中的值类型;因此,任何 String 都是值类型,通过值传递,而不是通过引用传递。

不可变性

字符串可以使用 let 定义为不可变。使用 var 定义的字符串将是可变的。

字符串字面量

String 字面量可用于创建 String 实例。在以下编码示例中,我们使用 String 字面量定义并初始化 aVegetable

let aVegetable = "Arugula" 

空字符串

空字符串可以按以下方式初始化:

var anEmptyString = ""
var anotherEmptyString = String()

这两个字符串都是空的,彼此等价。要确定一个 String 是否为空,可以使用 isEmpty 属性如下:

if anEmptyString.isEmpty {
    print("String is empty")
}

连接字符串和字符

字符串和字符可以按以下方式连接:

let string1 = "Hello"
let string2 = " Mr"
var welcome = string1 + string2

var instruction = "Follow us please"
instruction += string2

let exclamationMark: Character = "!"
welcome.append(exclamationMark)

字符串插值

字符串插值是一种通过在字符串字面量中包含它们的值来构造新的 String 值的方法,这些值可以是常量、变量、字面量和表达式。以下是一个示例:

let multiplier = 3
let message = "\(multiplier) times 7.5 is \(Double (multiplier) * 7.5)"
// message is "3 times 7.5 is 22.5"

字符串比较

字符串可以使用 == 进行相等比较,使用 != 进行不等比较。

hasPrefixhasSuffix 方法可用于前缀和后缀相等性检查。

集合

Swift 提供了诸如数组、字典和集合等类型化的集合。在 Swift 中,与 Objective-C 不同,集合中的所有元素都将具有相同的类型,并且我们无法在定义后更改集合的类型。

我们可以使用 let 定义不可变集合,使用 var 定义可变集合,如下例所示:

// Arrays and Dictionaries
var cheeses = ["Brie", "Tete de Moine", "Cambozola", "Camembert"]
cheeses[2] = "Roquefort"
var cheeseWinePairs = [
    "Brie":"Chardonnay",
    "Camembert":"Champagne",
    "Gruyere":"Sauvignon Blanc"
]

cheeseWinePairs ["Cheddar"] = "Cabarnet Sauvignon"
// To create an empty array or dictionary
let emptyArray = [String]()
let emptyDictionary = Dictionary<String, Float>()
cheeses = []
cheeseWinePairs = [:]

for-in 循环可用于遍历集合中的项。

控制流程

Swift 提供了不同的控制流程,以下小节将进行解释。

for 循环

Swift 提供了 forfor-in 循环。我们可以使用 for-in 循环遍历集合中的项、数字序列(如范围)或字符串表达式中的字符。以下示例展示了如何使用 for-in 循环遍历 Int 数组中的所有项:

let scores = [65, 75, 92, 87, 68]
var teamScore = 0

for score in scores {
    if score > 70 {
        teamScore = teamScore + 3
    } else {
        teamScore = teamScore + 1
    }
}

以下是如何遍历字典:

for (cheese, wine) in cheeseWinePairs {
    print("\(cheese): \(wine)")
}

我们可以使用带有条件和增量/减量器的for循环。以下示例展示了for循环的示例:

var count = 0
for var i = 0; i < 3; ++i {
    count += i
}

由于从 Swift 3.0 开始移除了具有增量器/减量器的 C 风格for循环,建议使用以下方式的for-in循环来代替,如下所示:

var count = 0
for i in 0...3 {
    count += i
}

while loops

Swift 提供了whilerepeat-while循环。一个whilerepeat-while循环会执行一系列表达式,直到条件变为假。让我们考虑以下示例:

var n = 2
while n < 100 {
    n = n * 2
}

var m = 2
repeat {
    m = m * 2
} while m < 100

while循环在每个迭代的开始时评估其条件。repeat-while循环在每个迭代的结束时评估其条件。

stride

stride函数使我们能够以非一为步长遍历范围。有两个stride函数:stride to函数,它遍历排他性范围,以及stride through函数,它遍历包含性范围。让我们考虑以下示例:

let fourToTwo = Array(stride(from: 4, to: 1, by: -1)) // [4, 3, 2]
let fourToOne = Array(stride(from:4, through: 1, by: -1)) // [4, 3, 2, 1]

if

Swift 提供了if来定义条件语句。只有当条件语句为true时,才会执行一组语句。例如,在以下示例中,由于anEmptyString为空,将执行print语句:

var anEmptyString = ""
if anEmptyString.isEmpty {
    print("An empty String")
} else {
    // String is not empty.
}

switch

Swift 提供了switch语句来比较一个值与不同的匹配模式。一旦匹配到模式,将执行相关的语句。与大多数其他基于 C 的编程语言不同,Swift 不需要在每个case中都有break语句,并支持任何值类型。switch语句可用于范围匹配,并且switch语句中的where子句可以用来检查额外的条件。以下示例展示了带有附加条件检查的简单switch语句:

let aNumber = "Four or Five"
switch aNumber {
    case "One":
        let one = "One"
    case "Two", "Three":
        let twoOrThree = "Two or Three"
    case let x where x.hasSuffix("Five"):
        let fourOrFive = "it is \(x)"
    default:
        let anyOtherNumber = "Any other number"
}

guard

A guard statement can be used for early exits. We can use a guard statement to require that a condition must be true in order for the code after the guard statement to be executed. The following example presents the guard statement usage:

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return
    }
    print("Hello Ms \(name)!")
}

在这个例子中,greet函数需要一个表示人的name的值。因此,它使用guard语句检查其是否存在,否则它将返回并继续执行。

函数

函数是执行特定任务的独立代码块。

在 Swift 中,函数是一等公民,这意味着它们可以被存储、传递和返回。函数可以被柯里化,并定义为接受其他函数作为其参数的高阶函数。Swift 中的函数可以使用元组具有多个输入参数和多个返回值。让我们看看以下示例:

func greet(name: String, day: String) -> String {
    return "Hello \(name), today is \(day)"
}

greet(name: "Ted", day:"Saturday")

函数可以有可变参数。让我们考虑以下示例:

// Variable number of arguments in functions - Variadic Parameters
func sumOf(numbers:Int...) -> (Int, Int) {
    var sum = 0
    var counter = 0
    for number in numbers {
        sum += number
        counter += 1
    }
    return (sum, counter)
}

sumOf()
sumOf(numbers: 7, 9, 45)

在 Swift 3.0 之前,函数可以有可变和不可变参数。让我们考虑以下示例:

func alignRight(var string: String, count: Int, pad: Character) -> String {
    let amountToPad = count - string.characters.count
    if amountToPad < 1 {
        return string
    }
    let padString = String(pad)
    for _ in 1...amountToPad {
        string = padString + string
    }
    return string
} 

可变参数在 Swift 函数式编程中不受欢迎,并且从 Swift 3.0 开始被移除。

函数可以有inout参数。让我们考虑以下示例:

func swapTwoInts( a: inout Int, b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

在函数式 Swift 中,inout 参数并不受欢迎,因为它们会改变状态并使函数变得不纯。

在 Swift 中,我们可以定义嵌套函数。以下示例展示了一个名为 add 的函数嵌套在另一个函数内部。嵌套函数可以访问其父函数的作用域内的数据。在这个例子中,add 函数可以访问 y 变量:

func returnTwenty() -> Int {
    var y = 10
    func add() {
        y += 10
    }
    add()
    return y
}

returnTwenty()

在 Swift 中,函数可以返回其他函数。在以下示例中,makeIncrementer 函数返回一个函数,该函数接收一个 Int 值并返回一个 Int 值 (Int -> Int):

// Return another function as its value
func makeIncrementer() -> (Int -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}

var increment = makeIncrementer()
increment(7)

闭包

闭包是包含特定功能的自包含代码块,可以被存储、传递并在代码中使用。闭包在 C 和 Objective-C 中相当于块。闭包可以捕获并存储它们定义的上下文中任何常量和变量的引用。嵌套函数是闭包的特殊情况。闭包是引用类型,可以作为变量、常量和类型别名存储。它们可以被传递给函数并从函数返回。

以下示例展示了来自网站 http://goshdarnclosuresyntax.com 的 Swift 中不同闭包声明的示例:

// As a variable:
var closureName: (parameterTypes) -> (returnType)

//As an optional variable:
var closureName: ((parameterTypes) -> (returnType))?

//As a type alias:
typealias closureType = (parameterTypes) -> (returnType)

映射、过滤和缩减

Swift 提供了 mapfilterreduce 函数,它们是高阶函数。

映射

map 函数解决了使用函数转换数组元素的问题。让我们考虑以下示例:

// Return an `Array` containing the results of calling `transform(x)` on
  each element `x` of `self`
// func map<U>(transform: (T) -> U) -> [U]
let numbers = [10, 30, 91, 50, 100, 39, 74]
var formattedNumbers: [String] = []

for number in numbers {
    let formattedNumber = "\(number)$"
    formattedNumbers.append(formattedNumber)
}

let mappedNumbers = numbers.map { "\($0)$" }

过滤

filter 函数接受一个函数,该函数给定数组中的一个元素,返回 Bool 值,指示该元素是否应该包含在结果数组中。让我们考虑以下示例:

// Return an Array containing the elements x of self for which
  includeElement(x)` is `true`
// func filter(includeElement: (T) -> Bool) -> [T]
let someEvenNumbers = numbers.filter { $0 % 2 == 0 }

缩减

reduce 函数将数组缩减为一个单一值。它接受两个参数:一个起始值和一个函数,该函数接受一个运行总和以及数组中的一个元素作为参数,并返回一个新的运行总和。让我们考虑以下示例:

// Return the result of repeatedly calling `combine` with an accumulated
  value initialized to `initial` and each element of `self`, in turn,
  that is return `combine(combine(...combine(combine(initial, self[0]),
  self[1]),...self[count-2]), self[count-1])`.
// func reduce<U>(initial: U, combine: (U, T) -> U) -> U
let total = numbers.reduce(0) { $0 + $1 }

枚举

在 Swift 中,枚举定义了一个相关值的公共类型,并使我们能够以类型安全的方式处理这些值。为每个枚举成员提供的值可以是 StringCharacterInt 或任何浮点类型。枚举可以存储任何给定类型的关联值,如果需要,枚举的每个成员的值类型可以不同。枚举成员可以预先填充默认值(称为原始值),它们都是同一类型。让我们考虑以下示例:

enum MLSTeam {
    case montreal
    case toronto
    case newYork
    case columbus
    case losAngeles
    case seattle
}

let theTeam = MLSTeam.montreal

枚举值可以用 switch 语句进行匹配,这在以下示例中可以看到:

switch theTeam {
    case .montreal:
        print("Montreal Impact")
    case .toronto:
        print("Toronto FC")
    case .newYork:
        print("Newyork Redbulls")
    case .columbus:
        print("Columbus Crew")
    case .losAngeles:
        print("LA Galaxy")
    case .seattle:
        print("Seattle Sounders")
}

Swift 中的枚举实际上是通过对其他类型进行组合创建的代数数据类型。让我们考虑以下示例:

enum NHLTeam { case canadiens, senators, rangers, penguins, blackHawks,
  capitals}

enum Team {
    case hockey(NHLTeam)
    case soccer(MLSTeam)
}

struct HockeyAndSoccerTeams {
    var hockey: NHLTeam
    var soccer: MLSTeam
}

MLSTeamNHLTeam枚举各有六个潜在值。如果我们将它们结合起来,我们将有两个新的类型。一个Team枚举可以是NHLTeamMLSTeam,因此它有 12 个潜在值——即NHLTeamMLSTeam潜在值的总和。因此,枚举Team是一个和类型。

要有一个HockeyAndSoccerTeams结构体,我们需要为NHLTeamMLSTeam选择一个值,这样它就有 36 个潜在值——即NHLTeamMLSTeam值的乘积。因此,HockeyAndSoccerTeams是一个乘积类型。

在 Swift 中,枚举的选项可以有多个值。如果它恰好是唯一选项,那么这个枚举就变成了乘积类型。以下示例展示了枚举作为乘积类型:

enum HockeyAndSoccerTeams {
    case Value(hockey: NHLTeam, soccer: MLSTeam)
}

由于我们可以在 Swift 中创建和或乘积类型,我们可以说 Swift 对代数数据类型有第一级支持。

泛型

泛型代码使我们能够编写灵活且可重用的函数和类型,它们可以与任何类型一起工作,前提是我们定义了要求。例如,以下使用inout参数来交换两个值的函数只能与Int值一起使用:

func swapTwoIntegers( a: inout Int, b: inout Int) {
    let tempA = a
    a = b
    b = tempA
}

为了使此函数与任何类型一起工作,可以使用泛型,如下例所示:

func swapTwoValues<T>( a: inout T, b: inout T) {
    let tempA = a
    a = b
    b = tempA
}

类与结构体

类和结构体是通用、灵活的构造,成为程序代码的构建块。它们具有以下特性:

  • 可以定义属性来存储值

  • 可以定义方法来提供功能

  • 可以定义下标以使用下标语法访问它们的值

  • 可以定义初始化器来设置其功能,超出默认实现

  • 它们可以遵守协议以提供某些类型的标准功能

类与结构体

本节比较类和结构体:

  • 继承使一个类能够继承另一个类的特性

  • 类型转换使我们能够在运行时检查和解释类实例的类型

  • 析构器使类的实例能够释放它所分配的任何资源

  • 引用计数允许对类实例有多个引用

  • 结构体是值类型,因此在代码中传递时总是被复制

  • 结构体不使用引用计数

  • 类是引用类型

选择类与结构体

当以下条件之一适用时,我们考虑创建结构体:

  • 结构体的主要目的是封装几个相对简单的数据值

  • 我们可以合理地预期,当我们分配或传递结构体的实例时,封装的值将被复制而不是被引用

  • 结构体存储的任何属性本身也是值类型,这也会期望被复制而不是引用

  • 结构体不需要从另一个现有类型继承属性或行为

结构体的良好候选者示例包括以下内容:

  • 几何形状的大小

  • 3D 坐标系统中的一个点

身份运算符

由于类是引用类型,多个常量和变量可能在实际中指向同一个类的单个实例。为了确定两个常量或变量是否精确地指向同一个类的实例,Swift 提供了以下身份运算符:

  • 等同于 (===)

  • 不等同于 (!==)

属性

属性将值与特定的类、结构或枚举相关联。Swift 允许我们直接设置结构属性的字段属性,而无需将整个对象属性设置为新的值。所有结构都有自动生成的成员初始化器,可以用来初始化新结构实例的成员属性。这并不适用于类实例。

属性观察者

属性观察者用于响应属性值的更改。每当属性的值被设置时,即使新值与属性的当前值相同,属性观察者都会被调用。我们可以在属性上定义以下观察者之一或两个:

  • willSet 观察者在值存储之前被调用

  • didSet 观察者在新值存储后立即被调用

在初始化器中设置属性之前,willSetdidSet 观察者不会被调用。

方法

方法是与特定类型相关联的函数。实例方法是调用特定类型实例的函数。类型方法是调用类型本身的函数。

以下示例展示了一个包含名为 someTypeMethod() 的类型方法的类:

class AClass {
    class func someTypeMethod() {
        // type method body
    }
}

我们可以将此方法称为如下:

AClass.someTypeMethod()

下标

下标是访问集合、列表、序列或任何实现下标的自定义类型的成员元素的快捷方式。让我们考虑以下示例:

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}

let fiveTimesTable = TimesTable(multiplier: 5)
print("six times five is \(fiveTimesTable[6])")
// prints "six times five is 30"

继承

一个类可以从另一个类继承方法、属性和其他特性:

class SomeSubClass: SomeSuperClass

Swift 类不继承自通用基类。我们定义的未指定超类的类自动成为我们构建的基础类。要覆盖继承的特性,我们使用 override 关键字作为覆盖定义的前缀。被覆盖的方法、属性或下标可以通过调用 super 来调用超类版本。要防止覆盖,可以使用 final 关键字。

初始化

准备一个类、结构体或枚举的实例以供使用的过程称为初始化。类和结构体必须在创建该类或结构体的实例时将所有存储属性设置为适当的初始值。存储属性不能处于中间状态。我们可以在初始化过程中修改常量属性的值,只要在初始化完成时将其设置为确定的值。Swift 为任何结构体或基类提供默认初始化器,如果它为所有属性提供默认值并且没有提供至少一个初始化器。让我们考虑以下示例:

class ShoppingItem {
    var name: String?
    var quantity = 1
    var purchased = false
}

var item = ShoppingItem()

如果我们没有定义任何自定义初始化器,struct 类型会自动接收一个成员初始化器,即使结构体的存储属性没有默认值。

Swift 为类类型定义了两种初始化器:

  • 指定初始化器:能够完全初始化对象的方法

  • 便利初始化器:依赖于其他方法来完成初始化的方法

解构

解构器在类实例被释放之前立即调用。Swift 在实例不再需要时自动释放实例,以释放资源(ARC)。

自动引用计数

引用计数仅适用于类的实例。结构体和枚举是值类型,不是引用类型,它们不是通过引用存储和传递的。

弱引用可以用来解决强引用循环,可以定义为如下:

weak var aWeakProperty 

一个非拥有引用不会对其引用的实例保持强引用。然而,与弱引用不同的是,非拥有引用始终定义为非可选类型。可以使用闭包捕获列表来解决闭包强引用循环。

当闭包和它捕获的实例将始终相互引用并在同一时间被释放时,可以在闭包中定义一个非拥有引用。

当捕获的引用可能在未来的某个时刻变为nil时,可以定义一个作为弱引用的捕获。弱引用始终为可选类型。让我们考虑以下示例:

class AClassWithLazyClosure {
    lazy var aClosure: (Int, String) -> String = {
        [unowned self] (index: Int, stringToProcess: String) -> String in
        // closure body goes here
        return ""
    }
}

可选值和可选链

可选值是 Swift 类型,可以具有一些或没有值。可选链是一个查询和调用可能当前为nil的可选值的属性、方法和子脚本的进程。Swift 中的可选链类似于 Objective-C 中的nil消息,但以一种适用于任何类型并且可以检查成功或失败的方式。让我们考虑以下示例:

// Optional chaining
class Residence {
    var numberOfRooms = 1
}

class Person {
    var residence: Residence?
}

let jeanMarc = Person()
// This can be used for calling methods and subscripts through optional
  chaining too
if let roomCount = jeanMarc.residence?.numberOfRooms {
    // Use the roomCount
}

在这个示例中,我们能够通过可选链访问numberOfRooms,这是可选类型(Residence)的一个属性。

错误处理

Swift 在运行时提供支持抛出、捕获、传播和处理可恢复错误的机制。

值类型应该符合 ErrorType 协议才能表示为错误。以下示例展示了某些 4xx 和 5xx HTTP 错误作为 enum

enum HttpError: ErrorType {
    case badRequest
    case unauthorized
    case forbidden
    case requestTimeOut
    case unsupportedMediaType
    case internalServerError
    case notImplemented
    case badGateway
    case serviceUnavailable
}

我们将能够使用 throw 关键字抛出错误,并使用 throws 关键字标记可以抛出错误的函数。

我们可以使用 do-catch 语句通过运行代码块来处理错误。以下示例展示了在 do-catch 语句中处理 JSON 解析错误:

protocol HttpProtocol{
    func didRecieveResults(results:NSDictionary)
}

struct WebServiceManager {
    var delegate:HttpProtocol?
    let data: NSData
    func test() {
        do {
            let jsonResult: NSDictionary = try
              NSJSONSerialization.JSONObjectWithData(self.data,
              options: NSJSONReadingOptions.MutableContainers) as!
              NSDictionary
            self.delegate?.didRecieveResults(jsonResult)
        } catch let error as NSError {
            print("json error" + error.localizedDescription)
        }
    }
}

我们可以使用 defer 语句在代码执行离开当前代码块之前执行一系列语句,无论执行如何离开当前代码块。

类型转换

类型转换是一种检查实例类型的方式,并且/或者将实例作为它类层次结构中其他地方的不同超类或子类来处理。有两种类型的运算符用于检查和转换类型:

  • 类型检查运算符(is):这检查一个实例是否为确定的子类类型。

  • 类型转换运算符(asas?):一个确定类类型的常量或变量可能在底层引用一个子类的实例。如果情况如此,我们可以尝试使用 as 将其向下转换为子类类型。

Any 和 AnyObject

Swift 提供了两个特殊的类型别名来处理非特定类型:

  • AnyObject 可以表示任何类类型的实例

  • Any 可以表示任何类型的实例,包括结构体、枚举和函数类型

AnyAnyObject 类型别名必须仅在我们明确需要它们提供的行为和能力时使用。在我们的代码中明确指定我们期望与之一起工作的类型比使用 AnyAnyObject 类型更好,因为它们可以表示任何类型,并提供了动态性而不是安全性。让我们考虑以下示例:

class Movie {
    var director: String
    var name: String
    init(name: String, director: String) {
        self.director = director
        self.name = name
    }
}

let objects: [AnyObject] = [
    Movie(name: "The Shawshank Redemption", director: "Frank Darabont"),
    Movie(name: "The Godfather", director: "Francis Ford Coppola")
]

for object in objects {
    let movie = object as! Movie
    print("Movie: '\(movie.name)', dir. \(movie.director)")
}

// Shorter syntax
for movie in objects as! [Movie] {
    print("Movie: '\(movie.name)', dir. \(movie.director)")
}

嵌套类型

枚举通常被创建来支持特定类或结构的功能。同样,在复杂类型的上下文中声明仅用于该上下文的实用类和结构体可能也很方便。

Swift 允许我们声明嵌套类型,即在它们支持的类型定义中嵌套支持枚举、类和结构体。以下示例,借鉴自 《Swift 编程语言(Swift 3.0)》,由 苹果公司 提供,展示了嵌套类型:

struct BlackjackCard {
    // nested Suit enumeration
    enum Suit: Character {
        case spades = "♠",
        hearts = "♡",
        diamonds = "♢",
        clubs = "♣"
    }

    // nested Rank enumeration
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace

        // nested struct
        struct Values {
            let first: Int, second: Int?
        }

        var values: Values {
            switch self {
            case .ace:
                return Values(first: 1, second: 11)
            case .jack, .queen, .king:
                return Values(first: 10, second: nil)
            default:
                return Values(first: self.rawValue, second: nil)
            }
        }
    }

    let rank: Rank, suit: Suit

    var description: String {
        var output = "suit is \(suit.rawValue),"
        output += " value is \(rank.values.first)"
        if let second = rank.values.second {
            output += " or \(second)"
        }
        return output
    }
}

扩展

扩展可以向现有的类、结构体或枚举类型添加新功能。这包括扩展我们无法访问原始源代码的类型的能力。

Swift 中的扩展使我们能够执行以下操作:

  • 定义实例方法和类型方法

  • 提供新的初始化器

  • 定义和使用新的嵌套类型

  • 定义下标

  • 添加计算属性和计算静态属性

  • 使现有类型符合新的协议

扩展使我们能够向类型添加新功能,但我们无法覆盖现有功能。

在以下示例中,我们通过使 AType 符合两个协议来扩展它:

extension AType: AProtocol, BProtocol {
}

以下示例展示了通过添加计算属性对 Double 的扩展:

// Computed Properties
extension Double {
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.2884 }
}

let threeInch = 76.2.mm
let fiveFeet = 5.ft

协议

一个 protocol 定义了适合特定任务或功能的方法、属性和其他要求签名或类型。协议实际上并没有实现任何功能。它只描述了实现将看起来像什么。一个提供实际要求实现的类、结构体或枚举可以采用协议。协议使用与正常方法相同的语法,但不允许为方法参数指定默认值。is 操作符可以用来检查一个实例是否遵从协议。只有当我们的协议为类标记了 @objc 时,我们才能检查协议遵从性。as 操作符可以用来转换到特定的协议。

协议作为类型

我们定义的任何协议都将成为我们代码中使用的完整类型。我们可以如下使用协议:

  • 函数、方法或初始化器中的参数类型或返回类型

  • 常量、变量或属性的类型

  • 数组、字典或其他容器中项的类型

让我们看看以下示例:

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

// Classes, enumerations and structs can all adopt protocols.
class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class example"
    var anotherProperty: Int = 79799

    func adjust() {
        simpleDescription += " Now 100% adjusted..."
    }
}

var aSimpleClass = SimpleClass()
aSimpleClass.adjust()
let aDescription = aSimpleClass.simpleDescription

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple struct"
    // Mutating to mark a method that modifies the structure - For classes
      we do not need to use mutating keyword
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}

var aSimpleStruct = SimpleStructure()
aSimpleStruct.adjust()
let aSimpleStructDescription = aSimpleStruct.simpleDescription

协议扩展

协议扩展允许我们在协议上定义行为,而不是在每个类型的单独遵从或全局函数中定义。通过在协议上创建扩展,所有遵从的类型自动获得此方法实现,而无需任何额外修改。当我们定义协议扩展时,我们可以指定遵从类型必须满足的约束,以便在扩展的方法和属性可用时。例如,我们可以扩展我们的 ExampleProtocol 以提供默认功能,如下所示:

extension ExampleProtocol {
    var simpleDescription: String {
        get {
            return "The description is: \(self)"
        }
        set {
            self.simpleDescription = newValue
        }
    }

    mutating func adjust() {
        self.simpleDescription = "adjusted simple description"
    }
}

访问控制

访问控制限制了来自其他源文件和模块的代码对我们代码的访问:

  • 公共: 这使得实体可以在定义它们的模块中的任何源文件中使用,也可以在导入定义模块的另一个模块的源文件中使用

  • 内部: 这使得实体可以在定义它们的模块中的任何源文件中使用,但不能在模块外部的任何源文件中使用

  • 私有: 这限制了实体只能在其定义的源文件中使用

概述

本章首先解释了函数式编程的重要性,然后介绍了函数式编程中的关键范式。此外,它还通过代码示例介绍了 Swift 编程语言的基础。到目前为止,我们应该对函数式编程概念和 Swift 编程语言有一个广泛的了解。本章的所有主题将在接下来的章节中详细讲解。

我们将通过函数来深入探讨这些主题,因为它们是函数式编程中最基本的构建块。因此,接下来的章节将解释函数,并给出纯函数、一等函数、高阶函数和嵌套函数的示例。此外,它还将解释一些稍微高级的话题,例如记忆化、函数柯里化和组合。

第二章. 函数和闭包

在上一章中,我们概述了函数式编程和 Swift 编程语言,介绍了一些关于函数的关键概念。由于函数是函数式编程的基本构建块,本章将深入探讨它,并解释与函数式 Swift 中函数的定义和用法相关的所有方面,同时附上代码示例。

本章从函数的定义开始,继续讨论其他相关主题,如函数类型和元组,最后以更高级的主题结束,如首等函数、高阶函数、函数组合、闭包、柯里化和缓存。

本章将涵盖以下主题,并附上代码示例:

  • 函数的一般语法

  • 定义和使用函数参数

  • 设置内部和外部参数

  • 设置默认参数值

  • 定义和使用可变参数函数

  • 从函数返回值

  • 定义和使用嵌套函数

  • 函数类型

  • 纯函数

  • 首等函数

  • 高阶函数

  • 函数组合

  • 自定义运算符的定义

  • 定义和使用闭包

  • 函数柯里化

  • 递归

  • 缓存

什么是函数?

面向对象编程OOP)对大多数开发者来说看起来非常自然,因为它模拟了现实生活中的类或换句话说,蓝图及其实例,但它带来了许多复杂性和问题,例如实例和内存管理、复杂的多线程和并发编程。

在面向对象编程(OOP)成为主流之前,我们习惯于使用过程式语言进行开发。在 C 编程语言中,我们没有对象和类;我们会使用结构体和函数指针。因此,我们现在讨论的是主要依赖于函数的过程式编程,就像过程式语言依赖于过程一样。我们能够在 C 语言中不使用类就开发出非常强大的程序;事实上,大多数操作系统都是用 C 语言开发的。还有其他多用途的编程语言,例如谷歌的 Go 语言,它不是面向对象的,但由于其性能和简单性而变得越来越受欢迎。

那么,我们是否能够在 Swift 中不使用类就编写非常复杂的应用程序呢?我们可能会想知道为什么我们应该这样做。通常,我们不应该这样做,但尝试这样做将使我们了解函数式编程的能力。这就是为什么在讨论其他构建块,如结构体枚举之前,我们将有一个关于函数的整个章节。

函数是一段执行特定任务的代码块,可以存储,可以持久化数据,并且可以被传递。我们将其定义在独立的 Swift 文件中作为全局函数,或者在其他构建块如结构体枚举协议中作为方法。

如果它们在类中定义,它们被称为方法,但在定义上,Swift 中函数和方法之间没有区别。

在其他构建块中定义它们使得方法可以使用父级的范围,或者能够改变它。它们可以访问其父级的范围,并且它们有自己的范围。在函数内部定义的任何变量都不可以在函数外部访问。它们内部定义的变量以及相应的分配的内存将在函数终止时消失。

函数在 Swift 中非常强大。我们可以仅使用函数来组合程序,因为函数可以接收和返回函数,捕获它们声明上下文中的变量,并且可以在内部持久化数据。为了理解函数式编程范式,我们需要详细了解函数的能力。我们需要思考是否可以避免类而只使用函数,因此我们将在本章的后续部分涵盖与函数相关的所有细节。

函数和方法的通用语法

我们可以如下定义函数或方法:

accessControl func functionName(parameter: ParameterType) throws
  -> ReturnType { }

如我们所知,当函数在对象中定义时,它们就变成了方法。

定义方法的第一步是告诉编译器它可以从哪里访问。这个概念在 Swift 中被称为访问控制,并且有三种访问控制级别。我们将如下解释方法级别的访问控制:

  • 公共访问:如果实体在同一模块中,它可以访问被定义为public的方法。如果实体不在同一模块中,我们需要导入该模块才能调用该方法。当我们开发框架时,我们需要将我们的方法和对象标记为public,以便其他模块可以使用它们。

  • 内部访问:任何被定义为internal的方法可以从模块中的其他实体访问,但不能从其他模块访问。

  • 私有访问:任何被定义为private的方法只能从同一源文件中访问。

默认情况下,如果我们没有提供访问修饰符,变量或函数就变为内部访问。

使用这些访问修饰符,我们可以正确地组织我们的代码;例如,如果我们定义一个实体为内部访问,我们可以从其他模块中隐藏细节。我们甚至可以将方法的细节隐藏在其他文件中,如果我们将它们定义为私有。

在 Swift 2.0 之前,我们必须将所有内容定义为公共的,或者将所有源文件添加到测试目标中。Swift 2.0 引入了@testable import语法,使我们能够定义内部或私有方法,这些方法可以从测试模块中访问。

方法通常可以以三种形式存在:

  • 实例方法:我们需要获取一个对象的实例(在这本书中,我们将classesstructsenums称为对象)才能调用其中定义的方法,然后我们才能访问该对象的范围和数据。

  • 静态方法:Swift 也称之为类型方法。它们不需要任何对象实例,并且不能访问实例数据。它们通过在对象类型名称后放置一个点来调用(例如,Person.sayHi())。static方法不能被它们所在的对象的子类覆盖。

  • 类方法:类方法类似于static方法,但它们可以被子类覆盖。

我们已经讨论了方法定义所需的关键字;现在我们将集中讨论函数和方法共有的语法。本书不涉及与方法相关的一些概念,因为我们将在 Swift 中专注于函数式编程。

继续讨论函数定义,现在出现的是强制使用的func关键字,它用于告诉编译器它将处理一个函数。

然后是函数名,这也是强制性的,建议使用驼峰式命名法,首字母小写。函数名应该说明函数的功能,并且在我们定义对象的方法时,建议使用动词形式。

基本上,我们的类将被命名为名词,方法将被命名为动词,它们是对类的命令。在纯函数式编程中,由于函数不驻留在其他对象中,它们可以按其功能命名。

参数跟在func名称之后。它们将在括号内定义,以向函数传递参数。即使我们没有参数,括号也是强制性的。我们将在本章的“定义和使用函数参数”部分中涵盖参数的所有方面。

接下来是throws关键字,它不是强制性的。标记了throws关键字的函数或方法可能会抛出错误,也可能不会。我们将在后续章节中介绍错误处理机制。目前,当我们看到函数或方法签名中的它们时,了解它们是什么就足够了。

函数类型声明中的下一个实体是返回类型。如果一个函数不是 void 类型,返回类型将位于->符号之后。返回类型表明了函数将要返回的实体类型。

我们将在本章的“从函数返回值”部分详细讨论返回类型,因此现在我们可以继续讨论大多数编程语言中普遍存在的函数的最后一部分,那就是我们喜爱的{ }。我们将函数定义为功能块,而{ }定义了块的边界,以便函数体在这里声明并执行。我们将在{ }内编写功能。

函数定义的最佳实践

有一些经过验证的最佳实践是由出色的软件工程资源提供的,例如 Clean Code: A Handbook of Agile Software Craftsmanship,由 Robert C. Martin 编著,Code Complete: A Practical Handbook of Software Construction, Second Edition,由 Steve McConnell 编著,以及 Coding Horror (blog.codinghorror.com/code-smells/),我们可以总结如下:

  • 尽量不要让每个函数的代码行数超过 8-10 行,因为较短的函数或方法更容易阅读、理解和维护。

  • 尽量保持参数数量最小,因为一个函数拥有的参数越多,它就越复杂。

  • 函数至少应该有一个参数和一个返回值。

  • 避免在函数名称中使用类型名称,因为这将是多余的。

  • 力求函数只实现一个功能。

  • 给函数或方法命名时,要确保它能正确描述其功能且易于理解。

  • 一致地命名函数和方法。如果我们有一个连接函数,我们可以有一个断开连接的函数。

  • 编写函数来解决当前问题,并在需要时进行泛化。尽量避免假设场景,因为可能你不会用到它(YAGNI)。

调用函数

如果函数定义在对象中,我们已经介绍了一种通用的语法来定义函数和方法。现在,我们来谈谈如何调用我们定义的函数和方法。要调用一个函数,我们将使用其名称并提供其所需的参数。在提供参数时存在一些复杂性,我们将在下一节中介绍。现在,我们将介绍最基本的参数类型,如下所示:

funcName(firstParam: "some String", secondParam: "some String")

这种类型的函数调用应该对 Objective-C 开发者来说很熟悉,因为第一个参数名称未命名,其余的都命名了。

要调用方法,我们需要使用 Swift 提供的点符号。以下示例适用于类实例方法和静态类方法:

let someClassInstance = SomeClass()
let paramName = "parameter name"
let secondParamName = "second Parameter"
someClassInstance.funcName(firstParam: paramName, secondParam:
  secondParamName)

定义和使用函数参数

在函数定义中,参数跟在函数名称后面,并且默认情况下是常量,因此如果我们不使用 var 标记它们,我们无法在函数体内部修改它们。在函数式编程中,我们避免可变性;因此,我们永远不会在函数中使用可变参数。

参数应放在括号内。如果我们没有任何参数,我们只需简单地放置一对括号,中间不包含任何字符:

func functionName() { } 

在函数式编程中,拥有至少一个参数的函数非常重要。我们将在接下来的章节中解释为什么它很重要。

我们可以有多个参数,用逗号分隔。在 Swift 中,参数是有名的,因此我们需要在冒号后提供参数名称和类型,如下例所示:

func functionName(firstParameter: ParameterType, secondParameter:
    ParameterType) {
    // function body
}

// To call:
functionName(firstParameter: paramName, secondParameter: secondParamName)

ParameterType 也可以是一个可选类型,因此如果我们的参数需要是可选的,函数将变为以下形式:

func functionName(parameter: ParameterType?, secondParameter: 
  ParameterType?) { }

Swift 允许我们提供外部参数名称,这些名称将在调用函数时使用。以下示例展示了语法:

Func functionName(externalParamName localParamName: ParameterType) 
// To call: 
functionName(externalParamName: parameter) 

在函数体中只能使用局部参数名称。

使用 _ 语法可以省略参数名称;例如,如果我们不想在调用函数时提供任何参数名称,我们可以使用 _ 作为 externalParamName 来表示第二个或后续参数。

如果我们想在函数调用中为第一个参数提供参数名称,我们基本上也可以将局部参数名称作为外部参数提供。在这本书中,我们将使用默认的函数参数定义。

参数可以有默认值,如下所示:

func functionName(parameter: Int = 3) {
    print("\(parameter) is provided.")
}

functionName(parameter: 5) // prints "5 is provided."
functionName() // prints "3 is provided."

参数可以被定义为 inout 以允许函数调用者获取在函数体中将要被改变的参数。由于我们可以使用元组作为函数的返回值,除非我们确实需要它们,否则不建议使用 inout 参数。

我们可以将函数参数定义为元组。例如,以下示例函数接受一个 (Int, Int) 类型的元组:

func functionWithTupleParam(tupleParam: (Int, Int)) {} 

在 Swift 中,变量在底层是以元组的形式表示的,因此函数的参数也可以是元组。例如,让我们有一个简单的 convert 函数,它接受一个 Int 类型的数组和乘数,并将其转换为不同的结构。现在我们不必担心这个函数的实现;我们将在 第六章 中介绍 map 函数:

let numbers = [3, 5, 9, 10]

func convert(numbers: [Int], multiplier: Int) -> [String] {
    let convertedValues = numbers.enumerated().map { (index, element) in
        return "\(index): \(element * multiplier)"
    }
    return convertedValues
}

如果我们将此函数用作 let resultOfConversion = convert(numbers: numbers, multiplier: 3),结果将是 ["0: 9", "1: 15", "2: 27", "3: 30"]

我们可以用元组来调用我们的函数。让我们创建一个元组并将其传递给我们的函数:

let parameters = (numbers: numbers, multiplier: 3)
convert(parameters)

结果与之前的函数调用相同。然而,从 Swift 3.0 开始,函数调用中传递元组已被移除,因此不建议使用它们。

我们可以定义高阶函数,这些函数可以接收函数作为参数。在以下示例中,我们将 funcParam 定义为 (Int, Int) -> Int 类型的函数类型:

func functionWithFunctionParam(funcParam: (Int, Int)-> Int) 

在 Swift 中,参数可以是泛型类型。以下示例展示了一个具有两个泛型参数的函数。在这个语法中,我们可以在 <> 内放置任何类型(例如,TV),这些类型应该用于参数定义:

func functionWithGenerics<T, V>(firstParam: T, secondParam) 

我们将在 第五章 中介绍泛型;到目前为止,了解语法就足够了。

定义和使用可变函数

Swift 允许我们定义具有可变参数的函数。可变参数可以接受零个或多个指定类型的值。可变参数类似于数组参数,但它们更易于阅读,并且只能作为多参数函数中的最后一个参数使用。

由于可变参数可以接受零值,我们需要检查它们是否为空。

以下示例展示了一个具有可变参数的String类型的函数:

func greet(names: String...) {
    for name in names {
        print("Greetings, \(name)")
    }
}

// To call this function
greet(names: "Steve", "Craig") // prints twice
greet(names: "Steve", "Craig", "Johny") // prints three times

从函数中返回值

如果我们需要我们的函数返回一个值、元组或另一个函数,我们可以在->之后提供ReturnType来指定它。例如,以下示例返回String

func functionName() -> String { } 

任何在其定义中包含ReturnType的函数,其体中都应该有一个与匹配类型的return关键字。

在 Swift 中,返回类型可以是可选的,因此如果需要返回值是可选的,函数可以如下所示:

func functionName() -> String? { } 

元组可以用来提供多个返回值。例如,以下函数返回一个(Int, String)类型的元组:

func functionName() -> (code: Int, status: String) { } 

由于我们使用括号表示元组,因此应避免在单返回值函数中使用括号。

元组返回类型也可以是可选的,因此语法如下:

func functionName() -> (code: Int, status: String)? { } 

这种语法使得整个元组都是可选的;如果我们只想使status可选,我们可以将函数定义为如下:

func functionName() -> (code: Int, status: String?) { } 

在 Swift 中,函数可以返回函数。以下示例展示了一个返回类型为函数的函数,该函数接受两个Int值并返回一个Int值:

func funcName() -> (Int, Int)-> Int {} 

如果我们预期一个函数不返回任何值、元组或函数,我们只需不提供ReturnType

func functionName() { } 

我们也可以使用Void关键字显式声明它:

func functionName() -> Void { } 

在函数式编程中,函数中具有返回类型非常重要。换句话说,避免具有Void作为返回类型的函数是一种良好的实践。具有Void返回类型的函数通常是一个会改变代码中另一个实体的函数;否则,我们为什么需要函数呢?好吧,我们可能想要将表达式记录到控制台/日志文件中,或将数据写入数据库或文件系统中的文件。在这些情况下,最好也有关于操作成功与否的返回或反馈。随着我们试图在函数式编程中避免可变性和有状态编程,我们可以假设我们的函数将以不同的形式返回。

这个要求与函数式编程的数学基础相一致。在数学中,一个简单的函数定义如下:

y = f(x) or f(x) -> y 

在这里,f是一个接受x并返回y的函数。因此,一个函数至少接收一个参数并返回至少一个值。在函数式编程中,遵循相同的范式可以使推理更容易,函数组合成为可能,代码更易于阅读。

纯函数

纯函数是没有任何副作用的功能;换句话说,它们不会改变或修改自身之外的数据或状态。此外,它们不访问任何数据或状态,除了提供的参数。纯函数就像数学函数,它们本质上是纯的。

纯函数返回的值仅由其参数值决定。纯函数易于测试,因为它们只依赖于它们的参数,并且不改变或访问任何自身之外的数据或状态。纯函数适合并发,因为它们不访问和改变全局数据或状态。

以下列表展示了纯函数和非纯函数的示例:

  • 将字符串字面量打印到控制台不是纯函数,因为它修改了外部状态。

  • 读取文件不是纯函数,因为它依赖于不同时间的外部状态。

  • 字符串的长度是纯函数,因为它不依赖于状态。它只接受一个字符串作为输入,并返回长度作为输出。

  • 获取当前日期不是纯函数,因为它在不同的日期调用时返回不同的值。

  • 获取随机数不是纯函数,因为它每次调用时都返回不同的值。

使用纯函数可能听起来非常限制性,在现实场景中似乎无法利用,但本书后面将讨论其他可以提供相同功能的工具。

我们将在下一章更详细地了解纯函数的好处。

函数类型

函数参数类型及其返回类型定义了函数的类型。例如,以下代码示例的函数类型是(Int, Double) -> String

func functionName(firstParam: Int, secondParam: Double) -> String 

我们将能够以使用其他类型的方式使用函数类型。以下代码示例展示了函数类型:

var simpleMathOperator: (Double, Double) -> Double 

在这里,simpleMathOperator是一个(Double, Double) -> Double类型的函数变量。

我们可以如下定义函数类型的typealias

typealias SimpleOperator = (Double, Double) -> Double 

我们可以在simpleMathOperator定义中使用此typealias如下:

var simpleMathOperator: SimpleOperator 

我们可以定义具有相同类型的函数并将它们分配给我们的simpleMathOperator。以下代码片段中的函数类型是(Double, Double) -> Double,这实际上是SimpleOperator

func addTwoNumbers(a: Double, b: Double) -> Double { return a + b } 

func subtractTwoNumbers(a: Double, b: Double) -> Double { return a - b } 

func divideTwoNumbers(a: Double, b: Double) -> Double { return a / b } 

func multiplyTwoNumbers(a: Double, b: Double) -> Double { return a * b } 

因此,我们可以将这些函数分配给simpleMathOperator,如下所示:

simpleMathOperator = multiplyTwoNumbers 

这意味着simpleMathOperator指向multiplyTwoNumbers函数:

let result = simpleMathOperator(3.0, 4.0) // result is 12

由于其他三个函数也有相同的函数类型,我们将能够将它们分配给相同的变量:

simpleMathOperator = addTwoNumbers 
let result = simpleMathOperator(3.5, 5.5) // result is 9 

我们可以将SimpleOperator用作其他函数的参数类型:

func calculateResult(mathOperator: SimpleOperator, a: Double, b: Double)
  -> Double {
    return mathOperator(a, b)
}

print("The result is \(calculateResult(mathOperator: simpleMathOperator,
  a: 3.5, b: 5.5))") // prints "The result is 9.0"

在这里,calculateResult函数有三个参数。mathOperator参数是函数类型。ab参数是Double类型。当我们调用这个函数时,我们传递一个simpleMathOperator函数和两个Double值作为ab

重要的是要知道我们只传递了simpleMathOperator的引用,这不会执行它。在函数体中,我们使用这个函数,并用ab调用它。

我们可以将SimpleOperator用作函数的返回类型:

func choosePlusMinus(isPlus: Bool) -> SimpleOperator {
    return isPlus ? addTwoNumbers : subtractTwoNumbers
}

let chosenOperator = choosePlusMinus(isPlus: true)
print("The result is \(chosenOperator(3.5, 5.5))") // prints "The result
  is 9.0"

在这里,choosePlusMinus函数有一个Bool参数;在其主体中,它检查此参数并返回具有相同类型SimpleOperatoraddTwoNumberssubtractTwoNumbers

重要的是要理解,调用choosePlusMinus(true)不会执行返回的函数,实际上只是返回了addTwoNumbers的引用。我们将这个引用保存在chosenOperator中。chosenOperator变量变成了以下:

func addTwoNumbers(a: Double, b: Double) -> Double { return a + b } 

当我们调用chosenOperator(3.5, 5.5)时,我们将这两个数字传递给addTwoNumbers函数并执行它。

定义函数类型的能力使得函数在 Swift 中成为一等公民。函数类型用于一等和更高阶函数。这些能力使我们能够在 Swift 中应用函数式编程范式。

定义和使用嵌套函数

在 Swift 中,我们可以在其他函数内部定义函数。换句话说,我们可以在其他函数内部嵌套函数。嵌套函数只能在它们的封装函数内部访问,并且默认情况下对外界隐藏。封装函数可以返回嵌套函数,以便允许嵌套函数在其他作用域中使用。以下示例展示了一个包含两个嵌套函数的函数,并根据其isPlus参数的值返回其中一个:

func choosePlusMinus(isPlus: Bool) -> (Int, Int) -> Int {
    func plus(a: Int, b: Int) -> Int {
        return a + b
    }
    func minus(a: Int, b: Int) -> Int {
        return a - b
    }
    return isPlus ? plus : minus
}

一等函数

在本章的“函数类型”部分,我们看到了我们可以定义函数类型并存储和传递函数。在实践中,这意味着 Swift 将函数视为值。为了解释这一点,我们需要检查几个示例:

let name: String = "Your name"

在这个代码示例中,我们创建了一个String类型的常量name,并将其中的值(`"Your name"”)存储在其中。

当我们定义一个函数时,我们需要指定参数的类型:

func sayHello(name: String)

在这个例子中,我们的name参数是String类型。这个参数可以是任何其他值类型或引用类型。简单来说,它可以是IntDoubleDictionaryArraySet,或者它可以是classstructenum等对象类型。

现在,让我们调用这个函数:

sayHello(name: "Your name") // or
sayHello(name: name)

这里,我们为这个参数传递了一个值。换句话说,我们传递了之前提到的类型及其相应的值。

Swift 将函数视为上述其他类型,因此我们可以像其他类型一样将函数存储在变量中:

var sayHelloFunc = sayHello

在这个例子中,我们将sayHello函数保存在一个变量中,稍后可以使用,并将其作为值传递。

在纯面向对象编程(OOP)中,我们没有函数;相反,我们有方法。换句话说,函数只能存在于对象中,然后它们被称为方法。在 OOP 中,类是一等公民,而方法不是。方法不是唯一可访问的,也不能被存储或传递。在 OOP 中,方法访问它们定义在其中的对象的数据。

在函数式编程中,函数是一等公民。就像其他类型一样,它们可以被存储和传递。与 OOP 不同,那种方法只能访问其父对象的数据并更改它;在函数式编程中,它们可以被存储并传递给其他对象。

这个概念使我们能够以函数作为另一种类型来组合我们的应用程序。我们将更详细地讨论这个问题;现在,重要的是要理解为什么我们在 Swift 中将函数称为一等公民。

高阶函数

正如我们在本章的定义和使用函数参数函数类型部分所看到的,在 Swift 中,函数可以接受函数作为参数。可以接受其他函数作为参数的函数称为高阶函数。这个概念与一等函数一起赋予了函数式编程和函数分解能力。

由于这个主题在函数式编程中至关重要,我们将通过另一个简单的例子来探讨。

假设我们需要开发两个函数,将两个Int值相加和相减,如下所示:

func subtractTwoValues(a: Int, b: Int) -> Int {
    return a - b
}

func addTwoValues(a: Int, b: Int) -> Int {
    return a + b
}

此外,我们还需要开发函数来计算两个Int值的平方和立方,如下所示:

func square(a: Int) -> Int {
    return a * a
}

func triple(a: Int) -> Int {
    return a * a * a // or return squareAValue(a) * a
}

假设我们需要另一个函数,该函数从两个平方值中减去:

func subtractTwoSquaredValues(a: Int, b: Int) -> Int {
    return (a * a) - (b * b)
}

假设我们需要添加两个平方值:

func addTwoSquaredValues(a: Int, b: Int) -> Int {
    return (a * a) + (b * b)
}

假设我们需要另一个函数,该函数将一个值乘以三并加到另一个乘以三的值上:

func multiplyTwoTripledValues(a: Int, b: Int) -> Int {
    return (a * a * a) * (b * b * b)
}

这样,我们不得不编写很多冗余和不灵活的函数。使用高阶函数,我们可以编写一个灵活的函数,如下所示:

typealias AddSubtractOperator = (Int, Int) -> Int
typealias SquareTripleOperator = (Int) -> Int
func calcualte(a: Int, b: Int, funcA: AddSubtractOperator, funcB:
  SquareTripleOperator) -> Int {
    return funcA(funcB(a), funcB(b))
}

这个高阶函数接受两个其他函数作为参数并使用它们。我们可以根据不同的场景调用它,如下所示:

print("The result of adding two squared values is: \(calcualte(a: 2, b: 2,
  funcA: addTwoValues, funcB: square))") // prints "The result of adding
  two squared value is: 8"

print("The result of subtracting two tripled value is: \(calcualte(a: 3,
  b: 2, funcA: subtractTwoValues, funcB: triple))") // prints "The result
  of adding two tripled value is: 19"

这个简单的例子展示了高阶函数在函数组合和程序模块化中的实用性。

函数组合

在上一节中,我们看到了一个可以接受两个不同函数并按预定义顺序执行它们的高阶函数的例子。这个函数在灵活性方面并不强,如果我们想以不同的方式组合这两个接受的函数,它就会崩溃。函数组合可以解决这个问题,并使其更加灵活。为了展示这个概念,我们首先将检查一个非函数组合的例子,然后我们将介绍函数组合。

假设在我们的应用中,我们需要与一个后端 RESTful API 进行交互,并接收一个包含按顺序排列的价格列表的String值。这个后端 RESTful API 是由第三方开发的,并且设计得并不完善。不幸的是,它返回的String中包含用逗号分隔的数字:

"10,20,40,30,80,60" 

在使用之前,我们需要格式化我们接收到的内容。我们将从String中提取元素并创建一个数组,然后我们将为每个项目添加$作为货币符号,以便在表格视图中使用。以下代码示例展示了解决这个问题的一种方法:

let content = "10,20,40,30,80,60"

func extractElements(_ content: String) -> [String] {
    return content.characters.split(separator: ",").map { String($0) }
}

let elements = extractElements(content)

func formatWithCurrency(content: [String]) -> [String] {
    return content.map {"\($0)$"}
}

let formattedElements = formatWithCurrency(content: elements)

在这个代码示例中,我们单独处理每个函数。我们可以将第一个函数的结果作为第二个函数的输入参数。两种方法都很冗长,并且不具有函数式风格。此外,我们使用了map函数,这是一个高阶函数,但我们的方法仍然不是函数式的。

让我们以函数式的方式解决这个问题。

第一步将是识别每个函数的函数类型:

  • extractElements: String -> [String]

  • formatWithCurrency: [String] -> [String]

如果我们将这些函数连接起来,我们将得到以下结果:

extractElements: String -> [String] | formatWithCurrency: [String] 
  -> [String]

我们可以将这些函数与函数组合结合,组合后的函数将是String -> [String]类型。以下示例显示了组合过程:

let composedFunction = { data in
    formatWithCurrency(content: extractElements(data))
}

composedFunction(content)

在这个例子中,我们定义了composedFunction,它由两个其他函数组成。我们能够以这种方式组合函数,因为每个函数至少有一个参数和一个返回值。这种组合类似于函数的数学组合。假设我们有一个函数f(x)返回y,以及一个g(y)函数返回z。我们可以将g函数组合为g(f(x)) -> z。这种组合使我们的g函数接受x作为参数并返回z作为结果。这正是我们在composedFunction中所做的。

自定义操作符

虽然composedFunction比非函数版本更简洁,但它看起来并不美观。而且,它也不容易阅读,因为我们需要从内向外阅读它。让我们使这个函数更简单、更易读。一个解决方案是定义一个自定义操作符,用它来代替我们的组合函数。在接下来的章节中,我们将探讨可以用来定义自定义操作符的标准操作符。我们还将探索自定义操作符定义技术。学习这个概念很重要,因为我们将在这本书的其余部分使用它。

允许的操作符

Swift 标准库提供了一些可以用来定义自定义操作符的操作符。自定义操作符可以以一个 ASCII 字符——/, =, -, +, !, *, %, <, >, &, |, ^, ?, 或 ~ 或一个 Unicode 字符开头。在第一个字符之后,允许组合 Unicode 字符。

我们还可以定义以点开头的自定义操作符。如果一个操作符不以点开头,它不能在其他地方包含点。尽管我们可以定义包含问号?的自定义操作符,但它们不能仅由一个问号字符组成。此外,尽管操作符可以包含感叹号!,后缀操作符不能以问号或感叹号开头。

自定义操作符定义

我们可以使用以下语法定义自定义操作符:

operatorType operator operatorName { } 

在这里,operatorType可以是以下之一:

  • 前缀

  • 中缀

  • 后缀

自定义中缀操作符也可以指定优先级和结合性:

infix operator operatorName { associativity left/right/none 
  precedence}

关联性的可能值是 leftrightnone。左结合性运算符如果与相同优先级的其他左结合性运算符相邻,则与左侧结合。同样,右结合性运算符如果与相同优先级的其他右结合性运算符相邻,则与右侧结合。非结合性运算符不能与具有相同优先级的其他运算符相邻。

如果未指定,关联性值默认为 none。如果未指定,优先级值默认为 100

使用前面的语法定义的任何自定义运算符在 Swift 中都不会有现有的意义;因此,应该定义并实现一个名为 operatorName 的函数。在下一节中,我们将检查自定义运算符定义及其相应函数定义的示例。

带有自定义运算符的组合函数

让我们定义一个新的自定义运算符来代替我们的组合函数:

infix operator |> { associativity left }
func |> <T, V>(f: T -> V, g: V -> V ) -> T -> V {
    return { x in g(f(x)) }
}

let composedWithCustomOperator = extractElements |> formatWithCurrency
composedWithCustomOperator("10,20,40,30,80,60")

在这个例子中,我们定义了一个新的运算符 |>,它接受两个泛型函数并将它们组合起来,返回一个函数,该函数的第一个函数的输入作为参数,第二个函数的返回值作为返回类型。

由于这个新运算符将要组合两个函数并且是二元的,我们将其定义为中缀运算符。然后我们需要使用运算符关键字。下一步将是选择我们新自定义运算符的表示法。由于我们将函数组合到左侧,我们需要将其指定为 左结合性

为了能够使用此运算符,我们需要定义相应的函数。我们的函数接受两个函数,如下所示:

  • f: 这个函数接受一个泛型类型 T 并返回一个泛型类型 V

  • g: 这个函数接受一个泛型类型 V 并返回一个泛型类型 V

在我们的例子中,我们有以下函数:

  • extractElements: String -> [String]

  • formatWithCurrency: [String] -> [String]

因此,T 变为 String,而 V 变为 [String]

我们的 |> 函数返回一个函数,该函数接受一个泛型类型 T 并返回一个泛型类型 V。我们需要从组合函数接收 String -> [String],因此,T 再次变为 String,而 V 变为 [String]

使用我们的 |> 自定义运算符可以使我们的代码更易读且更简洁。

闭包

闭包是没有 func 关键字的函数。闭包是包含特定功能的自包含代码块,可以被存储、传递并在代码中像函数一样使用。闭包捕获它们定义的上下文中的常量和变量。尽管闭包在 Objective-C 中等同于代码块,但与 C 和 Objective-C 的代码块语法相比,Swift 中的闭包语法更简单。我们在前面的部分中已经介绍过的嵌套函数是闭包的特殊情况。闭包是引用类型,可以作为变量、常量和类型别名存储。它们可以被传递给函数并从函数返回。

闭包语法

闭包的一般语法如下:

{ (parameters) -> ReturnType in
    // body of closure
}

闭包定义以{开始,然后我们定义闭包类型,最后使用in关键字将闭包定义与其实现分开。

in关键字之后,我们编写闭包体,并通过关闭}来完成我们的闭包。

闭包可以用来定义变量。以下闭包定义了一个接受Int并返回Int的类型闭包变量:

let closureName: (Int) -> (Int) = {/* */ }

闭包可以存储为可选变量。以下闭包定义了一个接受Int并返回Optional Int的类型闭包变量:

var closureName: (Int) -> (Int)?

闭包可以被定义为类型别名。以下示例展示了具有两个Int参数并返回Int的闭包的typealias

typealias closureType = (Int, Int) -> (Int)

同样的typealias也可以用于函数类型定义,因为在 Swift 中函数被命名为命名的闭包。

闭包可以用作函数调用的参数。例如,以下示例展示了一个使用闭包作为参数的函数调用,该闭包接收Int并返回Int

func aFunc(closure: (Int) -> Int) -> Int {
    // Statements, for example:
    return closure(5)
}

let result = aFunc(closure: { number in
    // Statements, for example:
    return number * 3
})

print(result)

闭包可以用作函数参数。以下示例展示了一个接收闭包的数组排序方法:

var anArray = [1, 2, 5, 3, 6, 4]

anArray.sort(isOrderedBefore: { (param1: Int, param2: Int) -> Bool in
    return param1 < param2
})

此语法可以通过隐式类型简化,因为 Swift 编译器能够从上下文中推断参数的类型:

anArray.sort(isOrderedBefore: { (param1, param2) -> Bool in
    return param1 < param2
})

使用 Swift 的类型推断,可以通过隐式返回类型进一步简化语法:

anArray.sort(isOrderedBefore: { (param1, param2) in
    return param1 < param2
})

当我们需要将闭包作为函数的最后一个参数传递时,Swift 允许我们省略开闭括号,换句话说,如果我们的闭包是尾随闭包:

anArray.sort { (param1, param2) in
    return param1 < param2
}

此外,Swift 还提供了一个简写参数符号,可以用作替代使用参数:

anArray.sort {
    return $0 < $1
}

我们可以通过消除return关键字进一步简化此语法,因为我们只有一行表达式如下:

anArray.sort {  0 < $1 }

使用 Swift 的类型推断,我们能够极大地简化闭包的语法。

捕获值

闭包可以捕获它们创建的周围上下文中的变量和常量。闭包可以在其体内部引用这些变量并修改它们,即使定义变量的原始作用域不再存在。

当闭包作为函数的参数传递但函数返回后调用时,称闭包逃逸了函数。闭包逃逸的一种方式是将其存储在函数外部定义的变量中。

以下是一个逃逸闭包的示例,换句话说,是完成处理程序:

func sendRequest(responseType: String.Type, completion:
  (responseData:String, error:NSError?) -> Void) {
    // execute some time consuming operation, if successful {
        completion(responseData: "Response", error: nil)
    //}
}

sendRequest(String.self) {
    (response: String?, error: NSError?) in
    if let result = response {
        print(result)
    } else if let serverError = error {
        // Error
    }
}

我们有一个名为sendRequest的函数,它有两个参数——responseTypeString.Type类型,以及completion,它是一个闭包类型,接受一个String和一个可选的NSError参数,并且不返回任何值。

假设我们在函数体中执行一些异步耗时操作,例如从文件中读取、从数据库中读取或调用网络服务。

要调用此函数,我们需要提供String.self和一个闭包作为参数。我们的闭包中有两个变量——一个名为responseOptional String类型的变量和一个名为errorNSError可选类型的变量。由于我们的函数没有返回类型,它不会向其调用者返回任何值。这就是函数逃逸的概念。

我们传递的闭包在函数外部逃逸,因为它将在耗时的异步操作成功完成后被调用,并且随后发生调用:

completion(responseData: "Response", error: nil)

在这个调用中,我们传递responseData和错误,并调用完成闭包。然后调用函数的闭包体使用传递的变量执行。这个概念是一个非常强大的概念,它简化了所有的异步操作。与委托和通知等机制相比,它非常易于阅读和跟踪。

函数柯里化

函数柯里化将具有多个参数的单个函数转换为一系列具有一个参数的函数。让我们看看一个例子。假设我们有一个将firstNamelastName组合起来返回全名的函数:

func extractFullUserName(firstName: String, lastName: String) -> String {
    return "\(firstName) \(lastName)"
}

此函数可以转换为以下柯里化函数:

func curriedExtractFullUserName(firstName: String)(lastName:
  String) -> String {
    return "\(firstName) \(lastName)"
}

如此,我们可以看到我们将逗号替换为) (括号。

因此现在我们可以使用此函数如下:

let fnIncludingFirstName = curriedExtractFullUserName("John")
let extractedFullName = fnIncludingFirstName(lastName: "Doe")

在这里,fnIncludingFirstName将包含firstName,这样当我们使用它时,我们可以提供lastName并提取全名。我们将在接下来的章节中使用这种技术。

从 Swift 2.2 开始,Apple 已经弃用了函数柯里化,并将其从 Swift 3.0 中移除。建议将函数柯里化转换为显式返回闭包:

// Before:
func curried(x: Int)(y: String) -> Float {
    return Float(x) + Float(y)!
}

// Swift 3.0 syntax:
func curried(x: Int) -> (String) -> Float {
    return {(y: String) -> Float in
        return Float(x) + Float(y)!
    }
}

让我们将我们的柯里化函数显式地转换为返回闭包版本:

func explicityRetunClosure(firstName: String) -> (String) -> String {
    return { (lastName: String) -> String in
        return "\(firstName) \(lastName)"
    }
}

我们可以使用此函数如下,结果将是相同的:

let fnIncludingFirstName = explicityRetunClosure(firstName: "John")
let extractedFullName = fnIncludingFirstName("Doe")

递归

递归是函数在其内部调用自己的过程。调用自己的函数是递归函数。

递归最适合用于可以将大问题分解为重复子问题的问题。由于递归函数会调用自己来解决这些子问题,最终函数将遇到一个它可以不调用自己就能处理的子问题。这被称为基本情况,这是防止函数不断调用自己而停止所需的。

在基本情况中,函数不会调用自己。然而,当一个函数必须调用自己以处理其子问题时,这被称为递归情况。因此,在使用递归算法时,有两种类型的情况:基本情况递归情况。重要的是要记住,在递归和尝试解决问题时,我们应该问自己:我的基本情况是什么,我的递归情况是什么?

要应用这个简单的过程,让我们从一个递归的例子开始:阶乘函数。在数学中,一个数字后面的感叹号(n!)表示该数字的阶乘。一个数字 n 的阶乘是介于 1n 之间所有整数的乘积。所以,如果 n 等于 3,那么 n 的阶乘将是 3 * 2 * 1,等于 6。我们也可以说 3 的阶乘等于 3 乘以 2 的阶乘,即 3 * 2!3 * 2 * 1。所以,任何数字 n 的阶乘也可以定义为以下形式:

n! = n * (n - 1)!

我们还需要了解以下内容:

0! = 1! = 1

注意我们如何定义一个数字的阶乘为该数字乘以比该数字小 1 的整数的阶乘 (n * (n - 1)!)。所以,我们实际上是将问题分解为一个子问题,为了找到某个数字的阶乘,我们不断寻找比该数字小的整数的阶乘并将其相乘。所以,3 的阶乘等于 3 乘以 2 的阶乘,而 2 的阶乘等于 2 乘以 1 的阶乘。所以,如果我们有一个函数来找到给定数字的阶乘,那么我们的递归情况代码将类似于以下这样:

func factorial(n: Int) -> Int {
    return n * factorial(n: n - 1)
}

在这里,我们想要找到 n 个数的阶乘。

在这个例子中,我们将问题分解为一个子问题。仍然有一个问题需要我们解决。我们需要检查基本情况,以便能够停止函数无限调用自身。

因此,我们可以修改我们的阶乘示例如下:

func factorial(n: Int) -> Int {
    return n == 0 || n == 1 ? 1 : n * factorial(n: n - 1)
}

print(factorial(n: 3))

如此例所示,我们检查 n;如果它是 01,则返回 1 并停止递归。

另一个简单递归函数的例子如下:

func powerOfTwo(n: Int) -> Int {
    return n == 0 ? 1 : 2 * powerOfTwo(n: n - 1)
}

let fnResult = powerOfTwo(n: 3)

这个示例的非递归版本如下:

func power2(n: Int) -> Int {
    var y = 1
    for _ in 0...n - 1 {
        y *= 2
    }
    return y
}

let result = power2(n: 4)

从这个例子中我们可以看出,递归版本更具有表达性和更简洁。

以下示例展示了一个重复给定字符串所需时间的函数:

func repateString(str: String, n: Int) -> String {
    return n == 0 ? "" : str + repateString(str: str , n: n - 1)
}

print(repateString(str: "Hello", n: 4))

以下代码片段展示了不使用递归实现相同功能的方式,换句话说,以命令式编程风格:

func repeatString(str: String, n: Int) -> String {
    var ourString = ""
    for _ in 1...n {
        ourString += str
    }
    return ourString
}

print(repeatString(str: "Hello", n: 4))

非递归、命令式版本的代码稍微长一些,我们需要使用 for 循环和变量才能达到相同的结果。一些函数式编程语言,如 Haskell,没有 for 循环机制,我们必须使用递归;在 Swift 中,我们有 for 循环,但正如我们在这里看到的,尽可能使用递归函数会更好。

尾递归

尾递归是递归的一种特殊情况,在这种情况中,调用函数在对自己进行递归调用后不再执行任何操作。换句话说,如果一个函数的最终表达式是一个递归调用,那么这个函数就被称为尾递归函数。我们之前介绍过的递归示例都不是尾递归函数。

为了理解尾递归,我们将使用尾递归技术开发我们之前开发的 factorial 函数。然后我们将讨论它们之间的区别:

func factorial(n: Int, currentFactorial: Int = 1) -> Int {
    return n == 0 ? currentFactorial : factorial(n: n - 1,
      currentFactorial: currentFactorial * n)
}

print(factorial(n: 3))

注意,我们为 currentFactorial 提供了一个默认参数 1,但这只适用于函数的第一次调用。当阶乘函数递归调用时,默认参数会被递归调用传递的任何值覆盖。我们需要有那个第二个参数,因为它将保存我们打算传递给函数的当前阶乘值。

让我们尝试理解它是如何工作的,以及它与另一个阶乘函数的不同之处:

factorial(n: 3, currentFactorial: 1)
return factorial(n: 2, currentFactorial: 1 * 3) // n = 3
return factorial(n: 1, currentFactorial: 3 * 2) // n = 2
return 6 // n = 1

在这个函数中,每次调用阶乘函数时,都会将一个新的 currentFactorial 值传递给函数。函数基本上通过每次对自己的调用来更新 currentFactorial。由于它接受 currentFactorial 作为参数,我们能够保存当前的阶乘值。

所有对阶乘的递归调用,如 factorial(2, 1 * 3),实际上并不需要返回才能得到最终值。我们可以看到,在任何一个递归调用实际返回之前,我们实际上已经到达了 6 这个值。

因此,如果一个函数的递归调用的最终结果——在这个例子中是 6——也是函数本身的最终结果,那么这个函数就是尾递归的。非尾递归函数在其最后一个函数调用中并没有达到最终状态,因为所有导致最后一个函数调用的递归调用都必须返回,才能得到最终结果。

记忆化

记忆化是将函数的输入结果存储起来的过程,以便提高我们程序的性能。我们可以记忆化纯函数,因为纯函数不依赖于外部数据,也不改变自身之外的内容。纯函数对于给定的输入每次都提供相同的结果。因此,我们可以保存或缓存结果——换句话说,记忆化结果——并使用它们在将来而不必经过计算过程。

为了理解这个概念,让我们看看以下示例,我们将手动记忆化 power2 函数:

var memo = Dictionary<Int, Int>()

func memoizedPower2(n: Int) -> Int {
    if let memoizedResult = memo[n] {
        return memoizedResult
    }
    var y = 1
    for _ in 0...n-1 {
        y *= 2
    }
    memo[n] = y
    return y
}
print(memoizedPower2(n: 2))
print(memoizedPower2(n: 3))
print(memoizedPower2(n: 4))
print(memo) // result: [2: 4, 3: 8, 4: 16]

如示例所示,我们定义了一个 [Int, Int] 类型的字典。我们将函数的输入结果保存到这个字典中。

这种方法可以正常工作,但我们需要手动修改和维护函数外部的一个集合,以便能够记忆化函数的结果。此外,它还为每个需要记忆化的函数添加了大量的模板代码。

在 2014 年的全球开发者大会WWDC)上展示的高级 Swift会议([developer.apple.com/videos/play/wwdc2014-404/](https://developer.apple.com/videos/play/wwdc2014-404/))提供了一个非常方便的记忆化函数,可以与任何纯函数一起使用。

观看视频是非常推荐的。让我们看看我们能否使用该会话中的 memoize 函数来自动化这个功能并重用它:

func memoize<T: Hashable, U>(fn: ((T) -> U, T) -> U) -> (T) -> U {
    var memo = Dictionary<T, U>()
    var result: ((T) -> U)!
    result = { x in
        if let q = memo[x] { return q }
        let r = fn(result, x)
        memo[x] = r
        return r
    }
    return result
}

这个函数看起来很复杂,但不用担心,我们将详细讲解它。

首先,它是一个泛型函数。不要担心泛型——我们将在第五章泛型和关联类型协议中详细讲解泛型,并且使用 Hashable 是因为我们需要将 T 作为键存储在字典中。

如果我们查看函数的签名,我们会看到 memoize 函数接受一个有两个参数和返回类型的函数。因此,fn 的签名,它是一个函数,如下所示:

((T) -> U, T) -> U

fn 的第一个参数是 (T) -> U 类型的函数,第二个参数是 T 类型,最后 fn 返回 U 类型。

好的,memoize 函数接收了前面代码片段中描述的 fn

最后,memoize 函数返回一个 (T) -> U 类型的函数。现在让我们看看 memoize 函数的主体。首先,我们需要一个字典来缓存结果。其次,我们需要定义结果类型,它是一个闭包。在闭包体中,我们检查是否已经在我们的字典中有了这个键。如果有,我们返回它,否则调用函数并将结果保存在我们的缓存字典中。

现在我们可以使用这个函数来缓存不同函数调用的结果,并提高我们程序的性能。

以下示例展示了阶乘函数的缓存版本:

let factorial = memoize { factorial, x in
    x == 0 ? 1 : x * factorial(x - 1)
}

print(factorial(5))

memoize 函数期望一个闭包作为输入,因此我们可以使用尾随闭包语法。在先前的例子中,我们将阶乘函数和 x 参数作为输入传递给闭包,in 关键字之后的行是闭包的主体。在先前的例子中,我们使用 memoize 对递归函数进行了缓存,并且它工作得很好。让我们看看另一个例子:

let powerOf2 = memoize { pow2, x in
    x == 0 ? 1 : 2 * pow2(x - 1)
}

print(powerOf2(5))

在这个例子中,我们使用 memoize 函数来获取 powerOf2 函数的缓存版本。

只需编写一次 memoize 函数,我们就能将其用于任何纯函数来缓存数据并提高我们程序的性能。

摘要

本章从详细解释函数定义和用法开始,通过给出参数和返回类型的示例。然后,它继续介绍与函数式编程相关的概念,如纯函数、一等函数、高阶函数和嵌套函数。最后,它涵盖了函数组合、闭包、柯里化和缓存。到这一点,我们应该熟悉不同类型的函数和闭包及其用法。

在下一章中,我们将介绍类型,并探讨值类型与引用类型的概念。同时,我们将详细探讨值类型的特性,包括类型相等性、身份和转换。

第三章:类型与类型转换

本章首先解释类型,非常简短地触及范畴论中的类型概念。然后,它解释值类型和引用类型,并详细比较它们。最后,它讨论了相等性、身份和类型转换。

本章将通过代码示例涵盖以下主题:

  • 类型

  • 值类型与引用类型

    • 值类型和引用类型常量

    • 混合值类型和引用类型

    • 复制

    • 值类型特性

  • 相等性、身份和比较

  • 类型检查和转换

你可能听说过函数式编程使用范畴论的概念。这个链接是为什么有些人觉得函数式编程更接近数学的原因。在下一章中,我们将简要介绍范畴论,所以我们现在不会深入探讨这些概念。在此阶段,重要的是要知道,从理论上讲,范畴指的是包含以下内容的集合:

  • 一组对象(Swift 中的类型)

  • 一组将两个对象联系在一起的形态(Swift 中的函数)

  • 范畴的形态组合(Swift 中的函数组合)

我们已经讨论了函数和函数组合,现在我们将探索类型。

有两种不同的方式来对类型进行分类。第一种是 Swift 中命名的类型和复合类型的概念。第二种是基于值与引用的类型分类。

我们在定义时可以为其命名的任何类型都是命名类型。例如,如果我们创建一个名为OurClass的类,OurClass的任何实例都将具有OurClass类型。

函数类型和元组类型是复合类型。复合类型可以包含命名类型和其他复合类型。例如,(String, (Double, Double))是一个复合类型,实际上是一个String和另一个(Double, Double)类型元组的元组。

我们可以在类型注释、标识和别名中使用命名类型和复合类型。

在前面的章节中,我们已经看到我们可以使用 Swift 的推断来推断类型,除非我们想要显式地指定类型。如果我们需要显式地指定类型,我们会注释类型。

此外,我们并没有过多地讨论引用类型与值类型以及类型转换。在本章的后续部分,我们将探讨这些概念。

值类型与引用类型

在 Swift 中,有两种类型的类型:值类型和引用类型。

值类型实例保留其数据的一个副本。每种类型都有自己的数据,并且不被其他变量引用。Structuresenumstuples是值类型;因此,它们不会在其实例之间共享数据。赋值会复制实例的数据到另一个实例,并且没有引用计数。以下示例展示了具有复制的struct

struct ourStruct {
    var data: Int = 3
}

var valueA = ourStruct()
var valueB = valueA // valueA is copied to valueB
valueA.data = 5 // Changes valueA, not valueB
print("\(valueA.data), \(valueB.data)") // prints "5, 3"

如前例所示,改变valueA.data不会改变valueB.data

在 Swift 中,数组、字典、字符串和集合都是值类型。

另一方面,引用类型实例共享相同的数据副本。类和闭包是引用类型,所以赋值只添加一个引用,而不复制数据。实际上,引用类型的初始化创建了一个共享实例,该实例将被引用类型的不同实例(如类或闭包)使用。同一类类型的两个变量将引用数据的一个单一实例,因此如果我们修改其中一个变量的数据,它也会影响另一个变量。以下示例展示了具有引用的类:

class ourClass {
    var data: Int = 3
}
var referenceA = ourClass()
var referenceB = referenceA // referenceA is copied to referenceB
referenceA.data = 5 // changes the instance referred to by
  referenceA and referenceB
print("\(referenceA.data), \(referenceB.data)") // prints "5, 5"

如前例所示,更改 referenceA.data 也会更改 referenceB.data,因为它们引用了相同的共享实例。

值类型和引用类型之间的这种基本差异可能对我们的系统架构产生巨大影响。在函数式编程中,建议优先使用值类型而不是引用类型,因为值类型更容易追踪和推理。由于我们总是得到数据的一个唯一副本,并且数据在实例之间不共享,我们可以推断出程序的其他部分不会更改数据。值类型的这一特性使它们在多线程环境中特别有用,因为在不同的线程中可以更改我们的数据而无需通知我们。这可能会创建非常难以调试和修复的错误。

为了能够在 Swift 中使用类实现此功能,我们可以仅使用不可变存储属性开发不可变类,并避免公开任何可以更改状态的 API。然而,Swift 并没有提供任何语言机制来强制执行类不可变性,就像它强制执行 structenum 的不可变性一样。任何 API 用户都可以子类化我们提供的类并将其变为可变的,除非我们将其定义为 final。这与 structenum 和元组不同,因为我们基本上不能对它们进行子类化。

值类型和引用类型常量

常量的行为取决于它们是值类型还是引用类型。我们可以在常量类中更改变量,但不能在结构体中更改。

让我们检查以下示例:

class User {
    var name: String
    init(name: String) {
        self.name = name
    }
}

let julie = User(name: "Julie")
let steve = User(name: "Steve")

struct Student {
    var user: User
}

let student = Student(user: julie)
student.user = steve // compiler error - cannot assign to
  property: 'student' is a 'let' constant

在这个例子中,我们有一个名为 User 的类和两个指向该类实例的常量。此外,我们还有一个具有 User 类型变量的 Student 结构体。

我们使用 Student 结构体创建 student。如果我们尝试更改 student 中的 user 变量,编译器会给出错误,指出 student 是一个常量,尽管我们定义 user 为一个变量。

因此,如果我们将 struct 实例化为一个常量,我们无法更改其中的任何变量。换句话说,let student = Student(user: julie) 使得整个 struct 都是不可变的。

让我们用类来尝试相同的操作。在下面的代码中,我们更改了名为 steve 的常量名称。编译器没有给出错误并接受这个赋值。

steve.name = "Steve Jr." 
steve.name // prints "Steve Jr." 

尽管我们将 steve 定义为常量,但我们仍然可以更改 name 变量,因为它是一个 class

从前面的例子中,我们已经看到我们可以改变一个常量实例的 class(引用类型)变量的值,但不能改变一个常量实例的 struct(值类型)变量的值。

由于 steve 是引用类型的实例,它引用了 User 的实例。当我们更改 name 时,我们实际上并没有改变 steve 是什么,steve 是对 User 的引用。我们改变的是我们通过将其定义为变量而使其可变的名称。这并不适用于我们的 student 常量,因为它是一个值类型。将其定义为常量也使其变量成为常量。

引用类型的这种特性使得它们难以追踪,并且由于我们将它们定义为常量,这并不会使它们免受更改的影响。为了使它们不可变,我们需要将它们的属性定义为常量。

混合值类型和引用类型

在现实世界的问题中,我们可能需要混合引用类型和值类型。例如,我们可能需要在 struct 中有对 class 的引用,就像我们之前的例子一样,或者我们可能需要在 class 中有 struct 变量。在这种情况下,我们如何推理赋值和复制呢?

让我们检查以下示例:

class User {
    var name: String
    init(name: String) {
        self.name = name
    }
}
let julie = User(name: "Julie")

struct Student {
    var user: User
}

let student = Student(user:julie)
student.user.name // prints "Julie"
let anotherStudent = student
julie.name = "Julie Jr."
anotherStudent.user.name // prints "Julie Jr."

在这个例子中,我们有一个 User 类,一个包含用户变量的 Student 结构体。我们定义一个常量 student,其值为 julie,它是 class 类型。如果我们打印 student.user.name,结果将是 julie

现在如果我们定义 anotherStudent 并通过赋值将其复制到 student,更改朱莉的名字也会更改 anotherStudent 的名字。

我们预计 anotherStudent 将有 student 的一个副本,但 name 已经更改了。这是因为 user 变量是 User 类型,它是 class 类型,因此是引用类型。

这个例子展示了在值类型中使用引用类型的复杂性。为了避免这些复杂性,建议避免在值类型内部使用引用类型变量。如果我们需要在我们的值类型中使用引用类型,正如我们之前所述,我们应该将它们定义为常量。

复制

值类型上的赋值操作将值从一个值类型复制到另一个值类型。在不同的编程语言中,有两种复制类型,浅度复制和深度复制。

浅度复制尽可能少地复制。例如,集合的浅度复制是集合结构的副本,而不是其元素。在浅度复制中,两个集合共享相同的单个元素。

深度复制会复制一切。例如,一个集合的深度复制将导致另一个集合,其中包含原始集合中所有元素的副本。

Swift 进行浅度复制,并且不提供深度复制的机制。让我们通过以下示例来了解浅度复制:

let julie = User(name: "Julie")
let steve = User(name: "Steve")
let alain = User(name: "Alain")
let users = [alain, julie, steve]

在前面的示例中,我们创建了一个名为 alain 的新 User 对象,并将三个用户添加到了一个名为 users 的新数组中。在下面的示例中,我们将 users 数组复制到了一个名为 copyOfUsers 的新数组中。然后我们按照以下方式更改 users 数组中一个用户的名称:

let copyOfUsers = users
users[0].name = "Jean-Marc"

print(users[0].name) // prints "Jean-Marc"
print(copyOfUsers[0].name) // prints "Jean-Marc"

打印 userscopyOfUsers 将显示,在 users 数组中将 Alainname 更改为 Jean-Marc 也会将 copyOfUsers 中的 Alain 的名称更改为 Jean-MarcuserscopyOfUsers 都是数组,我们预计赋值表达式会像数组是值类型一样从 users 复制值到 copyOfUsers,但正如前一个示例所示,在一个数组中更改 user 的名称也会更改复制数组中的用户名。这种行为有两个原因。首先,Userclass 类型的一种。因此,它是一个引用类型。其次,Swift 进行浅拷贝。

浅拷贝并不提供与示例中看到的不同实例的副本,浅拷贝只是复制了实例相同元素的引用。因此,这个示例再次展示了在 Swift 中使用引用类型作为值类型时的复杂性,因为 Swift 不提供深拷贝来克服这些复杂性。

复制引用类型

两个变量可以指向同一个对象,因此更改一个变量也会更改另一个变量。在某些情况下,让许多对象指向相同的数据可能是有用的,但大多数情况下,我们希望修改副本,以便修改一个对象不会影响其他对象。为了实现这一点,我们需要做以下事情:

  • 我们的类应该是 NSObject 类型

  • 我们的类应该遵守 NSCopying 协议(这不是强制性的,但可以使我们的 API 用户意图更明确)

  • 我们的类应该实现 copy(with: NSZone) 方法

  • 要复制对象,我们需要在对象上调用 copy() 方法

这里是一个完全符合 NSCopying 协议的 Manager 类的示例:

class Manager: NSObject, NSCopying {
    var firstName: String
    var lastName: String
    var age: Int

    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }

    func copy(with: NSZone? = nil) -> AnyObject {
        let copy = Manager(firstName: firstName, lastName: lastName,
          age: age)
        return copy
    }
}

copyWithZone() 函数通过使用当前 Manager 的信息创建一个新的 Manager 对象来实现。为了测试我们的类,我们创建了两个实例,并将一个实例复制到另一个实例上,如下所示:

let john = Manager(firstName: "John", lastName: "Doe", age: 35)
let jane = john.copy() as! Manager

jane.firstName = "Jane"
jane.lastName = "Doe"
jane.age = 40

print("\(john.firstName) \(john.lastName) is \(john.age)")
print("\(jane.firstName) \(jane.lastName) is \(jane.age)")

结果将如下所示:

"John Doe is 35"
"Jane Doe is 40"

值类型特性

我们已经研究了值类型和引用类型的观念。我们探讨了值类型与引用类型使用的简单场景。我们理解使用值类型可以使我们的代码更简单,更容易追踪和推理。现在让我们更详细地看看值类型的特性。

行为

值类型不具有行为。值类型存储数据并提供使用其数据的方法。值类型只能有一个所有者,并且由于没有引用,它没有析构器。一些值类型的方法可能会使值类型自身发生突变,但控制流严格由实例的单个所有者控制。由于代码仅在直接由单个所有者调用时执行,而不是来自多个来源,因此很容易推理值类型代码的执行流程。

另一方面,引用类型可能会将自己订阅为其他系统的目标。它可能会从其他系统接收通知。这种类型的交互需要引用类型,因为它们可以有多个所有者。在大多数情况下,开发能够自行执行副作用的价值类型是不必要的困难。

隔离

典型的值类型对外部系统的行为没有隐式依赖。因此,值类型是隔离的。它只与其所有者交互,与引用类型与多个所有者交互相比,其交互方式更容易理解。

如果我们访问一个可变实例的引用,我们隐式地依赖于所有其他所有者,并且他们可以在不通知我们的情况下随时更改实例。

可互换性

由于值类型在分配给新变量时被复制,因此所有这些副本都是完全可互换的。

我们可以安全地存储传递给我们的值,然后稍后将其用作新值。不可能使用除其数据之外的其他任何方式来比较实例。

可互换性还意味着给定值的定义方式并不重要。如果通过 == 比较结果为相等,则两个值类型在所有意义上都是相等的。

可测试性

对于处理值类型的单元测试,没有必要使用模拟框架。我们可以直接定义与我们的应用程序中的实例不可区分的值。

如果我们使用具有行为的行为类型,我们必须测试我们将要测试的行为类型与系统其余部分之间的交互。这通常意味着大量的模拟或大量的设置代码来建立所需的关系。

相反,值类型是隔离和可互换的,因此我们可以直接定义一个值,调用一个方法,并检查结果。更简单的测试具有更大的覆盖率,从而产生更容易更改和维护的代码。

威胁

虽然值类型的结构鼓励可测试性、隔离性和可互换性,但可以定义减少这些优势的值类型。包含在所有者未调用的情况下执行代码的值类型通常难以跟踪和推理,通常应避免。

此外,包含引用类型的值类型并不一定是隔离的。在值类型中使用引用类型通常应避免,因为它们依赖于该引用的所有其他所有者。这类值类型也不容易互换,因为外部引用可能会与系统的其余部分交互并引起一些复杂问题。

使用值类型和引用类型

《Swift 编程语言(Swift 3.0)》苹果公司 出版,其中有一节介绍了如何比较结构体(值类型)和类(引用类型),以及如何选择其中一种类型而舍弃另一种。强烈建议阅读该部分内容,以了解我们为什么选择其中一种类型而舍弃另一种。尽管我们在 第一章Swift 中的函数式编程入门 中简要地提到了这个话题,但我们仍将进一步探讨这个话题,因为在函数式编程中,引用类型和值类型的区别非常重要。

在面向对象编程中,我们将现实世界中的对象建模为类和接口。例如,为了模拟一家提供不同类型披萨的意大利餐厅,我们可能有一个披萨对象及其子类,如玛格丽塔、那不勒斯或罗马披萨。每种披萨都会有不同的配料。不同的餐厅可能制作得略有不同,而且每当我们在不同的书籍或网站上阅读它们的食谱时,我们可能会有不同的理解。这种抽象级别使我们能够引用特定的披萨,而不必关心其他人真正想象的那种披萨。当我们谈论那种披萨时,我们并不是转移它,我们只是引用它。

另一方面,在我们的意大利餐厅中,我们需要向顾客提供账单。每当他们要求账单时,我们将提供有关数量和价格的真实信息。任何人对于数量、美元价格以及实际上价值的感知都是相同的。我们的顾客可以计算发票总额。如果我们的顾客修改账单,它不会修改我们用来提供账单的原始数据。无论他们在账单上写什么,或者洒上酒,账单的价值和总额都不会改变。前面的例子展示了引用类型与值类型在现实世界中的简单应用。在 Swift 编程语言以及网络、移动或桌面应用程序编程中,值类型和引用类型都有它们自己的用途。

值类型使我们能够使架构更清晰、更简单、更易于测试。值类型通常对外部状态有较少或没有依赖,因此在推理它们时考虑的因素更少。

此外,值类型因其可互换性而本质上更具可重用性。

随着我们使用更多的值类型和不可变实体,我们的系统将随着时间的推移变得更加易于测试和维护。

相比之下,引用类型是系统中的行为实体。它们有身份。它们可以表现。它们的行为通常是复杂且难以推理的,但其中的一些细节通常可以用简单的值和涉及这些值的隔离函数来表示。

引用类型维护由值定义的状态,但这些值可以独立于引用类型来考虑。

引用类型会执行副作用,例如 I/O、文件和数据库操作,以及网络操作。

引用类型可以与其他引用类型交互,但它们通常发送值,而不是引用,除非它们真正计划与外部系统创建持久连接。

在不需要创建共享可变状态的情况下,尽可能多地使用值类型(enumstuplesstructs)。有些情况下我们必须使用类。例如,当我们使用 Cocoa 时,许多 API 期望 NSObject 的子类,因此在这些情况下我们必须使用类。每次我们需要使用类时,我们避免使用变量;我们将我们的属性定义为常量,并避免暴露任何可以改变状态的 API。

相等性与同一性

如果两个实例具有相同的值,则它们是相等的。相等性用于确定两个值类型的相等性。例如,如果两个 String 具有相同的文本值,则它们是相等的。== 运算符用于检查相等性。以下示例展示了两个 Int 数字(Int 是值类型)的相等性检查:

let firstNumber = 1
let secondNumber = 1

if firstNumber == secondNumber {
    print("Two numbers are equal") // prints "Two numbers are equal\n"
}

另一方面,如果两个实例引用了相同的内存实例,则它们是相同的。同一性用于确定两个引用类型是否相同。=== 运算符用于检查同一性。以下示例展示了我们之前定义的 User 类的两个实例的同一性检查:

let julie = User(name: "Julie")
let steve = User(name: "Steve")

if julie === steve {
    print("Identical")
} else {
    print("Not identical")
}

标识检查运算符仅适用于引用类型。

Equatable 和 Comparable

我们能够比较两种值类型,例如 StringIntDouble,但我们不能比较我们自己开发的两种值类型。为了使我们的自定义值类型可比较,我们需要实现 Equatable 和 Comparable 协议。让我们首先检查一个不遵守协议的相等性检查的例子:

struct Point {
    let x: Double
    let y: Double
}

let firstPoint = Point(x: 3.0, y: 5.5)
let secondPoint = Point(x: 7.0, y: 9.5)

let isEqual = (firstPoint == secondPoint)

在这个例子中,编译器会抱怨 二进制运算符 '==' 不能应用于两个 'Point' 操作数。让我们通过遵守 Equatable 协议来解决这个问题:

struct Point: Equatable {
    let x: Double
    let y: Double
}

func ==(lhs: Point, rhs:Point) -> Bool {
    return (lhs.x == rhs.x) && (lhs.y == lhs.y)
}

let firstPoint = Point(x: 3.0, y: 5.5)
let secondPoint = Point(x: 7.0, y: 9.5)

let isEqual = (firstPoint == secondPoint)

isEqual 的值将会是 false,因为它们并不相等。为了能够比较两个点,我们需要遵守 Comparable 协议。我们的例子如下:

struct Point: Equatable, Comparable {
    let x: Double
    let y: Double
}

func ==(lhs: Point, rhs:Point) -> Bool {
    return (lhs.x == rhs.x) && (lhs.y == lhs.y)
}

func <(lhs: Point, rhs: Point) -> Bool {
    return (lhs.x < rhs.x) && (lhs.y < rhs.y)
}

let firstPoint = Point(x: 3.0, y: 5.5)
let secondPoint = Point(x: 7.0, y: 9.5)

let isEqual = (firstPoint == secondPoint)
let isLess = (firstPoint < secondPoint)

比较的结果将是 true

类型检查和类型转换

Swift 提供类型检查和类型转换。我们可以使用 is 关键字检查变量的类型。它最常用于 if 语句中,如下面的代码所示:

let aConstant = "String"

if aConstant is String {
    print("aConstant is a String")
} else {
    print("aConstant is not a String")
}

由于 String 是值类型,编译器可以推断类型,因此 Swift 编译器会发出警告,因为它已经知道 aConstantString 类型。另一个例子如下,我们检查 anyString 是否是 String 类型:

let anyString: Any = "string"

if anyString is String {
    print("anyString is a String")
} else {
    print("anyString is not a String")
}

使用 is 操作符有助于检查类实例的类型,特别是具有子类的实例。我们可以使用 is 操作符来确定一个对象是否是特定类的实例。

同样,我们可以使用 as 操作符将对象强制转换为编译器推断的类型之外的类型。as 操作符有两种形式:普通的 as 操作符和 as?。前者在不需要询问的情况下将对象转换为所需的类型。如果对象无法转换为该类型,则会抛出运行时错误。as? 操作符询问对象是否可以转换为给定的类型。如果对象可以转换,则返回 some 值;否则,返回 nilas? 操作符通常用作 if 语句的一部分。

显然,在可能的情况下最好使用 as?。我们应该只在知道它不会导致运行时错误的情况下使用 as

摘要

在本章中,我们探讨了类型的一般概念,并详细探讨了引用类型和值类型。我们涵盖了值类型和引用类型常量、值类型和引用类型的混合以及复制等内容。然后我们学习了值类型的特征、值类型和引用类型之间的关键区别以及我们应该如何决定使用哪一个。我们继续探讨了相等性、身份、类型检查和类型转换等主题。尽管我们探讨了值类型的话题,但我们没有在本章中探讨一个相关的话题——不可变性。第九章,不可变性的重要性将涵盖不可变性的重要性。此外,为了深入了解这些概念,建议观看以下视频:WWDC 2015 - Session 414,WWDC 2016 - Session 418,和 WWDC 2016 - Session 419。

在下一章中,我们将探讨枚举和模式匹配主题。我们将熟悉关联值和原始值。我们将介绍代数数据类型,最后,我们将涵盖模式和模式匹配。

第四章:枚举和模式匹配

在第一章中,我们简要介绍了枚举。在本章中,我们将详细介绍枚举和代数数据类型。此外,我们还将探讨 Swift 中的模式和模式匹配。

本章将通过代码示例涵盖以下主题:

  • 定义枚举

  • 关联值

  • 原始值

  • 使用枚举

  • 代数数据类型

  • 模式和模式匹配

定义枚举

在 Swift 中,枚举定义了一个相关值的公共类型,并使我们能够以类型安全的方式处理这些值。为每个枚举成员提供的值可以是StringCharacterInteger或任何floating-point类型。以下示例展示了枚举的一个简单定义:

enum MLSTeam {
    case montreal
    case toronto
    case newYork
    case columbus
    case losAngeles
    case seattle
}

let theTeam = MLSTeam.montreal

MLSTeam enum为我们提供了选择 MLS 球队的选择。我们每次只能选择一个选项;在我们的示例中,选择了Montreal

可以定义多个情况,用单行上的逗号分隔:

enum MLSTeam {
    case montreal, toronto, newYork, columbus, losAngeles, Seattle
}

var theTeam = MLSTeam.montreal

当使用MLSTeam.montreal初始化theTeam时,其类型被推断为MLSTeam。由于theTeam已经定义,我们可以使用更短的语法进行更改,如下所示:

theTeam = .newYork

我们能够使用简短的语法更改theTeam,因为theTeam已经被推断,并且不是一个常量。

关联值

枚举可以存储任何给定类型的关联值,如果需要,枚举的每个成员的值类型可以不同。类似这些的枚举在其他编程语言中被称为有区别的联合、标记联合或变体。以下示例展示了关联值的一个简单用法:

enum Length {
    case us(Double)
    case metric(Double)
}

let lengthMetric = Length.metric(1.6)

枚举类型Length可以取US值,其关联值为Double类型,或者取metric值,其关联值为Double类型。

lengthMetric是一个变量,其值被分配为Length.metric,关联值为1.6

如前例所示,当我们基于枚举的某个情况创建新的常量或变量时,会设置关联值,并且每次都可能不同。

原始值

枚举成员可以预先填充默认值(称为原始值),这些值都是同一类型。以下示例展示了带有原始值的HttpError枚举的一个不完整示例:

enum HttpError: Int {
    case badRequest = 400
    case unauthorized = 401
    case forbidden = 403
}

在前例中,enum名为HttpError的原始值被定义为Int类型,并设置为一些整数代码。

原始值可以是StringCharacterInt或任何浮点数类型。每个原始值必须在枚举声明内是唯一的。

当我们首次定义枚举时,原始值被设置为预填充值,如前例中的HttpError;因此,枚举情况的原始值始终相同,并且不会改变,这与关联值不同。

如果我们定义一个具有原始值类型的枚举,枚举将自动接收一个初始化器,该初始化器接受原始值类型的值并返回枚举 casenil。我们可以使用这个初始化器来尝试创建枚举的新实例。以下示例展示了 HttpError 实例的初始化:

let possibleError = HttpError(rawValue: 400)
print(possibleError)

代数数据类型

Swift 中的枚举实际上是代数数据类型,这些类型是通过组合其他类型创建的。代数数据类型对于许多函数式编程语言(如 Haskell)至关重要。

代数数据类型基于代数结构的概念,这是一组可能的值和一个或多个运算符,可以将有限数量的这些值组合成一个单一的值。例如,一个著名的结构是 (, +, -),这是一个所有整数的集合,上面有加法和减法运算。

因此,代数数据类型是通过代数运算创建的数据类型,具体来说,使用加法和乘法作为我们的运算。

此外,代数数据类型是复合数据类型,可能包含多个值,例如具有多个字段的数据类型,或者它们可能由变体或多个有限的不同值组成。

简单类型

Boolean 类型是一种简单的代数数据类型,因为它可能取两个值之一:truefalse。一个 Boolean 类型的实例应该是 truefalse 中的一个,但实例不能同时是两者;它必须是一个或另一个,这与 struct/class 属性和变量不同。

复合类型

代数数据类型也可以是复合类型。例如,两个 Double 值的元组是一种简单的代数数据类型。这样的元组可以表示为 (Double, Double) 类型,这个类型的示例值可以是 (1.5, 3.2)

带有变体的复合类型

代数数据类型也可以是复合类型,具有变体。我们可以创建一个名为 Dimensionenum 来存储长度和宽度。我们可以用 us 英尺和 metric 米来表示这个 enum。在 Swift 中,我们可以如下定义这样的 enum

enum Dimension {
    case us(Double, Double)
    case metric(Double, Double)
}

然后,我们可以使用 Dimension 枚举来创建一个变量,如下所示:

let sizeMetric = Dimension.metric(5.0, 4.0)

数据类型的代数

我们已经看到,Swift 中的枚举实际上是代数数据类型。让我们通过一些例子来探索这个主题,以便更熟悉它。

以下示例展示了一个简单的 enum NHLTeam,具有不同的选项。enum Team 使用我们之前定义的 NHLTeamMLSTeam 来结合 HockeySoccer 队伍。Team 可以是一个 Hockey NHL 队伍或一个 Soccer MLS 队伍:

enum NHLTeam {
    case canadiens
    case senators
    case rangers
    case penguins
    case blackHawks
    case capitals
}

enum MLSTeam {
    case montreal
    case toronto
    case newYork
    case columbus
    case losAngeles
    case seattle
}

struct HockeyAndSoccerTeams {
    var hockey: NHLTeam
    var soccer: MLSTeam
}

MLSTeamNHLTeam 每个都有六个潜在值。如果我们将它们组合起来,我们将有两个新的类型。Team 可以是 NHLTeamMLSTeam,因此它有 12 个潜在值,这是 NHLTeamMLSTeam 潜在值的总和。因此,Team enum 是一个求和类型。

要有一个HockeyAndSoccerTeams结构,我们需要为NHLTeamMLSTeam选择一个值,因此它有 36 个潜在值,这是NHLTeamMLSTeam值的乘积。因此,HockeyAndSoccerTeams是一个乘积类型。

在 Swift 中,枚举的可选值可以有多个。如果它恰好是唯一的选择,那么这个枚举就变成了一个乘积类型。以下示例展示了如何将一个枚举作为乘积类型:

enum HockeyAndSoccerTeams {
    case Value(hockey: NHLTeam, soccer: MLSTeam)
}

递归类型是另一类代数数据类型。

递归数据类型是用于可能包含相同类型其他值的值的类型。计算机科学中递归的一个重要应用是在定义动态数据结构,如数组。递归数据结构可以根据运行时需求动态增长到理论上无限的大小。

用于执行简单整数算术的操作可以用枚举来建模。这些操作让我们能够组合简单的算术表达式。《Swift 编程语言(3.0)》由苹果公司提供,其中包含了一个简单整数算术的例子。

递归数据结构的另一个例子是作为递归数据类型实现的Tree

enum Tree {
    case empty
    case leaf(Int)
    indirect case node(Tree, Tree)
}

let ourTree = Tree.node(Tree.leaf(1), Tree.node(Tree.leaf(2),
  Tree.leaf(3)))
print(ourTree)

Tree可以是空的;它可以有一个叶子或另一个Tree作为node

由于数据是嵌套的,用于存储数据的枚举也需要支持嵌套,这意味着枚举需要是递归的。

当编译器与递归枚举一起工作时,它必须插入一层间接层。我们通过在枚举案例前写indirect来表示枚举案例是递归的。

以下示例展示了Tree上的搜索函数:

func searchInTree(_ search: Int, tree: Tree) -> Bool {
    switch tree {
    case .leaf(let x):
        return x == search
    case .node(let l as Tree, let r as Tree):
        return searchInTree(search, tree:l) || searchInTree(search, tree:r)
    default:
        return false
    }
}

let isFound = searchInTree(3, tree: ourTree) // will return true
print(isFound)

由于我们可以在 Swift 中创建sumproductrecursion类型,因此可以说 Swift 对代数数据类型提供了第一级支持。

模式匹配

支持代数数据类型的编程语言通常支持一组用于处理复合类型字段或类型变体的功能。这些功能在以类型安全的方式定义操作不同字段或类型变体的函数时是必不可少的。

一种这样的特性被称为模式匹配,它使我们能够定义在不同的类型变体上操作不同的函数,并从复合类型中提取单个字段,同时保持语言的类型安全保证。

事实上,许多具有模式匹配功能的语言的编译器,如果我们没有正确处理类型的所有字段或变体,将会发出警告或错误。这些警告帮助我们编写更安全、更健壮的代码。

以下示例展示了使用switch语句进行简单模式匹配:

let theTeam = MLSTeam.montreal

switch theTeam {
case .montreal:
    print("Montreal Impact")
case .toronto:
    print("Toronto FC")
case .newYork:
    print("Newyork Redbulls")
case .columbus:
    print("Columbus Crew")
case .losAngeles:
    print("LA Galaxy")
case .seattle:
    print("Seattle Sounders")
}

在此示例中,Swift 编译器推断出theTeamMLSTeam;因此,我们不需要为每个案例编写MLSTeam

我们使用switch案例来匹配模式,因为这是 Swift 中枚举的基本模式匹配方式。此代码块将打印Montreal Impact,因为它匹配了.montreal案例。

要进一步探索模式匹配,我们可以查看其他示例,即 Dimension 枚举。使用模式匹配,我们可以轻松编写一个函数 convertDimension,它将 Dimension 作为参数并转换为其他变体(US 测量单位到 Metric 以及反之亦然):

func convertDimension(dimension: Dimension) -> Dimension {
    switch dimension {
    case let .us(length, width):
        return .metric(length * 0.304, width * 0.304)
    case let .metric(length, width):
        return .us(length * 3.280, width * 3.280)
    }
}

let convertedDimension = convertDimension(dimension:
  Dimension.metric(5.0, 4.0))

在这个函数中,我们使用 switch case 代码块检查维度类型。我们使用 let 语句提取关联的值,并在 return 语句中使用 lengthwidth

为了测试我们的函数,我们提供了一个 metric 维度为 5.04.0,这样最终的 us length 将是 16.4,而 us width 将是 13.12

Swift 要求我们处理枚举类型的所有情况;如果我们没有涵盖所有情况,Swift 编译器将发出警告并阻止我们引入运行时错误。例如,如果我们删除第二个情况,编译器将发出警告,如下面的图像所示:

模式匹配

如果我们有很多想要通用处理的案例,我们可以使用默认关键字。例如,让我们给 convertDimension 函数添加一个默认情况:

func convertDimension(dimension: Dimension) -> Dimension {
    switch dimension {
    case let .us(length, width):
        return .metric(length * 0.304, width * 0.304)
    default:
        return .us(0.0, 0.0)
    }
}

前面的示例仅作为默认使用示例,我们应该尽可能避免使用 default 情况。

模式和模式匹配

在上一节中,我们查看了对枚举的简单模式匹配示例。在本节中,我们将详细探讨模式和模式匹配。

通配符模式

通配符模式匹配并忽略任何值。它由一个下划线 _ 组成。当我们不关心匹配的值时,我们使用通配符模式。

例如,以下代码示例忽略了匹配的值:

for _ in 1...5 {
    print("The value in range is ignored")
}

我们使用 _ 来忽略迭代中的值。

通配符模式可以与 optionals 结合使用如下:

let anOptionalString: String? = nil

switch anOptionalString {
    case _?: print ("Some")
    case nil: print ("None")
}

如前例所示,我们通过 _? 匹配了可选值。

通配符模式可以用来忽略我们不需要的数据以及我们不想匹配的值。以下代码示例展示了我们如何使用通配符模式来忽略数据:

let twoNumbers = (3.14, 1.618)

switch twoNumbers {
    case (_, let phi): print("pi: \(phi)")
}

值绑定模式

值绑定模式将匹配的值绑定到变量或常量名称。以下示例通过将 x 绑定到 5y 绑定到 7 来展示值绑定模式:

let position = (5, 7)

switch position {
    case let (x, y):
        print("x:\(x), y:\(y)")
}

标识符模式

标识符模式匹配任何值并将匹配的值绑定到变量或常量名称。例如,在以下示例中,ourConstant 是一个标识符模式,它匹配 7 的值:

let ourConstant = 7

switch ourConstant {
    case 7: print("7")
    default: print("a value")
}

标识符模式是值绑定模式的子模式。

元组模式

元组模式是一个由逗号分隔的零个或多个模式的列表,用括号括起来。元组模式匹配相应元组类型的值。

我们可以使用类型注解来约束元组模式以匹配某些类型的元组。例如,在声明let (x, y): (Double, Double) = (3, 7)中,元组模式(x, y): (Double, Double)仅匹配两个元素都是Double类型的元组类型。

在以下示例中,我们通过绑定名称、检查年龄是否有值,最后如果地址是String类型来匹配模式。我们只使用所需的name,对于ageaddress,我们使用通配符模式来忽略值:

let name = "John"
let age: Int? = 27
let address: String? = "New York, New York, US"

switch (name, age, address) {
    case (let name, _?, _ as String):
        print(name)
    default: ()
}

枚举案例模式

枚举案例模式匹配现有枚举类型的case。枚举案例模式出现在switch语句的case标签和ifwhileguardfor-in语句的case条件中。

如果我们试图匹配的枚举case有任何关联的值,相应的枚举案例模式必须指定一个包含每个关联值的元组模式。以下示例展示了枚举案例模式:

let dimension = Dimension.metric(9.0, 6.0)

func convertDimensions(dimension: Dimension) -> Dimension {
    switch dimension {
    case let .us(length, width):
        return .metric(length * 0.304, width * 0.304)
    case let .metric(length, width):
        return .us(length * 3.280, width * 3.280)
    }
}

print(convertDimensions(dimension: dimension))

在前面的示例中,我们为每个关联值(lengthwidth)使用元组模式。

可选模式

可选模式匹配Optional<Wrapped>ImplicitlyUnwrappedOptional<Wrapped>枚举中Some(Wrapped)案例的值。可选模式由一个标识符模式后跟一个问号组成,出现在与枚举案例模式相同的位置。以下示例展示了可选模式匹配:

let anOptionalString: String? = nil

switch anOptionalString {
    case let something?: print("\(something)")
    case nil: print ("None")
}

类型转换模式

有两种类型转换模式如下:

  • is:这会将类型与表达式的右侧进行匹配

  • as:这会将类型转换为表达式的左侧

以下示例展示了isas类型转换模式:

let anyValue: Any = 7

switch anyValue {
    case is Int: print(anyValue + 3)
    case let ourValue as Int: print(ourValue + 3)
    default: ()
}

anyValue变量是Any类型,存储一个Int值,那么第一个案例将被匹配,但编译器将报错,如下所示:

类型转换模式

我们可以用as!anyValue转换为Int类型以解决问题。

第一个案例已经匹配。第二个案例将不会达到。假设我们有一个不匹配的案例作为第一个案例,如下例所示:

let anyValue: Any = 7 

switch anyValue { 
     case is Double: print(anyValue) 
     case let ourValue as Int: print(ourValue + 3) 
     default: () 
} 

在这种情况下,第二个案例将被匹配,将anyValue转换为Int并绑定到ourValue,然后我们就能在我们的语句中使用ourValue

表达式模式

表达式模式表示表达式的值。表达式模式仅出现在switch语句的case标签中。表达式模式所表示的表达式使用~=运算符与输入表达式的值进行比较。

如果~=运算符返回true,则匹配成功。默认情况下,~=运算符使用==运算符比较相同类型的两个值。以下示例展示了表达式模式的示例:

let position = (3, 5)

switch position {
    case (0, 0):
        print("(0, 0) is at the origin.")
    case (-4...4, -6...6):
        print("(\(position.0), \(position.1)) is near the origin.")
    default:
        print("The position is:(\(position.0), \(position.1)).")
}

我们可以重载~=运算符以提供自定义的表达式匹配行为。

例如,我们可以重写前面的示例,将位置表达式与位置的String表示形式进行比较:

func ~=(pattern: String, value: Int) -> Bool {
    return pattern == "\(value)"
}

switch position {
    case ("0", "0"):
        print("(0, 0) is at the origin.")
    default:
        print("The position is: (\(position.0), \(position.1)).")
}

摘要

本章解释了枚举的定义和用法。我们涵盖了关联值和原始值,以及代数数据类型概念的介绍。我们探讨了几个示例,包括求和、乘积和递归类型。在第八章“函数式数据结构”中,当我们讨论函数式数据结构时,我们将使用代数数据类型的概念。在本章中,我们探讨了诸如通配符、值绑定、标识符、元组、枚举情况、可选、类型转换和表达式等模式,以及相关的模式匹配示例。

下一章将涵盖泛型和关联类型协议,这些是函数式编程、泛型编程和协议导向编程中非常有用的工具。

第五章:泛型和关联类型协议

泛型使我们能够编写灵活、可重用的函数、方法和类型,它们可以与任何类型一起工作。本章解释了如何定义和使用泛型,并通过示例介绍了 Swift 编程语言中泛型可以解决的问题。

本章将通过代码示例涵盖以下主题:

  • 泛型函数和方法

  • 泛型参数

  • 泛型类型约束和 where 子句

  • 泛型数据结构

  • 关联类型协议

  • 扩展泛型类型

  • 子类化泛型类

泛型是什么,它们解决了哪些问题?

Swift 是一种类型安全的语言。每次我们与类型一起工作时,我们都需要指定它们。例如,一个函数可以有特定的参数和返回类型。我们不能传递任何类型,而只能是指定的类型。如果我们需要一个可以处理多种类型的函数怎么办?

我们已经知道 Swift 提供了 AnyAnyObject,但除非我们不得不使用它们,否则这不是一个好的实践。使用 AnyAnyObject 将使我们的代码变得脆弱,因为我们无法在编译时捕获类型不匹配。泛型是我们需求的解决方案。让我们先看一个例子。以下函数简单地交换两个值(ab)。ab 的类型是 Int。我们必须只传递 Int 类型的值来编译应用程序:

func swapTwoValues( a: inout Int, b: inout Int) {
    let tempA = a
    a = b
    b = tempA
}

类型安全本应是一件好事,但在这个情况下它让我们的代码变得不那么通用。如果我们想交换两个 Strings 呢?我们应该为这个函数创建一个新的副本吗?

func swapTwoValues( a: inout String, b: inout String) {
    let tempA = a
    a = b
    b = tempA
}

这两个函数的主体是相同的。唯一的区别在于函数签名,更具体地说,是参数类型。有些人可能会认为将这些参数的类型更改为 AnyAnyObject 是一个好主意。记住 AnyObject 可以代表任何类类型的实例,而 Any 可以代表任何类型的实例,除了函数类型,让我们假设我们将类型更改为 Any

func swapTwoValues(a: Any, b: Any) -> (a: Any, b: Any) {
    let temp = a
    let newA = b
    let newB = temp
    return (newA, newB)
}

我们的 API 用户可以继续发送任何类型的参数。它们可能不匹配。编译器不会抱怨。让我们检查以下示例:

var name = "John Doe"
var phoneNumber = 5141111111

let (a, b) = swapTwoValues(a: name, b: phoneNumber)

我们的功能是通过 StringInt 参数调用的。我们的函数交换两个值,所以返回的 a 变成了 Int,而 b 变成了 String。这将使我们的代码容易出错,并且很难跟踪。

我们不希望那么灵活。我们不希望使用 AnyAnyObject,但我们仍然需要一定程度的灵活性。泛型是我们问题的解决方案。我们可以使用泛型使这个函数通用且健壮。让我们先看以下例子:

func swapTwoValues<T>(a: T, b: T) -> (a: T, b: T) {
    let temp = a
    let newA = b
    let newB = temp
    return (newA, newB)
}

在这个示例中,我们将 Any 替换为 T。它可以是代码中未定义的任何东西,或者不是 SDK 的一部分。我们在函数名之后和参数之前将此类型放入 <> 中。然后我们在参数或返回类型中使用此类型。这样,我们告诉编译器我们的函数接受一个泛型类型。任何类型都可以传递给此函数,但参数和返回类型都必须是同一类型。因此,我们的 API 用户将无法像以下这样传递 StringInt

var name = "John Doe"
var phoneNumber = 5141111111

let (a, b) = swapTwoValues(a: name, b: phoneNumber) // Compile error -
  Cannot convert value of type 'Int' to expected argument type 'String'

编译器会抱怨类型不匹配。这样,我们的代码是类型安全的且灵活的,因此我们可以用于不同类型而不用担心类型不匹配问题。

泛型是函数式编程中的伟大工具,因为有了它们,我们能够开发出强大、多用途和通用的函数。让我们考察一个泛型使用的函数式示例。

在 第二章 中,函数和闭包,我们有一个如下示例。

假设我们需要开发一个函数来添加两个 Int 值,如下所示:

func addTwoValues(a: Int, b: Int) -> Int {
    return a + b
}

此外,我们还需要开发一个函数来计算 Int 值的平方:

func square(a: Int) -> Int {
    return a * a
}

假设我们需要添加两个平方值:

func addTwoSquaredValues(a: Int, b: Int) -> Int {
    return (a * a) + (b * b)
}

如果我们需要开发乘法、减法或除法两个平方值的函数怎么办?

答案是使用高阶函数来编写一个灵活的函数,如下所示:

typealias AddSubtractOperator = (Int, Int) -> Int
typealias SquareTripleOperator = (Int) -> Int

func calcualte(a: Int,
               b: Int,
           funcA: AddSubtractOperator,
           funcB: SquareTripleOperator) -> Int {

    return funcA(funcB(a), funcB(b))
}

这个高阶函数接受两个其他函数作为参数并使用它们。我们可以为不同的场景调用它,如下所示:

print("The result of adding two squared values is: \(calcualte(a: 2, b: 2,
  funcA: addTwoValues, funcB: square))") // prints "The result of adding
  two squared value is: 8"

使用高阶函数使它们更加灵活和通用,但仍然不是那么通用。这些函数仅与 Int 值一起工作。使用泛型,我们可以使它们与任何数值类型一起工作。让我们使我们的 calculate 函数更加通用:

func calcualte<T>(a: T,
                  b: T,
              funcA: (T, T) -> T,
              funcB: (T) -> T) -> T {

    return funcA(funcB(a), funcB(b))
}

calculate 函数接受两个相同类型(T)的值和两个函数。funcA 函数接受两个 T 类型的值并返回一个 T 类型的值。funcB 函数接受一个 T 类型的值并返回相同类型的 T 值。

现在,我们可以使用 calculate 函数处理任何类型。例如,我们可以传递任何数值并让函数为该特定类型计算它。

这里有两个需要注意的地方。首先,相同的技巧可以应用于方法,其次,在 Swift 3.0 之前,我们无法直接在泛型类型中定义 typealiases。Swift 3.0 引入了如下泛型 typealiases

typealias StringDictionary<T> = Dictionary<String, T>
typealias DictionaryOfStrings<T: Hashable> = Dictionary<T, String>
typealias IntFunction<T> = (T) -> Int
typealias Vec3<T> = (T, T, T)
typealias BackwardTriple<T1, T2, T3> = (T3, T2, T1)

类型约束

我们的函数可以处理任何类型真是太好了,但如果我们 API 用户尝试在无法用于算术计算的类型上使用 calculate 函数怎么办?

为了减轻这个问题,我们可以使用类型约束。使用类型约束,我们将能够强制使用某种类型。类型约束指定类型参数必须继承自特定类或符合特定协议或协议组合。集合是我们已经在 Swift 编程语言中熟悉的类型约束的例子。集合是 Swift 中的泛型,因此我们可以有 IntDoubleString 等数组的数组。

与 Objective-C 不同,在 Objective-C 中我们可以在集合中使用不同类型,而在 Swift 中我们需要具有符合类型约束的相同类型。例如,dictionary 的键必须符合 Hashable 协议。

我们可以使用以下两种语法中的任何一种来指定类型约束:

<T: Class><T: Protocol>

让我们回到我们的 calculate 示例并定义一个数值类型约束。有如 HashableEquatable 这样的不同协议。然而,这些协议中的任何一个都无法解决我们的问题。最简单的解决方案是定义我们的协议并通过符合我们的协议来扩展我们想要使用的类型。这是一个可以用来解决类似问题的通用方法:

protocol NumericType {
    func +(lhs: Self, rhs: Self) -> Self
    func -(lhs: Self, rhs: Self) -> Self
    func *(lhs: Self, rhs: Self) -> Self
    func /(lhs: Self, rhs: Self) -> Self
    func %(lhs: Self, rhs: Self) -> Self
}

我们为数值类型定义了一个包含相关基本数学运算符的协议。我们将要求我们想要使用的类型符合我们的协议。因此,我们按照以下方式扩展它们:

extension Double : NumericType { }
extension Float  : NumericType { }
extension Int    : NumericType { }
extension Int8   : NumericType { }
extension Int16  : NumericType { }
extension Int32  : NumericType { }
extension Int64  : NumericType { }
extension UInt   : NumericType { }
extension UInt8  : NumericType { }
extension UInt16 : NumericType { }
extension UInt32 : NumericType { }
extension UInt64 : NumericType { }

最后,我们需要在我们的函数中定义类型约束,如下所示:

func calculate<T: NumericType>(a: T,
                               b: T,
                           funcA: (T, T) -> T,
                           funcB: (T) -> T) -> T {

    return funcA(funcB(a), funcB(b))
}

print("The result of adding two squared values is: \(calcualte(a: 2, b: 2,
  funcA: addTwoValues, funcB: square))") // prints "The result of adding
  two squared value is: 8"

因此,我们有一个只接受数值类型的函数。

让我们用一个非数值类型来测试它,以确保其正确性:

func format(a: String) -> String {
    return "formatted \(a)"
}

func appendStrings(a: String, b: String) -> String {
    return a + b
}

print("The result is: \(calculate("2", b: "2", funcA:
  appendStrings, funcB: format))")

这个代码示例由于我们的类型约束无法编译,这可以在以下屏幕截图中看到:

类型约束

where 子句

where 子句可以用来定义更复杂类型约束,例如,符合多个协议并带有一些约束。

我们可以通过在泛型参数列表之后包含一个 where 子句来指定类型参数及其关联类型的附加要求。where 子句由 where 关键字后跟一个逗号分隔的要求列表组成。

例如,我们可以表达泛型类型 T 继承自 C 类并符合 V 协议的约束,表示为 <T where T: C, T: V>

我们可以约束类型参数的关联类型符合协议。让我们考虑以下泛型参数子句:

<Seq: SequenceType where Seq.Generator.Element: Equatable>

这里,它指定了 Seq 符合 SequenceType 协议,并且关联的 Seq.Generator.Element 类型符合 Equatable 协议。这个约束确保序列中的每个元素都是 Equatable

我们还可以使用 == 运算符指定两个类型应该是相同的。让我们考虑以下泛型参数子句:

<Seq1: SequenceType, Seq2: SequenceType where 
  Seq1.Generator.Element == Seq2.Generator.Element>

在这里,它表达了 Seq1Seq2 符合 SequenceType 协议,并且两个序列的元素必须是同一类型的约束。

替换为类型参数的任何类型参数都必须满足对类型参数放置的所有约束和要求。

我们可以通过在泛型参数子句中提供不同的约束、要求或两者来重载泛型函数或初始化器。当我们调用重载的泛型函数或初始化器时,编译器使用这些约束来解决调用哪个重载函数或初始化器。

泛型数据结构

除了泛型函数之外,Swift 还允许我们定义自己的泛型类型和数据结构。在第四章,枚举和模式匹配中,我们使用枚举开发了一个简单的树。让我们使其泛型化,以便它可以接受不同的类型作为其叶子和节点:

enum GenericTree <T> {
    case empty
    case leaf(T)
    indirect case node(GenericTree, GenericTree)
}

let ourGenericTree = GenericTree.node(GenericTree.leaf("First"),
  GenericTree.node(GenericTree.leaf("Second"), GenericTree.leaf("Third")))
print(ourGenericTree)

使用泛型后,我们的树从只能接受 Int 作为叶子的树变成了可以接受任何类型的泛型树。

使用泛型,可以开发简单且通用的类型或数据结构,例如图、链表、栈和队列。

让我们通过创建一个泛型 struct 来检查一个队列数据结构的示例。队列是计算机科学中一个众所周知的数据结构,它提供了一种按 先进先出FIFO)顺序存储项的方法。一个泛型队列将能够按 FIFO 顺序存储任何类型。以下示例并不是一个完整的队列实现,但它给出了泛型如何帮助开发泛型数据结构的一些想法。此外,它不是一个函数式数据结构,因为它有可变变量和函数。在第八章,函数式数据结构中,我们将详细探讨函数式数据结构。

struct Queue<Element> {
    private var elements = [Element]()
    mutating func enQueue(newElement: Element) {
        elements.append(newElement)
    }

    mutating func deQueue() -> Element? {
        guard !elements.isEmpty else {
            return nil
       }
       return elements.remove(at: 0)
    }
}

关联类型协议

到目前为止,我们能够使函数、方法和类型泛型化。我们能否使协议泛型化呢?答案是,我们不能,但是协议支持一个名为关联类型的类似功能。关联类型为用作协议一部分的类型提供占位符名称或别名。实际用于关联类型的类型直到协议采用时才指定。关联类型使用 associatedtype 关键字指定。让我们通过一个示例来检查:

protocol Container {
    associatedtype ItemType
    mutating func append(item: ItemType)
}

此协议定义了一个 append 函数,该函数接受 ItemType 类型的任何项。此协议没有指定容器中项的存储方式或它们的类型。该协议仅指定了一个任何类型都必须提供的 append 函数,以便被认为是 Container

任何符合 Container 协议的类型都应该能够指定它存储的值的类型。具体来说,它必须确保只有正确类型的项被添加到容器中。

为了定义这些要求,Container协议需要一个占位符来引用容器将包含的元素类型,而不需要知道对于特定容器这个类型是什么。Container协议需要指定传递给append方法的任何值必须与容器的元素类型相同。

为了实现这一点,Container协议声明了一个名为ItemType的相关类型,写作associatedtype ItemType

协议没有定义ItemType作为associatedtype的类型是什么,并且这个信息留给了任何遵守的类型去提供。尽管如此,ItemType associatedtype为你提供了一种方式来引用Container中项的类型,并定义一个用于append的类型。

以下示例展示了我们将如何遵守一个带有相关类型的协议:

struct IntContainer: Container {
    typealias ItemType = Int
    mutating func append(item: ItemType) {
        // append item to the container
    }
}

在这里,我们定义了一个新的struct,它遵守Container协议并使用Int作为ItemType

扩展泛型类型

在 Swift 中,可以对泛型类型进行扩展。例如,我们可以扩展我们的Queue示例struct并为其添加新的行为:

extension Queue {
    func peek() -> Element? {
        return elements.first
    }
}

如此例所示,我们能够在扩展中使用通用的Element类型。

泛型类的子类化

在 Swift 中,可以对泛型类进行子类化。假设我们有一个泛型Container类。有两种不同的方式可以对其子类化。在我们的第一个例子中,GenericContainer子类化Container类并保持为泛型类。在我们的第二个例子中,SpecificContainer子类化Container并成为IntContainer,因此它不再泛型:

class Container<Item> {
}

// GenericContainer stays generic
class GenericContainer<Item>: Container<Item> {
}

// SpecificContainer becomes a container of Int type
class SpecificContainer: Container<Int> {
}

概述

在本章中,我们了解了如何定义和使用泛型。我们还了解了泛型解决的问题类型。然后我们通过示例探讨了类型约束、泛型数据结构和相关类型协议。泛型是伟大的工具,一旦习惯使用,可以使我们的代码更加灵活、有用和健壮,因此我们将在本书的其余部分大量使用它们。

在下一章中,我们将介绍一些范畴论概念,例如函子、应用函子和单子。我们还将探索高阶函数,如mapfilterreduce

第六章:映射、过滤和归约

在前面的章节中,我们简要介绍了map函数作为内置高阶函数的例子。在本章中,我们将进一步探讨这个主题,并通过示例熟悉 Swift 中的mapflatMapfilterreduce函数。我们还将熟悉诸如模态、函子和应用函子等范畴论概念。

本章将通过代码示例涵盖以下主题:

  • 函子

  • 应用函子

  • 模态

  • 映射

  • 平铺和扁平化

  • 过滤

  • 归约

  • 应用

  • 连接

  • 连接高阶函数

  • Zip

  • 实际例子

在我们的日常开发中,集合被到处使用,为了能够声明式地使用集合,我们需要像mapfilterreduce这样的手段。在探讨这些内置到 Swift 中的函数之前,让我们探索这些概念的理论背景。

函子

函子的名称来源于范畴论。在范畴论中,函子包含诸如map函数这样的态射,它转换函子。我们可以将函子视为一种函数式设计模式。

了解范畴论很好,但我们不必这样做,简单来说,函子是我们可以在其上映射的结构。换句话说,函子是任何实现了map函数的类型。函子的例子包括DictionaryArrayOptionalClosure类型。每当谈到函子时,我们首先想到的是我们可以调用map函数来操作和转换它们。

与其名字不同,这个概念非常简单。我们将在接下来的部分中更详细地讨论map函数,并探索函子的用法。

应用函子

应用函子的名称也来自范畴论,我们可以将应用函子视为一种函数式设计模式。

应用函子是一种配备了将值映射到包含该值的函子实例的函数的函子。应用函子为我们提供了在函子上下文中操作值的能力,例如可选值,而无需解包或对它们的内容进行map操作。

假设我们有一个可选函子(一个具有map函数的可选值)。我们不能直接在可选值上应用map函数,因为我们需要先解包它们。应用函子就派上用场了。它们为函子添加了一个新函数,例如apply,使得可以在函子上应用map。再次强调,与它的名字不同,这个概念很简单;我们将在接下来的部分中讨论apply函数。

模态

模态的名称也来自范畴论,我们也可以将模态视为一种函数式设计模式。

Monad 是一种 Functor 类型,它是一种类型,除了 map 之外,还实现了 flatMap 函数。这很简单,对吧?我们有一个具有额外功能的 Functor,那就是 flatMap 实现。所以,任何我们可以对其调用 mapflatMap 函数的类型都是 Monads。在接下来的章节中,我们将讨论 mapflatMap 函数。

到目前为止,我们了解到 Functors 是具有 map 函数的结构。Applicative Functors 是具有 apply 函数的 Functors,而 Monads 是具有 flatMap 函数的 Functors。现在,让我们来谈谈这些重要的函数。

Map

Swift 内置了一个名为 map 的高阶函数,它可以与数组等集合类型一起使用。map 函数解决了使用函数转换数组元素的问题。以下示例展示了两种不同的方法来转换一组数字:

let numbers = [10, 30, 91, 50, 100, 39, 74]
var formattedNumbers: [String] = []

for number in numbers {
    let formattedNumber = "\(number)$"
    formattedNumbers.append(formattedNumber)
}

let mappedNumbers = numbers.map { "\($0)$" }

解决这个问题的第一种方法是命令式,使用 for-in 循环遍历集合并转换数组中的每个元素。这种迭代技术被称为外部迭代,因为我们指定了如何迭代。它要求我们显式地从开始到结束按顺序访问元素。此外,它还需要在循环执行任务时创建一个被反复修改的变量。

这个过程容易出错,因为我们可能会错误地初始化 formattedNumbers。而不是使用外部迭代技术,我们可以使用内部迭代技术。

没有指定如何遍历元素或声明和使用任何可变变量,Swift 可以确定如何访问所有元素以执行任务,并隐藏这些细节。这种技术被称为内部迭代。

内部迭代方法之一是 map 方法。map 方法优雅地简化了我们的代码,使其具有声明性。让我们使用 map 函数来检查第二种方法:

let mappedNumbers = numbers.map { "\($0)$" }

如此示例所示,我们可以用一行代码实现相同的结果。使用 map 的一大好处是我们可以清楚地声明我们试图应用于元素列表的转换。map 函数允许我们声明我们想要实现的目标,而不是它的实现方式。这使得阅读和推理我们的代码变得更加简单。

map 函数可以应用于任何包含自身内部值或多个值的容器类型。任何提供 map 函数的容器都成为 Functor,正如我们之前所看到的。

我们知道 map 函数/方法使用的优点以及它的用法。让我们探索它的动态特性并创建一个 map 函数。

在 第五章 中,泛型和关联类型协议,我们有以下示例:

func calculate<T>(a: T,
                  b: T,
              funcA: (T, T) -> T,
              funcB: (T) -> T) -> T {

    return funcA(funcB(a), funcB(b))
}

calculate 函数可以接受 abfuncAfuncB 作为参数。让我们简化这个函数,只使用两个参数并更改返回类型:

func calculate<T, U>(a: T,
                 funcA: (T) -> U) -> U {

    return funcA(a)
}

现在,calculate 函数接受类型为 Ta 和将 T 转换为 UfuncAcalculate 函数返回 U。尽管这个函数不适用于数组,但添加数组转换会很容易:

func calculate<T, U>(a: [T],
                 funcA: ([T]) -> [U]) -> [U] {

    return funcA(a)
}

到目前为止,我们有一个 calculate 函数,它接受一个泛型类型 T 的数组和一个将 T 类型的数组转换为 U 类型的数组的函数,并最终返回转换后的 U 类型的数组。

只需更改函数和参数的名称,我们就可以使这个函数更加通用。所以让我们更改函数和参数的名称:

func map<T, U>(a: [T], transform: [T] -> [U]) -> [U] {
    return transform(a)
}

到目前为止,我们有一个半成品 map 函数,它接受一个 T 类型的数组,并将其应用于 transform 函数,以返回一个转换后的 U 类型的数组。

实际上,这个函数什么也不做,映射发生在 transform 中。让我们使这个函数可用并更易于理解:

func map<ElementInput, ElementResult>(elements: [ElementInput],
  transform: (ElementInput) -> ElementResult) -> [ElementResult] {
    var result: [ElementResult] = []

    for element in elements {
        result.append(transform(element))
    }

    return result
}

现在,我们的 map 函数接受一个元素数组(范畴论中的域),遍历数组中的每个元素,将其转换,并将其追加到新数组中(范畴论中的陪域)。

结果将是一个 ElementResult 类型的数组,它实际上已经转换了输入数组的元素。让我们测试这个函数:

let numbers = [10, 30, 91, 50, 100, 39, 74]

let result = map(elements: numbers, transform: { $0 + 2 })

结果将是 [12, 32, 93, 52, 102, 41, 76]

这个例子表明,通过高阶函数和泛型,我们能够定义像 map 这样的函数,这些函数已经是 Swift 语言的一部分。

现在,让我们检查 Swift 中提供的 map 函数:

public func map<T>(@noescape transform: (Self.Generator.Element) -> T) ->
  [T]

这个定义与我们的实现非常相似,只是在一些我们将在这里讨论的不同之处。

首先,这是一个可以在数组等集合上调用的方法,因此我们不需要任何如 [ElementInput] 这样的输入类型。

其次,@noescape 是 Swift 中用于告知函数用户该参数不会比调用持续更长时间的属性。在逃逸场景中,如果函数调度到不同的线程,参数可能会被捕获,以便在需要时在稍后的时间存在。@noescape 属性确保这种情况不会发生。

最后,transform 是参数的名称。参数的类型声明为 (Self.Generator.Element) -> T。这是一个闭包,它接受 Self.Generator.Element 类型的参数并返回 T 类型的实例。Self.Generator.Element 类型与集合中包含的类型相同。

FlatMap 和 flatten

数组的 flatMap 方法可以用来扁平化数组的某一维度的维度。以下示例展示了一个二维数组,换句话说,嵌套数组。在这个数组上调用 flatMap 会减少一个维度并将其扁平化,使得结果数组变为 [1, 3, 5, 2, 4, 6]

let twoDimensionalArray = [[1, 3, 5], [2, 4, 6]]
let oneDimensionalArray = twoDimensionalArray.flatMap { $0 }

在这个例子中,flatMap 返回一个包含映射转换结果的 Array,我们可以通过在数组上调用 flatten 然后进行 map 来达到相同的结果,如下所示:

let oneDimensionalArray = twoDimensionalArray.flatten().map { $0 }

为了能够将每个元素转换为一个数组,我们需要将 map 方法作为闭包提供给 flatMap 方法,如下所示:

let transofrmedOneDimensionalArray = twoDimensionalArray.flatMap { 
      $0.map { $0 + 2 } 
}

结果将是 [3, 5, 7, 4, 6, 8]

可以通过以下方式获得相同的结果:

let oneDimensionalArray = twoDimensionalArray.flatten().map { $0 + 2 }

让我们通过一个三维 Array 的例子来检查另一个例子:

let threeDimensionalArray = [[1, [3, 5]], [2, [4, 6]]]
let twoDimensionalArray = threeDimensionalArray.flatMap { $0 }

结果数组将是 [1, [3, 5], 2, [4, 6]]

因此,flatMapflatten 只能扁平化一个维度,要处理更多维度和转换,我们需要相应地多次调用 flatMapmap 方法。

我们还知道 twoDimensionalArraythreeDimensionalArray 是 Monads,因为我们可以在它们上调用 mapflatMap

过滤

filter 函数接收一个函数,该函数给定 Array 中的一个元素,返回一个 Bool 值,指示该元素是否应包含在结果 Array 中。在 Swift 中,filter 方法声明如下:

public func filter(@noescape includeElement: 
  (Self.Generator.Element) -> Bool) -> [Self.Generator.Element]

其定义与 map 方法类似,但有以下区别:

  • filter 函数接收一个闭包,该闭包接收自身元素并返回一个 Bool

  • filter 方法的输出将是一个其自身类型的数组

让我们检查以下代码以了解其工作原理:

let numbers = [10, 30, 91, 50, 100, 39, 74]
let evenNumbers = numbers.filter { $0 % 2 == 0 }

结果的 evenNumbers 数组将是 [10, 30, 50, 100, 74]

让我们亲自实现 filter 函数。实际上,它的实现将与 map 的实现类似,只是它不需要指定第二个泛型来指定陪域。相反,它有条件地将原始元素添加到新的 Array 中:

func filter<Element> (elements: [Element], 
                      predicate:(Element -> Bool)) -> [Element] {
    var result = [Element]()
    for element in elements {
        if predicate(element) {
            result.append(element)
        }
    }
    return result
}

filter 函数遍历 Array 中的每个元素,并对其应用谓词。如果谓词函数的结果为 true,则 element 被添加到我们的新 Array 中。我们可以如下测试我们的 filter 函数:

let filteredArray = filter(elements: numbers) { $0 % 2 == 0 }

结果数组将是 [10, 30, 50, 100, 74],这与 Swift 提供的 filter 方法相同。

减少

reduce 函数将列表缩减为一个单一值。通常被称为 foldaggregate,它接受两个参数:一个起始值和一个函数。

函数接受一个累计总和和列表中的一个元素作为参数,并返回一个通过组合列表中的元素创建的值。

mapfilterflatMap 不同,这些方法会返回相同类型的结果,reduce 会改变类型。换句话说,mapfilterflatMap 会将 Array 转换为改变了的 Array。这与 reduce 不同,因为它可以将数组转换为元组或单个值。

Swift 为数组提供了 reduce 方法,其定义如下:

func reduce<T>(initial: T, @noescape combine: (T, 
  Self.Generator.Element) -> T) -> T

如果我们在 numbers 数组上使用 reduce 方法,这次调用的结果变为 394

let total = numbers.reduce(0) { $0 + $1 }

我们也可以将 reduce 称为 + 操作符,因为在 Swift 中它是一个函数:

let total = numbers.reduce(0, combine: +)

mapfilter 方法一样,开发 reduce 函数也很简单:

func reduce<Element, Value>(elements: [Element],
                            initial: Value,
                            combine: (Value, Element) -> Value) ->     Value {
    var result = initial

    for element in elements {
        result = combine(result, element)
    }

    return result
}

我们可以通过以下调用获得相同的结果(394):

let total = reduce(elements: numbers, initial: 0) { $0 + $1 }

reduce 方法可以与其他类型一起使用,例如字符串数组。

reduce 的角度来理解 map 函数

减少模式非常强大,以至于任何遍历列表的其他函数都可以用它的术语来指定。让我们用 reduce 来开发一个 map 函数:

func mapIntermsOfReduce<Element, ElementResult>(elements: [Element],
  transform: Element -> ElementResult) -> [ElementResult] {
    return reduce(elements: elements, initial: [ElementResult]()) {
        $0 + [transform( $1 )]
    }
}

let result = mapIntermsOfReduce(elements: numbers, transform: { $0 + 2 })

结果与我们在本章早期开发的 map 函数的结果相同。这是一个理解 reduce 基础的好例子。

在函数体中,我们提供 elements,一个空的初始 ElementResult 数组,最后提供一个闭包来组合元素。

reduce 的角度来理解 filter 函数

也可以开发一个从 reduce 的角度来理解的 filter 函数:

func filterIntermsOfReduce<Element>(elements: [Element],
                                    predicate: Element -> Bool) -> [Element] {
    return reduce(elements: elements, initial: []) {
        predicate($1) ? $0 + [ $1 ] : $0
    }
}

let result = filterIntermsOfReduce(elements: numbers) { $0 % 2 == 0 }

再次,结果与之前开发的 filter 函数相同。

在函数体中,我们提供 elements,一个空的初始 ElementResult 数组,最后提供 predicate 作为组合器。

reduce 的角度来理解 flatMap 函数

为了理解 reduce 的强大之处,我们可以用 reduce 来实现 flatMap 函数:

func flatMapIntermsOfReduce<Element>(elements: [Element],
  transform: (Element) -> Element?) -> [Element] {
    return reduce(elements: elements, initial: []) {
        guard let transformationResult = transform($1) else {
            return $0
        }
        return $0 + [transformationResult]
    }
}

let anArrayOfNumbers = [1, 3, 5]
let oneDimensionalArray = flatMapIntermsOfReduce(elements:
  anArrayOfNumbers) { $0 + 5 }

reduce 的角度来理解 flatten 函数

最后,让我们用 reduce 来实现 flatten 函数:

func flattenIntermsOfReduce<Element>(elements: [[Element]]) -> [Element] {
    return elements.reduce([]) { $0 + $1 }
}

此函数接受一个二维数组并将其转换为单维数组。让我们测试这个函数:

let flattened = flattenIntermsOfReduce(elements: [[1, 3, 5], [2, 4, 6]])

结果将是 [1, 3, 5, 2, 4, 6]

Apply

Apply 是一个将函数应用于一系列参数的函数。

不幸的是,Swift 并没有在 Arrays 上提供任何 apply 方法。为了能够实现 Applicative Functors,我们需要开发 apply 函数。以下代码展示了 apply 函数的一个简单版本,它只有一个参数:

func apply<T, V>(fn: [T] -> V, args: [T]) -> V {
    return fn(args)
}

apply 函数接受一个函数和一个任意类型的数组,并将该函数应用于数组的第一个元素。让我们测试这个函数如下:

let numbers = [1, 3, 5]

func incrementValues(a: [Int]) -> [Int] {
    return a.map { $0 + 1 }
}

let applied = apply(fn: incrementValues, args: numbers)

Join

join 函数接受一个对象数组,并用提供的分隔符将它们连接起来。以下示例展示了 join 的一个简单版本:

func join<Element: Equatable>(elements: [Element],
                              separator: String) -> String {
    return elements.reduce("") {
        initial, element in
        let aSeparator = (element == elements.last) ? "" : separator
        return "\(initial)\(element)\(aSeparator)"
    }
}

此函数接受一个带有分隔符的数组,将 Array 中的元素连接起来,并提供一个单一的 String。我们可以如下测试它:

let items = ["First", "Second", "Third"]
let commaSeparatedItems = join(elements: items, separator: ", ")

结果将是 "First, Second, Third"

链式调用高阶函数

到目前为止,我们学习了不同的函数,并为每个函数提供了一些示例。让我们看看我们是否可以将它们结合起来解决我们在日常应用开发中可能遇到的问题。

假设我们需要从后端系统接收一个对象,如下所示:

struct User {
    let name: String
    let age: Int
}

let users = [
    User(name: "Fehiman", age: 60),
    User(name: "Negar", age: 30),
    User(name: "Milo", age: 1),
    User(name: "Tamina", age: 6),
    User(name: "Neco", age: 30)
]

然后我们需要计算 users 数组中年龄的总和。我们可以使用 mapreduce 函数的组合来计算 totalAge,如下所示:

let totalAge = users.map { $0.age }.reduce(0) { $0 + $1 }

我们能够通过链式调用 mapreduce 方法来实现这一点。

Zip

zip 函数是由 Swift 标准库提供的,它创建了一个由两个底层序列组成的序列对,其中第 i^(th) 对的元素是每个底层序列的第 i^(th) 个元素。

例如,在以下示例中,zip 接受两个 Arrays 并创建这两个数组的配对:

let alphabeticNumbers = ["Three", "Five", "Nine", "Ten"]
let zipped = zip(alphabeticNumbers, numbers).map { $0 }

zipped 的值将是 [("Three", 3), ("Five", 5), ("Nine", 9), ("Ten", 10)]

实际例子

让我们探索一些高阶函数的实际例子。

数组的和

我们可以使用 reduce 来计算数字列表的总和,如下所示:

let listOfNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let sumOfNumbers = reduce(elements: listOfNumbers, initial: 0, combine: +)

print(sumOfNumbers)

预期结果是 55

数组的乘积

我们可以使用 reduce 来计算数组值的乘积,如下所示:

let productOfNumbers = reduce(elements: listOfNumbers, initial: 1,
  combine: *)

print(productOfNumbers)

预期结果是 3628800

从数组中移除 nil

我们可以使用 flatMap 从可选数组中获取值并移除 nil 值:

let optionalArray: [String?] = ["First", "Second", nil, "Fourth"]
let nonOptionalArray = optionalArray.flatMap { $0 }

print(nonOptionalArray)

预期结果是 ["First", "Second", "Fourth"]

数组中的重复项移除

我们可以使用 reduce 来从数组中移除重复元素,如下所示:

let arrayWithDuplicates = [1, 1, 2, 3, 3, 4, 4, 5, 6, 7]

arrayWithDuplicates.reduce([]) { (a: [Int], b: Int) -> [Int] in
    if a.contains(b) {
        return a
    } else {
        return a + [b]
    }
}

预期结果是 [1, 2, 3, 4, 5, 6, 7]

分区数组

我们可以使用 reduce 来根据特定标准对数组进行分区。例如,在以下示例中,我们将 numbersToPartition 数组分为两个部分,将所有偶数保留在左侧部分:

typealias Accumlator = (lPartition: [Int], rPartition: [Int])

func partition(list: [Int], criteria: (Int) -> Bool) -> Accumlator {
    return list.reduce((lPartition: [Int](), rPartition: [Int]())) {
        (accumlator: Accumlator, pivot: Int) -> Accumlator in
        if criteria(pivot) {
            return (lPartition: accumlator.lPartition + [pivot],
              rPartition: accumlator.rPartition)
        } else {
            return (rPartition: accumlator.rPartition + [pivot],
              lPartition: accumlator.lPartition)
        }
    }
}

let numbersToPartition = [3, 4, 5, 6, 7, 8, 9]
partition(list: numbersToPartition) { $0 % 2 == 0 }

我们可以将此函数泛化如下:

func genericPartition<T>(list: [T],
                         criteria: (T) -> Bool) -> (lPartition: 
[T], 
                         rPartition: [T]) {
    return list.reduce((lPartition: [T](), rPartition: [T]())) {
        (accumlator: (lPartition: [T], rPartition: [T]), pivot: T) -> (
          lPartition: [T], rPartition: [T]) in
        if criteria(pivot) {
            return (lPartition: accumlator.lPartition + [pivot],
              rPartition: accumlator.rPartition)
        } else {
            return (rPartition: accumlator.rPartition + [pivot],
              lPartition: accumlator.lPartition)
        }
    }
}

let doublesToPartition = [3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
print(genericPartition(list: doublesToPartition) { $0.truncatingRemainder(dividingBy: 2.0) == 0 })

摘要

在本章中,我们从范畴论概念如函子(Functor)、应用函子(Applicative Functor)和单子(Monad)开始,探讨了高阶函数如 mapfilterflatMapflattenreduce。然后,我们检查了 Swift 提供的高阶函数版本,并实现了自己的简单版本。此外,我们还根据 reduce 函数开发了 mapfilterflatMapflatten 函数。

然后,我们继续使用 applyjoinzip 函数,并介绍了高阶函数的链式调用。

最后,我们探讨了高阶函数的一些实际例子,例如从数组中移除 nil 值、移除重复项和分区数组。

这些函数将成为我们日常开发工具箱中的优秀工具,用于解决各种不同类型的问题。

在下一章中,我们将熟悉可选类型,并讨论处理它们的非函数式和函数式方法。

第七章. 处理可选值

在日常 Swift 应用程序开发中,我们需要处理可选值,因为我们需要调用的某些方法可能返回一些值或没有返回值。本章探讨了可选值的概念,并提供了处理它们的多种技术。

本章将通过代码示例涵盖以下主题:

  • 可选类型

  • 解包可选值

  • 可选绑定

  • Guard

  • 合并

  • 可选链

  • 可选映射

  • 以函数式方式处理可选值

  • fmapapply 用于多级函数映射

可选类型

在我们的日常应用程序开发中,我们会遇到期望收到值但实际没有收到值的情况。例如,假设我们有一个项目列表,我们需要在列表中搜索特定的值。我们正在寻找的特定值可能不在列表中。我们已经遇到了很多这样的场景。

其他例子可以是调用一个网络服务并接收一个没有我们所需字段的 JSON 有效负载,或者查询数据库但没有收到预期的值。

当值不存在时我们将收到什么,我们将如何处理它?

在像 C 这样的编程语言中,我们可以在不赋予其值的情况下创建一个变量。如果我们尝试在赋值之前使用该变量,我们会得到一个未定义的值。

在 Swift 中,我们可以定义一个没有值的变量,但如果我们不为其赋值,则不能使用它。换句话说,在使用之前我们需要初始化它。Swift 的这个特性确保我们不会收到未定义的值。

那么当我们需要定义一个变量但不知道其值将是什么时怎么办?

为了克服这些类型的场景,Swift 提供了 Optional 类型,它可以有 SomeNone 值,并且可以在可能不存在值的情况下使用。

问号(?)用于定义一个可选变量。以下示例展示了可选定义的例子:

// Optional value either contains a value or contains nil
var optionalString: String? = "A String literal"
optionalString = nil

在这个例子中,我们定义了一个可选类型的变量。String? 类型是一个可选类型,它可能包含一个 String 值。

我们能够将 nil 赋值给 optionalString。如果我们尝试将 nil 赋值给 Swift 中的任何非可选类型,编译器将会对此提出警告,这与 Objective-C 等其他语言不同。

例如,在下面的例子中,编译器会抱怨 nil cannot be assigned to type 'String'

var aString: String = "A String literal"
aString = nil

Swift 中对不存在值的编译时检查以防止运行时错误是 Swift 类型安全特性之一。类型安全使得在开发早期阶段更容易捕捉到问题。让我们通过一个 Objective-C 的例子来查看可选值的实际价值:

NSString *searchedItem = [self searchItem:@"an item"];
NSString *text = @"Found item: ";
NSString *message = [text stringByAppendingString:searchedItem];

假设我们有一个列表,我们通过调用 searchItem 来启动搜索。在这种情况下,我们的 searchItem 方法接受 NSString 并返回 NSString。这个调用的结果可能是 nil。如果我们使用返回的 NSString 并尝试将其追加到另一个 NSString 上,它将能够编译,但如果 searchItemnil,则可能会使应用程序崩溃。

我们可以在使用之前检查 searchedItem 是否不是 nil 来解决这个问题。然而,可能存在一些情况,其他开发者忘记这样做或没有看到它的必要性。

当然,收到关于这类用法的编译时错误会更安全。由于 Swift 是类型安全的,我们将在运行时不会遇到任何这样的惊喜。

因此,我们已经了解了为什么我们需要可选类型,但它是什么,又是如何定义的呢?

在幕后,Optional 是一个包含两个情况的 enum——一个是 None,另一个是 Some,其关联的泛型值如下:

enum Optional<T> {
    case None
    case Some(T)
}

解包可选类型

到目前为止,我们知道可选类型会将自己包裹在内部。包裹意味着实际数据存储在外部结构中。

例如,我们这样打印 optionalString

print(optionalString)

结果将是 Optional("A String literal")

我们将如何解包可选类型并使用我们需要的值?在接下来的章节中,我们将介绍不同的解包可选类型的方法。

强制解包

解包可选类型最简单也是最危险的方法是强制解包。简而言之,! 可以用来强制从 Optional 中解包值。

以下示例强制解包 optionalString

optionalString = "An optional String"
print(optionalString!)

如果可选类型没有值,强制解包可选类型可能会导致错误,因此不建议使用这种方法,因为它很难确保在不同情况下可选类型中是否有值。

实际上,强制解包消除了类型安全的好处,并可能导致我们的应用程序在运行时崩溃。

nil 检查

强制解包一个 Optional 可能会使我们的应用程序崩溃;为了消除崩溃问题,我们可以在解包之前检查变量是否不是 nil

以下示例展示了一种简单的 nil 检查方法:

if optionalString != nil {
    print(optionalString!)
}

这种方法在编译和运行时都是安全的,但在编辑时可能会引起问题。例如,如果我们不小心将 print 行移出 if 块之外,编译器不会抱怨,但在运行时可能会使我们的应用程序崩溃。

Optional 绑定

更好的方法可能是使用 Optional 绑定技术来找出 Optional 是否包含一个值。如果它包含一个值,我们将能够解包它并将其放入一个临时常量或变量中。

以下示例展示了可选绑定:

let nilName: String? = nil
if let familyName = nilName {
    greetingfamilyName = "Hello, Mr. \(familyName)"
} else {
    // Optional does not have a value
}

if let familyName = nilName 语句将 Optional 值赋给一个名为 familyName 的新变量。赋值语句的右侧必须是一个 Optional,否则编译器将报错。此外,这种方法确保我们使用的是未包装的临时版本,因此是安全的。

这种方法也称为 if-let 绑定,它对于解包 Optionals 并访问底层值非常有用,但如果遇到复杂的嵌套对象结构,例如 JSON 有效负载,语法会变得繁琐。

在这种情况下,我们需要有很多嵌套的 if-let 表达式:

let dict = ["One": 1, "Two": 2, "Three": 3]

if let firstValue = dict["One"] {
    if let secondValue = dict["Two"] {
        if let thirdValue = dict["Three"] {
            // Do something with three values
        }
    }
}

为了解决这个问题,我们可以使用多个可选绑定,如下所示:

if let
firstValue = dict["One"],
secondValue = dict["Two"],
thirdValue = dict["Three"] {
    // Do something with three values
}

这种语法使代码更易于阅读,但当我们需要绑定多级可选类型时,这仍然不是最佳方法。在接下来的章节中,我们将探讨不同的方法来进一步提高我们处理可选类型的可读性和可维护性。

guard

guard 是 Swift 库中提供的另一种处理 Optionals 的方法。guard 方法与 Optional if-let 绑定不同,因为 guard 语句可以用于早期退出。我们可以使用 guard 语句要求在 guard 语句之后的代码执行之前,条件必须为 true

以下示例展示了 guard 语句的使用:

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return
    }
    print("Hello Ms \(name)!")
}

greet(person: ["name": "Neco"]) // prints "Hello Ms Neco!"

在这个例子中,greet 函数需要一个表示人名的值;因此,它使用 guard 语句检查其是否存在。否则,它将返回并停止执行。

使用 guard 语句,我们可以首先检查失败场景,并在失败时返回。与 if-let 语句不同,guard 不提供新的作用域,因此在前面的例子中,我们能够在 { } 之外使用 name 在我们的 print 语句中,这是不可能的。

if-let 语句类似,我们可以使用多个 guard 语句,如下所示:

func extractValue(dict: [String: Int]) {
    guard let
    firstValue = dict["One"],
    secondValue = dict["Two"],
    thirdValue = dict["Three"]
    else {
        return
    }
    // Do something with three values
} 

隐式解包的可选类型

我们可以通过在类型末尾附加一个感叹号 (!) 来定义隐式解包的可选类型。这些类型会自动解包。

以下示例展示了从字典中获取值两种方式。在第一个例子中,结果值将是一个可选类型。在第二个例子中,值将隐式解包:

let one = "One" 
let firstValue = dict["One"] 
let implictlyUnwrappedFirstValue: Int! = dict["One"]

与强制解包类似,隐式解包的可选类型可能会导致我们的应用程序在运行时崩溃,因此在使用它们时我们需要谨慎。

错误处理以避免可选类型

在我们的日常应用开发中,我们可能需要开发一些返回 Optionals 的函数。例如,假设我们需要读取一个文件并返回该文件的内容。使用可选类型,我们可以这样开发:

func checkForPath(path: String) -> String? {
    // check for the path
    return "path"
}

func readFile(path: String) -> String? {
    if let restult = checkForPath(path: path) {
        return restult
    } else {
        return nil
    }
}

这里,checkForPath 是一个不完整的函数,用于检查文件是否存在。

当我们调用 readFile 函数时,我们需要检查结果的可选类型:

if let result = readFile(path: "path/to") {
    // Do something with result
}

在这种情况下,我们不是使用可选值,而是可以使用错误处理来重定向控制流以消除错误并提供恢复:

enum Result: ErrorProtocol {
    case failure
    case success
}

func readFile(path: String) throws -> String {
    if let restult = checkForPath(path: path) {
        return restult
    } else {
        throw Result.failure
    }
}

当我们调用这个函数时,我们需要将其包裹在一个do块中并catchexception

do {
    let result = try readFile(path: "path/to")
} catch {
    print(error)
}

try!

如果我们知道方法调用不可能失败,或者如果它失败则我们的代码将损坏,我们应该崩溃应用程序,我们可以使用try!

当我们使用try!关键字时,我们不需要在代码块周围有docatch,因为我们承诺它永远不会失败!这是一个很大的承诺,我们应该避免。

如果我们必须绕过错误处理,例如检查数据库文件是否存在,我们可以这样做:

do {
    let result = try readFile(path: "path/to")
} catch {
    print(error)
}

try?

我们可以使用try?将错误转换为Optional值来处理错误。

如果在评估try?表达式时抛出错误,则表达式的值将是nil。例如,在以下示例中,如果我们无法读取文件,则结果将是nil

let result = try? readFile(path: "path/to")

空值合并

Swift 提供了??运算符用于空值合并。它解包Optionals并为nil情况提供回退或默认值。例如,a ?? b如果a有值则解包可选a,如果anil则返回默认值b

在这个例子中,如果Optional a不是nil,则空值合并运算符后面的表达式将不会被评估。空值合并适用于我们可以提供回退或默认值的情况。

可选链

可选链是一个查询和调用可能当前为nil的可选属性、方法和下标的进程。Swift 中的可选链类似于 Objective-C 中的nil消息,但以一种适用于任何类型并且可以检查成功或失败的方式工作。

以下示例展示了两个不同的类。其中一个类Person有一个类型为Optionalresidence)的属性,它包装了另一个类类型Residence

class Residence {
    var numberOfRooms = 1
}

class Person {
    var residence: Residence?
}

我们将创建一个Person类的实例,名为sangeeth

let residence = Residence()
residence.numberOfRooms = 5
let sangeeth = Person()
sangeeth.residence = residence

要检查numberOfRooms,我们需要使用Person类的居住属性,它是一个可选值。可选链使我们能够如下遍历可选值:

if let roomCount = sangeeth.residence?.numberOfRooms {
    // Use the roomCount
    print(roomCount)
}

roomCount变量将如预期的那样是五个。

这可以用来通过可选链调用方法和下标。

我们可以通过将问号替换为感叹号来强制解包任何链项:

let roomCount = sangeeth.residence!.numberOfRooms

再次提醒,当我们使用强制解包时,我们需要谨慎。

函数式处理可选值

到目前为止,我们已经介绍了许多不同的方法和工具来处理可选值。让我们看看我们是否可以使用函数式编程范式来简化这个过程。

可选映射

对数组进行映射将为数组中的每个元素生成一个元素。我们能否映射可选以生成非可选值?如果我们有Some,则映射它;否则,返回None。让我们看看这个例子:

func mapOptionals<T, V>(transform: (T) -> V, input: T?) -> V? {
    switch input {
        case .some(let value): return transform(value)
        case .none: return .none
    }
}

我们的input变量是一个泛型optional,我们有一个转换函数,它接受input并将其转换为泛型类型。最终结果将是一个泛型optional类型。在函数体中,我们使用模式匹配来返回相应的值。让我们测试这个函数:

class User {
    var name: String?
}

我们创建了一个名为User的虚拟类,其中包含一个Optional变量。我们如下使用该变量:

func extractUserName(name: String) -> String {
    return "\(name)"
}

var nonOptionalUserName: String {
    let user = User()
    user.name = "John Doe"
    let someUserName = mapOptionals(transform: extractUserName,
      input: user.name)
    return someUserName ?? ""
}

最终结果将是一个非可选的 String。我们的mapOptionals函数与 Haskell 中的fmap函数类似,它定义为<^>运算符。

让我们将这个函数转换为运算符:

infix operator <^> { associativity left }

func <^><T, V>(transform: (T) -> V, input: T?) -> V? {
    switch input {
        case .some(let value): return transform(value)
        case .none: return .none
    }
}

在这里,我们只定义了一个中缀运算符并定义了相应的函数。让我们尝试这个函数,看看它是否提供相同的结果:

var nonOptionalUserName: String {
    let user = User()
    user.name = "John Doe"
    let someUserName = extractUserName <^> user.name
    return someUserName ?? ""
}

结果与我们的上一个例子相同,但代码更易读,所以我们可能更喜欢使用它。

多个可选值映射

我们之前的例子展示了使用函数式编程技术对单个optional值进行映射。如果我们需要一起映射多个可选值怎么办?在Optional 绑定部分,我们介绍了处理多个Optional值绑定的一种非函数式方法,在本节中,我们将探讨多个可选值映射。由于可选值是applicative functors的实例,我们将开发一个apply函数来在可选值上使用:

func apply<T, V>(transform: ((T) -> V)?, input: T?) -> V? {
    switch transform {
        case .some(let fx): return fx <^> input
        case .none: return .none
    }
}

apply函数与fmap函数非常相似。transform函数是可选的,我们使用模式匹配来返回nonesome

在 Haskell 中,apply函数表示为<*>运算符。这个运算符已经被 Swift 函数式编程社区采用,因此我们将其用作apply函数:

infix operator <*> { associativity left }

func <*><T, V>(transform: ((T) -> V)?, input: T?) -> V? {
    switch transform {
        case .some(let fx): return fx <^> input
        case .none: return .none
    }
}

我们可以如下测试我们的apply函数:

func extractFullUserName(firstName: String)(lastName: String) -> String {
    return "\(firstName) \(lastName)"
}

extractFullUserName函数是一个柯里化函数,应该显式地转换为返回闭包,因为 Apple 在 Swift 2.2 中弃用了函数柯里化,并在 Swift 3.0 中将其移除。

让我们将其转换为 Swift 3.0 版本:

func extractFullUserName(firstName: String) -> (String) -> String { 
    return { (lastName: String) -> String in
        return "\(firstName) \(lastName)"
    }
}

现在我们可以使用这个函数来提取完整的用户名:

class User { 
    var firstName: String? 
    var lastName: String? 
} 
var fullName: String { 
    let user = User() 
    user.firstName = "John" 
    user.lastName = "Doe"
    let fullUserName = extractFullUserName <^> user.firstName <*> 
      user.lastName
    return fullUserName ?? "" 
}

通过组合fmapapply函数,我们能够map两个可选值。

事实上,Optional类型是一个monad,因此它实现了mapflatMap方法,我们不需要自己开发它。

以下示例展示了在optional类型上调用map方法:

let optionalString: String? = "A String literal" 
let result = optionalString.map { "\($0) is mapped" }

结果将是一个包含以下值的Optional String

A String literal is mapped

此外,我们可以使用flatMap来过滤nil值,并将可选值数组转换为未包装值数组。

在以下示例中,在optional Array上调用flatMap将消除我们的Array中的第三个元素(索引:2):

let optionalArray: [String?] = ["First", "Second", nil, "Fourth"] 
let nonOptionalArray = optionalArray.flatMap { $0 } 
print(nonOptionalArray) 

结果将是["First", "Second", "Fourth"]

摘要

在本章中,我们熟悉了处理可选值的不同技术。我们讨论了处理可选值的内置技术,例如可选绑定、守卫(guard)、合并(coalescing)和可选链(optional chaining)。然后我们探讨了函数式编程技术来处理可选值。我们创建了fmapapply函数以及相关的运算符来解决多个可选绑定问题。尽管一些开发者可能更喜欢使用内置的多个可选绑定,但实际探索函数式编程技术可以更好地理解我们将能够应用于其他问题的概念。

在接下来的章节中,我们将探讨一些函数式数据结构的例子,例如半群(Semigroup)、幺半群(Monoid)、二叉搜索树(Binary Search Tree)、链表(Linked List)、栈(Stack)和惰性列表(Lazy List)。

第八章. 函数式数据结构

我们熟悉命令式数据结构。实际上,在多种编程语言中都有许多关于命令式数据结构的参考资料。相比之下,关于声明式数据结构或函数式数据结构的参考资料并不多。这是因为函数式编程语言不像命令式编程语言那样主流。此外,由于以下原因,与命令式对应物相比,设计实现函数式数据结构更困难:

  • 在函数式编程中不建议使用可变性

  • 函数式数据结构预计比它们的命令式对应物更灵活

命令式数据结构严重依赖于可变性和赋值,使它们不可变需要额外的开发工作。每次我们更改命令式数据结构时,我们基本上都会覆盖之前的版本;然而,这与声明式编程不同,我们期望功能数据结构的旧版本和新版本都将继续存在并被使用。

我们可能会想,为什么要在函数式数据结构上费心,因为它们的设计和实现更困难?这个问题有两个答案:首先,函数式数据结构是高效的不可变数据结构。其次,它们支持函数式编程范式。当我们介绍到代数数据类型时,我们已经看到了这些例子,那就是在第四章第四章,枚举和模式匹配

在本章中,我们将通过编码示例进一步探索函数式数据结构。本章内容深受纯函数式数据结构Chris Okasaki剑桥大学出版社的启发,这是迄今为止该主题的一个很好的参考,其中包含各种 ML 和 Haskell 编程语言的示例。强烈推荐函数式程序员阅读 Okasaki 的书籍。在本章中,我们将涵盖这个主题,并探索 Okasaki 书籍中的一些示例,使用 Swift 实现。

尤其是我们将利用结构体和枚举来实现以下函数式数据结构:

  • 半群

  • 幂集

  • 二叉搜索树

  • 链表

  • 懒惰列表

这些数据结构的编码示例作为函数式编程范式和技术展示,它们不会是完整的。

我们知道不可变性是函数式数据结构最重要的属性。为了设计和实现不可变数据结构,我们不会改变函数式数据结构,而是创建一个新的版本,它与旧版本一起存在。实际上,我们将复制需要更改的部分,而不触及数据结构的原始版本。因此,我们将使用值类型,如结构体和枚举,以便能够实现这一点。此外,由于我们不会直接更改原始数据结构,我们将能够在新结构中共享原始数据结构的部分,而不用担心更改一个版本会如何影响另一个版本。让我们通过实现不同的函数式数据结构来检查我们将如何实现这一点。

半群

在计算机科学中,半群是一个代数结构,它有一个集合和一个二元运算,该运算接受集合中的两个元素并返回一个具有结合运算的半群。

首先,我们需要有一个集合和特定的二元运算,或者我们可以使这种行为通用,并定义以下协议:

protocol Semigroup {
    func operation(_ element: Self) -> Self
}

任何符合此协议的类型都需要实现operation方法。在这里,self代表符合此协议的类型。例如,我们可以扩展Int以符合半群协议,并为其提供求和操作:

extension Int: Semigroup {
    func operation(_ element: Int) -> Int {
        return self + element
    }
}

我们可以这样测试:

let number: Int = 5
number.operation(3)

这个测试并不能确保二元运算的结合律。让我们尝试这个:

let numberA: Int = 3
let numberB: Int = 5
let numberC: Int = 7

if numberA.operation(numberB.operation(numberC)) == (numberA.operation(
  numberB)).operation(numberC) {
    print("Operation is associative")
}

上述代码确保我们的二元运算符是结合的;因此,我们的半群得到了验证。但这看起来并不太美观;让我们实现一个操作符来使它看起来更好,更符合数学风格:

infix operator <> { associativity left precedence 150 }

func <> <S: Semigroup> (x: S, y: S) -> S {
    return x.operation(y)
}

让我们用<>运算符重写我们的测试:

if numberA <> (numberB <> numberC) == (numberA <> numberB) <> numberC {
    print("Operation is associative")
}

到目前为止,我们只扩展了Int,但我们可以扩展任何类型。让我们以数组为例进行扩展:

extension Array: Semigroup {
    func operation(_ element: Array) -> Array {
        return self + element
    }
}

operation方法与我们为Int所拥有的非常相似。唯一的区别在于类型,在这种情况下是一个数组:

print([1, 2, 3, 4] <> [5, 6, 7]) // prints "[1, 2, 3, 4, 5, 6, 7]"

此外,我们可以如下扩展String

extension String: Semigroup {
    func operation(_ element: String) -> String {
        return "\(self)\(element)"
    }
}

我们已经使用协议建立了一个组合的一般原则(两个对象结合成一个)。这种模式可以用于不同的目的。例如,我们可以为半群上的数组实现 reduce 的简短版本:

func sconcat <S: Semigroup> (initial: S, elements: [S]) -> S {
    return elements.reduce(initial, combine: <>)
}

sconcat函数名代表半群连接;我们可以这样测试它:

print(sconcat(initial: 0, elements:[1, 2, 3])) // 6
print(sconcat(initial: "", elements: ["A", "B", "C"])) // ABC
print(sconcat(initial: [], elements: [[1, 2], [3, 4, 5]])) // [1, 2, 3,
  4, 5]

我们最后的sconcat示例像flatMap一样工作,它会展平元素。

最后,我们的半群变成了以下形式:

infix operator <> { associativity left precedence 150 }

func <> <S: Semigroup> (x: S, y: S) -> S {
    return x.operation(y)
}

protocol Semigroup {
    func operation(_ element: Self) -> Self
}

extension Int: Semigroup {
    func operation(_ element: Int) -> Int {
        return self + element
    }
}

extension String : Semigroup {
    func operation(_ element: String) -> String {
        return self + element
    }
}

extension Array : Semigroup {
    func operation(_ element: Array) -> Array {
        return self + element
    }
}

func sconcat <S: Semigroup> (initial: S, elements: [S]) -> S {
    return elements.reduce(initial, combine: <>)
}

半群是一个简单的数据结构的绝佳例子,但它不像幂集那样受欢迎,我们将在下一节中探讨。

幂集

在计算机科学中,幂集是一个集合、一个二元运算以及集合中的一个元素,遵循以下规则:

  • 二元运算的结合律

  • 该元素是单位元

简而言之,一个结构是 Monoid,如果这个结构是一个 Semigroup,并且有一个是恒等元的元素。所以让我们定义一个新的协议,它扩展了我们的 Semigroup 协议:

protocol Monoid: Semigroup {
    static func identity() -> Self
}

extension Int: Monoid {
    static func identity() -> Int {
        return 0
    }
}

extension String: Monoid {
    static func identity() -> String {
        return ""
    }
}

extension Array: Monoid {
    static func identity() -> Array {
        return []
    }
}

我们可以像以下这样测试我们的结构:

numberA <> Int.identity() // 3
"A" <> String.identity() // A

由于 Monoid 有一个元素,我们可以使用这个元素作为初始值,并简化我们的 reduce 方法如下:

func mconcat <M: Monoid> (_ elements: [M]) -> M {
    return elements.reduce(M.identity(), combine: <>)
}

让我们来测试一下:

print(mconcat([1, 2, 3])) // 6
print(mconcat(["A", "B", "C"])) // ABC
print(mconcat([[1, 2], [3, 4, 5]])) // [1, 2, 3, 4, 5]

在计算机科学中,树是一个非常流行的 抽象数据类型ADT)或实现此 ADT 的数据结构,它模拟了一个具有根值和子树以及父节点的层次树结构,这些子树和父节点由一组链接的节点表示。

树数据结构可以递归地(局部地)定义为从根节点开始的节点集合,其中每个节点是一个包含值以及指向节点(子节点)的引用列表的数据结构,约束条件是没有任何引用是重复的,也没有指向根节点。

或者,一个树可以抽象地定义为一个整体(全局)有序树,每个节点都分配了一个值。这两种观点都很有用:虽然树可以作为一个整体进行数学分析,但实际上作为数据结构表示时,通常是由节点单独表示和处理的(而不是像表示有向图那样,作为节点列表和节点之间边的关系列表)。例如,将树作为一个整体来看,可以谈论给定节点的 父节点,但在一般情况下,作为一个数据结构,一个给定的节点只包含其子节点的列表,但不包含对其父节点的引用(如果有的话)。

在前一章中,我们实现了 Swift 中的泛型二叉树。以下是一个改进版本:

enum Tree<Element: Comparable> {
    case leaf(Element)
    indirect case node(lhs: Tree, rhs: Tree)
}

我们将 Tree 定义为一个具有三个不同情况的 enum

  • 叶子节点:如果我们处于 Tree 的一个分支的末端;简单来说,如果一个节点没有任何子节点,那么它就是一个 Leaf

  • 节点:一个具有左右两侧的结构

下图展示了一个示例 Tree

树

Tree 是一个泛型,其中的元素是可以比较的。

使用这个 Tree 的方法如下所示:

let functionalTree = Tree.node(lhs: Tree.leaf("First"),
                               rhs: Tree.node(lhs:
 Tree.leaf("Second"), 
                               rhs: Tree.leaf("Third")))

我们的 functionalTree 是不可变的,换句话说,它是持久的。它有一个作为 lhs 的叶子节点和一个包含两个叶子节点的节点作为 rhs。由于这个结构是不可变的,我们不必担心它是否会改变,并且我们可以与其他树共享这个树:

let secondFT = Tree.node(lhs: functionalTree, rhs: Tree.node(
                         lhs: Tree.leaf("Fourth"),
                         rhs: Tree.leaf("Fifth")))
let thirdFT = Tree.node(lhs: Tree.node(lhs: Tree.leaf("Fourth"),
                        rhs: Tree.leaf("Fifth")),
                        rhs: functionalTree)

在前面的例子中,我们使用了我们的第一个 Tree,即 functionalTree,作为 secondFTthirdFT 的一部分。

包含

这个 Tree 还远未完善,需要很多功能。例如,我们可能需要检查 Tree 是否包含特定的值。为了能够做到这一点,我们需要向我们的 Tree 添加以下方法:

static func contains(_ key: Element, tree: Tree<Element>) -> Bool {
    switch tree {
    case .leaf(let element):
        return key == element
    case node(let lhs, let rhs):
        return contains(key, tree:lhs) || contains(key, tree:rhs)
    }
 }

我们可以像以下这样测试 contains 方法:

let isFound = Tree.contains("First", tree: functionalTree) // will
  return true

二叉搜索树

在我们简单的 Tree 假设中,只有叶子节点包含值。这并不总是正确的。事实上,存在不同类型的树,它们具有不同的效用,而 二叉搜索树(BST) 就是其中之一。

在计算机科学中,二叉搜索树,有时称为有序或排序二叉树,是一种特定的容器:数据结构,它们在内存中存储 项目(如数字、名称等)。它们允许快速查找、添加和删除项目,并实现动态项目集或查找表,允许通过其键(例如,通过姓名查找某人的电话号码)来查找项目。

BSTs 将它们的键按排序顺序存储,以便查找和其他操作可以使用二分搜索的原则:当在树中查找一个键(或插入新键的位置)时,它们从根节点遍历到叶子节点,将树节点中存储的键进行比较,并根据比较结果决定是否在左子树或右子树中继续搜索。平均而言,这意味着每次比较都允许操作跳过大约一半的树,因此每次查找、插入或删除的时间与树中存储的项目数量的对数成比例。这比在(未排序的)数组中通过键查找项目所需的线性时间要好得多,但比哈希表上的相应操作要慢。

让我们改进我们的简单树并将其转换为 BST:

enum BinarySearchTree<Element: Comparable> {
    case leaf
    indirect case node(lhs: BinarySearchTree, element: Element,
                       rhs: BinarySearchTree)
}

BinarySearchTree 树与之前的 Tree 非常相似,唯一的区别是 node 包含 element 而不是 leaf。使用它的方法如下:

let functionalBST = BinarySearchTree.node(lhs: BinarySearchTree.node(
  lhs: BinarySearchTree.leaf, element: 1,
  rhs: BinarySearchTree.leaf),
  element: 5, rhs: BinarySearchTree.node(lhs:BinarySearchTree.leaf,
  element: 9, rhs: BinarySearchTree.leaf))

在这里,我们创建了一个 BST,因为存储在 lhs 中的值小于根节点,而存储在 rhs 中的值大于根节点。在这个例子中,lhs 是一个值为 1 的 BST。根节点的值为 5,而 rhs 是一个值为 9 的 BST,这个值大于根节点的值。

包含

此外,我们的 contains 方法需要修改,因为它将仅在叶子节点中搜索。让我们改进它,假设我们的树是一个 BST:

static func contains(_ item: Element, tree: BinarySearchTree<Element>)
  -> Bool {
    switch tree {
    case .leaf:
        return false
    case .node(let lhs, let element, let rhs):
        if item < element {
            return contains(item, tree: lhs)
        } else if item > element {
            return contains(item, tree: rhs)
        }
        return true
    }
}

此方法搜索特定的 element,如果它在 node 中找到它,则返回 true

以下展示了此方法的示例用法:

let isFound = BinarySearchTree.contains(9, tree: functionalBST)

isFound 变量在这种情况下将被设置为 true

大小

为了使这个 BST 更完整,让我们实现一个属性来检查它的大小:

var size: Int {
    switch self {
    case .leaf:
        return 0
    case .node(let lhs, _, let rhs):
        return 1 + lhs.size + rhs.size
    }
}

这个计算属性将提供 BST 的大小,我们可以如下使用它:

print(functionalBST.size) // prints "3"

元素

能够从 BST 元素生成一个数组将是非常棒的。这可以如下完成:

var elements: [Element] {
    switch self {
    case .leaf:
        return []
    case .node(let lhs, let element, let rhs):
        return lhs.elements + [element] + rhs.elements
    }
}

空的

我们可以实施一个辅助方法来生成空二叉搜索树(BST),如下所示:

static func empty() -> BinarySearchTree {
    return .leaf
}

以下是一个计算属性,用于检查 BST 是否为空:

var isEmpty: Bool {
    switch self {
    case .leaf:
        return true
    case .node(_, _, _):
        return false
    }
}

让我们测试这些函数:

let emptyBST = BinarySearchTree<Int>.empty()
print(emptyBST.isEmpty)

在前面的代码中,我们创建了一个空的 BST 并使用 isEmpty 属性检查它是否为空。显然,结果将是 true

这个 BST 实现远未完成,需要通过实现检查它是否为 BST 的方法来改进。

最后,我们的二叉搜索树(BST)变成了以下这样:

enum BinarySearchTree<Element: Comparable> {
    case leaf
    indirect case node(lhs: BinarySearchTree, element: Element,
                       rhs: BinarySearchTree)

    var size: Int {
        switch self {
        case .leaf:
            return 0
        case .node(let lhs, _, let rhs):
            return 1 + lhs.size + rhs.size
        }
    }

    var elements: [Element] {
        switch self {
        case .leaf:
            return []
        case .node(let lhs, let element, let rhs):
            return lhs.elements + [element] + rhs.elements
        }
    }

    var isEmpty: Bool {
        switch self {
        case .leaf:
            return true
        case .node(_, _, _):
            return false
        }
    }

    init() {
        self = .leaf
    }

    static func empty() -> BinarySearchTree {
        return .leaf
    }

    init(element: Element) {
        self = .node(lhs: .leaf, element: element, rhs: .leaf)
    }

    static func contains(_ item: Element,
                           tree: BinarySearchTree<Element>)
      -> Bool {
        switch tree {
        case .leaf:
            return false
        case .node(let lhs, let element, let rhs):
            if item < element {
                return contains(item, tree: lhs)
            } else if item > element {
                return contains(item, tree: rhs)
            }
            return true
        }
    }
}

尽管它并不代表 BST 的完整实现,但我们能够以函数式风格开发它,并且我们将在其他树之间共享和重用这棵树,因为它们是不可变的。

Lists

列表有多种类型,包括链表、双向链表、多重链表、循环链表、队列和栈。

在本节中,我们将展示一个简单的链表,这是命令式编程语言中最简单且最受欢迎的数据结构之一。

链表是由称为节点的数据元素线性集合,这些节点使用指针指向下一个节点。链表以线性顺序方式存储其数据。简单地说,每个节点由数据和指向序列中下一个节点的引用组成:

Lists

让我们从简单版本开始:

enum LinkedList<Element: Equatable> {
    case end
    indirect case node(data: Element, next: LinkedList<Element>)
}

我们的方法与我们的 BST 实现方法类似。区别在于 node 情况具有 data 元素和指向其下一个元素的指针,该指针也是一个 LinkedList

Empty LinkedList

我们的 LinkedList 需要一个方法来创建它为空:

static func empty() -> LinkedList {
    return .end
}

这就像返回 .end 一样简单。

Cons

我们需要有一种方法来向 LinkedList 添加项目,所以我们按照以下方式实现它:

func cons(_ element: Element) -> LinkedList {
    return .node(data: element, next: self)
}

这种简单的方法将数据追加到 LinkedList 的前面;换句话说,它就像对栈的推操作。我们可以如下测试它:

let functionalLinkedList = LinkedList<Int>.end.cons(1).cons(2).cons(3)
print(functionalLinkedList)

这个操作的最终结果应该是以下这样:

node(3, LinkedList<Swift.Int>.node(2, LinkedList<Swift.Int>.node(
  1, LinkedList<Swift.Int>.end)))

函数式编程语言,如 Haskell 和 Scala,有 cons 操作符。在 Haskell 中是 :,在 Scala 中是 ::。由于我们无法在 Swift 中使用 : 来定义中缀操作符,我们将使用 <| 代替:

infix operator <| { associativity right precedence 100 }

func <| <T>(lhs: T, rhs: LinkedList<T>) -> LinkedList<T> {
    return .node(data: lhs, next: rhs)
}

我们可以如下测试它:

let functionalLLWithCons = 3 <| 2 <| 1 <| .end

这个语句会产生完全相同的结果。

再次,这个 LinkedList 远未完成,但我们已经实现了很高的可重用性,因为它是以函数式实现的。我们可以使用/共享我们的 functionalLinkedList 与其他链表,而不用担心变化和不一致性。让我们检查以下内容:

let secondLL = functionalLinkedList.cons(4)
let thirdLL = functionalLinkedList.cons(5)
let fourthLL = LinkedList<Int>.node(data: 1, next: secondLL)

在前面的例子中,我们使用 functionalLinkedList 并向其中添加一个新项目(4)以获得 secondLL,以及 5 以获得 thirdLL。我们还使用 secondLL 来创建 fourthLL

Contains

为了使这个 LinkedList 更有趣,我们将开发一个类似于为 BST 开发的包含方法:

static func contains(_ key: Element, list: LinkedList<Element>) -> Bool {
    switch list {
    case .end:
        return false
    case .node(let data, let next):
        if key == data {
            return true
        } else {
            return contains(key, list: next)
        }
    }
}

这个方法递归地在 LinkedList 中检查特定元素,如果找到该元素则返回 true

print(LinkedList.contains(1, list: functionalLinkedList))

这个表达式的结果是 true

Size

我们可以实现一个计算出的 size 属性来计算链表的大小如下:

var size: Int {
    switch self {
    case .node(_, let next):
        return 1 + next.size
    case .end:
        return 0
    }
}

这个方法递归地遍历 LinkedList 并计算节点数:

print(functionalLinkedList.size)

在这个例子中,结果将是 3

Elements

我们可以实施一个计算属性来提供一个元素数组如下:

var elements: [Element] {
    switch self {
    case .node(let data, let next):
        return [data] + next.elements
    case .end:
        return []
    }
}

在这里,我们递归遍历 LinkedList 并返回一个数据数组。我们将能够使用这个属性如下:

print(functionalLinkedList.elements)

这个语句打印 [3, 2, 1]

isEmpty

LinkedList 需要的另一个常见操作是检查它是否为空。我们可以轻松地以下方式实现它:

var isEmpty: Bool {
    switch self {
    case .node(_ , _):
        return false
    case .end:
        return true
    }
}

为了测试这个计算属性,我们将创建一个空的 LinkedList 如下:

let emptyLL = LinkedList<Int>.end
print(emptyLL.isEmpty)

print(functionalLinkedList.isEmpty)

在前面的例子中,第一个 print 语句的结果是 true,第二个结果是 false

map, filter, 和 reduce

你可能想知道我们是否能够将高阶函数如 map、filter 和 reduce 应用到我们的链表上。我们已经使用递归 enum 实现了我们的链表,递归模式非常适合高阶函数。

让我们从 map 开始:

func map<T>(_ transform: (Element) -> T) -> LinkedList<T> {
    switch self {
    case .end:
        return .end
    case .node(let data, let next):
        return transform(data) <| next.map(transform)
    }
}

使用这个方法,我们将能够转换链表中的元素。这里没有什么特别的;我们使用之前定义的相同的 cons 操作符。以下语句将测试我们的方法:

let mappedFunctionalLL = functionalLinkedList.map { $0 * 2 }

结果应该是以下这样:

node(6, LinkedList<Swift.Int>.node(4, LinkedList<Swift.Int>.node(
  2, LinkedList<Swift.Int>.end)))

因此,我们可以轻松地将链表中的元素乘以 2

让我们继续 filter 方法:

func filter(_ predicate: ((Element) -> Bool)) -> LinkedList<Element> {
    switch self {
    case .end:
        return .end
    case .node(let data, let next):
        return predicate(data) ? data <| next.filter(predicate) :
          next.filter(predicate)
    }
}

在这里,我们首先检查 predicate 是否产生结果。如果产生了,然后我们将我们的 cons 操作符应用于数据,并递归地 filter 下一个元素。否则,我们只是递归地应用 filter 到下一个元素。我们可以如下测试这个方法:

let filteredFunctionalLL = functionalLinkedList.filter { $0 % 2 == 0 }

在前面的代码示例中,我们 filter 我们链表中的偶数元素。这个语句的结果如下:

node(2, LinkedList<Swift.Int>.end)

能够对链表进行 mapfilter 是很棒的,但我们还需要一个 reduce 方法。让我们来实现这个:

func reduce<Value>(_ initial: Value, combine: (Value, Element) -> Value)
  -> Value {
    switch self {
    case .end:
        return initial
    case .node(let data, let next):
        return next.reduce(combine(initial, data), combine: combine)
    }
}

在前面的代码示例中,我们递归遍历链表的元素并将值 reduce 到单个值。以下代码展示了使用示例:

let reducedFunctionalLL = functionalLinkedList.reduce(0) { $0 + $1}

这个表达式的结果是 6

最后,我们的 LinkedList 变成以下形式:

/// Operator
infix operator <| { associativity right precedence 100 }

func <| <T>(lhs: T, rhs: LinkedList<T>) -> LinkedList<T> {
    return .node(data: lhs, next: rhs)
}

/// LinkedList

enum LinkedList<Element: Equatable> {
    case end
    indirect case node(data: Element, next: LinkedList<Element>)

    var size: Int {
        switch self {
        case .node(_, let next):
            return 1 + next.size
        case .end:
            return 0
        }
    }

    var elements: [Element] {
        switch self {
        case .node(let data, let next):
            return [data] + next.elements
        case .end:
            return []
        }
    }

    var isEmpty: Bool {
        switch self {
        case .node(_ , _):
            return false
        case .end:
            return true
        }
    }

    static func empty() -> LinkedList {
        return .end
    }

    func cons(_ element: Element) -> LinkedList {
        return .node(data: element, next: self)
    }

    func map<T>(_ transform: (Element) -> T) -> LinkedList<T> {
        switch self {
        case .end:
            return .end
        case .node(let data, let next):
            return transform(data) <| next.map(transform)
        }
    }

    func filter(_ predicate: ((Element) -> Bool)) -> LinkedList<Element> {
        switch self {
        case .end:
            return .end
        case .node(let data, let next):
            return predicate(data) ? data <| next.filter(predicate)
              : next.filter(predicate)
        }
    }

    func reduce<Value>(_ initial: Value, combine: (Value, Element)
      -> Value) -> Value {
        switch self {
        case .end:
            return initial
        case .node(let data, let next):
            return next.reduce(combine(initial, data), combine: combine)
        }
    }

    static func contains(_ key: Element, list: LinkedList<Element>)
      -> Bool {
        switch list {
        case .end:
            return false
        case .node(let data, let next):
            if key == data {
                return true
            } else {
                return contains(key, list: next)
            }
        }
    }
}

栈是一种基于 后进先出LIFO)策略的集合。

下面的图展示了示例栈:

栈

要实现一个简单的函数式栈,我们需要提供 pushpopisEmptysize 操作。我们在前面的部分实现了一个函数式 LinkedList,它可以用来实现一个简单的函数式栈,具有以下操作:

  • push: LinkedList 中的 cons 操作

  • pop

  • isEmpty: LinkedList 中的 isEmpty 操作

  • 大小: LinkedList 中的 size 方法

如此看来,唯一缺少的操作是 pop。让我们来实现它:

func pop() -> (element: Element, linkedList: LinkedList)? {
    switch self {
    case .node(let data, let next):
        return (data, next)
    case .end:
        return nil
    }
}

为了测试,我们可以执行以下操作:

if let (element, linkedList) = functionalLinkedList.pop() {
    print(element)
    let newLinkedList = linkedList.pop()
    print(newLinkedList)
}

第一个 print 的结果将是 3,第二个 print 的结果将是以下这样:

Optional((2, LinkedList<Swift.Int>.node(1, LinkedList<Swift.Int>.end)))

这只是一个示例实现,我们使用 Optional Tuple 作为返回值以获取弹出的元素以及结果新的链表。

我们还需要做的一件事是将我们的 enum 名称更改为更通用的名称,例如 list。

最后,我们的栈变得与列表非常相似。

惰性列表

到目前为止,我们已经实现了一个链表和一个栈作为列表。函数式编程中的一个关键概念是惰性评估的概念。我们可以使我们的列表成为惰性的,这样元素将在我们访问它们时才被评估。我们需要以这种方式修改 node,使其返回一个包含 List 的函数作为 next,而不是列表本身。该函数将在调用时进行评估;因此,我们的列表将是惰性的。

我们首先修改我们的 node 情况。在我们的 LinkedList 示例中,nextLinkedList<Element> 类型。为了使我们的列表成为惰性的,我们将修改 next 以使其成为一个返回我们的 List 的函数:

enum LazyList<Element: Equatable> {
    case end
    case node(data: Element, next: () -> LazyList<Element>)
}

正如我们在前面的代码中所看到的,我们的 Node 情况并没有定义为间接的,因为 next 不是 LazyList 类型,而是返回 LazyList 的函数的引用。

我们需要将此更改纳入我们的属性和方法中。这就像将任何 next 改为 next() 一样简单。例如,我们的 size 属性变为以下内容:

var size: Int {
    switch self {
    case .node(_, let next):
        return 1 + next().size
    case .end:
        return 0
    }
}

如果我们遵循代码并正确修改它,我们会看到我们的 mapfilter 无法编译。我们需要按照以下方式更改操作符:

infix operator <|| { associativity right precedence 100 }

func <|| <T>(lhs: T, rhs: () -> LazyList<T>) -> LazyList<T> {
    return .node(data: lhs, next: rhs)
}

在这里,我们将 rhs 更改为与我们的 LazyList 的 next 匹配的函数类型。这种更改并没有解决我们的 mapfilter 问题。看起来 infix 操作符的右侧在传递给它之前就被评估了,而我们不希望这样。

这是因为我们在 mapfilter 方法中并没有将闭包传递给我们的操作符:

func map<T>(_ transform: (Element) -> T) -> LazyList<T> {
    switch self {
    case .end:
        return .end
    case .node(let data, let next):
        return transform(data) <|| next().map(transform)
    }
}

在我们的 map 方法示例中,next().map(transform) 不是一个闭包。如果我们将其包裹在 { } 中,那么它就变成了一个闭包。我们可以按照以下方式修改我们的 infix 操作符:

func <|| <T>(lhs: T, rhs: @autoclosure(escaping) () -> LazyList<T>)
  -> LazyList<T> {
    return .node(data: lhs, next: rhs)
}

@autoclosure 属性在表达式周围创建一个自动闭包。因此,当我们编写 next().map(transform) 这样的表达式时,它会在传递给我们的 infix 操作符之前自动包裹在一个闭包中,成为 { next().map(transform) }

从 Swift 1.2 开始,autoclosure 默认为 noescape。此属性确保参数不会被存储以供稍后执行,并且不会超出调用范围的生命周期。noescape 实现添加了轻微的性能优化,并绕过了对属性和方法使用 self 进行注解的需要。

括号中的 escaping 注解是必要的,以便表示闭包的持续时间将超过其声明的范围的生命周期。

最后,我们需要通过以下方式将 cons 方法包裹在 { } 中进行更改:

func cons(_ element: Element) -> LazyList {
    return .node(data: element, next: { self })
}

让我们测试我们的 LazyList 并看看它是否正常工作:

let ourLazyList = 3 <|| 2 <|| 1 <|| LazyList.end // node(3, (Function))
print(ourLazyList.size) // prints 3

我们惰性列表现在如下所示:

/// Operator
infix operator <|| { associativity right precedence 100 }

func <|| <T>(lhs: T, rhs: @autoclosure(escaping) () -> LazyList<T>)
  -> LazyList<T> {
    return .node(data: lhs, next: rhs)
}

/// Lazy List
enum LazyList<Element: Equatable> {
    case end
    case node(data: Element, next: () -> LazyList<Element>)

    var size: Int {
        switch self {
        case .node(_, let next):
            return 1 + next().size
        case .end:
            return 0
        }
    }

    var elements: [Element] {
        switch self {
        case .node(let data, let next):
            return [data] + next().elements
        case .end:
            return []
        }
    }

    var isEmpty: Bool {
        switch self {
        case .node(_ , _):
            return false
        case .end:
            return true
        }
    }

    static func empty() -> LazyList {
        return .end
    }

    func cons(_ element: Element) -> LazyList {
        return .node(data: element, next: { self })
    }

    func removeLast() -> (element: Element, linkedList: LazyList)? {
        switch self {
        case .node(let data, let next):
            return (data, next())
        case .end:
            return nil
        }
    }

    func map<T>(_ transform: (Element) -> T) -> LazyList<T> {
        switch self {
        case .end:
            return .end
        case .node(let data, let next):
            return transform(data) <|| next().map(transform)
        }
    }

    func filter(_ predicate: ((Element) -> Bool)) -> LazyList<Element> {
        switch self {
        case .end:
            return .end
        case .node(let data, let next):
            return predicate(data) ? data <|| next().filter(predicate)
              : next().filter(predicate)
        }
    }

    func reduce<Value>(_ initial: Value, combine: (Value, Element)
      -> Value) -> Value {
        switch self {
        case .end:
            return initial
        case .node(let data, let next):
            return next().reduce(combine(initial, data), combine: combine)
        }
    }

    static func contains(_ key: Element, list: LazyList<Element>) -> Bool {
        switch list {
        case .end:
            return false
        case .node(let data, let next):
            if key == data {
                return true
            } else {
                return contains(key, list: next())
            }
        }
    }
}

摘要

在本章中,我们介绍了函数式数据结构的概念,并探讨了以函数方式实现的数据结构示例,例如 Semigroup、Monoid、BST、链表、栈和惰性列表。

尽管这些数据结构都不完整,但它们作为展示函数式编程范式和技术结构的例子是有用的。检查这些数据结构中的任何一个的性能也将是有益的。

在下一章中,我们将通过考察其益处来探讨不可变性的重要性。我们还将检查可变与不可变实现的例子,以及以函数式方式获取和设置不可变对象的方法,例如复制构造函数和透镜。

第九章. 不可变性的重要性

在面向对象和函数式编程中,不可变对象是在初始化后其状态不能被改变或修改的对象。因此,可变对象在其生命周期结束时保持不变,此时它被解除初始化。相比之下,可变对象在初始化后可以被其他对象无数次地更改。

不可变对象提高了可读性和运行时效率,使用它们简化了我们的应用程序。

本章将通过讨论以下主题并附带代码示例来介绍不可变性的概念:

  • 不可变性

  • 不可变性的好处

  • 可变性的案例

  • 与方法比较的示例

    • 副作用和意外后果

    • 测试性

  • 复制构造函数

  • 镜头

不可变性

不可变对象是在初始化后其状态不能被修改的对象。不可变对象这一特性在多线程应用程序中至关重要,因为它允许一个线程在不担心其他线程更改的情况下操作由不可变对象表示的数据。

如果一个对象及其所有属性都是不可变的,则认为该对象是不可变的。在某些情况下,即使某些内部属性发生变化,但对象的状态从外部看起来是不可变的,这样的对象也被认为是不可变的。例如,使用缓存技术来缓存资源密集型计算结果的对象可以被视为不可变对象。

不可变对象具有以下特性:

  • 它们易于构建、测试和使用

  • 它们易于理解和推理

  • 它们天生是线程安全的,没有同步问题

  • 它们不需要复制构造函数

  • 它们总是具有失败原子性,所以如果不可变对象抛出异常,它将不会陷入不希望/不确定的状态

  • 它们提供更高的安全性

不可变变量

在命令式编程风格中,应用变量中保持不变的内容被称为常量,以区分那些在执行过程中可能被更改的变量。例如,可能包括视图的高度和宽度或π的几位小数值。

与 Objective-C 等编程语言不同,其中一些类型是可变的,而另一些不是,Swift 提供了一种创建同一类型不可变或可变版本的方法。在 Swift 中,我们使用letvar关键字来创建和存储值:

  • var 关键字用于创建可以稍后更改的变量,换句话说,用于创建可变变量

  • let 关键字用于创建不能稍后更改的常量,换句话说,不可变变量或常量

因此,在 Swift 中,我们不需要有像NSMutableArrayNSArrayNSMutableDictionaryNSDictionary这样的类型来区分可变性和不可变性。我们可以简单地使用varlet来定义Dictionary,使其可变或不可变。

此外,Swift 编译器总是建议并警告我们那些未更改且将来将被转换为常量的变量。

弱不可变与强不可变

有时,一个对象的一些属性可能是不可变的,而其他属性可能是可变的。这类对象被称为弱不可变对象。弱不可变性意味着即使对象的其他部分可能是可变的,我们也不能更改对象状态中的不可变部分。如果所有属性都是不可变的,那么该对象就是不可变的。如果对象创建后整个对象都不能被修改,则该对象被称为强不可变对象。

引用类型与值类型

我们在之前的章节中已经讨论了这个问题,但重要的是要强调,在大多数面向对象编程(OOP)语言中,实例可以被共享,对象可以通过它们的引用传递。Swift 类和闭包也是如此。在这些情况下,重要的是要理解,当对象通过引用共享时,对象的状态可能会被改变。换句话说,如果任何用户更改了可变对象的引用,所有其他使用该对象的用户都将受到影响。

不可变性的好处

我们已经知道不可变性有助于安全性和性能,但在实际应用开发中,不可变性可以为我们带来更多的好处,这将在以下章节中解释。

线程安全

在多线程应用程序中,不可变对象非常有用,因为多个线程可以操作不可变对象的数据,而不用担心其他线程对数据所做的更改。

由于不可变对象几乎不可更改,因此我们可以安全地假设在从不同线程访问对象时它们将保持不变。这个假设简化了大多数复杂且难以解决和维护的多线程问题。例如,我们根本不需要考虑同步/锁定机制。

假设我们有一个包含可变数组类型的可变对象,例如,一个具有四个属性的Product类:

struct Producer {
    let name: String
    let address: String
}

class Product {
    var name: String = ""
    var price: Double = 0.0
    var quantity: Int = 0
    var producer: Producer

    init(name: String,
        price: Double,
     quantity: Int,
     producer: Producer) {

        self.name = name
        self.price = price
        self.quantity = quantity
        self.producer = producer
    }
}

如前例所示,所有属性都被定义为可变的。现在让我们创建一个Product类型的数组:

let producer = Producer(name: "ABC",
                     address: "Toronto, Ontario, Canada")

var bananas = Product(name: "Banana",
                     price: 0.79,
                  quantity: 2,
                  producer: producer)

var oranges = Product(name: "Orange",
                     price: 2.99,
                  quantity: 1,
                  producer: producer)

var apples = Product(name: "Apple",
                    price: 3.99,
                 quantity: 3,
                 producer: producer)

var products = [bananas, oranges, apples]

假设我们需要products数组在不同的线程之间共享。不同的线程可能会以不同的方式更改数组。有些人可能会更改价格,而其他人可能会更改数量。有些人可能会向数组中添加或删除项目。

需要解决的首要问题是跟踪更改并知道谁更改了数组以及何时更改。这已经很复杂了,所以让我们简化问题,只向数组添加一个项目。此外,让我们假设我们只对最新的更改感兴趣。

为了能够跟踪最新的更改,让我们创建另一个对象:

class ProductTracker {
    private var products: [Product] = []
    private var lastModified: NSDate?

    func addNewProduct(item: Product) -> (date: NSDate,
                                  productCount: Int) {
        products.append(item)
        lastModified = NSDate()
        return (date: lastModified!, productCount: products.count)
    }

    func lastModifiedDate() -> NSDate? {
        return lastModified
    }

    func productList() -> [Product] {
        return products
    }
}

ProductTracker类有一个products数组和一个lastModified变量来跟踪最新的更改。此外,它有三个方法:一个用于向数组中添加新产品,另一个用于检索最后修改日期,最后一个用于检索产品列表。

假设我们想要使我们的ProductTracker类线程安全,并允许多个对象访问我们的ProductTracker对象。我们不能允许多个线程同时执行addNewProduct,同时其他线程列出产品。首先,我们需要一个锁定机制来锁定类在修改期间,其次,我们需要保护lastModified不被锁定修改,最后,需要一个解锁机制。

苹果提供了多种多线程机制,如NSThread、Grand Central Dispatch (GCD)和操作队列,以克服这些问题,但多线程仍然复杂。

引用透明性

引用透明性通常意味着我们可以始终用函数的返回值替换函数,而不会影响应用程序的行为。

引用透明性是代码可重用性的保证。它还否认了数据可变状态的存在。在可变状态的情况下,同一函数的两次调用可能产生两个不同的结果,这非常难以测试和维护。

低耦合

耦合是代码依赖性的度量。我们总是希望尽可能减少耦合,并使我们的代码组件尽可能独立。低耦合允许我们更改组件而不影响其他代码组件。低耦合的代码更容易阅读,因为每个组件都有其相对较小的责任区域,尽管我们只需要理解这部分代码,而不必花时间弄清楚整个系统是如何工作的。

不可变性有助于实现低耦合。不可变数据可以安全地通过不同的代码块,无需担心其被转换并影响代码的其他部分。纯函数转换数据并返回结果,而不影响输入数据。因此,如果函数包含错误,我们可以轻松找到它。此外,使用值类型和不可变数据结构意味着我们可以显著减少对象引用。

以下展示了数据转换的想法。我们有一个不可变数组numbers,我们需要计算它包含的所有偶数的总和:

let numbers: [Int] = [1, 2, 3, 4, 5]
let sumOfEvens = numbers.reduce(0){$0 + (($1 % 2 == 0) ? $1 : 0) }

numbers数组没有改变,可以传递给任何其他函数而不会产生任何副作用:

print(numbers) // [1, 2, 3, 4, 5] 
print(sumOfEvens) // 6

使用不可变数据的就地转换可以帮助我们减少耦合。

避免时间耦合

假设我们有一个依赖于另一个代码语句的代码语句,如下所示:

func sendRequest() {
    let sessionConfig = URLSessionConfiguration.default()
    let session = URLSession(configuration: sessionConfig,
                                  delegate: nil,
                             delegateQueue: nil)

    var url: NSURL?
    var request: URLRequest

    /* First request block starts: */
    url = URL(string: "https://httpbin.org/get")
    request = URLRequest(url: url! as URL)
    request.httpMethod = "GET"

    let task = session.dataTask(with: request,
                   completionHandler: {

        (data: Data?, response: URLResponse?, error: NSError?) -> Void in
        if (error == nil) {
            let statusCode = (response as! HTTPURLResponse).statusCode
            print("URL Session Task Succeeded: HTTP \(statusCode)")
        } else {
            print("URL Session Task Failed: %@",   error!.localizedDescription);
        }
        })
    task.resume()
    /* First request block ends */

    /* Second request block starts */
    url = URL(string: "http://requestb.in/1g4pzn21") // replace with
      a new requestb.in
    request = URLRequest(url: url! as URL)

    let secondTask = session.dataTask(with: request,
                         completionHandler: {

        (data: Data?, response: URLResponse?, error: NSError?) -> Void in
        if (error == nil) {
            let statusCode = (response as! HTTPURLResponse).statusCode
            print("URL Session Task Succeeded: HTTP \(statusCode)")
        } else {
            print("URL Session Task Failed: %@",
              error!.localizedDescription);
        }
    })
    secondTask.resume()
}

在前面的代码示例中,我们设置了两个不同的 HTTP 请求。假设我们不再需要第一个请求,我们删除以下代码块:

url = URL(string: "https://httpbin.org/get")
      request = URLRequest(url: url! as URL)
      request.httpMethod = "GET"

编译器不会抱怨,但因为我们删除了request.httpMethod = "GET",我们的第二个请求将无法工作。这种情况被称为时间耦合。如果我们使用let的不可变定义,我们就可以避免时间耦合。

避免身份可变性

如果对象的内部状态相同,我们可能需要对象是相同的。当修改对象的状态时,我们并不期望它改变其身份。不可变对象完全避免了这个问题。

失败原子性

如果一个类抛出运行时异常,它可能会处于损坏状态。不可变性防止了这个问题。由于对象的状态仅在初始化期间修改,因此对象永远不会处于损坏状态。初始化要么失败,拒绝对象初始化,要么成功,创建一个有效的坚固对象,该对象永远不会改变其封装的状态。

并行化

不可变性使得并行化代码执行更容易,因为对象和实例之间没有冲突。

异常处理和错误管理

如果我们只使用不可变类型,即使有异常,我们应用程序的内部状态也将保持一致,因为我们的不可变对象不会保持不同的状态。

缓存

对不可变对象的引用可以被缓存,因为它们不会改变;因此,下次我们尝试访问它时,将快速检索到相同的不可变对象。一个示例技术是第二章中解释的记忆化函数和闭包

状态比较

应用程序的状态是在给定时间点所有对象的集合状态。状态随时间快速变化,并且应用程序需要改变状态以继续运行。

然而,不可变对象在时间上具有固定的状态。一旦创建,不可变对象的状态就不会改变,尽管整个应用程序的状态可能会改变。这使得跟踪发生的事情和简化状态比较变得容易。

编译器优化

编译器对在其生命周期内值不会改变的项的let语句进行优化更好。例如,Apple 写道:“在所有情况下,如果集合不需要改变,创建不可变集合都是良好的实践。这样做可以使 Swift 编译器优化您创建的集合的性能。”(在适当的情况下,请优先使用let而不是var。)

可变性案例

每当我们需要更改不可变对象时,我们都需要创建一个新的、修改后的副本。这可能对于小型和简单的对象来说可能并不昂贵和繁琐,但在我们拥有具有大量属性和操作的大型或复杂对象的情况下,这将是如此。

对于具有独特身份的对象,例如用户的个人资料,更改现有对象比创建一个新的、修改过的副本要简单得多,也更直观。我们可能希望维护用户个人资料的单一对象,并在必要时对其进行修改。这可能不是一个很好的例子,因为很难看到这种情况下性能的惩罚,但执行速度对于某些类型的应用程序(如游戏)来说可能是一个非常重要的区别因素。例如,用可变对象表示我们的游戏角色可能会使我们的游戏比需要每次更改时都创建一个新的、修改过的游戏角色副本的替代实现运行得更快。

此外,我们的现实世界感知不可避免地基于可变对象的概念。我们在现实生活中处理周围的所有对象。这些对象大多数时候是相同的,如果需要,我们会改变它们的一些特性。

例如,我们在家里粉刷墙壁而不是更换整个墙壁。我们将墙壁视为具有修改过的属性(在这种情况下是颜色)的相同对象。当我们粉刷墙壁时,墙壁的身份保持不变,但其状态发生变化。

因此,无论何时我们在应用中用模型来表示现实世界的对象,使用可变对象来感知和实现领域模型都是不可避免的更容易。

一个例子

我们理解在某些情况下不可变性会使我们的生活变得更难。在前一节中,我们几乎没有触及这些问题的表面。我们将在接下来的章节中更详细地检查这些问题。

让我们以函数式编程(FP)风格重新开发我们的Product示例,并将其与面向对象(OOP)的对应版本进行比较。

让我们在本章中使Product示例不可变,并检查结果:

struct FunctionalProduct {
    let name: String
    let price: Double
    let quantity: Int
    let producer: Producer
}

现在我们有了struct而不是类,所有属性都是不可变的。此外,我们不需要init方法,因为struct会自动提供它。

我们还需要修改我们的ProductTracker类:

struct FunctionalProductTracker {
    let products: [FunctionalProduct]
    let lastModified: NSDate

    func addNewProduct(item: FunctionalProduct) -> (date: NSDate,
                                                products:
                                                  [FunctionalProduct]) {

        let newProducts = self.products + [item]
        return (date: NSDate(), products: newProducts)
    }
}

我们的FunctionalProductTracker简化了:它是具有不可变products数组的struct,我们的addNewProduct不会修改对象的状态,而是每次都提供一个包含products的新数组。实际上,我们可以从这个struct中移除addNewProduct方法,并在客户端对象中处理它。

副作用和意外后果

我们的可变示例设计可能会产生不可预测的副作用。如果有多个客户端持有ProductTracker实例的引用,产品可以通过以下两种方式从任何这些客户端的下面发生变化:

  • 我们可以直接重新分配products的值。这可以通过将其设置为private以供客户端调用来修复,但对于类内的修改则无法修复。

  • 我们可以从任何客户端调用addNewProduct()并修改products

无论哪种方式,由于修改,都可能会出现副作用和意外的后果。

在我们的不可变示例中,由于我们的 FunctionalProductTracker 是一个值类型,所有属性都是不可变的,因此不可能产生那些意外的后果。products 不能直接更改(它是一个常量),而 addNewProduct() 返回一个新的实例,因此所有客户端都将处理他们期望处理的实例。

可测试性

我们可变示例的 addNewProduct() 方法没有返回值。虽然我们可以为它编写单元测试,但如何实现断言并不明显,因为该方法在我们的现有实例中产生了副作用,我们需要了解这些副作用。

我们不可变示例的 addNewProduct() 方法返回一个新的 Product 数组。我们只需检查 products 的值并断言。我们仍然有旧的和新的实例,所以我们有我们需要的所有东西来确保我们的代码按预期工作。

虽然我们在这本书中没有涵盖单元测试,但我们强烈建议您探索基于 QuickCheck 的库,如 Quick (github.com/Quick/Quick) 和 SwiftCheck (github.com/typelift/SwiftCheck),因为它们使用函数式编程技术来简化我们应用程序的单元测试过程。

复制构造函数和透镜

在检查我们的不可变示例实现后,我们无法断言它涵盖了命令式方法的所有功能。例如,它没有提供更改 productproducer 的方法。毕竟,我们无法更改它。

每当我们需要更改 product 的任何属性时,我们都需要经过以下过程:

let mexicanBananas = FunctionalProduct(name: bananas.name,
                                      price: bananas.price,
                                   quantity: bananas.quantity,
                                   producer: Producer(name: "XYZ",
                                                   address: "New Mexico,
                                                     Mexico"))

这个解决方案很冗长,看起来也不美观。让我们看看我们如何改进这个过程。

复制构造函数

第一种解决方案是提供一个新的 init 方法来复制当前实例。这种方法被称为复制构造函数。让我们添加我们的新 init 方法并利用它:

init(products: [FunctionalProduct],
    lastModified: NSDate) {

    self.products = products
    self.lastModified = lastModified
    }

    init(productTracker: FunctionalProductTracker,
               products: [FunctionalProduct]? = nil,
           lastModified: NSDate? = nil) {

    self.products = products ?? productTracker.products
    self.lastModified = lastModified ?? productTracker.lastModified
 }

我们还添加了默认的 init,因为通过向我们的 struct 添加新的 init 方法,我们失去了自动 init 生成的好处。我们还需要更改我们的 addNewProduct 来适应这些更改:

  func addNewProduct(item: FunctionalProduct) -> FunctionalProductTracker {

    return FunctionalProductTracker(productTracker: self,
                                          products: self.products + [item])
}

无论何时我们需要部分修改我们的对象,我们都可以使用这种技术轻松地做到这一点。

透镜

在上一节中,我们介绍了复制构造函数。在这里,我们将检查一个名为透镜的功能结构。简单来说,透镜是针对整个对象及其部分实现的 函数式获取器和设置器

  • 获取器:我们可以 透过 透镜查看不可变对象以获取其部分

  • 设置器:我们可以使用透镜来改变不可变对象的一部分

让我们实现一个 Lens

struct Lens<Whole, Part> {
    let get: Whole -> Part
    let set: (Part, Whole) -> Whole
}

让我们用它来改变我们的 FunctionalProduct 对象以获取和设置 producer 属性:

let prodProducerLens: Lens<FunctionalProduct, Producer> =
  Lens(get: { $0.producer},
       set: { FunctionalProduct(name: $1.name,
                               price: $1.price,
                            quantity: $1.quantity,
                            producer: $0)})

让我们更改 mexicanBananas 的生产者:

let mexicanBananas2 = prodProducerLens.set(Producer(name: "QAZ",
                                                 address: "Yucatan,
                                                   Mexico"),
                                           mexicanBananas)

通过我们的透镜,我们可以像前面代码所示那样更改它。

让我们检查另一个示例。假设我们有一个如下所示的 Producer 对象:

let chineeseProducer = Producer(name: "KGJ",
                             address: "Beijing, China")

我们想要更改 address

let producerAddressLens: Lens<Producer, String> =
  Lens(get: { $0.address },
       set: { Producer(name: $1.name,
                    address: $0)})

let chineeseProducer2 = producerAddressLens.set("Shanghai, China",
  chineeseProducer)

假设我们有一个 mexicanBananas2,需要有一个中国香蕉生产者,那么我们可以使用:

let chineseBananaProducer = prodProducerLens.set(
  producerAddressLens.set("Shanghai, China", chineseProducer), 
  mexicanBananas2)

这种语法看起来并不简单,而且似乎我们并没有获得太多。在下一节中,我们将简化它。

镜头组成

镜头组成将有助于简化我们的镜头;让我们看看它是如何做到的:

infix operator >>> { associativity right precedence 100 }

func >>><A,B,C>(l: Lens<A,B>, r: Lens<B,C>) -> Lens<A,C> {
    return Lens(get: { r.get(l.get($0)) },
                set: { (c, a) in
                    l.set(r.set(c,l.get(a)), a)
    })
}

让我们来测试一下:

let prodProducerAddress = prodProducerLens >>> producerAddressLens
let mexicanBananaProducerAddress = prodProducerAddress.get(mexicanBananas2)
let newProducer = prodProducerAddress.set("Acupulco, Mexico",
  mexicanBananas2)
print(newProducer)

结果将是:

FunctionalProduct(name: "Banana",
                  price: 0.79,
                  quantity: 2,
                  producer: Producer(name: "QAZ", address: "Acupulco, Mexico"))

通过使用镜头和组成,我们能够获取和设置产品的生产地址。

摘要

在本章中,我们首先探讨了不可变性的概念。我们通过例子探讨了其重要性和好处。然后我们分析了可变性的情况,并通过一个例子来比较可变性和不可变性对我们代码的影响。

最后,我们探讨了以函数式方式获取和设置不可变对象的方法,例如复制构造函数和镜头。

在接下来的章节中,我们将介绍面向对象编程(OOP)、协议导向编程POP)和函数式响应式编程FRP)。然后,我们将探讨混合面向对象和函数式编程范式,换句话说,对象函数式编程。

第十章. 两全其美 – 结合函数式编程范式与面向对象编程

"对象是具有多个方法的闭包,闭包是具有单个方法的对象。所以是的[面向对象编程(OOP)和函数式编程(FP)可以一起使用。]"
--埃里克·梅耶尔

在前面的章节中,我们大多数时候都在谈论函数式编程(FP)。你学习了 FP 的各种技术和范式。相比之下,我们几乎没有触及到面向对象编程(OOP)。我们主要讨论了命令式编程的缺点。在实践中,我们中的大多数人都必须处理由面向对象编程原则设计的应用程序。现实是,即使我们不喜欢面向对象编程,我们也必须接受它。例如,在 iOS 和 macOS 开发中,我们必须处理由面向对象编程原则设计的 Cocoa 和Cocoa Touch框架。

另一方面,我们熟悉面向对象编程(OOP),因为大多数人在某个时候都学过它,有些人发现用它来模拟现实世界问题是很自然的。

关于一种范式相对于另一种范式的益处有着大量的讨论。有些人声称它们可以统一;有些人声称它们是互斥的,我们应该选择一种范式而不是另一种。此外,不同的编程语言及其社区遵循不同的方法。例如,Haskell 是一种纯函数式编程语言,几乎不可能用它来做面向对象编程。事实上,用它来做面向对象编程是荒谬的。另一方面,像 Java、Ruby、Python 和 C#这样的语言是具有有限 FP 能力的面向对象编程语言。还有像 Scala 这样的语言,它混合了面向对象编程和函数式编程,并拥抱了这两个世界。

我们如何想象 Swift 在这些环境中呢?我们知道 Swift 不是一种纯函数式编程语言,但它具有 FP 能力,但我们需要进一步评估它在这一点上的表现。

此外,Swift 编程社区还介绍了一种新的范式:协议导向编程(POP)。此外,函数式响应式编程(FRP)变得非常流行,受到许多开发者的喜爱。

与其他范式相比,这种范式的优点和缺点是什么?我们如何设计我们的应用程序以利用所有这些范式?这些问题是我们将在本章中尝试回答的。因此,我们将首先介绍面向对象编程(OOP)、过程式编程(POP)和函数式响应式编程(FRP),然后我们将混合面向对象范式和函数式编程(FP)。

本章将涵盖以下主题,并附上代码示例:

  • 面向对象编程(OOP)范式的简要介绍

  • 面向对象编程(OOP)设计模式/原则

  • 过程式编程(POP)的简要介绍

  • 函数式响应式编程(FRP)

  • 混合面向对象编程(OOP)和函数式编程(FP)

面向对象编程(OOP)范式

在本节中,我们将检查面向对象编程(OOP)中的通用范式。我们从对象开始,因为它们是面向对象编程中最基本的构件。接下来,我们将探讨类,它们是创建对象的蓝图。然后我们将继续讨论诸如继承、多态和动态绑定等范式。

对象

在面向对象的应用程序中,对象是运行时实体或实例,它们在内存中(更具体地说,在堆中)占用空间。对象有一个关联的/分配的内存地址来存储其状态,以及一组定义在对象状态上的合适操作或方法。简而言之,在面向对象编程中,对象封装状态和行为。

创建对象需要一个蓝图或脚本,这在面向对象编程(OOP)中被称为类。下一节将更详细地探讨类概念。现在,我们将定义一个非常简单的类,以便能够讨论对象:

class User { 
  let name = "Constant name" 
  var age: Int = 0 

  func incrementUserAgeByOne() { 
    self.age += 1 
  } 
}

在这个示例中,nameage 是可以用来存储对象状态的常量和变量。incrementUserAgeByOne 方法是一个行为定义,它改变对象的状态。我们必须创建这个类的实例/对象才能使用它:

let object1 = User() 
object1.age = 2 
object1.incrementUserAgeByOne()

在我们前面的示例的第一行中,我们使用我们的 User 脚本创建了一个对象。同时,我们为我们的对象分配了一个内存地址并初始化了它。这个对象,作为 User 的实例,可以被使用;我们可以改变其状态并使用其方法进行操作或改变其状态。

从设计角度来看,对象模型了应用域中的实体。在我们的示例中,对象代表 User

理解以下关于类的内容非常重要:

  • 类是引用类型

  • 类封装了可变的状态

假设我们按照以下方式创建类的新实例:

let object2 = object1

这个赋值不会复制 object1,而是会使 object2 指向同一个实例。让我们来检查以下内容:

print(object2.age) 
object2.incrementUserAgeByOne() 
print(object1.age)

在这里,当我们打印 object2.age 时,它会产生与 object1.age 相同的结果;当我们调用 incrementUserAgeByOne 时,它会改变实例的年龄;因此,它也会改变 object1object2

这种行为在某些情况下可能很有用,例如,如果我们需要在不同的对象之间共享实例。例如,可以是数据库或文件管理系统操作,以及 iOS 和 macOS 应用程序中的 AppDelegate

另一方面,它可能会使代码的推理变得复杂。例如,如果我们有很多对同一实例的引用,并且更改其中一个会更改所有实例,我们就需要对所有实例做出反应。

如果我们不需要共享实例,那么我们可以创建一个新的对象并使用它:

let object3 = User() 
object3.age = 5 

object3.incrementUserAgeByOne() 
print(object3.age) 
print(object1.age)

在前面的示例中,当我们为我们的 object3 分配和初始化新的内存空间时,它并不与 object1object2 指向相同的实例。对 object3 的任何更改都不会影响 object1object2

类定义了一组属性和合适的操作。从类型安全的编程语言角度来看,类是实现用户定义类型(如前面示例中的 User 类)的结构。

最好,一个类应该是一个 抽象数据类型ADT)的实现,它隐藏了实现细节。

一个 ADT 的类实现可以由两种方法组成:

  • 返回有关实例状态的有意义抽象的方法

  • 将有效实例状态转换为另一个有效状态的方法转换

为了隐藏实现细节和抽象的目的,一个类中的所有数据都应该对该类是私有的。

让我们改进我们的 User 类示例中的抽象:

class User { 
  private let name: String 
  private var age: Int 

  init(name: String, age: Int) { 
    self.name = name 
    self.age = age 
  } 

  func incrementUserAgeByOne() { 
    self.age += 1 
  } 
}

我们将属性设置为 private,这样其他对象就无法访问/修改它们,除非它们位于同一个 Swift 文件中。此外,我们还添加了一个 init 方法,用于从我们的 User 类初始化对象。类客户端将使用 init 方法来初始化对象,并带有初始的 nameage 信息:

let object1 = User(name: "John Doe", age: 34)

最后,我们将 incrementUserAgeByOne 的访问级别留为内部(默认为内部);因此,同一模块中的任何其他对象都将能够使用它。

incrementUserAgeByOne 方法改变了我们对象的状态,这种变化将影响所有引用同一实例的对象。我们可以这样改变它:

func incrementUserAge(n: Int) -> Int { 
  return self.age + n 
}

我们的 incrementUserAge 方法返回新的 age 而不会修改对象的当前状态。我们需要初始化一个新的对象并使用这个 age

最后,由于我们不需要修改 age,我们可以将其设置为不可变。我们的 User 类有两个不可变属性和一个不修改其属性的方法。因此,尽管它是一个非常简单的类,但它是有功能的。

继承

继承是类之间的一种关系,使得基于其他现有类定义和实现一个类成为可能。

此外,继承有助于代码重用,并允许通过公共类和接口独立扩展原始类(即 super 类)。通过继承建立起来的类之间的关系导致了一个层次结构。

继承在需要向现有类添加额外信息和功能时不可避免地会最小化重工作业量,因为我们可以使用该类作为 super 类,并从它派生出一个子类来添加新的状态信息和行为。

此外,当与多态和动态绑定结合使用时,继承最小化了在扩展类时需要更改的现有代码量。

在像 C++ 这样的编程语言中,可以从多个类中继承,但在 Swift 中,一个类只能从另一个类中派生。以下是一个 UIViewController 派生的示例:

class BaseViewController: UIViewController { 

}

我们的 BaseViewController 将继承 UIViewController 类的所有行为和属性,我们还将能够向它添加新的属性和行为。这样,我们就不需要从头开始重写一切,可以在 UIViewController 中重用属性和行为。

覆盖

Swift 允许一个类或对象替换它继承的行为/属性的实现。这个过程称为重写。override关键字用于在子类中指定重写的方法。

我们可以重写继承的实例或类属性,以提供我们自己的自定义/计算属性获取器和设置器,或者添加属性观察器,以便当底层属性值发生变化时,重写的属性可以观察。

我们可以将属性或行为标记为final以防止在子类中重写它。

重写带来了需要处理的复杂性。我们需要确保子类实例应该使用哪个版本的行为/属性:它自己类的一部分(self)还是父类(super)的一部分?

在 Swift 中,可以使用selfsuper关键字作为前缀来指定所需行为/属性的版本。

设计约束

在设计应用程序时广泛使用继承会施加某些约束。

例如,假设我们定义了一个名为WebAppUserUser子类,它包含额外的可接受行为,以及另一个名为MobileAppUserUser子类,它包含User的移动应用模块。

在定义这个继承层次结构时,我们已经定义了某些限制,并非所有这些限制都是可取的。

单一性

在 Swift 中,子类只能从单个超类继承。从前面的例子中,User可以是WebAppUserMobileAppUser,但不能同时是两者。

静态

对象的继承层次结构在初始化时是固定的,而对象类型的选择不会随时间改变。例如,继承图不允许一个MobileAppUser对象在保留其User超类状态的同时成为WebAppUser对象(这可以通过装饰器模式实现)。

可见性

当客户端代码可以访问一个对象时,它通常也可以访问该对象的所有父类数据。即使父类没有被声明为 public,客户端仍然可以将对象转换为它的父类类型。

组合复用

组合复用原则是继承的替代方案。这种技术通过将行为从主要类层次结构中分离出来,并在任何类中按需包含特定的行为类,支持多态和代码复用。这种方法通过允许在运行时改变行为并允许子类有选择地实现行为,而不是仅限于其超类的行为,避免了类层次结构的静态性质。

问题与替代方案

实现继承在面向对象程序设计和理论家中是有争议的。例如,《设计模式:可复用面向对象软件元素》一书的作者,Erich Gamma, John Vlissides, Ralph Johnson, 和 Richard Helm,提倡接口继承而不是实现继承,并建议优先考虑组合而不是继承。

例如,装饰器模式(如前所述)已被提出以克服类之间继承的静态性质。

此外,面向对象编程社区普遍认为继承引入了不必要的耦合并破坏了封装,因此对超类的修改可能导致子类中出现不希望的行为变化。

在 Swift 中,鼓励使用协议和扩展。使用协议可以避免耦合问题,因为没有任何实现被共享。我们将在本章的 POP 部分中更多地讨论协议和协议扩展。

何时继承

有时候,我们没有其他选择,只能进行子类化。以下是一些需要子类化的例子:

  • 当 API 需要时:例如,许多 Cocoa API 需要使用类,并且不建议有争议。例如,UIViewController 必须被子类化。

  • 当我们需要在其它类的实例之间管理和传递我们的值类型时:例如,当我们需要在另一个绘图类提供的 Cocoa 类中绘制自定义视图时,我们将在它们之间进行通信。在这种情况下使用值类型是不利的。

  • 当我们需要在多个所有者之间共享一个实例时:Core Data 持久化是一个例子。在使用 Core Data 时,拥有一个跨多个所有者的同步机制非常有用。这可能会引起并发问题,但我们必须处理它们,因为我们需要可变数据。

  • 当实例的生命周期与外部效应相关联或我们需要一个稳定的身份时:单例和 AppDelegate 是一些例子。

多态

多态意味着多种形式。一般来说,能够采取多种形式的能力被称为多态。在面向对象的编程语言如 Swift 中,多态引用是指随着时间的推移,可以引用多个类的实例。让我们考察一个 iOS SDK 的例子,UIView。有大量的 UIView 子类,包括以下内容:

  • UILabel

  • UITextField

  • UIButton

我们可以声明一个可以采取多种形式的视图,如下所示:

var view: UIView 

view = UIButton() 
view = UILabel() 
view = UITextField()

多态允许我们编写更通用的代码,这些代码可以与对象家族一起工作,而不是为特定类编写代码。在这个例子中,无论我们启动哪个类,我们都可以访问所有继承自 UIView 类的所有子类中声明的属性和方法。例如,我们可以检查任何一个的边界和原点,如下所示:

view.bounds 
view.frame.origin

我们能够引用多种类型的对象;因此,多态引用既有静态类型,也有动态类型相关联。

静态类型是由代码中对对象的声明确定的。它在编译时已知,并决定了对象在运行时可以接受的有效类型集合。这种确定是通过分析系统中的继承图来进行的。

在应用程序执行过程中,引用的动态类型可能会随时间改变。在 Swift 中,运行时系统自动将所有多态引用标记为它们的动态类型。

动态绑定

将方法调用与要执行的代码关联起来称为绑定。与在编译时绑定与方法调用相关联的代码的静态绑定相反,动态绑定意味着与给定方法调用相关联的代码在编译时是未知的,将在运行时确定。

动态绑定与多态和继承相关联,因为与多态引用相关联的方法调用可能依赖于该引用的动态类型。

例如,我们视图的静态类型是UIView,其动态类型可能是UILabelUITextFieldUIButton。假设UIView中的一些方法被UIButton重写。当我们调用这些方法时,运行时会动态绑定需要调用的方法。

面向对象设计原则

在本节中,我们将探讨面向对象(OOP)方法及其解决方案以及针对这些问题的函数式编程(FP)解决方案中的一些问题。

通常,面向对象(OOP)被以下方式批评:

  • 将数据结构绑定到行为是状态封装的一种机制,它隐藏了底层问题而不是解决问题。

  • 为了使继承成为可能,投入了大量的努力。讽刺的是,面向对象的模式本身更倾向于组合而不是继承。最终,在处理两个职责——子类型化和重用——时,继承在子类型化或重用方面都不是很好。

解决这些问题的面向对象解决方案包括 SOLID 原则和领域驱动设计(DDD)原则。以下为 SOLID 原则:

  • 单一职责原则(SRP)

  • 开闭原则(OCP)

  • Liskov 替换原则(LSP)

  • 接口隔离原则(ISP)

  • 依赖倒置原则(DIP)

领域驱动设计(DDD)原则被提出以解决面向对象(OOP)问题。

此外,函数式编程(FP)通过以下区分特征来解决这些问题:

  • 通过不可变性避免显式管理状态。

  • 更倾向于显式返回值而不是隐式副作用。

  • 强大的组合功能在不损害封装性的情况下促进重用。

  • 这些特征的最终结果是更声明式的范式。

SRP

SRP 指出,每个类都应该有一个单一职责,其中职责被定义为改变的理由。

这个原则支持反模式,其中大型类扮演多个角色。类可以因为几个原因而变得很大。面向对象编程(OOP)的一个核心原则是将数据结构绑定到行为上。问题是,优化数据结构封装不仅会削弱组合特性,还会隐藏显式状态的根本问题。因此,OOP 代码通常包含许多数据结构,每个数据结构中的函数相对较少。向类中添加方法会给 SRP(单一职责原则)带来压力,而减少方法数量可能会使数据结构难以组合,或者完全无用。此外,声明类的简单语法成本经常迫使程序员将其边缘化。

函数式编程的对应原则

在函数式编程中,抽象的基本单位是函数。鉴于函数只有一个输出,函数自然只有一个职责。当然可以定义一个任意通用的函数,但这并不直观。此外,函数在语法上更节省资源。

OCP(开闭原则)

OCP 指出,软件实体应该对扩展开放,但对修改封闭。

这个陈述的不确定性可以通过该原则的两个变体来解决:

  • 应仅修改现有类以纠正错误。这种限制提供了原则的封闭方面。开放方面是通过实现继承或换句话说,通过重用而不是子类型化的目标来实现的。

  • 通过多态实现开放性,这根据定义也提供了封闭性,因为可扩展性是通过替换而不是修改来支持的。不幸的是,替换往往会导致意外的复杂性,这必须通过另一个原则——LSP 来解决。

OCP 的主要效用是在提供可扩展性的同时限制级联变化。这是通过为可扩展性进行设计和禁止对现有实体进行更改来实现的。通过使用抽象类和虚拟函数的巧妙技巧来实现可扩展性。通过封装或更确切地说,通过隐藏移动部分来实现封闭性。

函数式编程的对应原则

在函数式编程(FP)中,函数可以随意替换,因此无需为可扩展性进行设计。需要参数化的功能自然地被声明为这样的功能。而不是发明一个虚拟方法和继承的概念,可以依赖一个现有的、基本的概念——高阶函数。

LSP(里氏替换原则)

LSP 指出,程序中的对象应该可以用其子类型实例替换,而不会改变该程序的正确性。

LSP 本质上是一种受限的子类型实例,旨在保证跨类层次结构的语义可移植性。可移植性是通过确保对基类型的所有断言对所有子类型都成立来实现的。子类不得加强前置条件。它们必须接受基类接受的所有输入和初始状态,并且子类不得弱化后置条件。超类声明的行为期望必须由子类满足。这些特性不能仅通过类型系统强制执行。

作为继承关系的一部分,LSP 因此具有误导性,因此需要补偿原则。因此,这一原则的需要证明了子类型(基于包含的)多态的一个陷阱。通过类层次结构隐式分解强加不必要的限制,并需要复杂的原则来对偶然复杂性设置边界。

FP 对应原则

函数式语言倾向于使用有界量词的参数多态,从而避免了继承的一些陷阱。非正式地说,函数式语言强调可替换性,并淡化实现重用,因为重用通过组合实现得更好。在函数式语言中,LSP 的大多数雄心壮志实际上都是微不足道的。

ISP

ISP 原则指出,许多针对特定客户端的接口比一个通用接口更好。换句话说,不应该强迫任何客户端依赖它不使用的功能。

从本质上讲,ISP 是对 SRP(单一职责原则)在接口上的重申,反映了相同的潜在问题——在面向对象设计中平衡职责分配、组合和封装的困难。一方面,封装是可取的;另一方面,组合也是可取的。此外,仅采用 ISP 的问题在于它并不能直接保护大型类,并且在某些方面掩盖了问题。

FP 对应原则

函数式编程通过摒弃状态来减少封装的需求,并在核心处培育组合。没有基于角色的接口增强概念,因为函数角色从一开始就是明确的。函数默认是隔离的。

DIP

DIP 原则指出,应该依赖于抽象,而不是具体实现。换句话说,高级模块应该通过抽象与低级模块解耦。这一原则表明代码应该围绕问题域进行结构化,而领域应该通过协议声明对所需基础设施的依赖。因此,依赖指向领域模型。

这个原则之所以是倒置的,是因为典型的 OOP(通过分层架构)推广的架构表现出依赖图,其中高级模块直接消耗低级模块。最初,这个依赖图看起来很自然,因为在用代码表达领域模型时,不可避免地依赖于语言的构造。过程式编程允许通过过程封装依赖关系。

子类型多态推迟了过程实现。不幸的是,在 OOP 实现中,经常忽视使用协议来表示领域依赖。鉴于基础设施代码通常更为庞大,代码的关注点会偏离领域。DDD 部分是为了平衡这种偏离而设计的。

FP 的对应物

函数式编程(FP)的声明性和无副作用特性提供了依赖倒置。在面向对象编程(OOP)中,高级模块主要依赖于基础设施模块来调用副作用。在 FP 中,副作用更自然地是在响应领域行为时触发的,而不是直接由领域行为触发的。因此,依赖关系不仅被倒置,而且完全推到了外部层。

DDD

DDD 是一种通过将实现与不断发展的模型连接起来,针对复杂需求进行软件开发的方法。

概念

模型的概念包括以下内容:

  • 上下文:一个词或陈述出现的背景,决定了其含义。

  • 领域:一个本体、影响或活动。用户将程序应用于的主题领域是软件的领域。

  • 模型:一个抽象系统,描述了领域的选定方面,并可用于解决与该领域相关的问题。

  • 通用语言:一种围绕领域模型构建的语言,并由所有团队成员使用,以将团队的所有活动与软件联系起来。

前提

DDD 的前提如下:

  • 将项目的重点放在核心领域和领域逻辑上

  • 在领域模型的基础上构建复杂设计

  • 在技术专家和领域专家之间启动创造性合作,以迭代地完善一个解决特定领域问题的概念模型。

构建模块

在 DDD 中,有一些用于表达、创建和检索领域模型的工件,以下几节将从 FP 的角度进行探讨。

聚合

一组由根实体绑定在一起的对象,也称为聚合根。聚合根通过禁止外部对象持有其成员的引用,确保了在聚合内进行的更改的一致性。

聚合的概念在 FP 中仍然存在;然而,它不是用类来表示的。相反,它可以表示为一个结构,包括一组聚合状态、初始状态、一组命令、一组事件以及将一组命令映射到给定状态的一组事件的功能。模块机制提供了内聚性。

不可变值对象

不可变值对象是包含属性但没有概念身份的对象。它们应该被视为不可变的。

在前一章中,我们看到了 Swift 提供了具有自动实现的结构相等性的不可变产品类型和求和类型,这可以简单地解决这个问题。在面向对象编程中,对状态的过度依赖使得引用成为一等公民,而不是数据结构本身的结构。

领域事件

领域事件是定义事件的领域对象。

领域事件是保持领域模型封装的强大机制。这可以通过允许外部层中的各种观察者注册领域事件(“信号”)来实现。

面向对象编程中领域事件的问题在于,典型的实现复杂且依赖于副作用。事件观察通常在组合根中声明,因此,从生产者的角度来看,并不立即明显哪些观察者将被调用。在函数式编程(FP)中,领域事件是聚合中函数返回的一个值。观察者可以显式注册为过滤器。

此外,函数式响应编程(FRP)可以非常有效地处理领域事件。另一方面,由于缺乏联合类型和模式匹配,在面向对象编程(OOP)中从聚合方法返回领域事件是受限制的。

意图揭示接口

在命令式面向对象代码中,意图通过副作用泄露,并关注“如何”而不是“什么”。总是需要将行为绑定到数据结构也可能有问题。

由于函数式编程更声明式,函数名称和接口往往更多地关注意图而不是底层机制。此外,无副作用函数的接口本质上更具有揭示性,因为行为通过返回值被明确表达。因此,除了命名具有意图的纯粹语言优势外,意图还通过类型系统进行编码。这并不是说在函数式编程中表达意图是不费力的——只是说它得到了函数式范式的更好支持。

无副作用函数

副作用与封装直接对立,但它们往往是最有用的工具。

与命令式编程不同,函数式编程避免了副作用。这个模式又是另一个例子,说明了精心设计的面向对象设计如何收敛到函数式风格。

断言

与许多根植于命令式面向对象设计的模式一样,断言声称使用隐式副作用。

与意图揭示接口一样,函数式编程语言中的断言除了函数名称外,还自动编码在函数的返回类型中。

概念轮廓

当领域知识在代码中传播到一定程度时,概念轮廓就会出现。在面向对象编程中,这可以通过仔细遵循领域驱动设计(DDD)的原则来实现。

在 FP 中,概念轮廓更容易出现,这再次归因于范式声明性和无副作用的特点。具体来说,领域模型客户端可以依赖通过组合获得的内聚功能,同时仍然可以访问组成部分而不破坏封装。

操作的关闭

操作的关闭说明了将组合和结构强加于面向对象设计的另一个例子。

从本质上讲,关闭操作通过限制讨论的范围来简化对问题的推理。一个领域实现的函数示例在基本层面上展示了这一特征。应用领域事件的操作在领域状态集合下是封闭的。在持久性的方面,这自然地转化为事件源,但也支持在无需修改的情况下使用键值存储或 ORM 进行持久化。

声明式设计

上文提到的模式的整体意图是培养声明式设计。如所见,FP 天生更具声明性,因此在这方面更具适应性。通过声明式设计,我们可以更好地提炼领域的区分特征,并减少或消除对基础设施正交关注点的耦合。因此,可重用性、可测试性、正确性、可维护性和生产力得到了极大的提升。

面向协议编程(POP)

POP 鼓励我们开发协议并扩展它们,而不是类和继承。在 Objective-C 和 Swift 开发社区中,POP 是一个新的概念,但它提供的内容与 Java 和 C#等语言中的Abstract类概念以及 C++中的pure-virtual函数并没有太大的区别。

在 Swift 中,类、结构和枚举可以符合协议。这使得协议更加可用,因为继承对结构和枚举不起作用。

POP 范式

在本节中,我们将探讨 POP 范式。首先,我们将查看一个示例:

protocol UserProtocol { 
    func greet(name: String) -> String 
    func login(username: String, password:String) -> Bool 
}

此协议定义了两个需要由符合此协议的结构、枚举或类实现的功能。

协议组合

协议组合允许类型符合多个协议。这是 POP 相对于 OOP 的许多优点之一。在 OOP 中,一个类只能有一个超类,这可能导致非常单调的超类。在 POP 中,我们被鼓励创建多个具有非常具体要求的小型协议。

协议扩展

协议扩展是 POP 范式最重要的部分之一。它们允许我们向所有符合给定协议的类型添加功能。如果没有协议扩展,如果我们有对所有符合特定协议的类型都必要的通用功能,那么我们就需要将此功能添加到每个类型中。这将导致大量代码重复。以下示例通过添加一个logout方法和其实现来扩展我们的协议;因此,任何符合UserProtocol的 struct、enum 或 class 都将具有logout功能:

extension UserProtocol { 
    func logout(userName: String) -> Bool { 
      return true 
  } 
}

协议继承

协议继承是其中一个协议可以继承一个或多个其他协议的要求,如下面的代码所示:

protocol MobileAppUserProtocol: UserProtocol { 

}

MobileAppUserProtocolUserProtocol继承,因此它将具有所有定义和扩展的方法。

关联类型

关联类型可用于使我们的协议与泛型类型一起工作:

protocol MobileAppUserProtocol: UserProtocol { 
  associatedtype applicationModuleList 
  func listSelectedModules() -> [applicationModuleList] 
}

符合协议

以下代码展示了与关联类型使用相关的协议符合性的示例:

enum MobileAppUserType: MobileAppUserProtocol { 
    case admin 
    case endUser 

    func greet(name: String) -> String { 
        switch self { 
        case .admin: 
          return "Welcome \(name) - You are Admin" 
        case .endUser: 
          return "Welcome \(name)!" 
      } 
    } 
    func login(username: String, password:String) -> Bool { 
      return true 
    } 
    func listSelectedModules() -> [String] { 
      return ["Accounting", "CRM"] 
    }   
  }

然后,我们可以创建一个新的移动用户如下:

let mobileUser: MobileAppUserType = MobileAppUserType.Admin 
mobileUser.logout("cindy") 

mobileUser.listSelectedModules()

POP 通过使我们能够符合协议并使用默认实现来扩展它们,从而最小化了继承和子类化的必要性。

函数式响应式编程(FRP)

函数式编程避免使用不可变性和副作用。在某些情况下,应用程序应该对动态值/数据变化做出反应。例如,我们可能需要更改 iOS 应用程序的用户界面以反映从后端或数据库系统接收到的数据。在没有状态和可变值的情况下,我们该如何做到这一点?

命令式编程仅通过状态和突变间接捕获这些动态值。完整的(过去、现在和未来)历史没有一等表示。此外,只有离散演变的值可以作为一等表示被(间接)捕获,因为命令式范式在时间上是离散的。

FRP 提供了一种处理动态值变化的同时仍保留 FP 风格的方法。正如其名称所暗示的,FRP 是 FP 和响应式编程的结合。响应式编程使得处理表示随时间变化的值的某些数据类型成为可能。这些数据类型在不同的函数式编程语言中被称为时间流或事件流。涉及这些随时间变化的值的计算本身也将具有随时间变化的值。FRP 直接捕获这些演变值,并且对持续演变的值没有困难。

此外,FRP 还可以表示为以下一组原则/规则:

  • 数据类型或随时间动态/演变的值应该是第一类公民。我们应该能够定义、组合并将它们传递给函数,并从函数中返回它们。

  • 数据类型应该由一些原始类型如常量/静态值和时间通过顺序和并行组合构建。n 个行为通过在时间上连续应用一个 n 元函数到静态值来组合。

  • 为了考虑离散现象,我们应该有额外的事件类型,每种类型都有一个(有限或无限)发生流。每个发生都与一个相关的时间和值相关联。

  • 为了构建所有行为和事件都可以组成的组合词汇,可以通过一些示例进行尝试。持续将它们分解成更通用/简单的部分。

  • 我们应该能够使用指称语义技术来构建整个模型:

    • 每种类型都有一个对应简单且精确的数学类型意义

    • 每个原始类型和操作符作为构成元素意义的函数具有简单且精确的意义

FRP 的构建块

理解 FRP 构建块对于理解 FRP 至关重要。以下章节将使用 GitHub 上为 Cocoa 框架开发的优秀 FRP 库 ReactiveCocoa 来解释这些构建块。ReactiveCocoa 是为 Objective-C 开发的,截至版本 3.0,所有主要功能开发都集中在 Swift API 上。

信号

信号是事件流,在时间上发送已经进行中的值。我们可以想象它们为发送值而不了解它们之前发送的值或将要发送的值的管道。信号可以声明性地组合、合并和链式连接。信号可以统一所有 Cocoa 的异步和事件处理常见模式:

  • 委托方法

  • 回调块

  • 通知

  • 控制动作和响应链事件

  • 未来和承诺

  • 键值观察KVO

由于所有这些机制都可以以相同的方式表示,因此它们很容易声明性地链式连接和组合在一起。

ReactiveCocoa 将信号表示为Signal。信号可以用来表示通知、用户输入等。随着工作的进行或数据的接收,事件被发送到信号上,并将它们推送到任何观察者。所有观察者同时看到这些事件。

用户必须观察一个信号才能访问其事件。观察信号不会触发任何副作用。换句话说,信号完全是生产者驱动和基于推送的,观察者不能对信号的生命周期有任何影响。在观察信号时,用户只能以与信号上发送的顺序相同的顺序评估事件。信号值没有随机访问。

可以通过以下操作来操作信号:

  • 使用mapfilterreduce来操作单个信号

  • 使用zip同时操作多个信号

这些操作只能应用于信号的下一个事件。

信号的生命周期可能由多个后续事件组成,之后跟随一个终止事件,该事件可能是以下之一:

  • 失败

  • 完成

  • 中断

终止事件不包括在信号的值中,并且应该特别处理。

管道

可以手动控制的signal称为pipe。在 ReactiveCocoa 中,我们可以通过调用Signal.pipe()来创建pipe

pipe方法返回signalobserversignal可以通过向observer发送事件来控制。

信号生产者

信号生产者创建信号并执行副作用。SignalProducer可用于表示操作或任务,例如网络请求,每次调用start()都会创建一个新的底层操作,并允许调用者观察结果。与信号不同,直到附加观察者,并且为每个额外的观察者重新启动工作之前,不会开始工作(因此不会生成事件)。

启动信号生产者返回一个可丢弃的对象,可以用来中断/取消与产生的信号相关的工作。

信号生产者也可以通过诸如 map、filter 和 reduce 之类的操作进行操作。每个信号操作都可以通过lift方法提升为操作信号生产者。

缓冲区

缓冲区是一个可选有界的事件队列。当从SignalProducer创建新的信号时,缓冲区会回放这些事件。通过调用SignalProducer.buffer()创建buffer。类似于pipe,该方法返回observer。发送到此观察者的事件将被添加到队列中。如果缓冲区在新的值到达时已满,则最旧的价值将被丢弃以腾出空间。

观察者

观察者是指观察或能够从signal中观察events的任何东西。可以使用基于回调的Signal.observe()SignalProducer.start()方法的版本隐式创建观察者。

动作

当与输入执行时,动作将执行一些工作。动作在执行副作用工作方面很有用,例如当按钮被点击时。动作还可以根据属性自动禁用,并且这种禁用状态可以通过禁用与动作相关的任何控件在用户界面中表示。

属性

属性存储一个值,并通知观察者该值的未来更改。可以从值获取器中获取属性的当前值。生产者获取器返回一个信号生产者,该生产者将发送属性的当前值,然后是随时间的变化。

可丢弃的

可丢弃的是一种内存管理和取消的机制。在启动信号生产者时,将返回一个可丢弃的对象。调用者可以使用此可丢弃对象来取消已启动的工作,清理所有临时资源,然后针对创建的特定信号发送一个最终的 Interrupted 事件。

调度器

调度器是一个串行执行队列,用于执行工作或交付结果。Signalssignal producers可以按顺序在特定调度器上交付事件。Signal producers还可以按顺序在特定调度器上开始工作。

调度器与Grand Central DispatchGCD)队列类似,但调度器支持通过可处置对象进行取消,并且始终按顺序执行。除了ImmediateScheduler之外,调度器不提供同步执行。这有助于避免死锁,并鼓励使用signalsignal producer操作而不是阻塞工作。

调度器也与NSOperationQueue有些类似,但调度器不允许任务重新排序或相互依赖。

一个例子

假设我们有一个输出端口,我们想观察其变化:

@IBOutlet weak var textFieldUserName: UITextField!

我们可以创建SignalProducer如下:

let userNameSignalProducer = 
  textFieldUserName.rac_textSignal().toSignalProducer.map { 
  text in text as! String }

rac_textSignal方法是一个用于UITextField的 ReactiveCocoa 扩展,可以用来创建信号生产者。

然后,我们可以这样开始我们的SignalProducer

userNameSignalProducer.startWithNext { results in 
      print("User name:\(results)") 
}

这会将textField中的任何更改打印到控制台。

此外,我们可以在这个信号生产者上执行诸如mapflatMapfilterreduce等操作,这些我们在第六章中介绍过,即Map、Filter 和 Reduce

混合面向对象编程与函数式编程

到目前为止,我们已经看到,将函数式编程能力添加到面向对象语言中,会在面向对象设计中带来好处。

总结来说,当我们的对象尽可能不可变时,面向对象编程与函数式编程完美匹配。为了使我们的对象尽可能不可变,我们可以考虑以下原则:

  • 对象应该是封装相关数据片段的类型

  • 对象可以有方法;然而,这些方法不应该改变对象,而应该返回一个适当类型的新对象

  • 所有必需的状态数据都应该在类的初始化中注入,以便它可以立即使用

  • 静态方法可以自由使用,而静态变量应避免使用

  • 应该使用协议和泛型来避免代码重复

这些原则不仅使我们能够使用函数式设计模式,还丰富了我们的面向对象代码。

问题

在统一和混合面向对象编程与函数式编程时,存在一些问题,我们将在以下章节中介绍。

粒度不匹配

函数式编程和面向对象编程在不同的设计粒度级别上操作:

  • 函数式编程:在小型级别上的函数/方法编程

  • 面向对象编程:在大型级别上的类/对象/模块编程

为了克服这种粒度不匹配,我们需要找到以下问题的答案:

  • 我们如何在面向对象架构中定位单个函数的来源?

  • 我们如何在面向对象架构中将这样的单个函数与对象相关联?

在 Swift 中,我们可以在源文件内部和外部放置函数,或者将它们作为静态或类方法放置。

函数式编程范式可用性

到目前为止,我们在 Swift 中探索了许多不同的 FP 模式。在这里,我们概念上检查 Swift 是否是一个适合 FP 的语言。我们将在以下章节中探索这些模式。

首类值

在 FP 语言中,函数/方法应该是首类公民。如果首类公民函数满足以下规则,它们将使我们能够使用大多数 FP 模式:

  • 函数/方法应可作为函数/方法参数和参数使用

  • 函数/方法可以作为函数/方法的返回结果

  • 函数可以存在于数据结构中

到目前为止,我们已经看到了所有这些规则的一个示例实现。

闭包

首类函数/方法应作为闭包实现。例如,它们应与特定的私有环境相关联。

Swift 函数作为闭包实现。

FP-OOP 交互工具

独立函数/方法应明确与类/对象级别相关联。

Swift 扩展使我们能够在不创建新派生类的情况下向现有类添加方法。

FP 支持

FP 模式应由相关构造、预定义定义、标准库中的出现等加强。

它们应满足以下规则:

  • 泛型函数类型的重载

  • 首类多次调用和多播

  • 函数打包和序列化(闭包作为数据结构)

Swift 支持上述 FP 模式。

在 OOP 中具有 FP 功能的影响

在 OOP 语言中具有 FP 功能会导致习惯性和架构效应,这些将在以下章节中探讨。

习惯性效应

  • 函数/方法粒度级别的代码重构(抽象)

  • 泛型迭代器和循环操作(映射)

  • 操作组合和序列理解(链式函数调用)

  • 函数部分应用和柯里化

架构效应

  • 减少对象/类定义的数量:避免用新类弄乱 OOP 架构

  • 函数/方法级别的命名抽象:使用首类方法允许任何满足其声明类型的任何方法实例化参数

  • 操作组合(以及序列理解)

  • 函数部分应用和柯里化

OOP 设计模式 - FP 视角

设计模式描述了面向对象软件设计中常见问题的重复解决方案。模式分为三种类型:

  • 创建型

  • 结构

  • 行为

本节从非常高的层次介绍了 OOP 设计模式,并介绍了 FP 对应模式:

  • 策略

  • 命令

  • 观察者

  • 代理

  • 访问者

策略模式

策略模式是一种行为模式,它允许算法独立于使用它的客户端变化。换句话说,它允许在运行时动态选择算法家族中的一个。

从 FP 视角来看,策略只是方法级别抽象代码的一个例子。

命令模式

命令模式是一种行为模式,它将请求(方法调用)封装为对象,以便它们可以轻松地传输、存储和应用。

FP 提供了闭包和一等函数。

观察者模式

观察者模式是一种行为模式,它允许对象之间存在一对一的依赖关系,这样当一个对象的状态发生变化时,所有依赖它的对象都会收到通知并更新。

FRP 以非常有效和声明性的方式处理此模式。

虚拟代理模式

虚拟代理模式是一种结构模式,它以这种方式提供其他对象的占位符,即只有在需要时才创建/计算其数据。

FP 提供了延迟实例化和评估。

访问者模式

访问者模式是一种行为模式,它允许我们在不改变操作元素所属的类的情况下定义新的操作。

FP 使函数独立于对象变化。

概述

在本章中,我们介绍了面向对象编程的原则和范式。然后我们讨论了协议导向编程。接下来,我们介绍了 FRP。最后,我们探讨了如何将 FP 与 OOP 范式混合。

在下一章中,我们将开发一个 Todo 后端和一个 iOS 应用,这些应用将采用我们迄今为止所涵盖的概念。

我们将使用函数式编程技术来解析和映射数据,并使用 FRP 来反应性地管理应用中的事件。此外,我们还将采用协议导向编程和面向对象编程技术。

第十一章:案例研究 - 使用 FP 和 OOP 范式开发 iOS 应用程序

在前面的章节中,我们介绍了各种概念和技术。我们从 FP 范式开始,并详细探讨了相关主题。此外,在前一章中,我们还介绍了其他范式,如 OOP、FRP 和 POP,并将它们混合在一起。在本章中,我们将使用这些范式创建一个简单的应用程序。

大多数 iOS 应用程序需要一个后端来提供与其他系统集成的先进功能。在本章中,我们将使用 Swift 创建一个简单的后端,该后端将用作待办事项应用的 REST API。然后,我们将开发一个 iOS 应用程序,该应用程序将利用我们的后端并提供一些基本功能,例如列出和更新来自后端的后待办事项。此外,iOS 应用程序还将能够创建新的待办事项。我们的 iOS 应用程序开发将包括 FP、OOP、POP 和 FRP 范式。

本章将涵盖以下主题:

  • 需求规范

  • 高级设计

  • 后端开发

    • 环境配置

    • Swift 包管理器

    • Vapor

    • 应用程序开发

  • 前端开发

    • CocoaPods 依赖管理配置

    • 第三方库

    • 后端通信

    • JSON 解析和模型映射

    • 状态管理

    • 使用 UITableView 列出项目

    • 更新和创建项目

    • 过滤项目

要求

本节介绍了案例研究的需求。由于本书的重点不是需求工程,我们将定义非常简单的需求。本节不介绍需求工程的最佳实践。

iOS 应用程序用户的要求如下:

  • 用户应能够列出待办事项

  • 用户应能够查看每个项目的详细信息

  • 用户应能够修改项目

  • 用户应能够创建新项目

  • 用户应能够根据其状态过滤项目

高级设计

本节解释了前端和后端的高级设计。

前端

应用程序设计遵循模型-视图-控制器MVC)模式的一个略微不同的版本,增加了ActionsStoreStateCommunication层以简化传统 iOS 应用程序 MVC 模式的控制器层。所有应用程序层将在以下各节中解释。

模型

简单的模型结构。这些模型没有任何逻辑,仅由属性组成。有四种类型的模型:

  • TodoRequest:这是一个用于后端请求调用并符合RequestProtocol的结构体

  • 待办事项:这是一个表示待办事项数据的结构体,并使用ArgoCurry库从 JSON 解码对象

  • TodoViewModel 和 TodosViewModel:这些结构体表示数据,并在视图中使用,并展示给用户

  • TodoLens:这些透镜修改待办事项模型

所述所有模型都是不可变值类型。

视图

我们有两个视图子类:一个提供自定义的UITableViewCell,称为TodoTableViewCell,以及一个名为FooterViewUIView子类。

这两个视图都是 iOS SDK 提供的类的子类。除了这些类,我们还将有我们的UIViewController场景在 Storyboard 中。

ViewController

ViewControllerUIViewControllerUITableViewController的子类,它将视图与逻辑连接起来:

  • MasterViewController: 这是一个UITableViewController的子类,用于展示待办事项

  • DetailsViewController: 这是一个UIViewController的子类,用于向用户展示每个待办事项的详细信息

要开发 iOS 应用程序,我们必须依赖于 iOS SDK 提供的类,如UIViewControllerUITableViewController。在这种情况下,我们将只使用ViewControllerUIView的子类。

State

在 iOS 应用程序开发中,我们需要处理状态。我们使用 Delta 和 ReactiveCocoa 库来管理我们的待办事项应用程序的状态。

Delta 接受一个应用程序,该应用程序在所有ViewControllers中具有分散的自定义状态管理,并通过提供一个简单的接口来更改状态和订阅其变化来简化它。

ReactiveCocoa 是一个 FRP cocoa 框架,它提供了在时间上组合和转换值流的 API。

我们将实现一个State结构体,它将提供可观察属性。

Store

我们的Store结构体会包装State结构体,并提供属性以观察其变化。Store遵循 Delta 库的StoreType协议,该协议定义了可观察状态的存储和修改它的分发方法。此外,Store使用 ReactiveCocoa 的MutableProperty值,并允许以线程安全的方式观察其变化。

Actions

操作是遵循 Delta 库的ActionType协议的结构体。当我们要对存储的状态进行修改时使用ActionType。所有对存储的更改都通过此类型进行。

我们将在应用程序中开发以下操作:

  • ClearCompletedTodosAction: 用于从列表中删除已完成的待办事项

  • CreateTodoAction: 用于创建新的待办事项

  • DeleteTodoAction: 用于删除待办事项

  • DetailsTodoAction: 用于展示项目的详细信息

  • LoadTodosAction: 用于列出所有待办事项

  • SetFilterAction: 用于过滤待办事项

  • ToggleCompletedAction: 用于标记待办事项为完成

  • UpdateTodoAction: 用于更新待办事项

管理员

TodoManager提供全局函数来处理后端 API 调用和 JSON 有效负载映射。TodoManager使用WebServiceManager进行后端调用,并使用 Argo 库将 JSON 有效负载映射到Todo模型。此外,TodoManager将通过LensesAction更新Store中的State

Communication

通信层负责后端通信。它包括以下组件:

  • WebServiceManager:它提供了一个名为 sendRequest 的全局函数,该函数被 TodoManager 用于调用后端 API。它还使用 configureHeaders 来对请求进行反射,以获取其属性和相应的值。

  • Urls:这个枚举通过模式匹配和扩展提供了一个适当的 HTTP 请求方法和完整的 URL 地址。

  • Alamofire:这是一个库,由 WebServiceManager 用于 HTTP 请求处理。

  • Argo:这个库以功能方式将模型对象映射到 JSON。

层之间的通信

应用程序使用闭包和 ReactiveCocoa 信号进行层之间的通信。

第三方库

以下第三方库/框架被用于我们的 iOS 应用程序中:

  • Alamofire:这是一个用于调用和管理网络服务的框架

  • Argo:这是一个功能性的 JSON 解析库

  • CocoaPods:它负责依赖管理

  • Delta:这是一个状态管理库

  • ReactiveCocoa:这是一个用于处理信号和流的 函数式响应式编程FRP)库

  • Quick:这是一个用于单元测试的行为驱动开发框架

跨切面关注点

本节解释了跨切面关注点,如错误管理、异常处理等。

错误管理和异常处理

如本书前几章所讨论的。

崩溃报告

我们将使用 Crashlytics,它是 Twitter 提供的 fabric.io 服务的一部分。

分析

我们将使用 fabric.io Answers 来监控应用程序的使用情况。还有其他分析服务,如 Google AnalyticsFlurry 和 Mixpanel,也可以用于本案例研究。为了简化,我们将使用 Answers。

工具

工具 我们将使用 Xcode 来开发我们的应用程序。JetBrains 的 AppCode 是另一个用于 iOS 应用程序开发的 IDE,它具有更好的重构功能,也可以用于本案例研究。

后端

对于 Swift,有各种网络框架和 HTTP 服务器,它们仍在开发中。KituraPerfect 和 Vapor 是其中最受欢迎的三个。它们都不是以函数式编程风格设计和开发的。在我们的示例中,我们将使用 Vapor 来提供一个可以被我们的前端应用程序利用的后端。

Vapor

Vapor (github.com/qutheory/vapor) 是一个流行的 Laravel/Lumen 启发的 MIT 许可的 Web 框架。它完全用 Swift 编写,并且是模块化的。

Vapor 提供了 CLI 工具来简化构建和运行 Vapor 应用程序。

vapor new <project-name> 可以用来创建一个新项目,vapor build 可以用来构建项目并下载依赖项,vapor xcode 可以用来创建 Xcode 项目,vapor run 可以用来运行项目。

Vapor 使用 Swift Package ManagerSPM)作为依赖管理器,使用 Vapor 启动应用程序就像导入 Vapor 并在 main 文件中添加以下行一样简单:

let app = Application()
app.start(port: 8080)

路由

在 Vapor 中进行路由很简单:

app.get("welcome") { request in
    return "Hello, World"
}

将上述代码添加到主文件中,将使我们的 Web 应用程序对所有localhost:8080/welcomeGET请求响应字符串Hello, World

JSON

以 JSON 形式响应很容易:

app.get("version") { request in
    return Json(["version": "0.1"])
}

上述代码响应了所有对localhost:8080/versionGET请求,返回 JSON 字典{"version": "0.1"}Content-Type: application/json

请求数据

每个路由调用都会传递一个request对象,可以用来获取查询和路径参数。

以下示例展示了如何从请求中访问 JSON、查询和表单编码的数据:

app.post("hello") { request in
    guard let name = request.data["name"]?.string else {
        return "Please include a name"
    }

    return "Hello, \(name)!"
}

在这个例子中,我们读取请求数据并返回一个字符串。

Vapor 还提供了会话管理、数据库连接以及使用 HTML 或包含 HTML 模板的 Stencil 模板的视图响应的途径。有一个示例 Vapor 项目(github.com/qutheory/vapor-example),可以用于我们的目的并进行修改。由于 Vapor 仍在开发中,我们不会深入探讨 Vapor。

SPM

SPM 是一个为 Swift 3.0 提供的开源构建和依赖管理工具。它与 Swift 构建系统集成,以自动化下载、编译和链接依赖项的过程。

Vapor 使用 SPM,要创建一个 Vapor 项目,我们需要在Packages.swift文件中添加以下依赖项:

.Package(url: "https://github.com/qutheory/vapor.git",
  majorVersion: xx, minor: x),
.Package(url: "https://github.com/qutheory/vapor-zewo-mustache.git",
  majorVersion: xx, minor: xx)

Vapor部分所述,我们可以使用 Vapor CLI 工具,结合 SPM 来构建和运行应用程序。

建议阅读更多关于 Vapor 和 SPM 的内容,因为我们在这本书中没有涵盖大多数相关主题。在下一节中,我们将使用 Vapor 开发一个非常简单的后端。

后端开发

我们想为 Todo 应用程序开发一个非常简单的后端。

模型

我们将首先创建我们的模型。代码如下:

import Vapor

final class Todo {
    var id: Int
    var name: String
    var description: String
    var notes: String
    var completed: Bool
    var synced: Bool

    init(id: Int, name: String, description: String, notes: String,
      completed: Bool, synced: Bool) {
        self.id = id
        self.name = name
        self.description = description
        self.notes = notes
        self.completed = completed
        self.synced = synced
    }
}

这个类导入了 Vapor,并包含了一些与Todo相关的属性以及一个init方法。

为了能够将此模型传递到 JSON 数组和字典中,我们需要扩展一个名为JsonRepresentable的协议:

extension Todo: JSONRepresentable {
    func makeJson() -> JSON {

        return JSON([
            "id":id,
          "name": "\(name)",
   "description": "\(description)",
         "notes": "\(notes)",
     "completed": completed,
        "synced": synced
        ])
    }
}

存储

然后,我们想在内存中存储 Todo 项的列表。为了实现这一点,我们将创建一个新的类,称为TodoStore。代码如下:

import Vapor

final class TodoStore {

    static let sharedInstance = TodoStore()
    private var list: [Todo] = Array<Todo>()
    private init() {
    }
}

为了简化,我们将这个类设计为单例,存储一个 Todo 项的列表。同时,我们将init方法设置为private以避免非共享实例的初始化。

为了允许 Todo 实例被传递到 JSON 数组和中,就像它是原生 JSON 类型一样,我们需要通过遵循以下方式扩展我们的TodoStore以符合JSONRepresentable协议:

extension TodoStore: JSONRepresentable {
    func makeJson() -> JSON {
        return JSON([
            "list": "\(list)"
        ])
    }
}

接下来,我们添加以下方法:

func addtem(item: Todo) {
    self.list.append(item)
}

func listItems() -> [Todo] {
    return self.list
}

如其名称所示,这些方法将用于添加和列出项目。我们需要一个非常简单的查找方法,让我们来开发它:

func find(id: Int) -> Todo? {
    return self.list.index { $0.id == id }.map { self.list[$0] }
}

在这里,我们使用indexmap高阶函数来查找索引并返回相应的数组元素。

然后,我们需要开发updatedelete方法:

func delete(id: Int) -> String {
    if self.find(id: id) != nil {
        self.list = self.list.filter { $0.id != id }
        return "Item is deleted"
    }
    return "Item not found"
}

func deleteAll() -> String {
    if self.list.count > 0 {
        self.list.removeAll()
        return "All items were deleted"
    }
    return "List was empty"

}

func update(item: Todo) -> String {
    if let index = (self.list.index { $0.id == item.id }) {
        self.list[index] = item
        return "item is up to date"
    }
    return "item not found"
}

此外,我们还可以将添加和更新合并如下:

func addOrUpdateItem(item: Todo) {
    if self.find(item.id) != nil {
        update(item)
    } else {
        self.list.append(item)
    }
}

到目前为止,我们的TodoStore能够执行所有 CRUD 操作。

控制器

下一步将是开发路由、请求和响应处理。为了简单起见,我们将修改 Vapor 示例中的main.swift

我们需要在以下定义之后进行更改:

let app = Application()

发布新的待办事项

第一步将是开发一个 POST 方法来创建待办事项,如下所示:

/// Post a todo item
app.post("postTodo") { request in
    guard let id = request.headers.headers["id"]?.values,
        name = request.headers.headers["name"]?.values,
        description = request.headers.headers["description"]?.values,
        notes = request.headers.headers["notes"]?.values,
        completed = request.headers.headers["completed"]?.values,
        synced = request.headers.headers["synced"]?.values
    else {
        return JSON(["message": "Please include mandatory parameters"])
    }

    let todoItem = Todo(id: Int(id[0])!,
                      name: name[0],
               description: description[0],
                     notes: notes[0],
                 completed: completed[0].toBool()!,
                    synced: synced[0].toBool()!)

    let todos = TodoStore.sharedInstance
    todos.addOrUpdateItem(item: todoItem)

    let json:[JSONRepresentable] = todos.listItems().map { $0 }
    return JSON(json)
}

前面的示例将创建一个待办事项。首先,我们使用保护表达式检查 API 用户是否提供了所有必要的 HTTP 标题,然后我们使用TodoStore类中的addItem()方法来添加该特定项。在前面的代码示例中,我们需要将completedBool转换为String,所以我们扩展了String函数如下,并在completed上调用toBool()

extension String {
    func toBool() -> Bool? {
        switch self {
        case "True", "true", "yes", "1":
            return true
        case "False", "false", "no", "0":
            return false
        default:
            return nil
        }
    }
}

我们需要在终端应用程序中使用vapor buildvapor run指令来构建和运行我们的后端应用程序。此时,我们应该得到以下提示:

发布新的待办事项

如果我们在网页浏览器中指向 localhost 8080,我们应该看到 Vapor 正在运行。此外,我们还可以使用curl工具在终端中通过复制和粘贴以下代码来测试我们的 POST 方法:

curl -X "POST" "http://localhost:8080/postTodo/" \
   -H "Cookie: test=123" \
   -H "id: 3" \
   -H "notes: do not forget to buy potato chips" \
   -H "Content-Type: application/json" \
   -H "description: Our first todo item" \
   -H "completed: false" \
   -H "name: todo 1" \
   -d "{}"

结果将类似于以下内容:

发布新的待办事项

如从截图所示,我们收到了一个包含我们添加的待办事项的 JSON 响应。

获取待办事项列表

我们的 POST 调用返回项目列表。此外,我们还可以这样获取项目:

/// List todo items
app.get("todos") { request in

    let todos = TodoStore.sharedInstance
    let json:[JSONRepresentable] = todos.listItems().map { $0 }
    return JSON(json)
}

我们将再次使用 Vapor CLI 构建和运行我们的应用程序,并且我们可以这样测试这个 GET 请求:

curl -X "GET" "http://localhost:8080/todos" \
   -H "Cookie: test=123"

获取特定待办事项

前面的调用检索所有项。如果我们想获取特定项,我们也可以这样做:

/// Get a specific todo item
app.get("todo") { request in

    guard let id = request.headers.headers["id"]?.values else {
        return JSON(["message": "Please provide the id of todo item"])
    }

    let todos = TodoStore.sharedInstance.listItems()
    var json = [JSONRepresentable]()

    let item = todos.filter { $0.id == Int(id[0])! }
    if item.count > 0 {
        json.append(item[0])
    }

    return JSON(json)
}

在这里,我们检查是否存在标题,并使用我们的TodoStore类中的listItems()方法来检索特定项。我们可以在 curl 中通过在终端执行以下命令来测试它:

curl -X "GET" "http://localhost:8080/todo/" \
   -H "id: 1" \
   -H "Cookie: test=123"

删除单个事项和删除所有待办事项

我们需要实现的下一个操作是从我们的TodoStore中删除项。让我们实现deletedeleteAll方法:

/// Delete a specific todo item
app.delete("deleteTodo") { request in
    guard let id = request.headers.headers["id"]?.values else {
        return JSON(["message": "Please provide the id of todo item"])
    }

    let todos = TodoStore.sharedInstance
    todos.delete(id: Int(id[0])!)

    return JSON(["message": "Item is deleted"])
}

/// Delete all items
app.delete("deleteAll") { request in
    TodoStore.sharedInstance.deleteAll()

    return JSON(["message": "All items are deleted"])
}

要测试删除功能,我们可以在终端中执行以下命令:

curl -X "DELETE" "http://localhost:8080/deleteTodo/" \
   -H "id: 1" \
   -H "Cookie: test=123"

要测试deleteAll功能,我们可以在终端中执行以下命令:

curl -X "DELETE" "http://localhost:8080/deleteAll" \
   -H "Cookie: test=123"

更新待办事项

最后,我们希望能够更新待办事项列表中的项以完成它或添加一些笔记:

/// Update a specific todo item
app.post("updateTodo") { request in
    guard let id = request.headers.headers["id"]?.values,
        name = request.headers.headers["name"]?.values,
        description = request.headers.headers["description"]?.values,
        notes = request.headers.headers["notes"]?.values,
        completed = request.headers.headers["completed"]?.values,
        synced = request.headers.headers["synced"]?.values
    else {
        return JSON(["message": "Please include mandatory parameters"])
    }

    let todoItem = Todo(id: Int(id[0])!,
                      name: name[0],
               description: description[0],
                     notes: notes[0],
                 completed: completed[0].toBool()!,
                    synced: synced[0].toBool()!)

    let todos = TodoStore.sharedInstance
    todos.update(item: todoItem)
    return JSON(["message": "Item is updated"])
}

在这里,我们首先检查标题是否存在,如果存在,我们就在TodoStore中使用更新方法来更新存储中的特定项。我们可以这样测试:

curl -X "POST" "http://localhost:8080/updateTodo" \
   -H "Cookie: test=123" \
   -H "id: 3" \
   -H "notes: new note" \
   -H "name: updated name" \
   -H "description: updated description" \
   -H "completed : yes"

到目前为止,我们应该有一个简单的后端 API 来在内存中创建、列出、更新和删除待办事项。在下一节中,我们将开发一个 iOS 应用程序来利用这个 API。

iOS 应用程序开发

到目前为止,我们探讨了需求,讨论了高级设计,并开发了一个简单的后端 API。现在,我们将开发一个利用后端的应用程序。

配置

我们将使用 CocoaPods (cocoapods.org/) 开始我们的应用程序开发。我们可以在终端中执行以下命令来安装它:

sudo gem install cocoapods

然后,我们将使用 Finder 创建一个文件夹,或者在终端中简单地执行以下命令:

mkdir Frontend

接下来,我们将在 Xcode 中创建一个 单视图应用程序 项目:

配置

我们将将其命名为 TodoApp 并提供组织名称和标识符。编程语言将是 Swift设备将是 通用。现在,我们可以关闭项目并返回到终端。

在终端中,我们将执行以下代码:

cd Frontend/TodoApp
pod init

这将创建一个名为 Podfile 的文件。这是我们定义依赖项的地方。

取消注释第一行和第三行,使其变为如下所示:

platform :ios, '8.0'
use_frameworks!

target 'TodoApp' do

end

现在,我们需要为我们的目标定义依赖项。我们可以访问 cocoapods.org/ 并搜索任何依赖项,复制定义,并将其粘贴到我们的 Podfile 中:

platform :ios, '8.0'
use_frameworks!

target 'TodoApp' do
    pod 'Alamofire'
    pod 'Argo'
    pod 'Curry'
    pod 'ReactiveCocoa'
    pod 'Delta', :git => "https://github.com/thoughtbot/Delta.git"
end

现在,我们可以保存并关闭我们的 Podfile,然后转到终端应用程序。在终端应用程序中,我们将执行以下命令:

Pod install

此指令将创建一个工作区,下载所有依赖项,并将它们作为框架链接到我们的项目中。现在,我们可以使用 Xcode 打开 TodoApp.xcworkspace

在工作区中,我们将看到两个项目:TodoAppPods。Pods 将包含所有依赖项。

接下来,让我们创建一个文件夹层次结构来组织我们的工作区。在工作区中,右键单击一个文件夹并选择 在 Finder 中显示。在这里,我们将创建以下文件夹和文件:

  • 动作

  • 通信

  • 控制器

  • 扩展

  • 管理人员

  • 模型

  • 资源

  • 状态

  • 视图

接下来,我们将通过在 TodoApp 文件夹上右键单击并选择 将文件添加到 "TodoApp" 来将这些文件夹添加到我们的项目中,如下面的截图所示:

配置

在这一点上,我们可以将 ViewController 移动到 Controllers 文件夹,并将任何图像移动到 Resources 文件夹。

当我们完成我们的应用程序后,文件夹和文件层次结构将如下所示:

配置

由于我们的后端不符合苹果强制实行的安全策略,我们需要在 .plist 文件中的 NSAppTransportSecurity 字典下将 NSAllowsArbitraryLoads 键设置为 YES

模型

显然,我们可以使用我们在后端示例中使用的 Todo 模型,但我们希望使我们的前端应用程序尽可能功能强大。有一个名为 Argo 的优秀功能 JSON 解析库,我们可以利用。让我们使用 Argo 定义我们的 Todo 模型:

import Argo
import Curry

enum TodoFilter: Int {
    case all
    case active
    case completed
    case notSyncedWithBackend
    case selected
}

struct Todo {
    let id: Int
    let name: String
    let description: String
    let notes: String?
    let completed: Bool
    let synced: Bool
    let selected: Bool?
}

extension Todo: Decodable {
    static func decode(json: JSON) -> Decoded<Todo> {
        return curry(Todo.init)
        <^> json <| "id"
        <*> json <| "name"
        <*> json <| "description"
        <*> json <|? "notes"
        <*> json <| "completed"
        <*> json <| "synced"
        <*> json <|? "selected"
    }
}

extension Todo: Equatable {}

func == (lhs: Todo, rhs: Todo) -> Bool {
    return lhs.id == rhs.id
}

首先,我们导入两个库:Argo 和 Curry。Curry 提供方便的 currying 功能。尽管 currying 将从 Swift 中移除,返回闭包将成为规范,但使用 Curry 库将是安全的。

我们的 Todo 模型变为一个 struct,然后我们通过遵守名为 Decodableprotocol 来扩展我们的 struct。为了遵守此协议,我们需要实现 decode 函数。此函数接受一个 JSON 负载数据并返回解码后的 Todo 对象。

在函数体中,我们将使用 currying 和自定义操作符。根据 Argo 文档,currying 允许我们在解码过程中部分应用 init 函数。这基本上意味着我们可以逐步构建 init 函数调用,每次添加一个参数,如果(并且仅当)Argo 成功解码它们。如果任何参数不符合我们的预期,Argo 将跳过 init 调用并返回一个特殊的失败状态。让我们检查 Curry 的语法:

public func curry<A, B, C, D, E, F>(function: (A, B, C, D, E) -> F) -> A
  -> B -> C -> D -> E -> F {
    return { (`a`: A) -> B -> C -> D -> E -> F in { (`b`: B) -> C -> D -> E
      -> F in { (`c`: C) -> D -> E -> F in { (`d`: D) -> E -> F in { (`e`:
      E) -> F in function(`a`, `b`, `c`, `d`, `e`) } } } } }
}

curry 函数接受一个具有五个参数 AE 的函数并返回 F,即 curry 返回 A -> B -> C -> D -> E -> F

这使我们能够部分应用我们的 init 方法。

操作符

现在我们将讨论不同的自定义中缀操作符:

  • <^> 用于有条件地将函数映射到值

  • <*> 用于将带有上下文的功能应用于带有上下文的值

  • <| 用于将特定键的值解码为请求的类型

  • <|? 用于将特定键的可选值解码为请求的类型

  • <|| 用于将特定键的值数组解码为请求的类型

<^>

在解码过程中的第一个操作符 <^>,用于将我们的 curried init 方法映射到值。定义如下:

public func <^> <T, U>(@noescape f: T -> U, x: Decoded<T>) -> Decoded<U> {
    return x.map(f)
}
func map<U>(@noescape f: T -> U) -> Decoded<U> {
    switch self {
        case let .Success(value): return .Success(f(value))
        case let .Failure(error): return .Failure(error)
    }
}

<*>

<*> 操作符用于有条件地将其他参数应用于我们的 curried init 方法。定义如下:

public func <*> <T, U>(f: Decoded<T -> U>, x: Decoded<T>) -> Decoded<U> {
    return x.apply(f)
}
func apply<U>(f: Decoded<T -> U>) -> Decoded<U> {
    switch f {
        case let .Success(function): return self.map(function)
        case let .Failure(error): return .Failure(error)
    }
}

<|

<| 操作符用于将指定键路径的值解码为请求的类型。此操作符使用名为 flatReduce 的函数来减少和扁平化序列:

public func <| <A where A: Decodable, A == A.DecodedType>(json: JSON, keys:
  [String]) -> Decoded<A> {
    return flatReduce(keys, initial: json, combine: decodedJSON)
      >>- A.decode
}

<|?

<|? 操作符用于将指定键路径的可选值解码为请求的类型:

public func <|? <A where A: Decodable, A == A.DecodedType>(json: JSON, key:
  String) -> Decoded<A?> {
    return .optional(json <| [key])
}

<||

<|| 操作符用于将特定键的值数组解码为请求的类型:

public func <|| <A where A: Decodable, A == A.DecodedType>(json: JSON,
  keys: [String]) -> Decoded<[A]> {
    return flatReduce(keys, initial: json, combine: decodedJSON) >>-
      Array<A>.decode
}

使用 Argo 模型

每当我们从后端接收到 JSON 负载数据时,我们将能够使用 decode 函数将我们的 JSON 负载数据解码到我们的模型:

let json: AnyObject? = try?NSJSONSerialization.JSONObjectWithData(data,
  options: [])

if let j: AnyObject = json {
    let todo: Todo? = decode(j)
}

我们可以看到 Argo 是一个优秀的 FP 库,可以作为掌握许多 FP 范例的示例。使用 Argo、Curry 和自定义操作符,我们能够声明性地解析和解码 JSON 负载数据到我们的模型对象。此外,我们的模型成为不可变值类型,我们可以在应用程序中使用它们而不用担心可变性。

此外,我们还定义了一个名为 TodoFilterenum。我们将使用此 enum 来过滤项目。

ViewModel

我们将有两个 viewModel,每个 ViewController 一个。

import ReactiveCocoa

struct TodosViewModel {
    let todos: [Todo]

    func todoForIndexPath(indexPath: NSIndexPath) -> Todo {
        return todos[indexPath.row]
    }
}

我们将使用TodosViewModel在我们的表格视图中列出Todo项。

struct TodoViewModel {
    let todo: Todo?
}

我们将使用TodoViewModel来展示每个Todo项的详细信息。

通信

到目前为止,我们有一个后端 API 可以用来 CRUD Todo 项,并且我们在 iOS 应用程序中有模型。让我们看看我们如何与后端通信并将接收到的有效载荷填充到我们的模型中。

请求协议

首先,我们需要为我们的请求模型定义一个协议:

protocol RequestProtocol {
    subscript(key: String) -> (String?, String?) { get }
}

extension RequestProtocol {
    func getPropertyNames()-> [String] {
        return Mirror(reflecting: self).children.filter {
$0.label !=
          nil }.map
        { $0.label! }}
}

在这里,我们定义了protocol,并扩展了该协议以能够反映对象并获取属性及其值。

此外,我们还向我们的协议中添加了subscript,任何想要遵守此协议的struct都应该实现它。

遵守请求协议

现在,让我们创建一个名为TodoRequest的请求模型:

struct TodoRequest: RequestProtocol {

    let id: Int
    let name: String
    let description: String
    let notes: String
    let completed: Bool
    let synced: Bool

    subscript(key: String) -> (String?, String?) {
        get {
            switch key {
            case "id": return (String(id), "id")
            case "name": return (name, "name")
            case "description": return (description, "description")
            case "notes": return (notes, "notes")
            case "completed": return (String(completed), "completed")
            case "synced": return (String(synced), "synced")
            default: return ("Cookie","test=123")
            }
        }
    }
}

如前所述的代码所示,此struct遵守RequestProtocol。你可能会想知道我们为什么这样做。首先,这是一个 POP 的例子,其次我们将在我们的后端服务调用中使用这个请求模型。

WebServiceManager

我们将创建一个名为WebServiceManager的文件并在其中添加一个函数:

import Alamofire
func sendRequest(method: Alamofire.Method, request: RequestProtocol) {

    // Add Headers
    let headers = configureHeaders(request)

    // Fetch Request
    Alamofire.request(method, "http://localhost:8080/todo/",
      headers: headers, encoding: .JSON)
    .validate()
    .responseJSON { response in
        if (response.result.error == nil) {
            debugPrint("HTTP Response Body: \(response.data)")
        }
        else {
            debugPrint("HTTP Request failed: \(response.result.error)")
        }
    }
}

func configureHeaders(request: RequestProtocol) -> [String: String] {
    let listOfProperties = request.getPropertyNames()
    var configuredRequestHeaders = Dictionary<String, String>()
    for property in listOfProperties {
        let (propertyValue, propertyName) = request[property]
        if propertyName != nil {
            configuredRequestHeaders[propertyName!] = propertyValue
        }
    }
    return configuredRequestHeaders
}

我们的sendRequest函数接受两个参数。第一个是 HTTP 请求方法,第二个是RequestProtocol的类型。在这里,我们使用实现的协议函数getPropertyNames来准备头信息,并使用Alamofire向我们的后端发送请求。

到目前为止,我们已经有一个工作的通信层。在这个时候,我们需要开发管理器和viewController来处理逻辑并向用户展示结果。

我们将首先在我们的MasterViewController中测试我们的通信层,并将相应的代码移动到我们的managers中。

创建待办事项

要创建待办事项,我们可以在MasterViewController viewDidLoad()方法中调用sendRequest函数以确保其工作:

let newRequest = TodoRequest(id: 1,
                             name: "First request",
                             description:"description",
                             notes: "notes",
                             completed: "no")
    sendRequest(Alamofire.Method.POST, request: newRequest)

这应该在我们的后端添加一个新的Todo项。

我们的sendRequest方法是不完整的,它不提供回调来接收数据。让我们改进它:

func sendRequest(method: Alamofire.Method,
                 request: RequestProtocol,
                 completion:(responseData: AnyObject?, error: NSError?) -> Void) {

    // Add Headers 
    let headers = configureHeaders(request) 

    // Fetch Request
    Alamofire.request(method, "http://localhost:8080/todo/", 
      headers: headers, encoding: .JSON)
        .validate() 
        .responseJSON { response in 
        if (response.result.error == nil) { 
            debugPrint("HTTP Response Body: \(response.data)") 
            completion(responseData: response.result.value, error: nil) 
        } 
        else { 
            debugPrint("HTTP Request failed: \(response.result.error)") 
            completion(responseData: nil, error: response.result.error) 
        } 
    } 
}

我们将闭包作为函数参数添加,并在函数体中调用该闭包。为了测试它,我们将更新我们的MasterViewController中的调用:

let newRequest = TodoRequest(id: 1,
                             name: "First request",
                             description:"description",
                             notes: "notes", 
                             completed: "no")
sendRequest(Alamofire.Method.POST, request: newRequest) {
    (response, error) in
    if error == nil {
        let todos: [Todo]? = decode(response!)
        print("request was successful: \(todos)")
    } else {
        print("Error")
    }
}

在这里,我们在调用中传递一个尾随闭包;一旦它被调用,我们就收到响应或错误。通过导入和使用 Argo,我们可以将有效载荷映射到我们的模型。我们只调用此函数进行测试,我们需要将其移动到适当的位置。毕竟,我们的MasterViewController类中的任何类都无法直接调用此函数,它们必须通过其他对象。此外,我们还需要改进我们的sendRequest函数以接受正确的url

import Alamofire

enum Urls {
    case postTodo
    case getTodos
    case getTodo
    case deleteTodo
    case deleteAll
    case update
}

extension Urls {
    func httpMethodUrl() -> (Alamofire.Method, String) {
        let baseUrl = "http://localhost:8080/"
        switch self {
        case .postTodo:
            return (.POST, "\(baseUrl)postTodo")
        case .getTodos:
            return (.GET, "\(baseUrl)todos")
        case .getTodo:
            return (.GET, "\(baseUrl)todo")
        case .deleteTodo:
            return (.DELETE, "\(baseUrl)deleteTodo")
        case .deleteAll:
            return (.DELETE, "\(baseUrl)deleteAll")
        case .update:
            return (.POST, "\(baseUrl)updateTodo")
        }
    }
}

在这里,我们定义了一个enum并扩展了它。在我们的httpMethodUrl函数中,我们执行模式匹配以返回一个由 HTTP 请求方法和完整的url组成的元组。我们需要将我们的sendRequest函数更改如下:

import Alamofire

func sendRequest(url: Urls,
             request: RequestProtocol,
          completion: (responseData: AnyObject?,
               error: NSError?) -> Void) {
    // Add headers
    let headers = configureHeaders(request)
    // Get request method and full url
    let (method, url) = url.httpMethodUrl()

    // Fetch request
    Alamofire.request(method, url, headers: headers, encoding: .JSON)
    .validate()
    .responseJSON { response in
        if (response.result.error == nil) {
            debugPrint("HTTP Response Body: \(response.data)")
            completion(responseData: response.result.value, error: nil)
        } else {
            debugPrint("HTTP Request failed: \(response.result.error)")
            completion(responseData: nil, error: response.result.error)
        }
    }
}

我们的功能调用应该如下更改:

let newRequest = TodoRequest(id: 1,
                           name: "First request",
                    description: "description",
                          notes: "notes",
                      completed: false)

sendRequest(Urls.postTodo, request: newRequest) { (response, error) in
    if error == nil {
        let todos: [Todo]? = decode(response!)
        print("request was successful: \(todos)")
    } else {
        print("Error")
    }
}

列出待办事项

要检索所有Todo项,与我们的 post 调用不同,我们不需要传递任何头参数,只需 cookie 信息。因此,我们添加以下struct来处理这种情况:

struct RequestModel: RequestProtocol {

    subscript(key: String) -> (String?, String?) {
        get {
            switch key {
                default: return ("Cookie","test=123")
            }
        }
    }
}

然后,我们可以使用以下代码检索Todo项的列表:

sendRequest(Urls.getTodos, request: RequestModel()) { (response, error) in
    if error == nil {
        let todos: [Todo]? = decode(response!)
        print("request was successful: \(todos)")
    } else {
        print("Error: \(error?.localizedDescription)")
    }
}

尽管我们添加了更好的错误打印,但我们还需要进一步改进它。

让我们提取前面的函数调用,创建一个名为TodoManager的 Swift 文件,并将这些函数放入其中:

import Alamofire
import Argo

func addTodo(completion:(responseData:[Todo]?, error: NSError?) -> Void) {
    let newRequest = TodoRequest(id: 1,
                               name: "Saturday Grocery",
                        description: "Bananas, Pineapple, Beer,
                          Orange juice, ...",
                              notes: "Cehck expiry date of orange juice",
                          completed: false,
                             synced: true)

    sendRequest(Urls.postTodo, request: newRequest) {
        (response, error) in
        if error == nil {
            let todos: [Todo]? = decode(response!)
            completion(responseData: todos, error: nil)
            print("request was successfull: \(todos)")
        } else {
            completion(responseData: nil, error: error)
            print("Error: \(error?.localizedDescription)")
        }
    }
}

func listTodos(completion:(responseData:[Todo]?, error: NSError?) -> Void) {
    sendRequest(Urls.getTodos, request: RequestModel()) {
        (response, error) in
        if error == nil {
            let todos: [Todo]? = decode(response!)
            completion(responseData: todos, error: nil)
            print("request was successfull: \(todos)")
        } else {
            completion(responseData: nil, error: error)
            print("Error: \(error?.localizedDescription)")
        }
    }
}

最后,我们将开发两个其他函数:一个用于添加或更新一个Todo项,另一个仅更新特定的Todo项。删除项也将很容易实现。代码如下:

func addOrUpdateTodo(todo: [Todo]?, completion:(responseData:[Todo]?, error: NSError?) -> Void) {
    if let todoItem = todo?.first {
        let newRequest = TodoRequest(id: todoItem.id,
                                   name: todoItem.name,
                            description: todoItem.description,
                                  notes: todoItem.notes!,
                              completed: todoItem.completed,
                                 synced: true)

        sendRequest(Urls.postTodo, request: newRequest) {
            (response, error) in
            if error == nil {
                let todos: [Todo]? = decode(response!)
                let newTodo = todoSyncedLens.set(true, todoItem)
                store.dispatch(UpdateTodoAction(todo: newTodo))
                completion(responseData: todos, error: nil)
                print("request was successfull: \(todos)")
            } else {
                completion(responseData: nil, error: error)
                print("Error: \(error?.localizedDescription)")
            }
        }
    }
}

func updateTodo(todo: [Todo]?, completion:(responseData:[Todo]?,
  error: NSError?) -> Void) {
    if let todoItem = todo?.first {
        let newRequest = TodoRequest(id: todoItem.id,
                                   name: todoItem.name,
                            description: todoItem.description,
                                  notes: todoItem.notes!,
                              completed: todoItem.completed,
                                 synced: true)

        sendRequest(Urls.update, request: newRequest) {
            (response, error) in
            if error == nil {
                let todos: [Todo]? = decode(response!)
                let newTodo = todoSyncedLens.set(true, todoItem)
                store.dispatch(UpdateTodoAction(todo: newTodo))
                completion(responseData: todos, error: nil)
                print("request was successfull: \(todos)")
            } else {   
                completion(responseData: nil, error: error)
                print("Error: \(error?.localizedDescription)")
            }
        }
    }
}

在这些函数中,有一些我们尚未详细讨论的概念:

  • dispatch:这个函数通过设置状态的值为其reduce方法的调用结果来分发一个动作(在这里,是UpdateTodoAction)。

  • todoSyncedLens:这是一个用于修改todo项同步属性的Lens。我们将在下一节中定义这些镜头。

  • UpdateTodoAction:这是一个符合ActionTypestruct,当我们想要修改StoreState时使用。所有对Store的更改都通过此类型进行。我们将在下一节中定义我们的动作。

  • State:这是一个将用于管理Statestruct。我们将在稍后定义它。

  • Store:正如其名所示,这是我们存储State的地方。我们将在稍后定义它。

镜头

我们将使用镜头来修改我们的Todo项。以下每个镜头都将用于修改Todo项的一部分:

struct Lens<Whole, Part> {
    let get: Whole -> Part
    let set: (Part, Whole) -> Whole
}

let todoNameLens: Lens<Todo, String> = Lens(
    get: { $0.name},
    set: {
        Todo(id: $1.id,
           name: $0,
    description: $1.description,
          notes: $1.notes,
      completed: $1.completed,
         synced: $1.synced,
       selected: $1.selected)
})

let todoDescriptionLens: Lens<Todo, String> = Lens(
    get: { $0.description},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $0,
          notes: $1.notes,
      completed: $1.completed,
         synced: $1.synced,
       selected: $1.selected)
})

let todoNotesLens: Lens<Todo, String> = Lens(
    get: { $0.notes!},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $1.description,
          notes: $0,
      completed: $1.completed,
         synced: $1.synced,
       selected: $1.selected)
})

let todoCompletedLens: Lens<Todo, Bool> = Lens(
    get: { $0.completed},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $1.description,
          notes: $1.notes,
      completed: $0,
         synced: $1.synced,
       selected: $1.selected)
})

let todoSyncedLens: Lens<Todo, Bool> = Lens(
    get: { $0.synced},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $1.description,
          notes: $1.notes,
      completed: $1.completed,
         synced: $0,
       selected: $1.selected)
})

状态

在我们的应用程序中,我们需要管理状态以使状态管理代码尽可能声明式。我们将使用一个名为Delta的库。

Delta 将与 ReactiveCocoa 一起用于管理状态和状态变化。代码如下:

import ReactiveCocoa
import Delta

extension MutableProperty: Delta.ObservablePropertyType {
    public typealias ValueType = Value
}

在前面的代码中,我们通过遵循Delta.ObservablePropertyType扩展了 ReactiveCocoa 库的MutableProperty

ObservablePropertyType协议必须由Store持有的State实现。要使用自定义的State类型,必须在该对象上实现此协议。

MutableProperty创建一个可变属性,其类型为值,并允许以线程安全的方式观察其变化。

使用扩展的MutableProperty,我们的State对象变为以下:

import ReactiveCocoa

private let initialTodos: [Todo] = []

struct State {
    let todos = MutableProperty(initialTodos)
    let filter = MutableProperty(TodoFilter.all)
    let notSynced = MutableProperty(TodoFilter.notSyncedWithBackend)
    let selectedTodoItem = MutableProperty(TodoFilter.selected)
}

存储

我们将在我们的Store对象中存储状态:

import ReactiveCocoa
import Delta

struct Store: StoreType {
    var state: MutableProperty<State>

    init(state: State) {
        self.state = MutableProperty(state)
    }
}

var store = Store(state: State())

Store遵循在Delta库中声明的StoreType协议。StoreType协议定义了可观察状态的存储和修改它的分发方法。

这里,我们创建一个MutableProperty作为state并将其存储在Store中。

我们需要定义属性来正确地访问和修改我们的状态,因此我们按如下方式扩展我们的Store

import ReactiveCocoa
import Result

// MARK: Properties
extension Store {
    var todos: MutableProperty<[Todo]> {
        return state.value.todos
    }

    var activeFilter: MutableProperty<TodoFilter> {
        return state.value.filter
    }

    var selectedTodoItem: MutableProperty<TodoFilter> {
        return state.value.selectedTodoItem
    }

}

// MARK: SignalProducers
extension Store {
    var activeTodos: SignalProducer<[Todo], NoError> {
        return activeFilter.producer.flatMap(.Latest) {
            filter -> SignalProducer<[Todo], NoError> in
                switch filter {
                case .all: return self.todos.producer
                case .active: return self.incompleteTodos
                case .completed: return self.completedTodos
                case .notSyncedWithBackend: return
                  self.notSyncedWithBackend
                case .selected: return self.selectedTodo
            }
        }
    }

    var completedTodos: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter { $0.completed }
        }
    }

    var incompleteTodos: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter { !$0.completed }
        }
    }

    var incompleteTodosCount: SignalProducer<Int, NoError> {
        return incompleteTodos.map { $0.count }
    }

    var allTodosCount: SignalProducer<Int, NoError> {
        return todos.producer.map { $0.count }
    }

    var todoStats: SignalProducer<(Int, Int), NoError> {
        return allTodosCount.zipWith(incompleteTodosCount)
    }

    var notSyncedWithBackend: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter { !$0.synced }
        }
    }

    var selectedTodo: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter {
                todo in
                if let selected = todo.selected {
                    return selected
                } else {
                    return false
                }
            }
        }
    }

    func producerForTodo(todo: Todo) -> SignalProducer<Todo, NoError> {
        return store.todos.producer.map {
            todos in
            return todos.filter { $0 == todo }.first
        }.ignoreNil()
    }
}

在我们的存储中,我们使用 ReactiveCocoa 的SignalProducer来创建可观察的信号。我们将在其他对象中观察这些信号并对信号变化做出反应。

动作

动作是符合 Delta 库中的 ActionType 协议的结构体。当我们要对存储的状态进行修改时使用 ActionType。所有对 Store 的更改都通过此类型进行。让我们看看一个例子:

import Delta

struct UpdateTodoAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.map {
            todo in
            guard todo == self.todo else { return todo }

            return Todo(id: todo.id,
                      name: self.todo.name,
               description: self.todo.description,
                     notes: self.todo.notes,
                 completed: self.todo.completed,
                    synced: !todo.synced,
                  selected: todo.selected)
        }

        return state
    }
}

在我们的管理器中,我们有一个这样的调用:

store.dispatch(UpdateTodoAction(todo: newTodo))

store 上调用 dispatch 方法并传入 UpdateTodoAction 将会调用 UpdateTodoActionreduce 方法。它还会对状态进行修改并返回一个新的状态版本。这是唯一允许修改 State 的地方;因此,任何对状态的修改都应该通过一个动作进行。

让我们定义其他动作:

import Delta

struct ClearCompletedTodosAction: DynamicActionType {
    func call() {
        let todos = store.completedTodos.first()?.value ?? []

        todos.forEach { todo in
            store.dispatch(DeleteTodoAction(todo: todo))
        }
    }
}

struct CreateTodoAction: ActionType {
    let id: Int
    let name: String
    let description: String
    let notes: String

    var todo: Todo {
        return Todo(id: id,
                  name: name,
           description: description,
                 notes: notes,
             completed: false,
                synced: false,
              selected: false)
    }

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value + [todo]

        return state
    }
}

struct DeleteTodoAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.filter { $0 != self.todo }

        return state
    }
}

struct DetailsTodoAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.map { todo in
            guard todo == self.todo else {

                return Todo(id: todo.id,
                          name: todo.name,
                   description: todo.description,
                         notes: todo.notes,
                     completed: todo.completed,
                        synced: todo.synced,
                      selected: false)
            }

            return Todo(id: self.todo.id,
                      name: self.todo.name,
               description: self.todo.description,
                     notes: self.todo.notes,
                 completed: self.todo.completed,
                    synced: self.todo.synced,
                  selected: true)
        }

        return state
    }
}

struct LoadTodosAction: ActionType {
    let todos: [Todo]

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value + todos
        return state
    }
}

struct SetFilterAction: ActionType {
    let filter: TodoFilter

    func reduce(state: State) -> State {
        state.filter.value = filter
        return state
    }
}

struct ToggleCompletedAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.map {
            todo in
            guard todo == self.todo else { return todo }

            return Todo(id: todo.id,
                      name: todo.name,
               description: todo.description,
                     notes: todo.notes,
                 completed: !todo.completed,
                    synced: !todo.synced,
                  selected: todo.selected)
        }

        return state
    }
}

视图

用户将能够从后端列出 Todo 项目,切换以标记项目为完成,或向左滑动以访问如 详情删除 等功能。

我们的应用程序将看起来像这样:

视图视图视图视图

我们可以在故事板中设计这些屏幕。为了能够在表格视图中显示适当的数据,我们需要实现一个自定义的 UITableViewCell,如下所示:

class TodoTableViewCell: UITableViewCell {

    var todo: Todo? {
        didSet {
            updateUI()
        }
    }

    var attributedText: NSAttributedString {
        guard let todo = todo else { return NSAttributedString() }

        let attributes: [String : AnyObject]
        if todo.completed {
            attributes = [NSStrikethroughStyleAttributeName:
              NSUnderlineStyle.StyleSingle.rawValue]
        } else {
            attributes = [:]
        }

        return NSAttributedString(string: todo.name,
          attributes: attributes)
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

    func configure(todo: Todo) {
        store.producerForTodo(todo).startWithNext { nextTodo in
            self.todo = nextTodo
        }
    }

    func updateUI() {
        guard let todo = todo else { return }

        textLabel?.attributedText = attributedText
        accessoryType = todo.completed ? .Checkmark : .None
    }

}

这个类中唯一有趣的部分是 configure 方法。它将在我们的 TableViewControllercellForRowAtIndexPath 方法中被调用,以从生产者创建一个 Signal,然后添加一个精确的观察者到 Signal 中,当收到下一个事件时将调用给定的回调。

ViewController

我们将有两个 ViewController 子类:

  • MasterViewController:这将列出 Todo 项目

  • DetailViewController:这将展示和修改每个项目的详情

MasterViewController

我们将在 MasterViewController 中向用户展示项目列表:

import UIKit

class MasterViewController: UITableViewController {

    @IBOutlet weak var filterSegmentedControl: UISegmentedControl!

    var viewModel = TodosViewModel(todos: []) {
        didSet {
            tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        listTodos() {
            (response, error) in
            if error == nil {
                store.dispatch(LoadTodosAction(todos: response!))
            } else {
                print("Error: \(error?.localizedDescription)")
            }
        }

        filterSegmentedControl.addTarget(self, action:
          #selector(ViewController.filterValueChanged),
          forControlEvents: .ValueChanged)

        store.activeFilter.producer.startWithNext {
            filter in
            self.filterSegmentedControl.selectedSegmentIndex =
              filter.rawValue
        }

        store.activeTodos.startWithNext {
            todos in
            self.viewModel = TodosViewModel(todos: todos)
        }

        store.notSyncedWithBackend.startWithNext {
            todos in
            addOrUpdateTodo(todos) { (response, error) in
                if error == nil {
                    print("Success")
                } else {
                    print("Error: \(error?.localizedDescription)")
                }
            }
        }
    }
}

我们有 viewModel,这是一个计算属性。在 viewDidLoad 中,我们从后端列出 Todo 项目并将它们存储在 State 中,使用 LoadTodosAction。然后,我们定义观察来更改我们的 viewModel 并与后端同步更改的项目。

IBActions

我们需要定义两个 IBAction,一个用于向列表添加新项目,另一个用于过滤项目:

// MARK: Actions
extension MasterViewController {
    @IBAction func addTapped(sender: UIBarButtonItem) {
        let alertController = UIAlertController(
          title: "Create",
        message: "Create a new todo item",
 preferredStyle: .Alert)

    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Id"
    }

    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Name"
    }

    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Description"
    }

    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Notes"
    }

    alertController.addAction(UIAlertAction(title: "Cancel",
      style: .Cancel) { _ in })

    alertController.addAction(UIAlertAction(title: "Create",
      style: .Default) { _ in
        guard let id = alertController.textFields?[0].text,
        name = alertController.textFields?[1].text,
        description = alertController.textFields?[2].text,
        notes = alertController.textFields?[3].text
        else { return }

        store.dispatch(CreateTodoAction(
          id: Int(id)!,
        name: name,
 description: description,
       notes: notes))
        })
        presentViewController(alertController, animated: false,
          completion: nil)
    }

    func filterValueChanged() {
        guard let newFilter = TodoFilter(rawValue:
          filterSegmentedControl.selectedSegmentIndex)
        else { return }

        store.dispatch(SetFilterAction(filter: newFilter))
    }
}

addTapped 方法中,我们使用 createTodoAction 将项目添加到列表中,并将 completedsynced 的值设置为 false。因此,store.notSyncedWithBackend.startWithNextviewDidLoad 中会观察这个项目为未同步,并将其与后端同步。

TableView Delegates 和 DataSource

最后,我们需要为 UITableViewController 实现相应的 delegatesdatasource 方法。代码如下:

// MARK: UITableViewController
extension MasterViewController {
    override func tableView(tableView: UITableView,
      numberOfRowsInSection section: Int) -> Int {
        return viewModel.todos.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath
      indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("todoCell",
          forIndexPath: indexPath) as! TodoTableViewCell
        let todo = viewModel.todoForIndexPath(indexPath)

        cell.configure(todo)

        return cell
    }

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath
      indexPath: NSIndexPath) {
        let todo = viewModel.todoForIndexPath(indexPath)
        store.dispatch(ToggleCompletedAction(todo: todo))
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }

    override func tableView(tableView: UITableView, commitEditingStyle
      editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath
      indexPath: NSIndexPath) {

    }

    override func tableView(tableView: UITableView,
      editActionsForRowAtIndexPath indexPath: NSIndexPath)
      -> [UITableViewRowAction]? {
        let delete = UITableViewRowAction(style: .Normal, title: "Delete")
          { action, index in
            let todo = self.viewModel.todoForIndexPath(indexPath)
            store.dispatch(DeleteTodoAction(todo: todo))
        }
        delete.backgroundColor = UIColor.redColor()

        let details = UITableViewRowAction(style: .Normal,
          title: "Details") { action, index in
            let todo = self.viewModel.todoForIndexPath(indexPath)
            store.dispatch(DetailsTodoAction(todo: todo))

            self.performSegueWithIdentifier("segueShowDetails",
              sender: self)
        }
        details.backgroundColor = UIColor.orangeColor()

        return [details, delete]
    }

    override func tableView(tableView: UITableView, canEditRowAtIndexPath
      indexPath: NSIndexPath) -> Bool {
        // the cells you would like the actions to appear need to
          be editable
        return true
    }
}

在前面的代码中,我们使用 DeleteTodoAction 通过向左滑动并选择 删除 来删除一个项目。我们使用 ToggleCompletedAction 在我们点击列表中的任何项目时将其标记为完成,并使用 DetailsTodoAction 在我们向左滑动并选择 详情 时导航到详情页面。

DetailsViewController

我们将使用 viewController 来展示和修改 Todo 项的详细信息。我们将有三个 textField 和一个开关。我们将观察 UI 的变化,并通过 UpdateTodoAction 修改 State 和后端。以下是代码:

import UIKit
import ReactiveCocoa

class DetailsViewController: UIViewController {

    @IBOutlet weak var txtFieldName: UITextField!
    @IBOutlet weak var txtFieldDescription: UITextField!
    @IBOutlet weak var txtFieldNotes: UITextField!
    @IBOutlet weak var switchCompleted: UISwitch!

    var viewModel = TodoViewModel(todo: nil)

    override func viewDidLoad() {
        super.viewDidLoad()
        store.selectedTodo.startWithNext { todos in
            let model = todos.first!
            self.txtFieldName.text = model.name
            self.txtFieldDescription.text = model.description
            self.txtFieldNotes.text = model.notes
            self.switchCompleted.on = model.completed
            self.viewModel = TodoViewModel(todo: model)
        }
        setupUpdateSignals()
    }

    func setupUpdateSignals() {
        txtFieldName.rac_textSignal().subscribeNext {
            (next: AnyObject!) -> () in
            if let newName = next as? String {
                let newTodo = todoNameLens.set(newName,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))
            }
        }

        txtFieldDescription.rac_textSignal().subscribeNext {
            (next: AnyObject!) -> () in
            if let newDescription = next as? String {
                let newTodo = todoDescriptionLens.set(newDescription,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))
            }
        }

        txtFieldNotes.rac_textSignal().subscribeNext {
            (next: AnyObject!) -> () in
            if let newNotes = next as? String {
                let newTodo = todoNotesLens.set(newNotes,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))

            }
        }

        switchCompleted.rac_newOnChannel().subscribeNext {
            (next: AnyObject!) -> () in
            if let newCompleted = next as? Bool {
                let newTodo = todoCompletedLens.set(newCompleted,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))

            }
        }
    }
}

在我们的 viewDidLoad 方法中,在导航到 DetailsViewController 之前,我们会在 MasterViewController 中查找选定的项。我们还将设置 UITextFieldUISwitch 的初始值。我们将订阅 UI 的变化,使用 lenses 更新 Todo 项,并通过 UpdateTodoAction 改变状态。任何项目更改都会将同步设置为 false。由于这个属性在 MasterViewController 中被观察,DetailsViewController 中 UI 的任何更改都将与后端同步,无需额外努力。

摘要

在本章中,我们使用 Swift Vapor 库开发了一个后端,该后端处理 Todo 项目的 POSTGETDELETE 操作。然后,我们开发了一个前端 iOS 应用程序,该应用程序利用函数式编程、响应式编程和状态管理技术进行声明式开发。我们首先以函数式风格开发我们的 Todo 模型,然后开发了 Store 及其扩展来处理 State 存储和 Action 以处理 State 变化。我们定义并使用了 Lens 来修改我们的属性,并使用反射技术创建了一个 WebServiceManager 来请求后端资源。

在这个案例研究中,我们能够使用 structenum 等值类型,并避免使用类。实际上,本案例研究中的四个类都与 iOS SDK 相关(UIViewControllerUITableViewControllerUITableViewCellUIView 子类)。我们能够仅使用 Action 来更改 Store 中的 State,将所有状态突变集中到 Store 中。尽管我们没有开发任何单元测试用例,但建议您探索函数式编程单元测试库,如 Quick,以确保代码质量。

posted @ 2025-10-24 10:06  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报