Swift5.0 - day7-扩展、访问控制、内存管理

一、扩展(Extension)

  • 1.1、扩展介绍

    • Swift中的扩展,有点类似于OC中的分类(Category)
    • 扩展可以为枚举、结构体、类、协议添加新功能;可以添加方法、计算属性、下标、(便捷)初始化器、嵌套类型、协议等等
    • 扩展不能办到的事情
      • 不能覆盖原有的功能
      • 不能添加存储属性,不能向已有的属性添加属性观察器 ;原因是:不允许改变原有的内存结构
      • 不能添加父类 ;因为牵扯到继承也会改变内存结构的,所以不能添加父类
      • 不能添加指定初始化器,不能添加反初始化器
      • ...
  • 1.2、计算属性、下标、方法、嵌套类型

    extension Double {
        var km: Double { self * 1_000.0 }
        var m: Double { self }
        var dm: Double { self / 10.0 }
        var cm: Double { self / 100.0 }
        var mm: Double { self / 1_000.0 }
    }
    extension Int {
        func repetitions(task: () -> Void) {
            for _ in 0..<self {
               task()
            }
        }
        mutating func square() -> Int { self = self * self
            return self
        }
        enum Kind { case negative, zero, positive }
    
        var kind: Kind {
            switch self {
            case 0: return .zero
            case let x where x > 0: return .positive default: return .negative
            }
        }
        subscript(digitIndex: Int) -> Int {
            var decimalBase = 1
            for _ in 0..<digitIndex { 
                 decimalBase *= 10 
            }
            return (self / decimalBase) % 10
        }
    }
    extension Array {
        subscript(nullable idx: Int) -> Element? {
            if (startIndex..<endIndex).contains(idx) {
                return self[idx]
            }
            return nil
        }
    }
    
  • 1.3、协议、初始化器

    • 如果希望自定义初始化器的同时,编译器也能够生成默认初始化器(可以在扩展中编写自定义初始化器),如下面的 Point 除了系统生成的 4 个初始化器,还多了扩展中的 1 个初始化器

    • required初始化器也不能写在扩展中

      class Person {
          var age: Int
          var name: String
          init(age: Int, name: String) {
              self.age = age
              self.name = name
          }
      }
      extension Person : Equatable {
         static func == (left: Person, right: Person) -> Bool { 
             left.age == right.age && left.name == right.name
         }
         convenience init() {
             self.init(age: 0, name: "") 
         }
      }
      struct Point {
         var x: Int = 0
         var y: Int = 0 
      }
      extension Point {
         init(_ point: Point) {
            self.init(x: point.x, y: point.y) 
         }
      }
      
      var p1 = Point()
      var p2 = Point(x: 10)
      var p3 = Point(y: 20)
      var p4 = Point(x: 10, y: 20)
      var p5 = Point(p4)
      
  • 1.4、协议

    • 如果一个类已经实现了协议的所有要求,但是还没有声明它遵守了这个协议,可以通过扩展来让它遵守这个协议,如下

      protocol TestProtocol {
          func test()
      }
      class TestClass {
          func test() {
             print("test")
          }
      }
      extension TestClass: TestProtocol {}
      
    • 编写 一个 函数,判断一个整数是否为奇数?

      extension BinaryInteger {
          func isOdd() -> Bool {
              self % 2 != 0
          }
      }
      

      提示:整数是继承于BinaryInteger,在扩展里面写比较好,这样只要是遵守 BinaryInteger 协议的都可以调用

    • 扩展可以为协议提供默认实现,也间接实现 [可选协议] 的效果

      protocol TestProtocol {
          func test1()
      }
      extension TestProtocol {
          func test1() { 
              print("TestProtocol test1")
          }
          func test2() {
              print("TestProtocol test2") 
          }
          // 扩展类方法
          static func test3() {
              print("TestProtocol test3") 
          }
      }
      
      var cls = TestClass()
      cls.test1() // TestClass test1
      cls.test2() // TestClass test2
      var cls2: TestProtocol = TestClass()
      cls2.test1() // TestClass test1
      cls2.test2() // TestProtocol test2
      

      提示:这里主要说下 cls2.test2() 为什么打印 TestProtocol test2

      • 这是一个细节在协议中没有声明一个方法,在协议扩展里面实现了一个新的方法如上面extension TestProtocol里面的 test2(),但是在遵守协议的类里面实现了协议中没声明的方法test2(),在 var cls2: TestProtocol = TestClass(),特指出协议TestProtocol,那么在 cls2.test2()调用的时候,如下协议里面没声明 test2(),就会去调用协议扩展里面实现的test2() 方法
  • 1.5、泛型

    class Stack<E> {
       var elements = [E]()
       func push(_ element: E) {
          elements.append(element) 
       }
       func pop() -> E { 
          elements.removeLast() 
       }
       func size() -> Int { 
          elements.count 
       }
    }
    

    扩展中依然可以使用原类型中的泛型类型 extension Stack {

    func top() -> E { elements.last! } }
    

    符合条件才扩展

    extension Stack : Equatable where E : Equatable {
        static func == (left: Stack, right: Stack) -> Bool { 
            left.elements == right.elements
        } 
    }
    

