从零学习Swift 13:swift中的访问级别,weak的使用,闭包循环引用以及逃逸闭包,非逃逸闭包

总结

Swfit中提供了5个不同的访问级别,按照访问权限的高低排序如下:

  1. open: 允许在定义实体的模块,其他模块访问.允许其他模块继承,重写.open 只能用在 类 , 类成员 上.

什么是模块?一个可执行文件就是一个模块,我们平常的项目就是一个模块.导入的其他动态库也是一个模块.

  1. public: 允许在定义实体的模块,其他模块访问.但是不允许其他模块继承,重写.

  2. internal: 只允许在定义实体的模块中访问,不允许其他模块访问.

  3. fileprivate: 只允许在定义实体的源文件中访问.

  4. private: 只允许在定义实体的作用域中使用.

绝大部分实体默认是 internal访问级别,也就是在当前模块都可以访问.

访问级别的使用准则:一个实体不可以被更低访问级别的实体定义

这句话什么意思呢?我的理解就是访问实体 A ,就会直接或者间接访问实体 B,所 B 的访问级别 一定要 ≥ A 的访问级别

这种情况大概分为以下几种情况:

我们针对上述几种情况分别举例说明:

  1. 变量\常量类型要 ≥ 变量\常量访问级别:

变量的访问级别是internal,而变量类型Person的访问级别是fileprivate.也就是说我们可以在整个模块的其他文件中访问变量xiaoMing却无法范文Person.显然这是矛盾的.

  1. 参数类型,返回值类型要 ≥ 函数的访问级别:

同上,可以在其他文件调用test方法,却无法在其他文件访问Person,矛盾.

  1. 父类的访问级别要 ≥ 子类的访问级别:
    因为我们访问子类时肯定也就访问了父类中的东西.所以父类的访问级别不能小于子类的访问级别.

  2. 父协议 ≥ 子协议
    我们知道协议也是可以继承的,和父类子类的情况相同.

  3. 原类型 ≥ typealias

可以在其他模块中访问MyDog,但是却无法在其他模块中访问Dog,矛盾.

  1. 原始值类型 \ 关联值类型 ≥ 枚举类型:

这个很好理解,访问枚举就会访问到枚举中的关联类型,所以关联类型的访问级别不能小于枚举的访问级别.

访问级别的水桶效应

元组和泛型的访问级别取决于所有成员中级别最低的一个:

元组的访问级别:

泛型的访问级别:

person的访问级别取决于Person , Dog , Cat中访问级别最低的一个.

成员(属性 , 方法 , 下标 , 初始化器)的访问级别
  1. 一般情况下,类型为private 或 fileprivate,那么成员也是private 或 fileprivate

  2. 一般情况下,类型为internal 或 public,那么成员默认是internal

  3. 子类重写父类的成员,必须大于子类的访问级别,或者大于父类被重写成员的访问级别.

在全局作用域下private等价于fileprivate

从上图可以看到,在test方法作用域内,父类Person的访问权限小于子类Student的访问权限,结果报错.

如果我们把PersonStudent写在方法外面,全局作用域中呢?

可以看到,在全局作用域下,private等价于fileprivate.代码并不会报错.

补充

上面说过,说过类型为private 或 fileprivate,那么成员也是private 或 fileprivate.但是下面这种情况就是例外:

DogStudent是写在全局作用域的.也就是说Dog的访问权限等价于fileprivate,所以Dog内部成员的访问权限跟随Dog的访问权限.所以也是fileprivate.所以能在Student内部调用.

相当于这样:

但是如果显示的写明访问权限,就不一样了:

getter,setter

getter , setter默认接受他们所属环境的访问级别,但是我们可以给setter设置更低的访问级别,用来限制写的权限.

像下面的sex跟随Person的访问级别internal:

我们可以单独给sexsetter方法设置一个更低的访问权限,禁止外部更改:

注意: setter 的访问级别可以比 getter 的访问级别低,但是 getter 的访问级别不能比 setter 的访问级别低:

初始化器
  1. 如果一个public类想在其他模块调用系统生成的无参初始化器,那么必须显示的提供public无参初始化器.因为上面已经说过类型为 public 或 internal , 成员默认是 internal.
  1. required初始化器必须 它的默认访问级别:
  1. 如果结构体有private / fileprivate的存储实例属性,那么它带有成员的初始化器也是private / fileprivate,否则默认就是internal:
枚举的访问级别:
  1. 不能给枚举的case单独设置访问级别:
  1. 每个case自动接收枚举的访问级别.如果枚举的访问级别是public,那么case的访问级别也是public,因为不能单独给case设置访问级别.
协议的访问级别:
  1. 同枚举一样,协议中定义的要求自动接收协议的访问级别,不能为协议中的成员单独设置访问级别,如果协议的访问级别是public,协议中成员的访问级别也是public

  2. 协议实现的访问级别必须 协议的访问级别 ; 或者 遵守协议实体的访问级别.

