Swift笔记

5.函数

1.基本定义
  • func 函数名(参数) -> 返回值 { }
func sum(num1:Int, num2: Int)-> Int {
    return num1 + num2
 }

调用:
sum (num1: 1, num2 :2)
2.相关注意点
  • 1.参数默认let,不是var,也只能是let(不能是变量)
  • 2.隐氏返回
当函数体内的表达式是简单的,可以不写return
func sum(num1:Int, num2: Int)-> Int {
     num1 + num2
 }
  • 3.可以使用元组返回多个值
  • 4.可以在形参前再加字符串修改参数标签(num1 与num2),可以使用下划线 _ 省略参数标签
func sum(_ num1:Int, _num2: Int))-> Int {
     num1 + num2
 }

func sum(by num1:Int, num2: Int))-> Int {
     num1 + num2
 }
  • 5.可变参数:
      1. 一个函数最多只能有1个可变参数,紧跟在可变参数后面的参数不能省略参数标签
可变参数定义: 在参数类型后面加上...
    func sum(_ number:Int...) -> Int {
        var total = 0
        for num in numbers {
            total += number
        }
        return total
 }
    sum(10,11,12)
  • 6.可以用inout定义一个输入输出参数,可以在函数内部修改外部实参的值(swift已经默认屏蔽对象地址)
    1. 可变参数不能标记为inout
    2. inout参数不能有默认值
    3. inout参数的本质是地址传递(引用传递)
    4. inout参数只能传入可以被多次赋值的(var变量,数组值等)
注:调用时,传入的实参需要加&,该函数可以令number的值修改为20
 
     var number = 10
     
     func sum (num: inout Int) {
        num = 20
     }
     sum(&number)
  • 7.函数重载
    • 规则:
      1.函数名相同
      2.参数个数不同/参数类型不同/参数标签不同
    • 注意点:
      1.返回值类型与函数重载无关
      2.默认参数值和函数重载一起产生二义性时,编译器并不会报错
      3.可变参数,省略参数标签,函数重载一起使用产生二义性时,编译器有可能会报错
  • 8.内联函数(即直接将函数的函数体放置在函数调用中)
    • 如果开启了编译器优化(release模式默认会开启优化),编译器会自动将某些函数变成内联函数
    • 那么,哪些不会被内联?
      1.当函数体内容很长时,并不会内联
      2.当有递归时,也并不会内联
      3.包含动态派发的函数,也不会内联(类似多态时)

注: swift有main函数,只是不需要我们自己写

  • 9.函数类型:
1.func sum(Int num1,Int num2)-> Int {
        return num1 + num2
   }
   函数类型为:(Int,Int) -> Int

2.func add() -> {

  }
  函数类型为:() ->()
 
 定义变量: var fn: (Int,Int) -> Int = sum
 fn(1,2) 结果为3 (注:此时调用函数是不需要参数标签的)

注:返回值是函数类型的函数,叫做高阶函数

  • 10.typealias关键字
    • 作用:用来给类型起别名
    • 按照swift标准库定义,void就是空元组
  • 11.@inline
    • 永远不会被内联(即使开启了编译器优化)
     @inline(never) func test(){ 
        print("test")
      }
    
    • 开启编译器优化后(相当于debug模式下是不会生效的),即使代码很长,也会被内联(递归调用函数,动态派发的函数除外)
     @inline(__always) func test(){ 
        print("test") 
      }
    

注: release模式下,没太大必要使用@inline

  • 12.嵌套函数
含义:将函数定义在函数内部
    func forward (_ isAdd :Bool) -> (Int) ->Int {
        func add (_ num :Int) ->Int {
            return num + 1
        }
 
        func minix (_ num:Int)-> Int {
            return num - 1
        }
        
        return isAdd == True? add:minix
    }
 
    forward(true)(3) 结果为:4
    forward(false)(3) 结果为:2

6.枚举

    1. 定义
enum Season {
     case Spring
     case Summer
     case Autumn
     case Winter
}
等价于:
enum Season {
     case Spring,Summer,Autumn,Winter
}

print(Season.spring)
    1. 关联值(将各种类型的数据直接存储在枚举变量里)
    有时会将枚举的成员值和其他类型关联存储起来,会很有用
enum Score {
    case Point(Int)
    case grade(Character)
 }
 var score = Score.Point(90)
 score = .grade("B")
    1. 原始值
    枚举成员可以使用相同类型的默认值预先关联买这个默认值叫做原始值rawValue
enum AAA :String {
    case aaa
    case bbb
 }
表示成员的关联值类型为String
  • 原始值与关联值的区别:
    • 关联值可以被外部赋值的,而原始值是无法被外部赋值的
    • 关联值会直接将存储进去的值直接存储在内存中,而原始值不会,原始值是绑定的,是不会占用枚举变量内存的
    • 直白来说:关联值今后是可以修改的,所以需要开辟空间去存储,而原始值是绑定的,不动的,不允许外部进行修改.所以只需要1个字节存储即可
  • 如果枚举的原始值Int,String,swift会自动分配原始值
  • 递归枚举,需要关键字indirect
  • 使用switch +enum,可以在switchcase中对枚举进行let赋值使用,在使用关联值时使用let
    1. MemoryLayout
  • 获取数据类型占用的内存大小
  • 用法: e.x MemoryLayout<Int>.size
    • MemoryLayout.stride : 分配占用的空间大小
    • MemoryLayout.size: 实际用到的空间大小
    • MemoryLayout.alignment : 对齐参数
enum Season : String {
    case spring = "1",summer = "2",autumn = "3",winter = "4"
 }
MemoryLayout<Season>.size = 1
MemoryLayout<Season>.stride = 1
MemoryLayout<Season>.alignment = 1
//因为外部无法修改spring等枚举值,所以spring为原始值,只需要1个字节进行存储即可
  • 当同时存在原始值与关联值时,关联值需要开辟空间进行存储,原始值需要开辟1个字节进行辨别,是关联值还是原始值
  • 现在的CPU基本是小端模式,内存越高的字节,实际也是高字节,内存比较低的字节,实际也是低字节(高高低低)
enum test {
    case num1(Int, Int, Int)
    case num2(Int,Int)
    case num3(Int)
    case num4(Bool)
    case num5
 }
MemoryLayout<test>.size = 25
MemoryLayout<test>.stride = 32
MemoryLayout<test>.alignment = 8
    1. 内存布局
  • 如果有关联值,则必定会有1个字节用于存储成员值(即第几个成员),N个字节存储关联值,N取占用内存最大的关联值,任何一个case的关联值都共用这N个字节
enum test {
    case num
 }
MemoryLayout<test>.size = 0
MemoryLayout<test>.stride = 1
MemoryLayout<test>.alignment = 1
  • 如果枚举里只有一个原始值,则可以这么说,该变量不占内存
enum test {
    case num(Int)
 }
MemoryLayout<test>.size = 8
MemoryLayout<test>.stride = 8
MemoryLayout<test>.alignment = 8
  • 存储成员值的前提是,有超过1个的成员,所以上述不需要专门一个字节去存储成员值
enum test {
    case num(Int)
    case num2
 }
MemoryLayout<test>.size = 8+1 = 9
MemoryLayout<test>.stride = 16
MemoryLayout<test>.alignment = 8

7.可选项

    1. 基本定义
    • 一般也叫可选类型,它允许将值设置为nil
    • 在类型名称后面加个问号?,来定义一个可选项
    var name:String? = "Jack"  , name = nil
    var num :Int? 
    //则num默认的初始值为nil
    
    • 可选项的本质是对其他类型的一层包装,可以将它理解成一个盒子
      • 如果为nil,那么它是一个空盒子
      • 如果不为nil,那么盒子里装的,是被包装类型的数据
    • 如果要从可选项中取出被包装的数据,需要使用!进行强制解包,进行强制解包,只是将可选项里的值拿出来用一下而已,不会因此改变可选项
    • 如果对值为nil的可选项进行强制解包,将会产生运行时错误
    1. 可选项绑定
    • 可以使用可选项绑定来判断可选项是否包含值,如果包含就自动解包,把值赋给一个临时的常量(let),或者变量(var),并返回true,否则返回false
    if let number = Int("123") {
        print(number) //(此时,number不需要加!)
    } else {
        print("error")
    }
    
    • 注意:
    • 涉及到可选项绑定时,若有且的关系时,不能使用&&等,需要使用逗号,,此时,意义是,两侧的表达式必须同时成立才会执行
    • number的作用域只在if那个{}之前
    1. 空合并运算符
    • 定义: a ?? b
      1. a一定要为可选项,b是可选项,也可以不是可选项
      2. ba的存储类型必须相同
      3. 如果a不为nil,则返回a,如果anil,则返回b
      4. 如果b不是可选项,返回a时会自动解包
      5. 空合并运算符的返回类型是取决于b
      6. 多个??一起使用,具体类型看最右边
      7. 空合并运算符与if let相结合使用