二、访问控制(Access Control)

  • 2.1、访问权限的 5 个级别
    在访问权限这块,Swift提供了 5 个不同的访问级别(以下是从 高 -> 低 排列,实体指被访问级别修饰的内容,模块等同于文件)

    • open:允许在定义实体的模块、其他模块中访问,允许其他模块进行继承、重写(open 只能用在类、类成员上)
    • public:允许在定义实体的模块、其他模块中访问,不允许其他模块进行继承、重写
    • internal:只允许在定义实体的模块中进行访问,不允许在其他模块中进行访问

      提示:internal只允许在当前的项目访问,不允许成为一个库,在其他模块进行使用

    • fileprivate:只允许在定义实体的原文件中访问

      提示:fileprivate 代表只能在当前的 .swift 文件中进行访问

    • private:只允许在定义实体的封闭声明中访问

      提示:private:只允许在封闭的实体内访问的意思是,如下:age 只能在person 的大括号内进行访问

      class person {
          private var age: Int = 0
      }
      

    提示:绝大部分实体默认都是 internal 级别

  • 2.2、访问级别的使用准侧,一个实体不可以被更低访问级别的实体定义,如下

    • 变量/常量类型 变量/常量,如下:Person的类型(fileprivate)是小于 person 变量类型(internal)的

    • 参数类型、返回值类型 函数,如下, 默认:Int、Double 都是 public

      fileprivate  func test(_ num: Int) -> Double  {
            return 2.0
      }
      
    • 父类 子类,这个比较好理解,因为我们访问子类的时候,必然会用到父类

    • 父协议 子协议

    • 原类型 typealias

    • 原始值类型、关联值类型 枚举类型

    • 定义类型 A 时用到其他类型 类型A

  • 2.3、元组类型的访问级别是所有成员类型最低的那个,也就是下面的 data1data2 的级别要小于右边元祖中最小的级别

    internal struct Dog {}
    fileprivate class Person {}
    // (Dog, Person)的访问级别是fileprivate 
    fileprivate var data1: (Dog, Person) 
    private var data2: (Dog, Person)
    
  • 2.4、泛型类型
    泛型类型的访问级别是 类型的访问级别 以及 所有泛型类型参数的访问级别 中最低的那个

    internal class Car {}
    fileprivate class Dog {}
    public class Person<T1, T2> {}
    fileprivate var p = Person<Car, Dog>()
    

    Person<Car, Dog>的访问级别是 fileprivate

  • 2.5、成员、嵌套类型
    类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别

    • 一般情况下,类型为 privatefileprivate,那么成员\嵌套类型默认也是 privatefileprivate

    • 一般情况下,类型为 internalpublic,那么成员\嵌套类型默认是internal

      public class PublicClass {
          public var p1 = 0 // public
          var p2 = 0 // internal
          fileprivate func f1() {} // fileprivate private
          func f2() {} // private
      }
      class InternalClass { // internal
          var p = 0 // internal
          fileprivate func f1() {} // fileprivate private
          func f2() {} // private
      }
      fileprivate class FilePrivateClass { // fileprivate
          func f1() {} // fileprivate
          private func f2() {} // private
      }
      private class PrivateClass { // private
          func f() {} // private
      }
      
  • 2.6、成员的重写

    • 子类重写成员的访问级别必须 ≥ 子类的访问级别,或者 ≥ 父类被重写成员的访问级别
    • 父类的成员不能被成员作用域外定义的子类重写
  • 2.7、观察编译代码

    • 下面的代码能过不能通过,要看放的位置,如果:在一个文件内编译器不报错,不在同一个文件内会报错

      private class Person {}
      fileprivate class Student : Person {}
      
      private struct Dog {
          var age: Int = 0
          func run() {}
      }
      fileprivate struct Person {
          var dog: Dog = Dog()
          mutating func walk() {
               dog.run()
               dog.age = 1
          }
      }
      
    • 特别指出一下下面的代码: private struct Dog 里面的 agerun() 其实也是 private ,只是在访问域是 和 Dog一个等级,所以在 Person 里面也可以访问

      class test {
           private struct Dog {
               var age: Int = 0
               func run() {}
           }
           fileprivate struct Person {
               var dog: Dog = Dog()
               mutating func walk() {
                   dog.run()
                   dog.age = 1
               }
           }
      }
      

      提示:如果我们可以在 var age: Int = 0前面加 private,那么 dog.age = 1 会直接报错

  • 2.8、getter、setter

    class Person {
        private(set) var age = 0
        fileprivate(set) public var weight: Int {
            set {}
            get { 10 }
      
        }
        internal(set) public subscript(index: Int) -> Int {
            set {}
            get { index }
        }
     }
    
    var person = Person()
    person.age = 100
    print(person.age)
    

    提示:person.age = 100 会直接报错,因为我们在 private(set) var age = 0 设置的是 private(set),这样属性只能访问不能修改

    定义一个全局变量,只能在该文件内进行修改,其他文件内只能读,如下

    fileprivate(set) public var num = 10
    
  • 2.9、初始化器

    • 如果一个 public 类想在另一个模块调用编译生成的默认无参初始化器,必须显式提供public的无参初始化器;因为public类的默认初始化器是internal级别;如下如果 Person 是在一个 .dylib 里面的,那么想要访问 var person = Person(),Person 类里面的 init() {} 前面必须加 public

      public  class Person {
           public init() {
           }
      }
      var person = Person()
      
    • required 初始化器 ≥ 它的默认访问级别

    • 如果结构体有 private\fileprivate 的存储实例属性,那么它的成员初始化器也是private\fileprivate;否则默认就是internal

      struct Point {
            fileprivate var x = 0
            var y = 0
      }
      var point = Point(x: 10,y: 20)
      

      提示:在其他文件 var point = Point(x: 10,y: 20) 会报错,因为fileprivate var x = 0 设置后,整个指定初始化器都是 fileprivate

  • 2.10、枚举类型的 case

    • 不能给enum的每个case单独设置访问级别

    • 每个case自动接收enum的访问级别;public enum定义的case也是public

      public enum  Season {
          case spring
          case summer
      }
      
  • 2.11、协议

    • 协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别;public协议定义的要求也是public

    • 协议实现的访问级别必须 ≥ 类型的访问级别,或者 ≥ 协议的访问级别

      public protocol Runnable {
          func run()
      }
      fileprivate class Person : Runnable {
          fileprivate func run() {}
      }
      

      提示:fileprivate func run() {} 的权限要大于等于 类 Person协议Runnable 中的一个

  • 2.12、扩展

    • 如果有显式设置扩展的访问级别,扩展添加的成员自动接收扩展的访问级别,如下:run()的权限就是 fileprivate

      class Person {}
      fileprivate extension Person {
           func run() {
           }
      }
      
    • 如果没有显式设置扩展的访问级别,扩展添加的成员的默认访问级别,跟直接在类型中定义的成员一样 ,如下: func run() 写在 类 和 扩展里面没啥区别

      class Person {
          func run() {
           }
      }
      extension Person {
      }
      
    • 可以单独给扩展添加的成员设置访问级别,如下面给 func run() 设置 fileprivate

      class Person {}
      extension Person {
         fileprivate  func run() {
           }
      }      
      
    • 不能给用于遵守协议的扩展显式设置扩展的访问级别,如下:不能在 extension Person 前面设置访问级别

      protocal Runnable {}
      class Person {}
      extension Person:  Runnable {
          func run() {
           }
      }
      
    • 在同一文件中的扩展,可以写成类似多个部分的类型声明

      • 在原本的声明中声明一个私有成员,可以在同一文件的扩展中访问它

      • 在扩展中声明一个私有成员,可以在同一文件的其他扩展中、原本声明中访问它

        public class Person {
             private func run0() {}
             private func eat0() {
             run1() }
        }
        extension Person {
             private func run1() {}
             private func eat1() {
                  run0() 
             }
        }
        extension Person {
             private func eat2() {
                  run1() 
             }
        }
        

        提示:如果上面的段代码在同一个文件内,那么后面两个代码的功能类似写在第一段代码里面,所有虽然写的 private,但是可以访问

  • 2.13、讲方法赋值给let或者var
    方法也可以像函数那样,赋值给一个let或者var

    struct Person {
         var age: Int
         func run(_ v: Int) { print("func run", age, v) }
         static func run(_ v: Int) { print("static func run", v) }
    }
    let fn1 = Person.run
    fn1(10) // static func run 10
    
    let fn2: (Int) -> () = Person.run
    fn2(20) // static func run 20
    
    let fn3: (Person) -> ((Int) -> ()) = Person.run
    fn3(Person(age: 18))(30) // func run 18 30
    

