Swift5.2 拾遗笔记(一)

本文为私人学习笔记,仅仅做为记录使用,详情内容请查阅 中文官方文档


属性

  • 属性包装器

属性包装器在管理如何存储和定义属性的代码之前添加了一个分隔层。举个例子,如果你想要对属性进行线程安全地存取,那你势必要在所有存取的地方编写相同的代码进行线程安全管理,是不是非常的麻烦?属性包装器则帮你实现一次编写,终身复用的效果。

定义一个属性包装器,你需要创建一个具有 warppedValue 属性的结构体、枚举或者类。这个 warppedValue 属性就是包装器需要的包装属性。

定义一个 TwelveOrLess 结构体,确保包装器所修饰的属性存储的值都是小于或等于 12 的数字。

@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

你现在可以在属性前面写上包装器,将包装器作为特性的方式来使用。

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// 打印 "0"

rectangle.height = 10
print(rectangle.height)
// 打印 "10"

rectangle.height = 24
print(rectangle.height)
// 打印 "12"

上述过程中,当你操作 rectangle 的属性进行读写时,都会经过包装器 wrappedValue 属性的读写。编译器将会合成包装器所提供的存储空间和访问属性的代码。过程就像下面这样。

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}
  • 类型属性

类型属性存储的数据为共享数据。存储型类型属性可以是常量也可以是变量,但是计算型类型属性和实例的计算型属性一样,只能定义成变量属性。

需要注意的是,由于类型本身并没有构造器,也就无法在初始化过程中为类型属性赋值,因此必须给存储型类型属性指定默认值。存储型类型属性是延迟初始化的,它们只有在第一次被访问时才被初始化。即使它们被多个线程同时访问,系统也保证智慧对其进行一次初始化,并且不需要对其使用 lazy 修饰符。这种特性也保证了对内存的优化。

方法

结构体和枚举能够定义方法是 Swift 和 OC 的主要区别之一。

  • mutating 可变方法

和类不同,值类型的结构体和枚举默认情况下,其属性不能够修改,但是你确实需要修改时,可以为方法添加 可变 mutating 行为。

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}
  • 类型方法

和 OC 一样,类型方法属于类本身。有两种方式定义类型方法,func 前添加 staticclass后者允许子类重写。类型方法位于方法体 {} 中的最前面,作用域属于该类的范围。

  • self 属性

在实例方法中,self 属性指向实例本身,你可以使用 self 来引用当前实例,当然 Swift 并不推荐使用它,self 的使用场景主要是在实例方法的某个参数名和实例的属性名重复时,用 self 区分参数还是属性,否则,参数优先,两个都被认为是参数名。

struct Point {
    var x = 0.0, y = 0.0
    func isToTheRightOf(x: Double) -> Bool {
        return self.x > x
    }
}

在类型方法中,self 属性指向类本身,而不是该类型的某个实例。一般来说,在类型方法的方法体中,可以直接通过类型方法的名称调用本类中的其他类型方法,而不需要在方法名称前面加上类型名,类似的,类型属性也同样可以通过名称直接访问其他类型属性,而不需要前面加上本类的类名。

下标

快捷访问集合、列表或序列中的元素而不需要使用存取方法,可以定义在类、结构体和枚举中。下标不限于一维。

  • subscript
subscript(index: Int) -> Int {
    get {
      // 返回一个适当的 Int 类型的值
    }
    set(newValue) {
      // 执行适当的赋值操作
    }
}

示例

struct Collection {
    let data : [Any]
    subscript(index: Int) -> Any? {
        get {
            if index < data.count {
                return data[index]
            } else {
                return nil
            }
        }
    }
}
let cc = Collection(data : ["0","1","2","3"])
print(cc[1] ?? "无")
  • 类型下标

类似类型方法,通过 staticclass 来定义类型下标,后者可以重写。

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
    static subscript(n: Int) -> Planet {
        return Planet(rawValue: n)!
    }
}
let mars = Planet[4]
print(mars)

继承

继承和引用特性是区分类和结构体、枚举的基本特征。Swift 中的类不需要像 OC 中从一个通用的 NSObject 基类继承而来。不继承其他类的类,称之为基类。

  • 重写属性的 Getters 和 Setters

重写的属性的限制不变小,即:可以将一个继承来的只读属性重写为一个读写属性,也可以将读写属性重写为一个只读属性。另外,如果你在重写属性中提供来了setter,那么你一定要提供对应的 getter,反之没有限制。

  • 属性观察器

可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,无论被继承属性原本是如何实现的,当其属性值发生改变时,你就会被通知到。