let a:Int? = nil
let b:Int? = 2
if let c = a ?? b {
    print(c)
}
类似于if a!= nil || b != nil
 
if let c = a, let d =b {
    print(c,d)
}
类似于if a!= nil && b != nil
    1. guard
    1. 通过字典取值返回dict["name]的类型是可选类型,而数组取出的值为真实类型(因为数组如果越界,则程序直接crash)
    2. 用法: guard 条件 else { 退出当前作用域 }
    3. guard语句的条件为false时,就会执行大括号里面的代码
      guard语句的条件为true时,就会跳过guard语句
      guard语句特别适合用来提前退出
    4. 当使用guard语句进行可选项绑定时,绑定的常量let,变量var也能在外层作用域中使用
    1. 隐式解包(自动解包) (尽量不要使用)
    1. 在某些情况下,可选项一旦被设定值之后,就会一直拥有值
    2. 在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为它能确定每次访问的时候都有值
    3. 可以在类型后面加个感叹号!,定义一个隐式解包的可选项
    4. 如果隐式解包的可选项是nil,则程序直接会崩溃
    5. 用途:希望别人给你具体的值,但也有可能会返回nil
    1. 多重可选项 ??
    1. 实际上,??可以理解为对可选项的可选项
      let num1 : Int? = 10
      let num2 : Int?? = num1
      let num3 : Int?? = 10
      实际上.num2与num3等价
    
    然而,
      let num1 : Int? = nil
      let num2 : Int?? = num1
      let num3 : Int?? = nil
      此时,num3就是个空值,故num2与num3不等价
      因为num1与num3的类型不同,所以num1与num3不等价
    
    1. 可选项的本质
    可选项的本质是enum类型
    public enum Optional<Wrapped>: ExpressibleByNilLiteral {
        case none
        case some(Wrapped)
        public init (_ some: Wrapped)
    }
    
    var age: Int? = 10
    switch age {
        case let v?:
            print("1",v)
        case nil:
            print("2")
    }
    通过switch判断一个可选类型是否为空,可以在case中使用v?,但不为nil,则会解包,并将解包后的值赋值给v
    又因为可选项本身是一个枚举,也可以直接将枚举值用在case中
    

8.结构体与类

  • 结构体

    1.定义:

    struct Data {
        var year: Int
        var month: Int
        var day: Int
    }
    var date = Date(year: 2020, month:2, day:20)
    所有的结构体都有一个编译器自动生成的初始化器
    其中,year,month,day称之为存储属性,可以给存储属性增加初始值
    

    2.编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
    3.一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器

  • 1.定义:类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器

    class Data {
      var year: Int = 2020
      var month: Int = 2
      var day: Int = 20
    }
    若存储属性有初始值,编译器还是会为类生成一个无参的初始化器,成员的初始化就是在初始化器里面完成的
    但若没有初始值,连无参的初始化器也都没有
    

    2.结构体与类的本质区别:结构体是值类型(枚举也是值类型),类是引用类型(指针类型)

    class Size {                        
      var width: Int = 20                  
      var height: Int = 10                 
    }                                       
    
    struct origin {
      var x: Int = 20
      var y: Int = 10
    }
    
    func test() {
      var size = Size()
      var origin = origin()
    }
    
    • 此时,结构体origin的内存位于栈空间,第一个地址即是结构体的地址,类(指针)对象的内存地址也是位于栈空间,占8个字节,里面存的是Size对象地址,位于堆空间
    • 通过C语言函数malloc_size,可以获取引用对象占用多少字节
    • 在Mac,iOS中的malloc函数分配的内存大小总是16的倍数
    • 结构体对象地址在哪里取决于该结构体在哪里被使用(创建),如果在函数里,那么结构体地址就是在栈空间,如果在类里,则地址是在堆空间,若在外部,就是在全局数据段
    • 指针对象同理,指针对象的内存地址取决于在哪里创建对象,但指针对象的地址是始终位于堆空间
    • 只要是函数,都是在栈空间,至于函数是位于哪里,这个是不影响的

    3.值类型引用类型
    3.1 引用类型:赋值给var,let或者给函数传参,是将内存地址拷贝一份,指向同一个文件,属于浅拷贝

    tip:今后在汇编中看到rbp-某个值,基本都是局部变量(栈空间),rip + 某个值,基本都是全局变量(全局区)

    • swift中的字符串,数组和字典都是结构体,同理,支持上述理论
      1. 在swift标准中,为了提升性能,字符串,数组,字典采取了copy on write的方式,即若仅仅是将数组1 = 数组2,则此时是浅拷贝,当数组2需要进行修改等写操作时,才为深拷贝
      1. 自己定义的结构体是不符合这条的

    3.2 引用类型:赋值给var,let或者给函数传参,是将内存地址拷贝一份,指向同一个文件,属于浅拷贝

    tip:内存地址格式为rax + 某个值,一般是堆空间

    • 堆空间的前16个字节中,前8个字节是用于记录类的一些信息,后8个是记录引用计数,是所有类的堆空间固定的

    4.class_getInstanceSize : 类的对象至少需要占用多少内存,并非堆空间实际分配的内存大小

    • 一般把定义在枚举,结构体,类内部的函数,叫做方法,本质就是一个函数
    • 方法不占用对象的内存空间
    • 方法,函数都存放在代码段,至于函数放在全局区,类里还是哪里,是无关的

    5.程序的内存分布: 代码段(函数等)=>数据段(全局变量等)=>堆空间=>栈空间

9.属性

    1. 属性可以分为两大类:存储属性与计算属性
  • 存储属性
  1. 类似于成员变量这个概念
  2. 存储在实例的内存中
  3. 结构体、类可以定义存储属性
  4. 枚举不可以定义存储属性
  • 关于存储属性:swift有个明确的规定,在创建类或者结构体的实例时,必须为所有的存储属性设置一个合适的初始值

    1. 可以在初始化器里给存储属性设置一个初始值
    2. 可以分配一个默认的属性值作为属性定义的一部分
  • 计算属性
    1. 本质就是方法(函数),本质上增加了2个方法(setget)
    2. 不占用实例的内存(若一个属性与另一个属性存在逻辑关系,则该属性使用计算属性)
  class Person {
      var age:Int = 0
      //计算属性
      var num :Int {
          set {
              age = newValue *2
          }
          get {
              return newValue
          }
      }
  }
  • 关于计算属性:
      1. set传入的新值默认叫做newValue,也可以自定义,在setter后面加上括号(),括号里面是新值的名称
      set(value) {
          num = value * 2
      }
    
    • 2.只读计算属性:只有get,没有set,可以省略get
    • 3.计算属性只能用var,不能用let
    • 4.有set,必须有get

tips: 枚举的原始值就是通过只读计算属性实现的,不需要存储原始值(rawValue)

    1. 延迟存储属性(懒加载lazy)

使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会初始化

  • 注意点:
  • lazy属性必须是var,不能是let(let必须在实例的初始化方法完成之前就拥有值)
  • 如果多条线程同时第一次访问lazy属性,无法保证属性只被初始化1次(即不是线程安全的)
  • 当结构体包含一个延迟存储属性时,只有var才能访问延迟存储属性,因为延迟属性初始化时需要改变结构体的内存
struct Point {
  var x = 0
  var y = 0
  lazy var z = 0
}
let p = Point ()
print(p.z) //会报错
因为当创建p时,结构体的内存已经固定,而当p.z调用时,才会令z赋值为0,此时需要改变对应结构体的内存,令结构体Point的z值改变,但p又是使用let,无法改变内存,矛盾,所以只有var才可以访问,但此时依然可以访问x和y
    1. 属性观察器

可以为非lazyvar定义的存储属性设置属性观察者

struct Circle {
  var radius :Double {
      willSet {
        print(newValue)
      }
      didSet {
        print(oldValue, radius)
      }
  }
  init() {
      self.radius = 1.0
  }
}
radius依然是个存储属性,因为括号里面是willSet和didSet
  • 计算属性因为本身就带有setget,需要监听直接在其set里面写代码监听就行,无需另写willSet,且willSetset不能共存
  • willSet会传递新值,默认叫newValue
  • didSet会传递旧值,默认叫oldValue
  • 在初始化器中设置属性值不会触发willSetdidSet
  • 若直接在定义存储属性时直接定义初始值也不会触发属性观察器,因为在定义存储属性时直接定义初始值,本质就是在init初始化器里面进行设置初始值
  • 属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量上
var num : Int {
    set {
      print (newValue)
    }
    get {
      return 10
    }
}
num = 11 // 11
print (num) // 10
    1. inout底层知识
  • 计算属性被带有inout标签的函数调用,底层逻辑是:会先调用计算属性中的get方法,将值对应存储在一个局部变量中,将该局部变量的地址传入函数,修改赋值对应的局部变量,当函数执行完毕,将赋值后的局部变量,调用计算属性的set方法
  • 属性观察器被带有inout标签的函数调用,底层逻辑是:会先将属性观察器中的存储属性的值存储到一个局部变量中,将该局部变量的地址传入函数,修改赋值对应的局部变量,当函数执行完毕,将赋值后的局部变量调用willSetdidSet
  • 输入输出函数的本质,还是地址(引用)传递,区别只是是谁的地址值
  • 总结:
    1.如果实参有物理内存地址(存储属性),且没有设置属性观察器,则直接将实参的内存地址传入函数(实参进行引用传递)
    2.如果实参是计算属性,或者设置了属性观察器,则会才去"Copy In Copy Out"的做法
    • 调用函数时,先复制实参的值,产生副本(get)
    • 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
    • 函数返回后,再将副本的值覆盖实参的值(set)
    1. 类型属性(类属性)
  • 严格来说,属性可以分为:
    • 实例属性:只能通过实例去访问
      1.存储实例属性:存储在实例的内存中,每个实例都有1份
      2.计算实例属性
    • 类型属性:只能通过类型去访问
      1.存储类型属性:整个程序运行过程中,就只有1份内存(类似于全局变量)
      2.计算类型属性
可以通过static定义类型属性,如果是类,也可以用关键字class
struct Car {
    static var count: Int = 0 //初始值一定要有
    init() {
        Car.count += 1
    }
}

注:存储实例属性时可以没有初始值,但存储类型属性必须要有初始值,因为类型没有像实例那样的init初始化器来初始化存储属性
  • 存储类型属性默认就是lazy,会在第一次使用的时候才初始化
  • 就算被多个线程同时访问,保证只会初始化一次,即是线程安全的,因为底层使用了gcddispatch_once
  • 类型存储属性也可以使用let,与之前的lazy时不能使用let不相违背.因为那个针对的是实例,这个是类型
  • 枚举类型也可以定义类型属性(存储类型属性,计算类型属性),但不可以定义存储实例属性
    1. 单例模式
public class fileManager {
  public static let share = fileManager()
  private init() {
  
  }
}
1.类型存储属性本质上就是一个全局变量,只是编译器在限制了对象的访问范围.
2.使用static修饰的变量,只能说,是变量位于全局区

10.方法

枚举、结构体、类都可以定义实例方法、类型方法

  • 实例方法:通过实例调用
  • 类型方法:通过类型调用,用static或者class关键字定义
class Car {
  static var count = 0
  init() {
    Car.count += 1
  }
  static func getCount() -> Int { count} //此时,在类型方法中,只能调用类型属性,实例属性无法在类型方法里调用
}

let c0 = Car()
let c1 = Car()
let c2 = Car()
print (Car.getCount()) //3 

/* self: 在实例方法中,self代表实例,
         在类型方法中,self代表类型
*/
  • 结构体中是不可以在实例方法中修改实例对象的,如果要完成这个操作.要在方法前添加关键字mutating,结构体和枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改
struct Point { 
  var x = 0.0
  var y = 0.0
  @discardableResult  mutating func moveX(alterX : Double) ->Double {
      x += alterX
      return x
  }
}
var p  = Point ()
p.moveX(alterX: 10)

@discardableResult 可以消除函数调用后返回值未被使用的警告
  • 同理,枚举也是如此
enum stateSwitch {
  case low,middle
  mutating func next() {
    switch self {
      case .low:
            self = .middle
      case .middle:
            self = .low
    }
  }
}
  • 下标(subscript)
  • 使用subscript 可以给任意类型(枚举、结构体、类)添加下标功能
    subscript的语法类似于实例方法、计算属性,本质就是方法(函数)
  class Point {
    var x = 0.0
    var y = 0.0
    subscript (index: Int) ->Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
           }
        }
        get {
          if index == 0 {
                return x
            } else if index == 1 {
                return y
           }
          return 0
        }
    }
  }
