Swift5.2 拾遗笔记(四)

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


内存安全

Swift 自动管理内存,大部分时候你完全不需要考虑内存访问的事情。并且在默认情况下,Swift 是安全的,它会阻止你代码不安全的代码,例如,Swift 需要你保证变量在使用前完成初始化,数据索引会做越界检测等。

这里讨论的是单线程下的内存安全。一般情况下,单线程是不会出现内存访问下的安全问题。但是在一些特定条件下,依然会发生内存访问冲突问题。冲突问题会发生在当你有两个符合下列情况:

  • 至少有一个是写访问
  • 它们访问的是同一个存储地址
  • 它们的访问在时间上部分重叠

重叠访问

导致重叠访问的主要问题是在使用 in-out 参数的函数或者方法,或者是结构体中的 mutating 方法。

in-out 参数冲突访问

函数会对它所有的 in-out 参数进行长期写访问。in-out 参数的写访问会在所有非 in-out 参数处理完成之后开始,直到函数执行完毕为止。如果有多个 in-out 参数,则写访问开始的顺序与参数的顺序一致。

不能在访问以 in-out 形式传入后的原始变量。

var stepSize = 1
func increment(_ number: inout Int) {
    number += stepSize // 访问原始变量
}
increment(&stepSize)
// 错误:stepSize 访问冲突

冲突的原因是 stepSize 的读访问与 number 的写访问重叠了。解决的方案就是拷贝一份地址的索引。

// 显式拷贝
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// 更新原来的值
stepSize = copyOfStepSize
// stepSize 现在的值是 2

另外,在多个 in-out 参数时传入同一个变量同样容易发生冲突。

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // 正常
balance(&playerOneScore, &playerOneScore)
// 错误:playerOneScore 访问冲突

更多的例子。

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}
extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}
oscar.shareHealth(with: &oscar)
// 错误:oscar 访问冲突

因为 Player 是结构体,属于值类型,修改值的任何一个部分都是对于整个值的修改,这意味着其中的一个属性的读写访问都需要访问整个值。和结构体类似的值类型还有 元组、枚举类型。

下面的代码展示了一样的错误,对于一个存储在全局变量里的结构体属性的写访问重叠了。

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // 错误

将全局变量改为本地变量,编译器就可以保证重叠访问是安全的。

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // 正常
}

遵循以下原则,可以保证结构体属性的重叠反问安全。

  • 访问的实例是存储属性,而不是计算属性或类型属性
  • 结构体是本地变量的值,而不是全局变量
  • 结构体只能被非逃逸闭包捕获

访问控制

模块和源文件

模块:每个 target 都会被看作独立的模块。如框架和应用程序
源文件:模块中的源代码文件

访问级别

  • open 和 public
    开放级别:包内包外都可以访问。如框架中的 api。

  • internal
    包内级别:只能被包内访问。一般情况下的默认访问级别。

  • fileprivate
    文件级别:只能在定义的源文件内部访问。

  • private
    私有级别:只能在定义的作用域访问。

open 针对类和类的成员,它和 public 的区别主要在于 open 限定的类和成员能够在模块之外被继承和重写。

基本原则

实体不能定义在具有更低级别的实体中。

例如,函数的访问级别不能高于它的参数类型和返回类型的访问级别。

实体的访问级别决定了实体中的所有成员变量的上限。

public class SomePublicClass {                  // 显式 public 类
    public var somePublicProperty = 0            // 显式 public 类成员
    var someInternalProperty = 0                 // 隐式 internal 类成员
    fileprivate func someFilePrivateMethod() {}  // 显式 fileprivate 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

class SomeInternalClass {                       // 隐式 internal 类
    var someInternalProperty = 0                 // 隐式 internal 类成员
    fileprivate func someFilePrivateMethod() {}  // 显式 fileprivate 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

fileprivate class SomeFilePrivateClass {        // 显式 fileprivate 类
    func someFilePrivateMethod() {}              // 隐式 fileprivate 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

private class SomePrivateClass {                // 显式 private 类
    func somePrivateMethod() {}                  // 隐式 private 类成员
}
  • 元组类型

元组的访问级别由元组中访问级别最低的类型决定,不能被显示指定。

  • 函数类型

函数的访问级别根据访问级别最低的参数类型或者返回类型来决定。

  • 枚举类型

枚举成员的访问级别和该枚举类型相同,不能单独指定枚举成员的访问级别。
原始值或者关联值的访问级别不能低于枚举类型的访问级别。

  • 嵌套类型

嵌套类型的访问级别和包含它的类型的访问级别相同。但是 public 除外,在一个 public 的类型中定义嵌套类型,该嵌套类型默认情况下自动拥有 internale 的访问级别,你可以显示指定嵌套类型访问级别为 public

  • 子类

子类的访问级别不能高于父类的访问级别。
但是你可以通过重写给子类的成员提供更高的访问级别。

public class A {
    fileprivate func someMethod() {}
}
internal class B: A {
    // 重写更改方法的访问级别
    override internal func someMethod() {}
}
  • 常量、变量、属性、下标

常量、变量、属性不能拥有比它们类型更高的访问级别,下标也不能拥有比索引类型或返回类型更高的访问级别。

private var privateInstance = SomePrivateClass()

SomePrivateClass 是私有类,所以变量 privateInstance 必须明确指定访问级别为私有。

Getter 和 Setter

常量、变量、属性、下标的 Getters 和 Setters 的访问级别和它们所属类型的访问级别相同。但是 Setter 可以拥有更低的访问级别,这样可以控制读写权限。

下面例子限制了结构体中 numberOfEdits 写的访问级别,只读。

struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}
  • 构造器

必要构造器的访问级别和所属类型的访问级别相同。便利构造器的访问级别则不能高于所属类型的访问级别,因为便利构造器需要横向使用到必要构造器。