无论是存储类型还是计算类型的属性都可以添加属性观察器,但是依旧有限制,那些常量存储类型或者只读计算类型属性无法添加,因为这些值是不可以被改变的,为它们设置 willSetdidSet 是不太恰当的操作,另外,属性观察器和 setter 是不能同时存在的,因为 setter 本身就已经可以观察到属性的变化了。

  • final

final 关键字来限制被重写、继承,这意味着被修饰的属性、方法或者类都是最终类型。例如:final varfinal funcfinal class func 以及 final subscript

构造过程

设置实例存储属性的初始值和执行其他必须的设置或构造过程。

你可以在构造器中为存储类属性设置初始值,或者在定义属性时分配默认值。(这两种方式是直接设置的,不会触发任何属性观察者)

// 构造器中设置初始值
struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
// 默认属性值
struct Fahrenheit {
    var temperature = 32.0
}
  • 构造过程中常量属性赋值

你可以在构造过程中的任意时间点给常量属性赋值,只要在构造过程结束时特设置成确定的值。一旦常量属性被赋值,它将永远不可更改。

class SurveyQuestion {
    let text: String
    init(text: String) {
        self.text = text // 常量属性只能在定义它的类的构成过程中修改,不能在其子类中修改
    }
}
  • 默认构造器

如果结构体或类为所有属性提供类默认值,又没有提供任何自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。

  • 结构体的逐一成员构造器

结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能获得逐一成员构造器。

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
  • 值类型的构造器代理

构造器可以通过调用其他构造器来完成实例的部分构造过程,这一过程称为构造器代理,它能避免过个构造器间的代码重复。

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
}

如果你为某个值类型定了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法反问逐一成员构造器)。这种限制避免了在一个更复杂的构造器中做了额外的重要设置,但是使用的人不小心使用自动生成的构造器而导致错误的情况。

如果你仍然希望默认构造器、逐一成员构造器以及自定义构造器都能用来创建实例,那可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。

类的继承和构造过程

类中的所有存储属性(包括所有继承自父类的属性)都必须在构造过程中设置初始值。

Swift 为类提供类两种构造器来确保实例中所有存储属性都能获得初始值:指定构造器和便利构造器。

每一个类都必须至少拥有一个指定的构造器。便利构造器是类中比较次要的、辅助性的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

// 指定构造器
init(parameters) {
    statements
}
// 便利构造器
convenience init(parameters) {
    statements
}
  • 类构造器代理

Swfit 构造器之间的代理调用遵循三条规则:

  1. 指定构造器必须调用其直接父类的指定构造器
  2. 便利构造器必须调用同类中定义的其他构造器
  3. 便利构造器最后必须调用指定构造器

即:指定构造器必须总是向上(父类)代理,便利构造器必须总是横向(同类)代理。

构造器代理图
  • 构造器的继承和重写

指定构造器

当你编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override 修饰符,该修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否被按照预想中被指定。

便利构造器

如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(构造器代理规则中指出便利构造器需要横向调用其他构造器),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写,因此你“重写”一个父类便利构造器时,不需要添加 override 修饰符。

子类可以在构造过程中修改继承来的变量属性,但是不能修改继承来的常量属性。

构造器的自动继承

子类在默认情况下不会继承父类的构造器。但是如果满足一定的条件,父类构造器是可以被自动继承的,这意味着在许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

假如你为子类中引入的所有新属性都提供了默认值,下面两个规则将适用:

  1. 如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器
  2. 如果子类提供了所有父类指定构造器的实现,它将自动继承父类所有的便利构造器

即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。

实例:

class Food {
    var name: String
    init(name: String) { // 指定构造器
        self.name = name
    }
    convenience init() { // 便利构造器,需要横向代理其他构造器
        self.init(name: "[Unnamed]")
    }
}

下图展示了 Food 的构造器链:

Food 的构造器
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 类的构造器

RecipeIngredient 类拥有一个新的指定构造器 init(name: String, quantity: Int) 用来设置新引入的属性,在该构造器中,向上代理到父类的指定构造器。

RecipeIngredient 也定义类一个便利构造器,init(name: String),它横向代理到本类中的指定构造器,用来方便地创建 quantity 为 1 的实例。另外,该便利构造器使用了跟 Food 中指定构造器 init(name: String) 相同的形参,因此这个便利构造器重写了父类的指定构造器,前面需要使用修饰符 override

尽管 RecipeIngredient 将父类的指定构造器重写为了便利构造器,但是它依然提供了父类所有的指定构造器的实现,因此,RecipeIngredient 会自动继承父类的所有便利构造器。在 Food 中,它的一个便利构造器是 init(),这个会给 RecipeIngredient 继承。它会代理到 RecipeIngredient 重写的 init(name: String) 中,接着代理到其他构造器中。

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