p[0] = 22.2
  • subscript中定义的返回值类型决定了:

    • get方法的返回值类型
    • set方法中newValue的类型
  • subscript可以接受多个参数,并且类型任意

  • subscript可以没有set方法,但必须有get方法

  • subscript如果只有get方法,可以省略get

  • subscript可以像函数一样,设置参数标签,但在调用时,只有设置了参数标签的,才需要加上标签名称,不然默认是不需要标签名称的

  • subscript可以是类型方法,对应的调用是通过类型进行调用的

struct Point {
  var x = 0
  var y = 0
  class PointManager {
      var point = Point()
      subscript (index: Int) -> Point {
          set {
              point = newValue
          }
          get {
              return point
          }
      }
  } 
}

var pm = PointManager ()
pm[0].x = 2
//以上调用不会报错,等价于pm[0] = Point(2,pm[0].y)
//若不加上上面的set,则会报错
//若将point从结构体改为类,则以上调用也不会报错

11.继承

  • 值类型(枚举、结构体)不支持继承,只有类支持继承
  • 没有父类的类,称之为基类,swift并没有像ocjava那样的规定,任何类最终都要继承自某个基类
  • 实例对象
    • 子类可以重写父类的下标方法属性,重写必须加上关键字override,之所以下标计算属性可以重写,是因为他们本质是方法
    • 继承对于存储属性而言,等价于直接将其从父类拿到自己的类中,且父类的存储属性放在内存的前面
class Animal {
    func speak() {
          print("speak")
    }
    subscript (index:Int) ->Double {
          return index
    }
}

class dog : Animal {
    //重写实例方法
    override func speak() {
          super.speak()
          print("speak")
    }
    //重写下标
    override subscript (index:Int) ->Double {
          return super[index] +1
    }
}
  • 类型对象

    • class修饰的类型方法、下标,允许被子类重写
    • static修饰的类型方法、下标,不允许被子类重写
    • 如果父类是用class创建的类型方法,子类是可以使用static创建类型方法,影响的只是子类的子类
    • class修饰的计算类型属性,可以被子类重写,
    • static修饰的类型属性(计算、存储),不可以被子类重写
    • 存储类型属性本身就不可以被class修饰
  • 子类可以将父类的属性(存储、计算)重写为计算属性,但不可以将父类属性重写为存储属性

  • 只能重写var属性,不能重写let属性

  • 重写时,属性名、类型要一致

  • 子类重写后的属性权限不能小于父类属性的权限

    • 如果父类属性是只读的,那么子类重写后的属性可以是只读的,也可以是可读写的
    • 如果父类属性是可读写的,那么子类重写后的属性也必须是可读写的
  • 子类虽然将父类的存储属性重写为计算属性,但创建的子类依然还需要有额外的空间拿来存储父类的存储属性(从父类继承过来的存储属性,在子类都需要分配存储空间,无论子类要不要重写,此时调用使用super拿到父类以前的存储属性)

  • 如果在子类重写父类属性的set或者get里,使用父类的存储属性,需要使用super.属性的方式,否则会产生死循环

class Circle {
    var radius : Int = 1
}
class subCircle : Circle {
    override var radius: Int {
          willSet {
                print(newValue)
          }
          didSet {
                print(oldValue,radius) //此时子类只是为父类增加属性观察器,并没有将父类的存储属性重写为计算属性.所以此时调用属性不需要super
          }
    }
}
  • 如果父类本身就包含属性观察器,而子类重写了属性生成器,会先调用子类的willSet->父类的willSet->父类的didSet ->子类的didSet
  • 子类可以为父类的计算属性增加属性观察器(原先说的计算属性和属性观察器不能共存,是在一个类里,现在是继承关系,所以不存在这个问题)
  • final关键字
    • final修饰的方法、下标、属性,禁止被重写
    • final修饰的类,禁止被继承
  • 多态的实现原理
    类似于C++的虚表,类的类型信息是存放在一张表里,在编译时就可以确定
    • 同一个类无论创建多少个对象实例,这些对象的前8个字节都是一样的,都是用于存储一些类信息的
    • 由打印地址基本可以判断,类信息的存储信息,位于全局区

