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 +++
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值