  • 协议

协议中的每个方法或属性都必须具有和该协议相同的访问级别,不能将协议中的方法或属性设置为其他访问级别。

和其他类型不同,具有 public 访问级别的协议,其成员也都是 public。而其他类型,如类,访问级别为 public ,其成员的访问级别除非你显示指定,否则默认都是 internal

协议继承

继承来的新协议只能拥有比原始协议更低的访问级别。

协议遵循

一个类型可以遵循比它访问级别更低的协议,但是所遵循的上下文级别取类型和协议级别最小的那个。如果一个类型是 public 级别,但它要遵循的协议是 internal 级别,那么这个类型对该协议的遵循上下文就是 internal 级别。

当你编写或扩展一个类型让它遵循一个协议时,你必须确保该类型对协议的每一个要求的实现,至少与遵循协议的上下文级别一致。例如,一个 public 类型遵循一个 internal 协议,这个类型对协议的所有实现至少都应是 internal 级别的。

  • Extension

Extension 的新增成员具有和原始类型成员一致的访问级别。当然你可以显示为新增成员指定更低的级别。

在同一文件类,原始类型声明中的私有成员,可以在多个 extension 中访问,extension 中私有成员同样可以在另一个 extension 或原始类型声明中访问。因此,你可以通过 extension 来任意组织你的代码。这一点比 OC 方便得多。

  • 泛型

泛型类型或者泛型函数的访问级别取决于泛型类型或者泛型函数本身的访问级别,另外还需要结合类型参数的类型约束的访问级别,选择最小的访问级别。

高级运算符

  • 位运算符

~ :按位取反,比特位取反。
& :按位与,两个数对应位都为 1 时,新数的对应位才为 1。
| :按位或,两个数对应位中有任意一个为 1 时,新数的对应位就为 1。
^ :按位异或,当两个数对应位不相同时,新数的对应位就为 1,并且对应位相同时则为 0。

  • 溢出运算符

&+ :溢出加法
&- :溢出减法
&* :溢出乘法

  • 运算符重载

类和结构体可以为现有的运算符提供自定义的实现,以达到类和结构体进行运算。

struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func + (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y + right.y)
    }
}

let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 是一个值为 (-3.0, -4.0) 的 Vector2D 实例
let alsoPositive = -negative
// alsoPositive 是一个值为 (3.0, 4.0) 的 Vector2D 实例

前后缀运算符

要实现前缀或者后缀运算符,需要在声明运算符函数的时候在 func 关键字之前指定 prefix 或者 postfix 修饰符。

extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}

复合赋值运算符

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right
    }
}

var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original 的值现在为 (4.0, 6.0)

等价运算符

通常情况下,自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为相等运算符(==)与不等运算符(!=)。

与其它中缀运算符一样, 并且增加对标准库 Equatable 协议的遵循。

extension Vector2D: Equatable {
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }
}

let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
    print("These two vectors are equivalent.")
}
// 打印“These two vectors are equivalent.”

多数简单情况下,您可以使用 Swift 为您提供的等价运算符默认实现。Swift 为以下数种自定义类型提供等价运算符的默认实现:

  1. 只拥有存储属性,并且它们全都遵循 Equatable 协议的结构体
  2. 只拥有关联类型,并且它们全都遵循 Equatable 协议的枚举
  3. 没有关联类型的枚举

例如:

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}
  • 自定义运算符

新的运算符要使用 operator 关键字在全局作用域内进行定义,同时还要指定 prefixinfix 或者 postfix 修饰符。

例如:

prefix operator +++
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容