12.初始化

    1. 类、结构体、枚举都可以定义初始化器
    • 类有2中初始化器:指定初始化器便捷初始化器
//指定初始化器
init (Parameters) {
    //初始化代码
}
//便捷初始化器
convenience init (Parameters) {
    //初始化代码
}

//枚举的初始化器调用
enum Season :Int {
  case Spring = 0,Summer,Autumn,Winter
}
Season(rawValue:1)
  • 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
  • 默认初始化器总是类的指定初始化器
  • 类偏向于少量指定初始化器,一个类通常只有一个指定初始化器
    1. 初始化器的相互调用规则:
      1.指定初始化器必须从它的直系父类调用指定初始化器
      2.便捷初始化器必须从相同的类里调用另一个初始化器
      3.便捷初始化器最终必须调用一个指定初始化器
      4.类的指定初始化器必须调用父类的指定初始化器,便捷初始化器必须调用类自己的指定初始化器
      5.类的指定初始化器不能调用类自己的其他指定初始化器,便捷初始化器可以调用类自己其他的便捷初始化器,但便捷初始化器最终必须调用另一个指定初始化器
      6.子类的指定初始化器只能调用父类的指定初始化器,而不能调用父类的便捷初始化器
      • 创建类对象时,系统会自动帮忙创建一个无参的指定初始化器
      • 如果有自定义的指定初始化器,则系统默认创建的无参的指定初始化器就会消失,无法再调用
      • 但如果创建的是便捷初始化器,则系统默认创建的无参的指定初始化器还在
      • 今后编程时,把主要的初始化器使用指定初始化器,把其他的设置成便捷初始化器
      • 这一套规则,保证了使用任意的初始化器,都可以完整的初始化实例
    1. swift在编码安全方面,为了保证初始化过程的安全,设定了两段式初始化安全检查
  • 两段式初始化
    • 第1阶段:初始化所有存储属性
      1.外层调用指定/便捷初始化器
      2.分配内存给实例,但未初始化
      3.指定初始化器确保当前类定义的存储类型都初始化
      4.指定初始化器调用父类的初始化器,不断向上调用,形成初始化器链

    • 第2阶段:设置新的存储属性值
      1.从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例
      2.初始化器现在能够使用self(访问、修改属性、调用它的实例方法等)
      3.最终,链中任何便捷初始化器都有机会定制实例以及使用self

  • 安全检查
    1.指定初始化器必须保证在调用父类初始化器之前,其所在类定义的所有存储属性都要
    2.指定初始化器必须先调用父类初始化器,然后才能为继承的属性设置新值
    3.便捷初始化器必须先调用同类中的其他初始化器,然后再为任意属性设置新值
    4.初始化器在第1阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self
    5.直到第1阶段结束,实例才算完全合法
  • 当重写父类的指定初始化器时,必须加上override(即使子类的实现是便捷初始化器)
  • 如果子类写了一个匹配父类便捷初始化器的初始化器,不用加上override,严格意义上说,这个不叫重写,便捷初始化器只能横向调用,是无法被子类调用的
    1. 自动继承
      1. 如果子类没有自定义任何指定的初始化器,它会自动继承父类所有的指定初始化器,在这种情况下,因为默认已经继承了父类的指定初始化器,所以系统默认的无参的初始化器也就不存在了,如果自己写了一个指定初始化器,则父类的指定初始化器不会自动继承了
      2. 如果子类提供了父类所有指定初始化器的实现(要么通过方式1继承,要么重写),则子类自动继承所有的父类便捷初始化器
      3. 就算子类添加了更多的便捷初始化器,这些规则仍然使用(如果自定义的初始化器是便捷初始化器,则规则1,2还是能满足的)
      4. 子类以便捷初始化器的形式重写父类的指定初始化器.也仍然可以作为满足规则2的一部分
    1. required
    • required修饰指定初始化器,表明其所有子类都必须实现该初始化器(通过继承或者重写实现)
    • 如果子类重写了required初始化器,也必须加上required,不用加override
    • 父类的属性在它自己的初始化器中赋值不会触发属性观察器,但在子类的初始化器中赋值会触发属性观察
    1. 可失败初始化器(init?)
    • 类、结构体、枚举都可以使用init?定义可失败初始化器,可失败初始化器返回的对象是个可选项,可能为nil
    • 之前接触的可失败初始化器
    var num = Int("123")
    public init?(_ description:String)
    
    enum Answer :Int {
        case Wrong, Right
    }
    var ans = Answer(rawValue: 1)
    
    • 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器
    • 可以用init!定义隐式解包的可失败初始化器
    • 可失败初始化器可以调用非可失败初始化器,非可失败初始化器调用可失败初始化器需要进行解包
    • 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行
    • 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来不行
    1. 反初始化器(deinit)
    • deinit叫做反初始化器,类似于C++的析构函数、OC中的dealloc函数
    • 当类的实例对象被释放内存时,就会调用实例对象的deinit方法
    • deinit不接受任何参数,不能写小括号,不能自行调用
      父类的deinit能被子类继承
      子类的deinit实现执行完毕后,会调用父类的deinit

13.可选链

  • 在swift中,nil是无法拿来使用的
  • 可选链返回的值都是可选类型
    • 如果可选项为nil,调用方法、下标、属性会失败,结果为nil
    • 如果可选项不为nil,调用方法、下标、属性会成功,结果会被包装成可选项(如果结果本身就是可选项,不会进行再次包装)
    func getName() -> String { "name" }
    //如果person结果是nil,不会调用getName()
    person?.name = getName()
    
    • 多个?可以连接在一起,如果链中任何一个节点是nil,那么整个链就会调用失败
    var dog = person?.dog // Dog?
    var weight = person?.dog.weight // Int?
    var price = person?.car?.price // Int?
    
    var num1: Int? = 5
    num1? = 10 //Optional(10)
    
    var num2: Int? = nil
    num2? = 10 //nil
    
    带?进行赋值,会先判断?前的变量是否为nil,不为nil则赋值成功,类型为可选类型,为nil则赋值不成功,依旧为nil  
    
    var dict: [String : (Int,Int)->Int] = [
        "sum" : (+),
        "different" : (-)
    ]
    var result = dict["sum"]?(10,20) // Optional(30)
    (+)与(-)是编译器特性,取出的是函数也需要进行为空判断
    
    • 可选类型进行属性引用或方法调用,需要加上?

14.协议

    1. 协议可以用来定义方法、属性、下标的声明,协议可以被枚举、结构体、类遵守(多个协议之间用,隔开)
protocol Drawble {
    func draw()
    var x: Int { get set }
    var y: Int { get }
    subscript { index: Int } -> Int { get set }
}

protocol Drink {
    var z: Int { set get }
}

class Person : Drawble , Drink {

}
  • 有关协议定义注意点
    1. 协议中定义方法时不能有默认参数值
    2. 默认情况下,协议中定义的内容必须被全部实现,也有办法办到只实现部分内容(方式1: 定义协议的extension,在extension中实现的协议即是默认协议,方式2: optional关键字)
    3. 协议里的属性不要求是计算属性或是存储属性,只要告知是只读属性还是可读可写属性
    4. 协议中定义属性时必须用var关键字
    5. 实现协议时的属性权限要不小于协议中定义的属性权限
      协议定义setget,用var存储属性或getset计算属性去实现
      协议定义get,用任何属性实现都可以
  • 协议中的类型方法,类型属性,类型下标
    1. 为了保证通用,协议中必须用static定义类型方法,类型属性,类型下标
  • 协议中的mutating
    1. 只有将协议中的实例方法标记为mutating,才允许结构体、枚举的具体实现修改自身内存,类在实现方法时不用加mutating,且用在类中会报错,枚举、结构体才需要加mutating
    2. 只有协议中方法加上mutating,才能在后续结构体实现协议时,在方法前加上mutating,在方法内修改成员属性
  • 协议中的初始化
    1. 协议中可以定义初始化器init,非final类中实现时必须加上required(当非final类实现了协议,其子类也必然需要实现协议,为了约束子类必须实现协议中的init,则非final类需要在init前使用required)
    2. 如果协议实现的初始化器,刚好是重写了父类的指定初始化器,那么这个初始化器必须同时加requiredoverride
    protocol Livable {
        init (age: Int) 
    }
    
    class Person {
        init (age: Int) {}
    }
    
    class Student {
        required override init (age: Int) {
            super.init(age: age)
        }
    }
    
    1. 协议中定义的init?init!可以用initinit?init!去实现
      协议中定义的init、可以用initinit!去实现
  • 协议中的继承
    一个协议可以继承其他协议
  • 协议组合
    可以包含1个类类型
    协议组合只能包含1个类类型(只能是类类型,不能为结构体等),且最多只有1个
