写点什么

判断回文数字算法,swift 5 初始化详解,时间管理计划落地,swift5 多线程高级用法 John 易筋 ARTS 打卡 Week 33

用户头像
John(易筋)
关注
发布于: 2021 年 01 月 04 日

1. Algorithm: 每周至少做一个 LeetCode 的算法题

笔者的文章:

算法:回文数字9. Palindrome Number


LeetCode 全集请参考:LeetCode Github 大全


题目

9. Palindrome Number


Determine whether an integer is a palindrome. An integer is a palindrome when it reads the same backward as forward.


Follow up: Could you solve it without converting the integer to a string?


Example 1:


Input: x = 121Output: true
复制代码


Example 2:


Input: x = -121Output: falseExplanation: From left to right, it reads -121. From right to left, it becomes 121-. Therefore it is not a palindrome.
复制代码


Example 3:


Input: x = 10Output: falseExplanation: Reads 01 from right to left. Therefore it is not a palindrome.
复制代码


Example 4:


Input: x = -101Output: false 
复制代码


Constraints:


-231 <= x <= 231 - 1
复制代码


全部求余数解法

用上回得到的结果乘以 10,加上 x 除以 10 余数,得到的最终结果,和 x 比较即可。

class Solution {    public boolean isPalindrome(int x) {        // check edge        if (x < 0) {            return false;        }                int reverse = 0;        int copy = x;        while (copy > 0) {            int last = copy % 10;            reverse = reverse * 10 + last;            copy = copy / 10;        }                return reverse == x;    }}
复制代码


折半求余数解法

实际上只要折半后就能得到结果,也就是x > reverse

这里需要注意,除以 10 整除的数,都不行。

class Solution {    public boolean isPalindrome(int x) {        // check edge        if (x < 0 || (x > 0 && x % 10 == 0)) {            return false;        }                        int reverse = 0;        while (x > reverse) {            int last = x % 10;            reverse = reverse * 10 + last;                        x = x / 10;        }                return x == reverse || x == reverse / 10;    }}
复制代码


2. Review: 阅读并点评至少一篇英文技术文章

笔者文章:

翻译:swift 5初始化 被忽略的Convenience便捷初始化、Required和继承

初始化

初始化是准备使用的类,结构或枚举实例的过程。此过程涉及为该实例上的每个存储属性设置一个初始值,并执行新实例准备使用之前所需的任何其他设置或初始化。


您可以通过定义初始值设定项来实现此初始化过程,初始值设定项类似于可以调用以创建特定类型新实例的特殊方法。与 Objective-C 初始值设定项不同,Swift 初始值设定项不会返回值。它们的主要作用是确保首次使用类型之前,正确初始化类型的新实例。


类类型的实例还可以实现一个 deinitializer,它在释放该类的实例之前执行任何自定义清除。有关反初始化程序的更多信息,请参见反初始化。


1. 设置存储属性的初始值

类和结构必须在创建该类或结构的实例时将其所有存储的属性设置为适当的初始值。存储的属性不能处于不确定状态。


您可以在初始化程序中为存储的属性设置初始值,或者通过将默认属性值分配为属性定义的一部分来设置初始值。以下各节介绍了这些操作。


注意

当您为存储的属性分配默认值,或在初始化程序中设置其初始值时,将直接设置该属性的值,而无需调用任何属性观察器。


1.1 初始化器

调用初始化程序以创建特定类型的新实例。最简单的形式是,初始化程序就像没有参数的实例方法,使用 init 关键字编写:

init() {    // perform some initialization here}
复制代码

下面的示例定义了一个新结构,称为 Fahrenheit 存储以华氏度表示的温度。该 Fahrenheit 结构具有一个存储属性 temperature,其类型为 Double:


struct Fahrenheit {    var temperature: Double    init() {        temperature = 32.0    }}var f = Fahrenheit()print("The default temperature is \(f.temperature)° Fahrenheit")// Prints "The default temperature is 32.0° Fahrenheit"
复制代码

该结构定义了一个 init 没有参数的初始化程序,该初始化程序使用值 32.0(水的冰点,以华氏度为单位)初始化存储的温度。

1.2 默认属性值

您可以从初始化程序中设置存储属性的初始值,如上所示。或者,指定默认属性值作为属性声明的一部分。您可以通过在定义属性时为其分配初始值来指定默认属性值。


注意


如果属性始终采用相同的初始值,请提供默认值,而不要在初始化程序中设置值。最终结果是相同的,但是默认值将属性的初始化与其声明紧密联系在一起。它使初始化程序更短,更清晰,并使您能够从其默认值推断属性的类型。默认值还使您更容易利用默认初始化程序和初始化程序继承,如本章稍后所述。


您可以 Fahrenheit 通过 temperature 在声明属性时为其属性提供默认值,以更简单的形式从上面编写结构:

struct Fahrenheit {    var temperature = 32.0}
复制代码

2 自定义初始化

您可以使用输入参数和可选属性类型,或通过在初始化期间分配常量属性来自定义初始化过程,如以下各节所述。

2.1 初始化参数

您可以提供初始化参数作为初始化程序定义的一部分,以定义自定义初始化过程的值的类型和名称。初始化参数具有与功能和方法参数相同的功能和语法。


以下示例定义了一个名为的结构 Celsius,该结构存储以摄氏度表示的温度。该 Celsius 结构实现了两个自定义的初始化程序,称为 init(fromFahrenheit:)和 init(fromKelvin:),它们使用不同温度范围内的值初始化该结构的新实例:

struct Celsius {    var temperatureInCelsius: Double    init(fromFahrenheit fahrenheit: Double) {        temperatureInCelsius = (fahrenheit - 32.0) / 1.8    }    init(fromKelvin kelvin: Double) {        temperatureInCelsius = kelvin - 273.15    }}let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)// boilingPointOfWater.temperatureInCelsius is 100.0let freezingPointOfWater = Celsius(fromKelvin: 273.15)// freezingPointOfWater.temperatureInCelsius is 0.0
复制代码

第一个初始化程序具有一个初始化参数,其参数标签为 fromFahrenheit,参数名称为 fahrenheit。第二个初始化程序具有一个初始化参数,其参数标签为 fromKelvin,参数名称为 kelvin。两个初始化程序都将其单个参数转换为相应的摄氏值,并将此值存储在名为的属性中 temperatureInCelsius。


2.2 参数名称和参数标签

与函数和方法参数一样,初始化参数可以具有用于初始化程序主体的参数名称和用于调用初始化程序的参数标签。


但是,初始化程序在其括号前没有以函数和方法那样的方式标识函数的名称。因此,初始化器参数的名称和类型在确定应调用哪个初始化器中起着特别重要的作用。因此,如果您不提供初始化方法,则 Swift 会为初始化程序中的每个参数提供一个自动参数标签。


下面的例子定义了一个名为结构 Color,具有三个恒定属性叫做 red,green,和 blue。这些属性在 0.0 和之间存储一个值,1.0 以指示颜色中红色,绿色和蓝色的数量。


Color 为初始化程序 Double 的红色,绿色和蓝色分量提供三个适当命名的类型的参数。Color 还提供了带有单个 white 参数的第二个初始化器,该初始化器用于为所有三个颜色分量提供相同的值。

struct Color {    let red, green, blue: Double    init(red: Double, green: Double, blue: Double) {        self.red   = red        self.green = green        self.blue  = blue    }    init(white: Double) {        red   = white        green = white        blue  = white    }}
复制代码

Color 通过为每个初始化器参数提供命名值,可以使用这两个初始化器来创建新实例:


let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)let halfGray = Color(white: 0.5)
复制代码

请注意,如果不使用参数标签,则无法调用这些初始化程序。如果已定义参数标签,则必须始终在初始化程序中使用它们,而忽略它们是编译时错误:


let veryGreen = Color(0.0, 1.0, 0.0)// this reports a compile-time error - argument labels are required
复制代码

2.3 不带参数标签的初始化参数

如果不想为初始化参数使用参数标签,请为该参数写下划线(_)而不是显式参数标签,以覆盖默认行为。


这是上述“初始化参数”中 Celsius 示例的扩展版本,带有一个附加的初始化程序,可使用已经在摄氏度范围内的值来创建新实例:CelsiusDouble


struct Celsius {    var temperatureInCelsius: Double    init(fromFahrenheit fahrenheit: Double) {        temperatureInCelsius = (fahrenheit - 32.0) / 1.8    }    init(fromKelvin kelvin: Double) {        temperatureInCelsius = kelvin - 273.15    }    init(_ celsius: Double) {        temperatureInCelsius = celsius    }}let bodyTemperature = Celsius(37.0)// bodyTemperature.temperatureInCelsius is 37.0
复制代码

初始化调用Celsius(37.0)的意图很明确,不需要参数标签。因此,应将此初始化程序编写为:可以通过提供未命名的值来调用它。init(_ celsius: Double)Double

2.4 可选属性类型

如果您的自定义类型具有逻辑上允许具有“无值”的存储属性(可能是因为在初始化期间无法设置其值,或者是因为稍后允许其具有“无值”),请使用可选类型。可选类型的属性将使用值自动初始化 nil,表明该属性在初始化过程中故意具有“没有值”。


以下示例定义了一个名为的类 SurveyQuestion,其具有一个可选 String 属性 response:


class SurveyQuestion {    var text: String    var response: String?    init(text: String) {        self.text = text    }    func ask() {        print(text)    }}let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")cheeseQuestion.ask()// Prints "Do you like cheese?"cheeseQuestion.response = "Yes, I do like cheese."
复制代码

直到询问问题后,才能知道对调查问题的回答,因此该 response 属性用 String?或的类型声明为“可选 String”。初始化 nil 新实例时,会自动为其指定默认值,表示“没有字符串” SurveyQuestion。


2.5 在初始化期间分配常量属性

您可以在初始化期间的任何时候为常量属性分配一个值,只要在初始化完成时将其设置为确定值即可。为常数属性分配值后,就无法再对其进行修改。


注意

对于类实例,只能在引入常量的类的初始化期间对其进行修改。子类不能修改它。


您可以SurveyQuestion从上面修改示例,以text对问题的属性使用常量属性而不是变量属性,以表明一旦SurveyQuestion创建了实例,问题就不会更改。即使该text属性现在是常量,仍可以在类的初始化程序中进行设置:

class SurveyQuestion {    let text: String    var response: String?    init(text: String) {        self.text = text    }    func ask() {        print(text)    }}let beetsQuestion = SurveyQuestion(text: "How about beets?")beetsQuestion.ask()// Prints "How about beets?"beetsQuestion.response = "I also like beets. (But not with cheese.)"
复制代码

3 默认初始化器

迅速提供了一个默认初始值对于所有其属性提供缺省值,并且不提供至少一个初始值设定本身的任何结构或类。默认初始化程序仅创建一个新实例,并将其所有属性设置为其默认值。


本示例定义了一个名为的类 ShoppingListItem,该类将购物清单中商品的名称,数量和购买状态封装起来:


class ShoppingListItem {    var name: String?    var quantity = 1    var purchased = false}var item = ShoppingListItem()
复制代码

由于 ShoppingListItem 该类的所有属性都具有默认值,并且由于它是没有超类的基类,因此会 ShoppingListItem 自动获得一个默认的初始化器实现,该实现会创建一个将其所有属性都设置为其默认值的新实例。(该 name 属性是一个可选 String 属性,因此 nil 即使该值未写在代码中,它也会自动接收默认值。)上面的示例使用 ShoppingListItem 该类的默认初始化程序,使用初始化程序创建该类的新实例。语法,表示为 ShoppingListItem(),并将此新实例分配给名为的变量 item。


3.1 结构类型的成员初始化器

如果结构类型没有定义任何自己的自定义初始化程序,则它们会自动接收一个成员初始化程序。与默认初始化程序不同,该结构即使在存储了没有默认值的属性的情况下也会接收成员初始化程序。


逐成员初始化器是初始化新结构实例的成员属性的简便方法。可以通过名称将新实例的属性的初始值传递给成员初始化器。


下面的示例定义了一个结构 Size,该结构具有两个称为 width 和的属性 height。Double 通过指定默认值,可以推断这两个属性均为类型 0.0。


该 Size 结构会自动接收一个 init(width:height:)成员级初始化程序,您可以使用该初始化程序来初始化新 Size 实例:

struct Size {    var width = 0.0, height = 0.0}let twoByTwo = Size(width: 2.0, height: 2.0)
复制代码

调用成员初始化程序时,可以忽略具有默认值的任何属性的值。在上面的示例中,该 Size 结构的 height 和 width 属性都具有默认值。您可以忽略一个属性或两个属性,并且初始化程序将对所有忽略的内容使用默认值,例如:

let zeroByTwo = Size(height: 2.0)print(zeroByTwo.width, zeroByTwo.height)// Prints "0.0 2.0"
let zeroByZero = Size()print(zeroByZero.width, zeroByZero.height)// Prints "0.0 0.0"
复制代码

4 值类型的初始化程序委托

初始化程序可以调用其他初始化程序来执行实例初始化的一部分。此过程称为初始化程序委托,可避免在多个初始化程序之间重复代码。


对于值类型和类类型,初始化程序委派的工作方式以及允许何种形式的委派的规则是不同的。值类型(结构和枚举)不支持继承,因此它们的初始化程序委托过程相对简单,因为它们只能委托给自己提供的另一个初始化程序。但是,类可以从其他类继承,如Inheritance中所述。这意味着类还有其他责任,以确保在初始化期间为它们继承的所有存储属性分配适当的值。这些职责在下面的[类继承和初始化中](https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID216)进行了描述。


对于值类型,self.init在编写自己的自定义初始化程序时,通常使用同一值类型引用其他初始化程序。您self.init只能在初始化程序中调用。


请注意,如果您为值类型定义自定义初始化程序,则将不再有权使用该类型的默认初始化程序(或成员初始化程序,如果它是结构)。该约束防止了使用自动初始化程序之一的人意外绕过更复杂的初始化程序中提供的其他基本设置的情况。


注意

如果您希望自定义值类型可以使用默认的初始值设定项和成员明智的初始值设定项以及自己的自定义初始化项进行初始化,请在扩展名中编写自定义初始化项,而不是将其作为值类型的原始实现的一部分。有关更多信息,请参见扩展


以下示例定义了一个自定义Rect结构来表示几何矩形。该示例需要两个称为Size和的支持结构Point,这两个结构均为其0.0所有属性提供默认值:


struct Size {    var width = 0.0, height = 0.0}struct Point {    var x = 0.0, y = 0.0}
复制代码

您可以通过 Rect 以下三种方式之一来初始化下面的结构:使用其默认的零初始化值 origin 和 size 属性值,提供特定的原点和尺寸,或提供特定的中心点和尺寸。这些初始化选项由 Rect 结构定义中的三个自定义初始化器表示:

struct Rect {    var origin = Point()    var size = Size()    init() {}    init(origin: Point, size: Size) {        self.origin = origin        self.size = size    }    init(center: Point, size: Size) {        let originX = center.x - (size.width / 2)        let originY = center.y - (size.height / 2)        self.init(origin: Point(x: originX, y: originY), size: size)    }}
复制代码

如果结构没有自己的自定义初始化 Rect 程序,init()则第一个初始化程序在功能上与该结构将收到的默认初始化程序相同。此初始值设定项的主体为空,由一对空的花括号表示{}。调用此初始值设定项将返回一个 Rect 实例,该实例的originsize属性均使用和从其属性定义的默认值初始化:Point(x: 0.0, y: 0.0) Size(width: 0.0, height: 0.0)

let basicRect = Rect()// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)
复制代码

第二个 Rect 初始化程序,init(origin:size:)在功能上与该结构如果没有自己的自定义初始化程序时将接收的成员初始化程序相同。此初始化程序仅将 origin 和 size 参数值分配给适当的存储属性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),                      size: Size(width: 5.0, height: 5.0))// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)
复制代码

第三个 Rect 初始化init(center:size:)器稍微复杂一些。首先根据一个center点和一个size值计算一个合适的原点。然后,它调用(或委托)init(origin:size:)初始化器,该初始化器将新的originsize值存储在适当的属性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),                      size: Size(width: 3.0, height: 3.0))// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)
复制代码