ShoppingListItem 为自己引入的所有属性提供了默认值,并且没有定义自己的任何构造器,因此它将自动继承所有父类中指定构造器和便利构造器。你可以使用其父类中的所有构造器来创建实例。

下图展示三个类的构造器链:

三个类的构造器
  • 可失败的构造器

所谓的失败指的是给构造器传入类无效的形参,或者缺少某种所必须的外部资源而导致构造失败的情况。

语法为在 init 关键字后面添加问号(init?)。

可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过 return nil 语句来表明可失败构造器在何种情况下应该失败。

严格来讲,Swift 的构造器都不支持返回值,因为构造器本身的作用,只是为了确保对象能被正确的构造。因此你只需要用 return nil 表明可失败构造器构造失败,而不要使用关键字 return 来表明构造成功。

带原始值的枚举类型的可失败构造器

此种枚举类型会自带一个可失败构造器 init?(rawValue:),该可失败构造器有一个合适的原始值类型的参数 rawValue 形参,找到匹配的原始值则构造成功,否则构造失败。

enum TemperatureUnit: Character {
    case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}
// 构造成功
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
// 构造失败
let unknownUnit = TemperatureUnit(rawValue: "X")

枚举类型的可失败构造器

你可以通过一个或者多个形参的可失败构造器来获取枚举类型中特定的枚举成员。

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")
// 将无法匹配,构造失败
let unknownUnit = TemperatureUnit(symbol: "X")

构成失败的传递

类、结构体、枚举的可失败构造器可以横向代理到它们自己的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

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)
    }
}

Product 类的构造条件是 name 不为空,CartItem 类首先要保证 quantity 的常量存储类型的属性的值至少为 1,然后向上代理到父类,同样要保证 name 的值不能为空,两个条件不满足任意一个都会导致构造失败。

重写可失败构造器

你可以在子类中可以重写父类的可失败构造器。当你重写父类可失败构造器为非可失败构造器时,需要对父类的可失败构造器进行强制解包。

你可以将可失败的构造器重写为非可失败的构造器,反过来则不可以

class Document {
    var name: String?
    // 该构造器创建了一个 name 属性的值为 nil 的 document 实例
    init() {}
    // 该构造器创建了一个 name 属性的值为非空字符串的 document 实例
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = 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
        }
    }
}

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")! // 提供默认值,并强制解包
    }
}

必要构造器

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器:

class SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符:

class SomeSubclass: SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。

通过闭包或函数设置属性的默认值

如果某个 存储型 属性的默认值需要一些自定义或者设置,你可以使用闭包或者全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,那么对应的闭包或者函数会被调用,而它们返回值会当作默认值赋值给这个属性。

class SomeClass {
    let someProperty: SomeType = {
        // 在这个闭包中给 someProperty 创建一个默认值
        // someValue 必须和 SomeType 类型相同
        return someValue
    }()
}

SomeClass 类实例化时, 属性 someProperty 后的闭包会被调用,注意花括号后面的一对小括号,这使用告诉 Swift 立即执行此闭包,如果忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。

如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其他部分都还没有初始化。这意味着你不能在闭包里访问其他属性,即使这些属性有默认值。同样,你也不能使用隐式的 self 属性,或者调用任何实例方法。

如果你依旧想使用 self 属性,或者调用其他实例方法,你可以使用懒加载的形式,通过关键字 lazy 来创建。之所以可以使用其他属性或者实例方法,是因为该属性已经是在类实例化之后才被调用,实例中的属性都已经被初始化,所以可以使用。

lazy var someProperty: SomeType = {
    // 在这个闭包中给 someProperty 创建一个默认值
    // someValue 必须和 SomeType 类型相同
    // 可以使用 self 等实例属性或方法
    return someValue
}()

析构过程

析构器只适用于类,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字 deinit 来标记。

deinit {
    // 执行析构过程
}

Swift 会自动释放不再需要的实例以释放资源。通常不需要你手动去清理,但是,当你需要进行额外的清理时,你则可能需要使用到析构器。

析构器是被自动调用的,你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器依然会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,739评论 2 9
  • 标签(空格分隔): 未分类 基础(相关概念) 1.元祖 元组(tuples)把多个值组合成一个复合值。元组内的值可...
    一生信仰阅读 597评论 0 0
  • 本章将会介绍 存储属性的初始赋值自定义构造过程默认构造器值类型的构造器代理类的继承和构造过程可失败构造器必要构造器...
    寒桥阅读 769评论 0 0
  • 构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个...
    CDLOG阅读 336评论 0 1
  • 下标脚本 下标脚本 可以定义在类、结构体和枚举这些目标中,可以认为是访问集合(collection),列表(li...
    cht005288阅读 446评论 0 0