protocol Livable { }
protocol Runner { }
class Person { }

//接收Person或者子类的实例
func fn0(obj: Person) {}
//接收遵守Livable协议的实例
func fn1(obj: Livable) {}
//接收同时遵守Livable、Runner协议的实例
func fn2(obj: Livable & Runner) {}
//接收同时遵守Livable、Runner协议,并且是Person或者其子类的实例
func fn3(obj: Livable & Runner & Person) {}
//接收同时遵守Livable、Runner协议,并且是Person或者其子类的实例,也可以这么写
typealias RealPerson = Livable & Runner & Person
func fn3(obj: RealPerson) {}
  • CaseIterable协议
    让枚举遵守CaseIterable,可以实现枚举值遍历
enum Season : CaseIterable {
    case spring, summer, autumn, winter
}
let season  = Season.allCases
print (season.count) // 4
  • CustomStringConvertibleCustomDebugStringConvertible协议
    1.遵守CustomStringConvertibleCustomDebugStringConvertible可以自定义实例的打印字符串
    2.print调用的是CustomStringConvertible协议的description
    debugPrintpo调用的是CustomDebugStringConvertible协议的debugDescription

  • AnyAnyObject
    Any:可以代表任意类型(枚举、结构体、类、函数类型)
    AnyObject:可以代表任意类型(在协议后面写上: AnyObject代表只有类能遵守这个协议)
    另外还有一个AnyClass,相当于AnyObject.type

  • 其他一些杂项

    1. Swift中定义一个数组有两种方式:
      var ary = Array<Int>(),或者 [Int]()
      定义一个字典:
      var dic = Dictionary<Int,Int>(),或者[Int: Int]()

    2. is用来判断是否为某种类型,as用来做强制类型转换

    3. .self是一个元类型(metadata)的指针,metadata存放着类型相关信息
      .self属于.type类型,X.type类型是一个指针变量类型,里面存放着X的前八个字节的内存地址值

    4. Type函数是直接取出对应变量的前8个字节赋值给前面的变量.实际上是没有分配栈空间,type(of:X)实际是拿到X.self

//元类型调用的初始化器必须是required修饰的
class Animal { required init () { }}
class Cat: Animal { }
class Dog: Animal { }
func create(_ clses: [Animal.Type]) -> [Animal] {
  var arr = [Animal]()
  for cls in clses {
      arr.append(cls.init())
  }
return arr
}
print (create([Cat.self,Dog.self]))
  1. swift还有个隐藏的基类:Swift._SwiftObject
  2. Self代表当前类型,一般用作返回值类型,限定返回值跟方法调用者必须是同一类型(也可以作为参数类型),但如果Self用在类中,要求返回时调用的初始化是required
protocol Runner {
   func test() -> Self
}
class Person : Runner {
   required init() { }
   func test() -> Self {
       type(of: self).init()
   }
}

//以下4句代码本质是一样的
var p0 = Person()
var p1 = Person.self()
var p2 = Person.init()
var p3 = Person.self.init()
  1. XX.self的区别
    若只想表达元类型,则需要使用X.self

15.错误处理

  • 自定义错误
    想要自定义错误信息需要:
    1.在函数声明里加上throws
    2.函数体内抛出一个遵守了Error协议的实例对象(可以是类,枚举,结构体)
class MyError : Error  { }

func divide (_ num1:Int, _ num2:Int) throws -> Int {
    if num2 == 0 {
        throw MyError()
    } 
    return num1 / num2
}
  • 错误处理
    1. 通过do-catch捕捉Error
    2. 不捕捉Error,在当前函数增加throws声明,Error将自动抛给上层函数,如果最顶层函数(main函数)依然没有捕捉Error,那么程序将终止
    3. 可以使用do-catch来处理异常,当try中的部分为异常时,会停止执行try所在作用域结束之前的全部代码,去catch中匹配对应异常,以上的结果会跳过print(“3”)这个过程
    4. 当函数声明有使用throws时,在函数调用时需要使用try进行尝试调用,若函数调用不在最外层,则需要进行do-catch异常处理,或在函数声明中加上throws再往上层函数抛出异常
func test () {
print("1")
do {
    print("2")
    print(try divide(200,0))
    print("3")
} catch let SomeError.illegalArg(msg) {
    print("参数异常")
} catch let SomeError.illegalArg(msg) {
    print("数组越界")
}
print("4")
}
test()
  1. 可以使用try?try!调用可能会抛出Error的函数,这样就不用去处理Error
func test () {
  print("1")
  print("2")
  print(try? divide(200,10)) // Optional(20),Int?
  print(try? divide(200,0)) // nil
  print(try! divide(200,10)) // 20,Int
  print("3")
}
test()
var a = try? divide(20,0)
var b = Int?
do {
    b = try divide(20,0)
} catch {
    b = nil
}
//a与b是等价的

6.rethrows声明:函数本身不会抛出错误,但调用闭包参数抛出错误,那么它将会向上抛

func exec (_ fn: (Int,Int) throws ->Int,_ num1: Int, _ num2 : Int ) rethrows {          
    print (try fn(num1,num2))
}
try exec(divide, 20, 0)

rethrows 仅代表抛出的异常是因为传入的参数导致异常,而与函数(闭包)内部的逻辑导致异常无关
throws 代表抛出的异常是因为函数(闭包)内部的逻辑可能导致的异常
其他二者并无本质区别,作用其实差不多

  1. defer语句:用来定义以任何方式(抛错误、return等)离开代码块前必须要执行的代码
    defer语句将延迟至当前作用域结束之前执行
func open (_ filename: String) -> Int {
    print("open")
    return
}
func close (_ file: Int) {
    print("close")
}

func processFile (_ filename: String) throws {
    let file = open(filename)
    defer {
        close (file)
    }
    try divide (20,0)
    // close将会在这里调用
}

defer语句的执行顺序与定义顺序相反

func fn1 () {}
func fn2 () {}
func fn3 () {
    defer { fn1() }
    defer { fn2() }
}
fn3()

//执行顺序为fn2,fn1
  1. 断言
  • 自定义错误进行解决(throws->try),是会被do-catch捕获的,而断言则不会,断言时,只会抛出错误,并不会被捕捉到
  • 很多编程语言都有断言机制,不符合指定条件就弹出运行时错误,常用于调试(Debug)阶段的条件判断
  • 默认情况下,swift的断言只会在Debug模式下生效,Release模式下会忽略
func divide (_ v1:Int, _ v2:Int) {
    assert(v2 != 0, "除数不能为0")
    return v1 / v2
}
divide (20,0)

9.fatalError函数
如果遇到严重问题,希望结束程序运行时,可以直接使用fatalError函数抛出错误(这里是无法通过do-catch捕捉的错误),使用了fatalError函数,就不需要再写return

func test (_ num : Int) {
    if num > 0 {
        return 1
    }
    fatalError("num不能小于0")
}

在某些不得不实现,但不希望别人调用的方法,可以考虑内部使用fatalError函数

class Person { required init() {} } 
class Student : Person { 
     required init() { fatalError("不要使用这个初始化!")}
     init (score: Int) {}
}

16.泛型

  • 泛型的作用: 泛型可以将类型参数化,提高代码复用率,减少代码量
  • 泛型的定义:
func swapValue<T> (_ a: inout T, _ b: inout T) {
    (a,b) = (b,a)
}

// 同理,泛型也可以写多个
func swapValue<T,T1,T2,T3> (_ a: inout T, _ b: inout T) {
    (a,b) = (b,a)
}