三、内存管理

  • 3.1、内存管理

    • 跟OC一样,Swift也是采取基于引用计数的ARC内存管理方案(针对堆空间)
    • Swift的ARC中有3种引用
      • 强引用(strong reference):默认情况下,引用都是强引用

      • 弱引用(weak reference):通过weak定义弱引用

        提示:弱引用

        • 必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用设置为nil
        • ARC自动给弱引用设置nil时,不会触发属性观察器
      • 无主引用(unowned reference):通过unowned定义无主引用

        • 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的 unsafe_unretained)
        • 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)
        • Fatal error: Attempted to read an unowned reference but object 0x0 was already deallocated
  • 3.2、weak 和 unowned 的使用限制
    weakunowned 只能用在类实例上面,类是放在 堆空间

    protocol Livable : AnyObject {}
    class Person {}
    
    weak var p0: Person?  
    weak var p1: AnyObject?  //  AnyObject  代表所有类的实例
    weak var p2: Livable? // 写上 : AnyObject 代表这个协议只能被类遵守
    
    unowned var p10: Person?
    unowned var p11: AnyObject?
    unowned var p12: Livable?
    
  • 3.3、autoreleasepool,有时候为了缓解内存压力,我们可以把创建实例放在自动释放池里面

    public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
    autoreleasepool {
         let p = MJPerson(age: 20, name: "Jack")
         p.run()
    }
    
  • 3.4、循环引用 (Reference Cycle)

    • weak 和 unowned 都能解决循环引用的问题,unowned 要比 weak 少一些性能消耗
    • 在生命周期中可能会变为 nil 的使用 weak
    • 初始化赋值后再也不会变为 nil 的使用 unowned
  • 3.5、闭包的循环引用

    • 闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行 retain 操作)

    • 下面的代码会造成循环引用,导致 Person 对象无法释放(看不到 Person 的 deinit 被调用)

      class Person {
          var fn: (() -> ())?
          func run() { print("run") }
          deinit {
              print("deinit") 
          }
      }
      func test() {
          let p = Person()
          p.fn = { p.run() }
      }
      test()
      

      提示:

      • 对象 p 强引用闭包表达式,而闭包表达式又强引用对象 p,从而造成相互强引用(循环引用)

      • 解决循环引用的办法- 捕获列表

      • 办法一:[weak 对象],这样就代表闭包表达式对 对象p 进行弱引用

        p.fn = { 
             [weak p] in 
             p?.run()
        }
        
      • 办法二:[unowned 对象],这样就代表闭包表达式对 对象p 进行弱引用

        p.fn = { 
             [unowned p] in 
             p.run()
        }
        
      • 如果闭包有参数,比如上面的 var fn: ((Int) -> ())?,那么解决循环引用如下,age 是参数的名字,捕获列表 [unowned p] 要写在参数列表的前面,否则会报错

        p.fn = { 
             [unowned p](age) in 
             p.run()
        }
        
    • 如果想在定义闭包属性的同时引用 self,这个闭包必须是 lazy的(因为在实例初始化完毕之后才能引用 self),下面的闭包 fn 内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出self

      class Person {
          lazy var fn: (() -> ()) = {
               [weak self] in
               self?.run()
          }
          func run() { print("run") }
          deinit { print("deinit") }
      }
      
      func test() {
          let person = Person()
          person.fn()
      }
      
      test()
      
    • 如果 lazy 属性是闭包调用的结果,那么不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)

      class Person {
          var age: Int = 0
          lazy var getAge: Int = {
               self.age
          }()
          deinit { print("deinit") }
      }
      

      提示 : lazy var getAge: Int = { self.age }() 虽然闭包表达式对 self 进行了强引用,但是这个闭包表达式是直接 () 执行的,对self的强引用也就结束了,等同于 lazy var getAge: Int = self.age,所以不会产生循环引用

  • 3.6、@escaping

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

    • 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内,函数结束之前闭包就会调用结束

    • 逃逸闭包:闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过 @escaping 声明,也就说函数结束后闭包也可能没有进行调用

      import Dispatch
      typealias Fn = () -> ()
      // fn是非逃逸闭包
      func test1(_ fn: Fn) { fn() }
      // fn是逃逸闭包
      var gFn: Fn?
      func test2(_ fn: @escaping Fn) { gFn = fn }
      // fn是逃逸闭包
      func test3(_ fn: @escaping Fn) {
          DispatchQueue.global().async {
             fn()
          }
      }
      
    • DispatchQueue.global().async也是一个逃逸闭包

      class Person {
          var fn: Fn
          // fn是逃逸闭包
          init(fn: @escaping Fn) {
              self.fn = fn
          }
          func run() {
              // DispatchQueue.global().async也是一个逃逸闭包
              // 它用到了实例成员(属性、方法),编译器会强制要求明确写出self 
              DispatchQueue.global().async {
                  self.fn()
              }
      }
      

      提示:DispatchQueue.global().async { self.fn() } 里面使用self不会造成循环引用,因为是闭包表达式强引用 self,而 self 没有对闭包表达式进行强引用,单向强引用不会造成循环引用

  • 3.7、逃逸闭包的注意点:逃逸闭包不可以捕获 inout 参数

    typealias Fn = () -> ()
    func other1(_ fn: Fn) { fn() }
    func other2(_ fn: @escaping Fn) { fn() }
    func test(value: inout Int) -> Fn {
         other1 { value += 1 } 
    
         // error: 逃逸闭包不能捕获inout参数
         other2 { value += 1 }
    
         func plus() { value += 1 }
         // error: 逃逸闭包不能捕获inout参数 
         return plus
    }
    
    逃逸闭包不可以捕获 inout 参数
    • 逃逸闭包不可以捕获 inout 参数,假如可以捕获,我们可以试想一下代码,abc()函数结束后 x 变量就销毁了,但是 other2 里面的代码可能还没执行,就会造成内存坏访问

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

推荐阅读更多精彩内容