init(center:size:)初始化可能分配的新值origin,并size以相应的属性本身。但是,对于init(center:size:)初始化程序而言,利用已经提供了该功能的现有初始化程序更加方便(意图更清晰)。


注意

有关无需自行定义 init()和 init(origin:size:)初始化程序的另一示例编写方式,请参见Extensions


5 类继承和初始化

在初始化期间,必须为类的所有存储属性(包括该类从其超类继承的所有属性)分配一个初始值。


Swift 为类类型定义了两种初始化器,以帮助确保所有存储的属性都接收初始值。这些被称为指定的初始化程序和便捷初始化程序。


5.1 指定的初始化程序和 Convenience 便利性初始化程序

指定的初始化器是类的主要初始化器。指定的初始化程序将完全初始化该类引入的所有属性,并调用适当的超类初始化程序以继续超类链中的初始化过程。


类往往只有很少的指定初始化器,而一个类只有一个很常见。指定的初始化程序是“漏斗”点,通过它们进行初始化,并通过该“漏斗”点在父类链中继续进行初始化过程。


每个类必须至少有一个指定的初始化程序。在某些情况下,可以通过从超类继承一个或多个指定的初始化程序来满足此要求,如下面的“自动初始化程序继承”中所述。


便利初始化器是辅助的,支持类的初始化器。您可以定义一个便捷初始化程序,以从与便捷初始化程序相同的类中调用一个指定初始化程序,并将某些指定初始值设定项的参数设置为默认值。您还可以定义一个便捷初始化程序,以针对特定用例或输入值类型创建该类的实例。