//当需要将泛型赋值给一个函数时,只需要明确函数的具体类型即可
var fn: (inout Int, inout Int) -> () =  swapValue
fn(&1,&2)
为类时,进行类对象创建,由于需要明确具体的类型,但又无法通过初始化器等方式明确具体类型,则需要在创建时明确类型,即加<类型名>
为函数时,因为函数在调用时就知道内部泛型是什么类型,所以不需要明确类型
class stack<E> {
    var elements = [E]()
    func pop() ->E {
        elements.removeLast()
    }
    func top() ->E {
        elements.last!
    }
}

泛型的继承:

class stack<E> {
    var elements = [E]()
    func pop() ->E {
        elements.removeLast()
    }
    func top() ->E {
        elements.last!
    }
}
class subStack<E> : Stack<E> { }

枚举中的泛型:

enum Score <T> {
    case point(T)
    case grade(String)
}
let score3 = Score<Int>.grade("A")
枚举中,就算没有用到泛型,如上,没有使用到point,但当为score3分配内存时,还是需要知道泛型T的类型的,所以需要如上写法
  • 从本质上来说,泛型的实现是通过类型的元类型实现(metadata),而并非实现多个函数的方式

associatedtype关键字
以上定义泛型的方式成为泛型参数,但泛型参数只能用在类,结构体,枚举中,不能用在协议中,协议想使用泛型需要使用associatedtype(关联类型)

  1. 作用:给协议中用到的类型定义一个占位名称(协议中可以拥有多个关联类型)
protocol Stackable {
    associatedtype Element // 关联类型
    mutating func push(_ element: Element)
    mutating func pop() -> Element
}

class StringStack : Stackable {
    typealias Element = String //可不写
    //这一步即明确遵守协议的类型(有关联类型,在实现协议时都需要指明类型的名称)
    var elements = [String]()
    func push(_ element: String) { elements.append(element)}
    func pop()-> String { elements.removeLast()}
}

var ss = StringStack()
ss.push("aaa")
ss.pop()

//可以通过where对泛型进行进一步的约束,就是一个有条件约束的泛型
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2:S2) -> Bool where S1.Element == S2.Element,S1.Element : Hashable {
      return false
}

不透明类型

  • 定义: 使用some修饰的那一个整体
    • some关键字: 返回值只能有一种返回类型,除了用在返回值类型上,一般还可以用在属性类型上
  • 作用: 当只想返回遵守某个协议的对象,但是又不希望外部知道对象的真实类型,只想暴露协议的接口,则可以使用不透明类型

17.关于String与Array

关于String
1.内存地址从:
代码区--->常量区--->全局区(数据段)--->堆空间--->栈空间--->动态库
2.常通过对Mach-O文件进行窥探,字符串是位于常量区
3.字符串的两种内存方式:

  • 当字符串的长度<15位时:
    var str = “0123456789” 汇编完内存为: 0x3736353433323130 0xea0000000000003938
    会类似ocTaggerPoint 一样,分配16个字节内存,第一位存储类型,多为e,第二位为长度,后面则直接存储字符串的内容
  • 当字符串长度>15位时:
    var str = “0123456789ABCDEF”
    分配的16位内存中,后8位为字符串的真实地址(通过后8位字符地址- 一个固定的长度(或者+0x20),即为真实地址),前8位为字符串的长度,内容存放在__TEXT.cstring中(常量区)

4.append操作

  • append后的字符串长度<15位时,添加完字符串依然存放在字符串变量的内存中
  • append后的字符串长度>15位时,会开辟堆空间进行存储

5.符号的延迟绑定时通过dyld_stub_binder完成
6.注:jmpq *0xb31(%rip)格式的汇编指令,占用6个字节

18.高级运算符

溢出运算符 : &+,&-,&*
溢出的结果依然在取值的范围内,实际上是一个循环取值
重载运算符 : 类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做运算符重载
1.类似于函数重载,当运算符重载写在结构体中时,需要令重载函数修饰为static
2.在函数定义前加上关键字prefix(前缀函数)或者关键字postfix(后缀函数)
3.要想得知2个实例是否等价,一般做法是遵守Equatable协议,重载==运算符,与此同时,等价于重载了!=运算符

enum Answer: Equatable {
    case wrong(Int)
    case right
}

var s1 = Answer.wrong(10)
var s2 = Answer.wrong(10)

print(s1 == s2) //结果为true

4.Swift为以下类型提供默认的Equatable实现

  • 没有关联类型的枚举
  • 只拥有遵守Equatable协议关联类型的枚举
  • 只拥有遵守Equatable协议存储属性的结构体

5.引用类型比较存储的地址值是否相等(是否引用着同一个对象),使用恒等运算符===!==
6.要想比较2个实例的大小(Comparable)

  • 遵守Comparable协议
  • 重载相应的运算符
struct Student: Comparable {
  var age: Int
  var socre: Int
  init(score: Int, age: Int) {
      self.socre = score
      self.age = age
  }
  
  static func < (lhs: Student, rhs: Student) -> Bool {
      (lhs.socre < rhs.socre) || (lhs.socre == rhs.socre && lhs.age < rhs.age)
  }
  
  static func > (lhs: Student, rhs: Student) -> Bool {
      (lhs.socre > rhs.socre) || (lhs.socre == rhs.socre && lhs.age > rhs.age)
  }
  
  static func <= (lhs: Student, rhs: Student) -> Bool {
      !(lhs > rhs)
  }
  
  static func >= (lhs: Student, rhs: Student) -> Bool {
      !(lhs < rhs)
  }
}
  1. 自定义运算符
    在全局作用域使用operator进行声明
// MARK - 定义
prefix operator +++  //前缀运算符
postfix operator ---  // 后缀运算符
infix operator +- : PlusMinusPrecedence // 中缀运算符 : 优先级组

// MARK - 优先级组
precedencegroup PlusMinusPrecedence { 
    associativity: none //结合性(left/right/none)
    higherThan: AdditionPrecedence //比谁的优先级高
    lowerThan: MultiplicationPrecedence //比谁的优先级低
    assignment: true //代表在可选链操作中拥有跟赋值运算符一样的优先级
}

struct IntType {
    var num1: Int
    var num2: Int
    init(num1:Int, num2: Int) {
        self.num1 = num1
        self.num2 = num2
    }
    
    static func +- (lhs: IntType, rhs: IntType) -> Int {
        (lhs.num1 + rhs.num1) * (lhs.num2 - rhs.num2)
    }
}

let opera1 = IntType(num1: 10, num2: 5)
let opera2 = IntType(num1: 5, num2: 1)
print(opera1 +- opera2)

19.扩展

Swift中的扩展,有点类似于OC中的分类

  1. 扩展可以为枚举结构体协议添加新功能
    可以添加方法、计算属性、下标、(便捷)初始化器、嵌套类型、协议等等

  2. 协议不能办到的事情:

    • 不能覆盖原有的功能
    • 不能添加存储属性,不能向已有的属性添加属性观察者
    • 不能添加父类
    • 不能添加指定初始化器,不能添加反向初始化器

1.扩展是不允许影响原来类型的内存结构,所以不能添加存储属性
2.扩展是不允许添加父类,因为涉及到继承,也有可能影响原来类型的内存结构

注:便捷初始化器和指定初始化器只有在类中有具体的区分,在结构体中只有初始化器这一说,只能定义为init方式

  • 如果希望自定义初始化器的同时,编译器也能够生成默认初始化器,可以在扩展中编写自定义初始化器
  • 类遵守协议实现的required初始化器,不能写在扩展中
  1. 如果一个类型已经实现了协议的所有要求,但是还没有声明它遵守了这个协议,可以通过扩展的方式让它遵守这个协议
protocol testProtocol {
    func run ()
}

class Person {
    func run (){
        print("run")
    }
}

extension Person: testProtocol{}
  1. Swift的扩展可以给协议扩展方法
    • 扩展可以给协议提供默认实现,也间接实现可选协议的效果
    • 扩展可以给协议扩充协议中从未声明过的方法
    protocol testProtocol {
        func run ()
    }
    
    extension testProtocol {
        func run () {
            print("testProtocol_run")
        }
    
        func talk() {
            print("testProtocol_talk")
        }
    }
    
    class Person : testProtocol {
        func run() {
            print("Person_run")
        }
    
        func talk() {
            print("Person_talk")
        }
    }
    
    let p : testProtocol = Person()
    p.run() // Person_run
    p.talk() // testProtocol_talk
    
    talk方法出现这个结果是因为,在协议中是没有talk的声明的,因而不能保证今后实例中会包含这个talk的实现,就不会去实例中寻找,优先从扩展中寻找
    
  2. 带条件的扩展