实现的访问级别必须≥ 1 , 2 中的一个
扩展的访问级别
  1. 如果显示的设置扩展的访问级别,扩展添加的成员自动接收扩展的访问级别:

扩展的run()接收扩展的访问级别fileprivate

  1. 如果没有显示的设置扩展的访问级别,扩展添加的成员的访问级别,跟随类型的访问级别:
  1. 可以单独给扩展添加的成员设置访问级别.

  2. 不能给遵守了协议的扩展,显示设置访问级别

  1. 在同一个文件中的多个扩展,可以理解为一个类的声明拆分多个部分:

相当于这样:

方法赋值给常量\变量

我们知道函数可以赋值给一个常量或者变量,然后直接调用:

函数赋值给变量

方法也可以赋值给变量\常量,只不过麻烦一些:

image.png

如图所示,Person类中有一个实例方法run,现在我们把run方法赋值给一个变量fn:

可以看到,fn的类型是接收一个Person参数,返回一个函数,返回的函数就是run函数.

直接调用fn2:

weak , unowned
  1. weak引用计数不会 +1,同 OC 一样,当实例对象销毁后,weak会自动将指针置为nil.所以weak修饰的引用必须是var的可选项.

  2. unowned:无主引用,不会对引用计数+1,当实例对象销毁后,不会将指针置为nil.类似于 OC 中的 unsafe_unretained

  3. weak 和 unowned只能用在类实例上面

  4. unowned要比weak少一些性能消耗,因为它不会置为nil

闭包的循环引用

闭包表达式默认会对外层对象产生额外的强引用,所以在使用时要格外注意:

先看一下正常情况:

再来看一下闭包引用外层对象,导致循环引用的情况:

循环引用

person强引用了fn,fn内部又对p产生了强引用,形成了循环引用.

要解决这种循环引用的问题,需要在闭包表达式的捕获列表声明weak , unowned:

如果在定义闭包属性的同时引用了self ,那么这个闭包必须是lazy的:

因为此时self还未初始化完成,不能使用self.可以把闭包定义为lazy的,等到self初始化完成后,用到闭包的时候在初始化闭包:

闭包定义为 lazy

这时候又出现了另一个错误,这是因为.编译器认为这里可能会出现循环引用,要求必须明确写出self,提醒可能会强引用self:

解决循环引用,在闭包表达式的捕获列表声明weak:

看看下面这种情况:

同样是闭包,为什么这里不用声明捕获列表,并且编译器也没有强制写明self呢?因为这里是闭包的调用,直接把闭包的结果赋值给了fn,并且看清楚这里的fnInt类型,相当于lazy var fn: Int = 5.所以并不会产生强引用.

所以,如果 lazy 属性是闭包调用的结果,那么并不会产生强引用,因为闭包调用完后,闭包的生命周期就结束了.

逃逸闭包 , 非逃逸闭包

逃逸闭包,非逃逸闭包一般都是当做参数传递给函数.

  1. 非逃逸闭包: 闭包调用发生在函数结束前,闭包调用在函数作用域内:
闭包调用在函数作用域内
  1. 逃逸闭包: 闭包调用在有可能在函数结束后调用,闭包逃离了函数作用域,需要通过@escaping声明:
逃逸闭包

声明@escaping:

GCD 的 async

GCD 传递 async函数也是一个逃逸闭包:

所以如果在async内部访问了实例成员(属性, 方法),编译器要求在async内部要显示的写出self:

因为async是一个逃逸闭包,它的调用逃离了函数的作用域,所以我们使用的时候就要考虑要不要声明闭包表达式的捕获列表:

逃逸闭包不能捕获inout参数

逃逸闭包不能捕获inout参数,我们看看下面这种情形:

逃逸闭包不能捕获 inout 参数

解读一下为什么会这样:因为逃逸闭包逃离了函数的作用域,它的调用时机不确定,有可能 test() 函数执行完毕后 , 才会调用逃逸闭包.而 test 函数调用完毕后 , age 变量已经销毁,此时逃逸闭包在访问 age 肯定就会报错.

所以,逃逸闭包捕获不能捕获 inout 参数.

最后补充一下swift中的两个打印

swfit中有两个协议能实现自定义打印:

  1. CustomStringConvertibledescription
  2. CustomDebugStringConvertibledebugDescription

class Person: CustomStringConvertible,CustomDebugStringConvertible{
    var description: String
    
    var debugDescription: String
    
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.description = "release person's name \(name) , age is \(age)"
        self.debugDescription = "degub person's name \(name) , age is \(age)"
    }
}

var xiaoMing = Person(name: "小明", age: 18)

print(xiaoMing)
debugPrint(xiaoMing)

这两个协议基本上没什么区别,两个协议在debugrelease模式下都能打印.但是如果两个协议都实现后,我们在控制台使用po指令打印的是CustomDebugStringConvertible的打印信息:

po 指令打印的是debug

你可能感兴趣的:(从零学习Swift 13:swift中的访问级别,weak的使用,闭包循环引用以及逃逸闭包,非逃逸闭包)