如果您的类不需要便利初始化器,则不必提供它们。只要通向通用初始化模式的快捷方式可以节省时间或使类的初始化更清晰,就可以创建便利的初始化器。

5.2 指定和便捷初始化程序的语法

指定的类初始化器的编写方式与值类型的简单初始化器的编写方式相同:


init(parameters) {    statements}
复制代码

便捷初始化程序以相同的样式编写,但 convenience 修饰符放在 init 关键字之前,并用空格分隔:


convenience init(parameters) {    statements}
复制代码

5.3 类类型的初始化程序委托

为了简化指定的初始化器和便捷的初始化器之间的关系,Swift 将以下三个规则应用于初始化器之间的委托调用:


规则 1

指定的初始值设定项必须从其直接超类调用指定的初始值设定项。

规则二

便利初始化程序必须从同一类调用另一个初始化程序。

规则三

便捷初始化程序必须最终调用指定的初始化程序。


记住这一点的一种简单方法是:

  • 指定的初始值必须始终委派了。

  • 便利的初始化必须始终委派跨越。


这些规则如下图所示:

在这里,超类具有一个指定的初始值设定项和两个便利的初始化项。一个便利初始化程序调用另一个便利初始化程序,后者又调用单个指定的初始化程序。这从上方满足规则 2 和 3。超类本身没有其他超类,因此规则 1 不适用。


该图中的子类具有两个指定的初始化程序和一个便捷的初始化程序。便捷初始化程序必须调用两个指定的初始化程序之一,因为它只能调用同一类中的另一个初始化程序。这从上方满足规则 2 和 3。两个指定的初始值设定项都必须从超类中调用单个指定的初始值设定项,以满足上方的规则 1。


注意


这些规则不会影响您的类的用户如何创建每个类的实例。上图中的任何初始化程序都可用于创建它们所属类的完全初始化的实例。这些规则仅影响您如何编写类的初始化程序的实现。


下图显示了四个类的更复杂的类层次结构。它说明了此层次结构中指定的初始化程序如何充当类初始化的“漏斗”点,从而简化了链中各类之间的相互关系:

5.4 两阶段初始化

Swift 中的类初始化是一个分为两个阶段的过程。在第一阶段,每个存储的属性都由引入它的类分配一个初始值。一旦确定了每个存储属性的初始状态,便开始第二阶段,并且在考虑将新实例准备就绪之前,每个类都有机会自定义其存储属性。


两阶段初始化过程的使用使初始化安全,同时仍为类层次结构中的每个类提供了完全的灵活性。两阶段初始化可防止在初始化属性值之前对其进行访问,并防止其他初始化程序意外地将属性值设置为其他值。


注意


Swift 的两阶段初始化过程类似于 Objective-C 中的初始化。主要区别在于,在阶段 1 中,Objective-C 为每个属性分配零或空值(例如 0 或 nil)。Swift 的初始化流程更加灵活,因为它可以让您设置自定义初始值,并且可以处理有效值 0 或 nil 无效值的类型。


Swift 的编译器执行四项有用的安全检查,以确保两阶段初始化完成且没有错误:

安全检查 1

指定的初始化程序在将其委托给超类初始化程序之前,必须确保初始化其类引入的所有属性。