class Stack<E> {
    var elements = [E]()
    func push(_ element:E) {
        elements.append(element)
    }
    
    func pop() {
        elements.removeLast()
    }
    
    func size() -> Int {
        return elements.count
    }
}

//扩展中依然可以使用原类型中的泛型类型
extension Stack {
    func top() -> E {
        return  elements.last!
    }
}

//符合条件才扩展
extension Stack : Equatable where E: Equatable {
    static func == (left: Stack, right: Stack) -> Bool {
        left.elements == right.elements
    }
}

20.访问控制

  1. 在访问控制这块,Swift提供了5个不同的访问级别(以下是从高到低排序,实体指被访问级别修饰的内容)
    • open: 允许在定义实体的模块、其他模块中访问,允许其他模块进行继承、重写(open只能用在类、类成员上)
    • public: 允许在定义实体的模块、其他模块中访问,不允许其他模块进行继承、重写
    • internal: 只允许在定义实体的模块中访问,不允许在其他模块中访问
    • fileprivate: 只允许在定义实体的源文件中访问
    • private: 只允许在定义实体的封闭声明中访问

绝大部分实体默认都是internal级别
系统类型默认是public级别

  1. 一个实体不可以被更低访问级别的实体定义,比如:
    • 变量/常量类型 >= 变量/常量
    • 参数类型、返回值类型 >= 函数
    • 父类 >= 子类
    • 父协议 >= 子协议
    • 原类型 >= typealias
    • 原始值类型、关联值类型 >= 枚举类型
    • 定义类型A时使用到的其他类型 >= 类型A
    fileprivate class Person { }
    
    internal var person : Person
    //会报错,因为不满足条件1
    
  2. 元组
    • 元组类型的访问级别是所有成员类型最低的那个
    internal struct Dog {}
    
    fileprivate struct Cat {}
    
    //(Dog,Cat)的访问级别是fileprivate
    fileprivate var data1: (Dog , Cat)
    private var data2: (Dog, Cat)
    
  3. 泛型
    • 泛型类型的访问类型是类型的访问级别以及所有泛型类型参数的访问级别中最低的那个
    internal class Car {}
    fileprivate class Dog {}
    public class Person<T1,T2> {}
    
    //Person<Car,Dog>的访问级别是fileprivate
    fileprivate var p = Person<Car,Dog>()
    
  4. 类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别
    • 一般情况下,类型为privatefileprivate,那么成员/嵌套类型默认也是privatefileprivate
    • 一般情况下,类型为internalpublic,那么成员/嵌套类型默认是internal
  5. 直接在全局作用域下定义的private等价于fileprivate
  6. getter/setter
    • gettersetter默认自动接收它们所属环境的访问级别
    • 可以给setter单独设置一个比getter更低的访问级别,用以限制写的权限
    fileprivate(set) public var num = 10
    
    class Person {
        private(set) var age = 0
        fileprivate(set) public var weight: Int {
            set {}
            get { return 10 }
        }
        internal(set) public subscript(index: Int) -> Int {
            set {}
            get { index }
        }
    }
    
    • getter是不能比setter更低的访问级别的,只能有private(set),这个操作
  7. 初始化器
    • 如果一个public类想在另一个模块调用编译生成的默认无参初始化器,必须显式提供public的无参初始化器,因为public类的默认初始化器是internal级别
    • required初始化器必须跟它所属类拥有相同的访问级别
    • 如果结构体有private/fileprivate的存储实例属性,那么它的成员初始化器也是private/fileprivate,否则默认就是internal (其中有关required的部分替换为required初始化器 >= 它的默认访问级别)
    • 若在结构体中,成员变量中至少有一个成员变量使用private修饰,则所有带成员变量的默认初始化器都不能使用,只能使用无参的默认初始化器
  8. 枚举类型的case
    • 不能给enum的每个case单独设置访问级别
    • 每个case自动接收enum的访问级别(public enum定义的case也是public)
  9. 协议
    • 协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别(public协议定义的要求也是public)
    • 协议实现的访问级别必须>=类型的访问级别,或者>=协议的访问级别
    • 即协议的访问控制与枚举一致
  10. 扩展
    • 如果有显示设置扩展的访问级别,扩展添加的成员自动接收扩展的访问级别
    • 如果没有显示设置扩展的访问级别,扩展添加的成员的默认访问级别,跟直接在类型中定义的成员一样
    • 可以单独给扩展添加的成员设置访问级别
    • 不能给用于遵守协议的扩展显示设置扩展的访问级别
  11. 在同一文件中的扩展,可以写成类似多个部分的类型声明
    • 在原本的声明中声明一个私有成员,可以在同一文件的扩展中访问它
    • 在扩展中声明一个私有成员,可以在同一文件的其他扩展中、原本声明中访问它
    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()
        }
    }
    
  12. 有关varlet定义函数变量
    struct Person {
        var age: Int
        func run(_ v: Int) {
            print("func run",age,v)
        }
        static func run(_ v: Int) {
            print("static func run", v)
        }
    }
    
    //当存在类型方法和实例方法重名时,默认是调用类型方法
    var fn = Person.run
    fn(20) //static func run 20
    
    
    //可以显示声明调用实例方法类型来调用
    //即需要调用一次person实例,才能获取到真正的对象,通过该对象去调用对应的对象方法
    var fn1: (Person) -> (Int) -> () = Person.run
    fn1(Person(age: 20))(30) //func run 20 30
    
  13. 方法的重写与协议的访问控制类似,即重写的访问级别>= 类的访问级别,或者>=被重写方法的访问级别
    • 父类的成员不能被成员作用域外定义的子类重写
    class Person {
        private func run (){}
    }
    
    class Student: Person {
        override func run (){} //会报错
    }
    
    //若将Student的类定义在Person中,则可以
    

21.内存管理

跟OC一样,Swift也是采取基于引用计数的ARC内存管理方案(针对堆空间)

1. Swift的ARC中有3种引用

  • 强引用: 默认情况下,引用都是强引用
  • 弱引用: 通过weak定义弱引用
    1. 必须是可选类型var,因为实例销毁后,ARC会自动将弱引用设置为nil
    2. ARC自动给弱引用设置为nil时,不会触发属性观察器
  • 无主引用: 通过unowned定义无主引用
    1.不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC的unsafe_unretained)
    2.试图在实例销毁后访问无主引用,会产生运行时错误( 野指针 )

注: weakunowned只能用在类实例上面

Swift中自动释放池的创建:
class Person {
    var name : String
    var age : Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    func run() {
        print("Person Run")
    }
}

autoreleasepool {
    let p = Person(name: "Jack", age: 20)
    p.run()
}

  1. 循环引用问题

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

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

    以下代码会产生循环引用,导致Person对象无法释放(看不到Person的deinit被调用)
    class Person {
        var fn: (() -> ())?
        func run() {
            print("Person Run")
        }
        deinit {
            print("deinit")
        }
    }
    
    func test() {
        let p = Person()
        p.fn = {
            p.run()
        }
    }
    
    test()
    

    上述代码的解决循环引用的方式是: 通过添加捕获列表,解决循环引用问题

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

    Person类函数定义有参数类型时,在捕获列表后紧跟(),声明参数即可,即:

    class Person {
        var fn: ((Int) -> ())?
        func run() {
           print("Person Run")
        }
        deinit {
            print("deinit")
        }
    }
    
    func test() {
        let p = Person()
        p.fn = {
            [weak p](num: Int) in
            p?.run()
        }
    }
    
    test()
    

    另外,也可以在捕获列表中定义新的变量,或者重新定义变量名等

    p.fn = {
        [weak wp = p, unowned up = p, a = 10 + 20]() in
        wp?.run()
    }
    

    如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的,因为在实例初始化完毕之后才能引用self

    class Person {
        lazy var fn: (() -> ()) = {
            [weak self] in
            self?.run()
        }
        func run() {
            print("Person Run")
        }
        deinit {
            print("deinit")
        }
    }
    上述的闭包fn内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出self
    

    如果lazy属性是闭包调用的结果,那么不用考虑循环引用的问题,因为闭包调用后,闭包的生命周期就结束了

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

    其中,第一个Person需要加lazy关键字,是因为在初始化时可知,self需要在第二阶段才能使用,因而在定义fn时直接使用self是不允许的,因而需要在调用时懒加载初始化fn,此时,可以等到使用fn时再初始化
    以上第一个与第二个的区别:第二个在闭包后面添加了(),即直接直接执行了闭包,即本质上第二个Person中,getAge不是闭包表达式,而是一个int类型,此时getAge中可以不用显示写self.

  2. 逃逸闭包与非逃逸闭包

    1. 一般都是当做参数传递给函数
    • 非逃逸闭包: 闭包调用发生在函数结束前,闭包调用在函数作用域内
    • 逃逸闭包: 闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过@escaping声明
    typealias Fn = () -> ()
    //fn 是非逃逸闭包
    func test1 (_ fn: Fn) {
        fn()
    }
    
    var gFn: Fn?
    // fn 是逃逸闭包
    func test2 (_ fn: @escaping Fn) {
       gFn = fn
    }
    
    // fn 是逃逸闭包
    func test3 (_ fn: @escaping Fn) {
        DispatchQueue.global().async {
            fn()
        }
    }
    
    其中,GCD的异步函数是逃逸闭包
    
    1. 举例:在GCD函数中,若直接使用self,则无论如何都会都会执行到函数结束才会销毁self,这就有可能产生例如空指针等问题,因而如果使用捕获列表,则可以进行判断,若此时self已经被释放,则不会执行后续代码
    2. 逃逸闭包不可以捕获inout参数,非逃逸闭包则可以捕获inout参数
  3. 内存访问冲突

    1. 内存访问冲突会在两个访问同时满足下列条件时发生:
      • 至少一个是写入操作
      • 它们访问的是同一块内存
      • 它们的访问时间重叠(比如在同一个函数内)
    //存在内存访问冲突
    //Thread 1: Simultaneous accesses to 0x100008020, but modification requires exclusive access
    var step = 1
    func increment (_ num: inout Int) {
        num += step
    }
    increment(&step)
    

    解决方式:

    var step = 1
    func increment (_ num: inout Int) {
        num += step
    }
    
    var copyOfStep = step
    increment(&copyOfStep)
    step = copyOfStep
    
    1. 如果下面的条件可以满足,就说明重叠访问结构体的属性是安全的
      • 你只访问实例存储属性,不是计算属性或者类属性
      • 结构体是局部变量而非全局变量
      • 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获
    2. 以下代码如果是在全局区,则会报错
    var tulpe = (health: 10, energy:20)
    balance(&tulpe.health, &tulpe.energy)
    
    var holly = Player(name: "Jack", health: 10, energy:20)
    balance(&holly.health, &holly.energy)
    

22.指针

  1. Swift中也有专门的指针类型,这些都被定性为"Unsafe"(不安全的),常见的有以下4种类型:
    • UnsafePointer<Pointee>类似于const Pointee *
    • UnsafeMutablePointer<Pointee>类似于Pointee *
    • UnsafeRawPointer类似于const void
    • UnsafeMutableRawPointer类似于void
  2. 相关操作(API)
    1. pointee: 取出指针里面的值
    func test1 (_ ptr: UnsafeMutablePointer<Int>) {
        // int *
        ptr.pointee = 20
        print(ptr.pointee)
    }
    
    2.load: 取出对应字节的值
    func test2 (_ ptr: UnsafeRawPointer) {
        // const void *
        var d = ptr.load(as: Int.self)
        print(d)
        此时,d就是int类型的
    }
    
    1. storeBytes: 存储对应字节的值
    func test3 (_ ptr: UnsafeMutableRawPointer) {
        // void *
        ptr.storeBytes(of: 30, as: Int.self)
    }
    
    1. 数组的遍历操作
    var arr = NSArray(objects: 11,22,33,44)
    for (idx,element) in arr.enumerated() {
        print((idx,element))
        if idx == 3 {
            break
        }
    }
    
    1. 正常情况下,是无法将指针类型赋值给一个变量的,需要通过定义withUnsafePointer这个函数
    var age = 20
    var ptr = withUnsafePointer(to: &age) { $0 }
    若要赋值的变量今后是可以修改的,需要使用withUnsafeMutablePointer这个函数
    
    指向某个变量的指针:
    var age = 20
    var ptr = withUnsafePointer(to: &age) { UnsafeRawPointer($0) }
    
    var ptr = withUnsafeMutablePointer(to: &age) { UnsafeMutableRawPointer( $0 ) }
    
    withUnsafePointerwithUnsafemMutablePointer中闭包的返回类型就是该指针类型的返回类型,即上述age是什么类型,该返回就是什么类型
    var p = Person(name: "Jack")
    var ptrP = withUnsafePointer(to: &p) { $0 }
    此时,ptrP是指向Person对象p的指针
    今后,ptrP.pointee等价于p
    
    1. 指向堆空间实例的指针:
    var ptr1 = UnsafeMutableRawPointer(bitPattern: 0x000000010000ef40)
    
    此时,ptr1就是后面传的内存地址
    1. address就是p堆空间的地址值
    class Person {
        var name: String
        init(name: String) {
            self.name = name
          }
    }
    
    var p = Person(name: "Jack")
    var ptr2 = withUnsafePointer(to: &p) {UnsafeRawPointer($0)}
    var address = ptr2.load(as: UInt.self)
    
    1. ptr2获取了person堆空间的地址值
    class Person {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    
    var p = Person(name: "Jack")
    var ptr2 = withUnsafePointer(to: &p) {UnsafeRawPointer($0)}
    var address = ptr2.load(as: UInt.self)
    var ptr3 = UnsafeMutableRawPointer(bitPattern: address)
    
    也可以直接使用unsafeBitCast实现
    var ptr4 = unsafeBitCast(p, to: UnsafeRawPointer.self)
    
    1. 有关指针的存、取、创建、销毁
    //创建
    var ptr = malloc(16)
    
    //存
    ptr?.storeBytes(of: 11, as: Int.self)
    ptr?.storeBytes(of: 22, toByteOffset: 8, as: Int.self)
    
    //取
    ptr?.load(as: Int.self)
    ptr?.load(fromByteOffset: 8, as: Int.self)
    
    //销毁
    free(ptr)
    
    1. 只有带mutable的才能使用allocate去调用内存
    var ptr2 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
    ptr2.deallocate()
    
    1. advanced函数,直接将指针偏移字节
    prt.advanced(by: 8).storeBytes(of: 22, as Int.self)
    
    上述直接将22存储到ptr后面的8个字节
    var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
    ptr.initialize(to: 11)
    ptr.successor().initialize(to: 22)
    //successor为后继,即ptr.successor取的是ptr的后8个字节
    ptr.successor().successor().initialize(to: 33)
    
    //如果是通过initialize初始化,则一定需要deinitialize方式,不然会有内存泄漏
    ptr.deinitialize(count: 3)
    ptr.deallocate()
    其中:ptr + 1与ptr[1] 与ptr.successor()都等价
    
    1. 指针类型转换
    • 方式1:
    因为ptr是rawPointer,所以ptr+8确实是+8个字节,而泛型的的指针,假如是int,则+1即+8个字节(1个int有8个字节)
    
    var ptr2 = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
    ptr2.assumingMemoryBound(to: Int.self).pointee = 11
    (ptr2 + 8).assumingMemoryBound(to: Double.self).pointee = 22.0
    
    • 方式2:
    var ptr3 = unsafeBitCast(ptr, to: UnsafeMutablePointer<Int>.self)
    
    unsafeBitCast是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据,类似于C++ 中的reinterpret_cast,相当于直接将二进制数据搬过去,只是数据类型发生了变化
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 初见swift 打印‘hello world' print("hello world"); 教程 类型 基础类型:...
    PanPan1127阅读 851评论 0 2
  • 5-1 如何定义和使用函数 定义和调用函数 当你定义了一个函数的时候,你可以选择定义一个或者多个命名的分类的值作为...
    75b9020bd6db阅读 160评论 0 0
  • 1. 格式 1). 无返回值格式 2). 有返回值格式 sum(v1: Int, v2: Int): 参数; ->...
    南城同學阅读 319评论 0 0
  • Swift中weak与unowned的区别 在闭包里面为了解决循环引用问题,使用了 [unowned self]。...
    小二郎_Ejun阅读 468评论 0 1
  • 常量与变量使用let来声明常量,使用var来声明变量。声明的同时赋值的话,编译器会自动推断类型。值永远不会被隐式转...
    莫_名阅读 436评论 0 1