如上所述,仅在知道对象所有存储属性的初始状态后,才认为该对象的内存已完全初始化。为了满足此规则,指定的初始值设定项必须确保在传递链之前初始化其自身的所有属性。


安全检查 2

在将值分配给继承的属性之前,指定的初始值设定项必须委托一个超类初始值设定项。如果不是这样,则指定的初始化器分配的新值将被超类覆盖,作为其自身初始化的一部分。

安全检查 3

便利初始化程序必须在将值分配给任何属性(包括由同一类定义的属性)之前委托给另一个初始化程序。如果不是,便利初始化程序分配的新值将被其自己类的指定初始化程序覆盖。

安全检查 4

在初始化 self 的第一阶段完成之前,初始化器无法调用任何实例方法,读取任何实例属性的值或将其称为值。

在第一阶段结束之前,该类实例并不完全有效。一旦在第一阶段结束时知道类实例是有效的,就只能访问属性,并且只能调用方法。


根据上述四个安全检查,以下是两阶段初始化如何进行:


阶段 1


  • 指定的或便捷的初始化程序在类上调用。

  • 分配了该类新实例的内存。内存尚未初始化。

  • 该类的指定初始化程序确认该类引入的所有存储属性都具有值。现在已初始化这些存储属性的内存。

  • 指定的初始值设定项移交给超类初始值设定项,以为其自身的存储属性执行相同的任务。

  • 这将继续类继承链,直到到达链的顶部。

  • 一旦到达链的顶部,并且链中的最后一个类确保其所有存储的属性都具有值,则实例的内存被视为已完全初始化,并且阶段 1 已完成。


阶段 2


  • 从链的顶部向下追溯,链中的每个指定的初始化程序都可以选择进一步自定义实例。初始化程序现在可以访问 self 并可以修改其属性,调用其实例方法,等等。

  • 最后,链中的所有便利初始化程序都可以选择自定义实例并使用 self。


以下是第 1 阶段寻找假设的子类和超类的初始化调用的方式:

在此示例中,初始化始于对子类的便捷初始化程序的调用。此便捷初始化程序尚无法修改任何属性。它委托来自同一类的指定初始化器。


根据安全检查 1,指定的初始化器确保子类的所有属性都有一个值。然后,它在其父类上调用指定的初始化器,以继续进行链上的初始化。


超类的指定初始化器确保所有超类属性都有一个值。没有其他可初始化的超类,因此不需要进一步的委派。


一旦超类的所有属性都具有初始值,就将其内存视为已完全初始化,并且阶段 1 已完成。


以下是第 2 阶段寻找相同初始化调用的方式:

现在,超类的指定初始化器有机会进一步自定义实例(尽管不必如此)。


一旦超类的指定初始值设定项完成,子类的指定初始值设定项即可执行其他自定义(尽管不必如此)。


最后,一旦子类的指定初始化程序完成,最初调用的便捷初始化程序就可以执行其他自定义。


5.5 初始化程序的继承和覆盖

与 Objective-C 中的子类不同,Swift 子类默认情况下不会继承其超类初始化程序。Swift 的方法可防止出现以下情况:超类中的简单初始化程序被更专门的子类继承,并用于创建未完全或正确初始化的子类的新实例。


注意


超类初始化程序在某些情况下会被继承,但是只有在安全且适当的情况下才可以这样做。有关更多信息,请参见下面的自动初始化继承


如果希望自定义子类提供与其父类相同的一个或多个初始化器,则可以在子类中提供这些初始化器的自定义实现。


当编写与超类指定的初始化程序匹配的子类初始化程序时,实际上是在提供该指定的初始化程序的替代。因此,必须 override 在子类的初始化程序定义之前编写修饰符。即使您要覆盖自动提供的默认初始化程序,这也是正确的,如Default Initializers中所述


与覆盖的属性,方法或下标一样,override 修饰符的存在会提示 Swift 检查超类是否具有匹配的指定初始化器要被覆盖,并验证是否已按预期指定了覆盖初始化器的参数。


注意


override 重写超类指定的初始值设定项时,您始终会编写修饰符,即使您的子类对初始值设定项的实现是便利的初始值设定项也是如此。


相反,如果您编写与超类便利性初始化程序匹配的子类初始化程序,则根据上面的“类类型的初始化程序委托”中所述的规则,该子类将永远无法直接调用该超类便利性初始化程序。因此,您的子类(严格地说)没有提供超类初始值设定项的替代。因此,override 在提供超类便捷初始化程序的匹配实现时,您无需编写修饰符。


下面的示例定义了一个名为的基类 Vehicle。此基类声明一个称为的存储属性 numberOfWheels,默认 Int 值为 0。该 numberOfWheels 属性由计算属性使用,该属性被称为 description 创建 String 车辆特性的描述:

class Vehicle {    var numberOfWheels = 0    var description: String {        return "\(numberOfWheels) wheel(s)"    }}
复制代码

Vehicle类提供了其唯一的存储属性的默认值,并没有提供任何自定义初始化本身。结果,它会自动接收一个默认的初始化程序,如Default Initializers中所述。默认的初始值设定项(如果有)始终是类的指定初始值设定项,可用于创建Vehicle带有的numberOfWheelsof的新实例 0:

let vehicle = Vehicle()print("Vehicle: \(vehicle.description)")// Vehicle: 0 wheel(s)
复制代码

下一个示例定义了一个 Vehicle 名为的子类 Bicycle:


class Bicycle: Vehicle {    override init() {        super.init()        numberOfWheels = 2    }}
复制代码

该 Bicycle 子类定义指定初始化一个自定义的,init()。此指定的初始值设定项与的超类中的指定的初始值设定项匹配 Bicycle,因此 Bicycle 此初始值设定项的版本标记有override修饰符。


init()初始值设定项 Bicycle 始于super.init(),该会调用 Bicycle 该类的超类的默认初始值设定项 Vehicle。这样可以确保 numberOfWheels 继承的属性 Vehicle 在 Bicycle 被机会修改之前被初始化。调用后super.init(),的原始值将 numberOfWheels 替换为的新值 2。


如果您创建的实例 Bicycle,则可以调用其继承的 description 计算属性,以查看其 numberOfWheels 属性如何更新:

let bicycle = Bicycle()print("Bicycle: \(bicycle.description)")// Bicycle: 2 wheel(s)
复制代码

如果子类初始化程序在初始化过程的第 2 阶段未执行任何自定义操作,并且超类具有零参数指定的初始化程序,则可以super.init()在将值分配给所有子类的所有存储属性后省略对的调用。


本示例定义的另一个子类 Vehicle,称为 Hoverboard。在其初始值设定项中,Hoverboard 该类仅设置其 color 属性。super.init()该初始化程序没有显式调用,而是依靠对其父类的初始化程序的隐式调用来完成该过程。

class Hoverboard: Vehicle {    var color: String    init(color: String) {        self.color = color        // super.init() implicitly called here    }    override var description: String {        return "\(super.description) in a beautiful \(color)"    }}
复制代码

的实例 Hoverboard 使用 Vehicle 初始化程序提供的默认轮子数量。


let hoverboard = Hoverboard(color: "silver")print("Hoverboard: \(hoverboard.description)")// Hoverboard: 0 wheel(s) in a beautiful silver
复制代码

注意


子类可以在初始化期间修改继承的变量属性,但不能修改继承的常量属性。


5.6 自动初始化程序继承

如上所述,默认情况下,子类不继承其超类初始化程序。但是,如果满足某些条件,则会自动继承超类初始化器。实际上,这意味着您不需要在许多常见情况下编写初始化程序覆盖,并且可以在安全的情况下以最小的努力继承超类初始化程序。


假设您为子类中引入的任何新属性提供默认值,则适用以下两个规则:


规则 1

如果您的子类没有定义任何指定的初始值设定项,它将自动继承其所有超类指定的初始值设定项。

规则二

如果您的子类提供了其所有超类指定初始化器的实现(通过按规则 1 继承它们,或通过提供自定义实现作为其定义的一部分),那么它将自动继承所有超类便利性初始化器。

即使您的子类添加了进一步的便捷初始化程序,这些规则也适用。


注意


子类可以将指定的超类初始化器实现为子类便捷性初始化器,作为满足规则 2 的一部分。


5.7 指定的便捷初始化器

下面的示例显示了指定的初始化程序,便捷初始化程序和自动初始化程序继承的运行方式。这个例子定义了三类所谓的层次 Food,RecipeIngredient 和 ShoppingListItem,并演示了如何自己初始化互动。


层次结构中的基类称为 Food,这是一个封装食品名称的简单类。该 Food 课程介绍一个 String 叫做物业 name 并提供两个初始化创建 Food 实例:

class Food {    var name: String    init(name: String) {        self.name = name    }    convenience init() {        self.init(name: "[Unnamed]")    }}
复制代码

下图显示了 Food 该类的初始化程序链:


类没有默认的成员初始化器,因此 Food 该类提供了一个指定的初始化器,该初始化器带有一个称为的参数 name。该初始化程序可用于创建 Food 具有特定名称的新实例:


let namedMeat = Food(name: "Bacon")// namedMeat's name is "Bacon"
复制代码

该类的初始值设定项作为指定的初始值设定项提供,因为它可确保新实例的所有存储属性都已完全初始化。本类没有超类,所以初始化不需要调用来完成初始化。

init(name: String)FoodFoodFoodinit(name: String)super.init()
复制代码

该 Food 级还提供了一个方便的初始化,init()不带参数。该 init()初始化通过委派跨越到一个新的食品提供了默认的占位符名称 Food 类的具有的价值:init(name: String)name[Unnamed]

let mysteryMeat = Food()// mysteryMeat's name is "[Unnamed]"
复制代码

层次结构中的第二类的子类 Food 叫 RecipeIngredient。本 RecipeIngredient 类机型中的烹饪配方的成分。它引入了一个 Int 称为的属性 quantity(除了 name 继承自的属性之外 Food),并定义了两个用于创建 RecipeIngredient 实例的初始化程序:


class RecipeIngredient: Food {    var quantity: Int    init(name: String, quantity: Int) {        self.quantity = quantity        super.init(name: name)    }    override convenience init(name: String) {        self.init(name: name, quantity: 1)    }}
复制代码

下图显示了 RecipeIngredient 该类的初始化程序链:

该 RecipeIngredient 班有一个单一的指定初始化,,它可以用来填充的所有新的属性的实例。此初始化程序首先将传递的参数分配给属性,这是引入的唯一新属性。这样做之后,初始化器将委托给该类的初始化器。该过程满足上述两阶段初始化的安全检查1init(name: String, quantity: Int)


RecipeIngredient 还定义了一个便利的初始化程序,该初始化程序仅用于按名称创建实例。对于所有创建的实例,该便利初始化程序均假定其数量为,而没有显式数量。此便捷初始化程序的定义使实例创建起来更快捷,更便捷,并且在创建多个单量实例时避免了代码重复。这种便利初始化跨越简单地委托类的指定初始化,传递一个值。init(name: String)RecipeIngredient


提供的便捷初始化程序采用与中指定的初始化程序相同的参数。因为此便捷初始化程序会覆盖其父类中的指定初始化程序,所以必须使用修饰符对其进行标记(如Initializer Inheritance和Overriding中所述)。init(name: String) RecipeIngredient init(name: String) Foodoverride


尽管 RecipeIngredient 将初始化程序提供为方便的初始化程序,但仍然提供了其超类的所有指定初始化程序的实现。因此,也会自动继承其超类的所有便利初始化程序。init(name: String)RecipeIngredient


在该示例中,超类为 RecipeIngredientIS Food,其具有单一的方便称为初始化 init()。因此,此初始值设定项由继承 RecipeIngredient。init()函数的继承版本与版本完全相同 Food,只不过它是委派给 RecipeIngredient 版本而不是版本。init(name: String)Food


这三个初始化程序均可用于创建新 RecipeIngredient 实例:


let oneMysteryItem = RecipeIngredient()let oneBacon = RecipeIngredient(name: "Bacon")let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
复制代码

层次结构中的第三和最后一类是一个子类 RecipeIngredient 叫 ShoppingListItem。该 ShoppingListItem 级车型,因为它出现在购物清单配方成分。


购物清单中的每个项目都以“未购买”开始。为了表示这一事实,我们 ShoppingListItem 引入了一个布尔属性 purchased,其默认值为 false。ShoppingListItem 还添加了计算 description 属性,该属性提供了 ShoppingListItem 实例的文本描述:

class ShoppingListItem: RecipeIngredient {    var purchased = false    var description: String {        var output = "\(quantity) x \(name)"        output += purchased ? " ✔" : " ✘"        return output    }}
复制代码

注意


ShoppingListItem 并未定义初始值设定 purchased 项来为提供初始值,因为购物清单中的商品(如此处建模)始终始于未购买的商品。


因为它为它引入的所有属性提供了默认值,并且本身未定义任何初始化程序,所以它会 ShoppingListItem 自动从其超类继承所有指定的初始化和便捷初始化程序。


下图显示了所有三个类的整体初始化程序链:

您可以使用所有继承的三个初始化器来创建一个新 ShoppingListItem 实例:


var breakfastList = [    ShoppingListItem(),    ShoppingListItem(name: "Bacon"),    ShoppingListItem(name: "Eggs", quantity: 6),]breakfastList[0].name = "Orange juice"breakfastList[0].purchased = truefor item in breakfastList {    print(item.description)}// 1 x Orange juice ✔// 1 x Bacon ✘// 6 x Eggs ✘
复制代码

在这里,breakfastList 从包含三个新 ShoppingListItem 实例的数组文字中创建了一个名为的新数组。数组的类型推断为[ShoppingListItem]。创建阵列后,阵列 ShoppingListItem 开头的的名称从更改为"[Unnamed]",并标记为已购买。打印数组中每个项目的描述将显示它们的默认状态已按预期设置。"Orange juice"


6 初始化失败

有时定义初始化可能失败的类,结构或枚举有时很有用。无效的初始化参数值,缺少必需的外部资源或其他阻止初始化成功的条件可能触发此失败。


为了应对可能失败的初始化条件,请将一个或多个可失败的初始化程序定义为类,结构或枚举定义的一部分。您通过在 init 关键字(init?)后面放置问号来编写失败的初始化程序。


注意


您不能使用相同的参数类型和名称来定义可失败的初始化程序和不可失败的初始化程序。


失败的初始化程序会创建一个初始化类型的可选值。您在有故障的初始化程序中编写代码,以指示可以触发初始化失败的点。return nil


注意


>严格来说,初始化器不返回值。相反,它们的作用是确保 self 在初始化结束时已完全正确地对其进行了初始化。尽管您编写触发初始化失败的代码,但是不要使用关键字来指示初始化成功。return nil


例如,为数字类型转换实现了失败的初始化器。为确保数字类型之间的转换准确地保留了该值,请使用init(exactly:)初始化程序。如果类型转换不能保持该值,则初始化程序将失败。


let wholeNumber: Double = 12345.0let pi = 3.14159
if let valueMaintained = Int(exactly: wholeNumber) { print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")}// Prints "12345.0 conversion to Int maintains value of 12345"
let valueChanged = Int(exactly: pi)// valueChanged is of type Int?, not Int
if valueChanged == nil { print("\(pi) conversion to Int does not maintain value")}// Prints "3.14159 conversion to Int does not maintain value"
复制代码

以下示例定义了一个名为的结构 Animal,其常量 String 属性为 species。该 Animal 结构还使用一个名为的单个参数定义了一个失败的初始化程序 species。此初始化程序检查 species 传递给初始化程序的值是否为空字符串。如果找到空字符串,则会触发初始化失败。否则,将 species 设置属性的值,并且初始化成功:

struct Animal {    let species: String    init?(species: String) {        if species.isEmpty { return nil }        self.species = species    }}
复制代码

您可以使用此失败的初始化程序尝试初始化新 Animal 实例,并检查初始化是否成功:


let someCreature = Animal(species: "Giraffe")// someCreature is of type Animal?, not Animal
if let giraffe = someCreature { print("An animal was initialized with a species of \(giraffe.species)")}// Prints "An animal was initialized with a species of Giraffe"
复制代码

如果将空字符串值传递给失败的初始值设定项的 species 参数,则初始值设定项将触发初始化失败:


let anonymousCreature = Animal(species: "")// anonymousCreature is of type Animal?, not Animal
if anonymousCreature == nil { print("The anonymous creature could not be initialized")}// Prints "The anonymous creature could not be initialized"
复制代码

注意


>检查空字符串值(例如""而不是"Giraffe")与进行检查 nil 以表明没有可选 String 值的情况不同。在上面的示例中,空字符串("")是有效的,非可选的 String。但是,让动物使用空字符串作为其 species 属性值是不合适的。为了对此限制建模,如果发现空字符串,则可失败的初始化程序将触发初始化失败。


6.1 枚举失败的初始化程序

您可以使用一个失败的初始化程序来基于一个或多个参数选择适当的枚举大小写。如果提供的参数与适当的枚举情况不匹配,则初始化器可能会失败。


下面的例子定义称为枚举 TemperatureUnit,具有三种可能的状态(kelvin,celsius,和fahrenheit)。一个有故障的初始化器用于为 Character 代表温度符号的值找到合适的枚举形式:


enum TemperatureUnit {    case kelvin, celsius, fahrenheit    init?(symbol: Character) {        switch symbol {        case "K":            self = .kelvin        case "C":            self = .celsius        case "F":            self = .fahrenheit        default:            return nil        }    }}
复制代码

您可以使用此故障初始化程序为三种可能的状态选择合适的枚举用例,并在参数与以下状态之一不匹配时导致初始化失败:


let fahrenheitUnit = TemperatureUnit(symbol: "F")if fahrenheitUnit != nil {    print("This is a defined temperature unit, so initialization succeeded.")}// Prints "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(symbol: "X")if unknownUnit == nil { print("This is not a defined temperature unit, so initialization failed.")}// Prints "This is not a defined temperature unit, so initialization failed."
复制代码

6.2 带有原始值的枚举失败的初始化程序

带有原始值的枚举会自动接收一个失败的初始化器,init?(rawValue:)该初始化器采用称为 rawValue 合适原始值类型的参数,并在找到匹配的枚举情况下选择匹配的枚举情况,如果不存在匹配值,则触发初始化失败。


您可以 TemperatureUnit 从上面重写示例,以使用 type 的原始值 Character 并利用init?(rawValue:)初始化程序:


enum TemperatureUnit: Character {    case kelvin = "K", celsius = "C", fahrenheit = "F"}
let fahrenheitUnit = TemperatureUnit(rawValue: "F")if fahrenheitUnit != nil { print("This is a defined temperature unit, so initialization succeeded.")}// Prints "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(rawValue: "X")if unknownUnit == nil { print("This is not a defined temperature unit, so initialization failed.")}// Prints "This is not a defined temperature unit, so initialization failed."
复制代码

6.3 初始化失败的传播

一个类,结构或枚举的故障初始化器可以委托同一类,结构或枚举的另一个故障初始化器。类似地,子类可故障初始化器可以委托最多超类可故障初始化器。


在任何一种情况下,如果委派给另一个导致初始化失败的初始化程序,则整个初始化过程将立即失败,并且不会再执行任何初始化代码。


注意


一个失败的初始化器也可以委派给一个不失败的初始化器。如果您需要将潜在的失败状态添加到不会失败的现有初始化过程中,请使用此方法。


以下示例定义了一个 Product 名为的子类 CartItem。该 CartItem 级车型在在线购物车的商品。CartItem 引入一个称为的存储常量属性,quantity 并确保该属性始终具有至少一个值 1:


class Product {    let name: String    init?(name: String) {        if name.isEmpty { return nil }        self.name = name    }}
class CartItem: Product { let quantity: Int init?(name: String, quantity: Int) { if quantity < 1 { return nil } self.quantity = quantity super.init(name: name) }}
复制代码

失败的初始化程序 CartItem 通过验证其是否已接收到或更大的 quantity 值来启动 1。如果 quantity 无效,则整个初始化过程将立即失败,并且不再执行任何初始化代码。同样,失败的初始化程序用于 Product 检查该 name 值,如果 name 为空字符串,则初始化程序进程立即失败。


如果 CartItem 使用非空名称创建实例并且数量等于 1 或大于 0,则初始化成功:


if let twoSocks = CartItem(name: "sock", quantity: 2) {    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")}// Prints "Item: sock, quantity: 2"
复制代码

如果尝试创建一个值为的 CartItem 实例,则初始化程序将导致初始化失败:quantity0CartItem


if let zeroShirts = CartItem(name: "shirt", quantity: 0) {    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")} else {    print("Unable to initialize zero shirts")}// Prints "Unable to initialize zero shirts"
复制代码

同样,如果您尝试 CartItem 使用空 name 值创建实例,则超类 Product 初始化程序会导致初始化失败:


if let oneUnnamed = CartItem(name: "", quantity: 1) {    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")} else {    print("Unable to initialize one unnamed product")}// Prints "Unable to initialize one unnamed product"
复制代码

6.4 覆盖失败的初始化程序

您可以在子类中覆盖超类可失败的初始化程序,就像其他任何初始化程序一样。或者,您可以使用子类不可失败的初始化程序来覆盖超类可失败的初始化程序。这使您可以定义一个子类,即使超类的初始化被允许失败,初始化也不会失败。


请注意,如果使用不可失败的子类初始化器覆盖了可失败的超类初始化器,则委托给超类初始化器的唯一方法是强制展开可失败的超类初始化器的结果。


注意


您可以使用不可失败的初始值设定项来覆盖可失败的初始设定项,但反之则不能。


下面的示例定义了一个名为的类 Document。此类可为文档建模,该文档可以使用 name 非空字符串值或 nil,但不能为空字符串的属性进行初始化:


class Document {    var name: String?    // this initializer creates a document with a nil name value    init() {}    // this initializer creates a document with a nonempty name value    init?(name: String) {        if name.isEmpty { return nil }        self.name = name    }}
复制代码

下一个示例定义了一个 Document 名为的子类 AutomaticallyNamedDocument。该 AutomaticallyNamedDocument 子类覆盖都是由引入的指定初始化的 Document。这些覆盖可确保 AutomaticallyNamedDocument 实例的初始 name 值是,"[Untitled]"如果实例被初始化而没有名称,或者是否将空字符串传递给 init(name:)初始化器:


class AutomaticallyNamedDocument: Document {    override init() {        super.init()        self.name = "[Untitled]"    }    override init(name: String) {        super.init()        if name.isEmpty {            self.name = "[Untitled]"        } else {            self.name = name        }    }}
复制代码

将 AutomaticallyNamedDocument 覆盖其超类的 failable init?(name:)与 nonfailable 初始化init(name:)初始化。因为 AutomaticallyNamedDocument 以不同于其超类的方式处理空字符串情况,所以其初始化程序不需要失败,因此它提供了初始化程序的不可失败版本。


您可以在初始化器中使用强制展开来从超类中调用失败的初始化器,作为子类的非失败初始化器实现的一部分。例如,UntitledDocument 下面的子类始终被命名为"[Untitled]",并且init(name:)在初始化期间使用其父类中的故障初始化器。


class UntitledDocument: Document {    override init() {        super.init(name: "[Untitled]")!    }}
复制代码

在这种情况下,如果 init(name:)曾经用空字符串作为名称调用超类的初始化程序,则强制展开操作将导致运行时错误。但是,由于使用字符串常量调用了它,因此可以看到初始化程序不会失败,因此在这种情况下不会发生运行时错误。


6.5 初始化!初始化失败

通常,您可以定义一个失败的初始化程序,该初始化程序通过在 init 关键字(init?)后面放置问号来创建适当类型的可选实例。另外,您可以定义一个失败的初始化程序,该初始化程序创建适当类型的隐式展开的可选实例。为此,可以在 init 关键字(init!)后面而不是问号旁放置一个感叹号。


您可以从委托 init?到 init!,反之亦然,你可以覆盖 init?与 init!反之亦然。您也可以从委托 init 到 init!,但如果这样做会引发一个断言 init!初始化原因初始化失败。


7 required 必需的初始化器

required 在类初始化器的定义之前编写修饰符,以指示该类的每个子类都必须实现该初始化器:

class SomeClass {    required init() {        // initializer implementation goes here    }}
复制代码

您还必须 required 在所需的初始化程序的每个子类实现之前编写修饰符,以指示初始化程序要求适用于链中的其他子类。override 覆盖必需的指定初始值设定项时,您无需编写修饰符:

class SomeSubclass: SomeClass {    required init() {        // subclass implementation of the required initializer goes here    }}
复制代码

注意


如果可以通过继承的初始化程序满足要求,则不必提供所需的初始化程序的显式实现。


8 使用闭包或函数设置默认属性值

如果存储的属性的默认值需要一些自定义或设置,则可以使用闭包或全局函数为该属性提供自定义的默认值。每当初始化属性所属类型的新实例时,都会调用闭包或函数,并将其返回值分配为属性的默认值。


这些类型的闭包或函数通常会创建与属性相同类型的临时值,定制该值以表示所需的初始状态,然后返回该临时值以用作属性的默认值。


这是有关如何使用闭包提供默认属性值的框架概述:

class SomeClass {    let someProperty: SomeType = {        // create a default value for someProperty inside this closure        // someValue must be of the same type as SomeType        return someValue    }()}
复制代码

请注意,闭包的大括号后面是一对空括号。这告诉 Swift 立刻执行关闭。如果省略这些括号,则尝试将闭包本身分配给属性,而不是闭包的返回值。


注意


如果使用闭包来初始化属性,请记住,在执行闭包时实例的其余部分尚未初始化。这意味着您无法从闭包内部访问任何其他属性值,即使这些属性具有默认值也是如此。您也不能使用隐式 self 属性,或调用任何实例的方法。


下面的示例定义了一个名为的结构 Chessboard,该结构为国际象棋的棋盘建模。国际象棋在 8 x 8 的棋盘上进行游戏,黑白方块交替出现。

为了表示此游戏板,该 Chessboard 结构具有一个称为的属性 boardColors,该属性是 64 个 Bool 值的数组。true 数组中的值表示黑色正方形,而的值 false 表示白色正方形。阵列中的第一项代表板上的左上角正方形,而阵列中的最后一项代表板上的右下角正方形。


boardColors 使用闭包初始化该数组以设置其颜色值:

struct Chessboard {    let boardColors: [Bool] = {        var temporaryBoard = [Bool]()        var isBlack = false        for i in 1...8 {            for j in 1...8 {                temporaryBoard.append(isBlack)                isBlack = !isBlack            }            isBlack = !isBlack        }        return temporaryBoard    }()    func squareIsBlackAt(row: Int, column: Int) -> Bool {        return boardColors[(row * 8) + column]    }}
复制代码

每当 Chessboard 创建新实例时,都会执行闭包,并 boardColors 计算并返回默认值。上面的示例中的闭包在名为的临时数组中为板上的每个正方形计算并设置适当的颜色 temporaryBoard,并在完成设置后将该临时数组作为闭包的返回值返回。返回的数组值存储在其中,boardColors 并可以使用squareIsBlackAt(row:column:)实用程序函数进行查询:

let board = Chessboard()print(board.squareIsBlackAt(row: 0, column: 1))// Prints "true"print(board.squareIsBlackAt(row: 7, column: 7))// Prints "false"
复制代码


参考

https://docs.swift.org/swift-book/LanguageGuide/Initialization.html


3. Tips: 学习至少一个技术技巧

笔者的文章:

时间管理的技巧 - 计划表格和截止日期

时间管理一直都是丢三落四的,而且形不成体系。最近学习了得到专栏,汤君健老师的《怎样成为时间管理的高手》。发现这个表格做计划确实很清晰和进度更进。截止日期确实利用了心理学,人承诺以后,会一直记在心里,有紧迫感。

笔者优化为月度表格,在腾讯文档中记录。


4. Share: 分享一篇有观点和思考的技术文章

笔者的文章:

翻译:swift 5通过使用泛型进行高级异步操作Operation

说明

异步操作使您可以编写长时间运行的任务,而又可以在多个任务之间添加依赖关系。可以跟踪进度,并且通过使用可以使调度变得容易 OperationQueue。通过添加泛型和 Swift 结果类型,我们可以从异步操作中获得更多收益。


开始操作并使用异步操作编写并发解决方案之后,现在该看看如何为我们的代码库提供更高级的解决方案。这完全基于我们在[WeTransferCollect](https://collect.bywetransfer.com/)应用程序中使用的实现,其中我们使用了 50 多种不同的操作。


创建结果驱动的异步操作

通常情况下,某个运算会产生特定值。至少有一种捕获已发生的错误的方法很有价值。Result<Success, Failure>在 Swift 中,值和错误都归为一个类型。


将泛型添加到异步操作

通过利用AsyncOperation博客文章中创建的异步操作编写并发解决方案,我们为自己提供了一个起点。


class AsyncOperation: Operation {    private let lockQueue = DispatchQueue(label: "com.swiftlee.asyncoperation", attributes: .concurrent)
override var isAsynchronous: Bool { return true }
private var _isExecuting: Bool = false override private(set) var isExecuting: Bool { get { return lockQueue.sync { () -> Bool in return _isExecuting } } set { willChangeValue(forKey: "isExecuting") lockQueue.sync(flags: [.barrier]) { _isExecuting = newValue } didChangeValue(forKey: "isExecuting") } }
private var _isFinished: Bool = false override private(set) var isFinished: Bool { get { return lockQueue.sync { () -> Bool in return _isFinished } } set { willChangeValue(forKey: "isFinished") lockQueue.sync(flags: [.barrier]) { _isFinished = newValue } didChangeValue(forKey: "isFinished") } }
override func start() { print("Starting") guard !isCancelled else { finish() return
}
isFinished = false isExecuting = true main() }
override func main() { /// Use a dispatch after to mimic the scenario of a long-running task. DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(1), execute: { print("Executing") self.finish() }) }
func finish() { isExecuting = false isFinished = true }}
复制代码


我们在其顶部添加与结果类型相同的泛型,从而得出以下基础:


class AsyncResultOperation<Success, Failure>: AsyncOperation where Failure: Error {
private(set) var result: Result<Success, Failure>?
}
复制代码

这为异步任务提供了一个链接的结果类型,其值和错误均具有泛型。


确保完成结果

为了确保结果更新,我们在 finish 方法中添加了一个检查,以帮助实现者并在开发过程中向他们提供反馈:


final override func finish() {    guard !isCancelled else { return super.finish() }    fatalError("Make use of finish(with:) instead to ensure a result")}
func finish(with result: Result<Success, Failure>) { self.result = result super.finish()}
复制代码

首先,我们确保默认finish()方法不再有用。当其实现者之一使用它时,它将引发致命异常:


致命错误:改用finish(with :)来确保结果:文件UnfurlURLOperation.swift,第 44 行


其次,我们添加了一个新finish(with:)方法,该方法强制实现者设置结果。

确保取消结果

最后一部分确保我们有一个关于取消的结果集。由于我们喜欢某种错误,因此我们不得不在类中创建另一个替代:

override func cancel() {    fatalError("Make use of cancel(with:) instead to ensure a result")}
func cancel(with error: Failure) { self.result = .failure(error) super.cancel()}
复制代码

我们必须在错误枚举中定义一个案例以进行取消。这样做给我们带来了强类型错误的好处,同时在取消时仍然可以得到结果。

全部放到一个示例操作中

最终结果AsyncResultOperation如下所示:


open class AsyncResultOperation<Success, Failure>: AsyncOperation where Failure: Error {
private(set) public var result: Result<Success, Failure>?
final override public func finish() { guard !isCancelled else { return super.finish() } fatalError("Make use of finish(with:) instead to ensure a result") }
public func finish(with result: Result<Success, Failure>) { self.result = result super.finish() }
override open func cancel() { fatalError("Make use of cancel(with:) instead to ensure a result") }
public func cancel(with error: Failure) { self.result = .failure(error) super.cancel() }}
复制代码

我们可以通过创建自定义操作来使用此类,例如取消短 URL 的任务。


final class UnfurlURLOperation: AsyncResultOperation<URL, UnfurlURLOperation.Error> {    enum Error: Swift.Error {        case canceled        case missingRedirectURL        case underlying(error: Swift.Error)    }
private let shortURL: URL private var dataTask: URLSessionTask?
init(shortURL: URL) { self.shortURL = shortURL }
override func main() { var request = URLRequest(url: shortURL) request.httpMethod = "HEAD"
dataTask = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (_, response, error) in if let error = error { self?.finish(with: .failure(Error.underlying(error: error))) return }
guard let longURL = response?.url else { self?.finish(with: .failure(Error.missingRedirectURL)) return }
self?.finish(with: .success(longURL)) }) dataTask?.resume() }
override func cancel() { dataTask?.cancel() cancel(with: .canceled) }}
复制代码

此类采用短 URL,执行 HEAD 获取长 URL 的请求,然后将其返回。如果发生任何错误,则是由于操作导致的。最后,我们确保在取消时取消数据任务,并cancel(with:)使用canceled错误情况调用该方法。


执行此方法最终将导致www.avanderlee.com展开的 URL:

let queue = OperationQueue()let unfurlOperation = UnfurlURLOperation(shortURL: URL(string: "https://bit.ly/33UDb5L")!)queue.addOperations([unfurlOperation], waitUntilFinished: true)print("Operation finished with: \(unfurlOperation.result!)")
// Prints: Operation finished with: success(https://www.avanderlee.com/)
复制代码


结论

而已!我们通过在 Swift 中使用泛型来创建高级操作。通过将它们链接在一起,我们允许自己在任务之间建立关系,同时保持代码分离。


这篇文章是系列文章的一部分:


  1. Swift中的Operations和OperationQueues入门

  2. 在Swift中编写并发解决方案的异步操作

  3. 通过使用泛型进行高级异步操作


也可以以 Swift Playground 的形式找到:https://github.com/AvdLee/AsyncOperations


如果您想进一步提高 Swift 知识,请查看 Swift类别页面


参考

https://www.avanderlee.com/swift/advanced-asynchronous-operations/


发布于: 2021 年 01 月 04 日阅读数: 85
用户头像

John(易筋)

关注

问渠那得清如许?为有源头活水来 2018.07.17 加入

工作10+年,架构师,曾经阿里巴巴资深无线开发,汇丰银行架构师/专家。擅长架构、算法、数据结构、设计模式、iOS、Java Spring Boot。易筋为阿里巴巴花名。

评论

发布
暂无评论
判断回文数字算法,swift 5初始化详解,时间管理计划落地,swift5 多线程高级用法 John 易筋 ARTS 打卡 Week 33