Swift学习笔记(二)

Posted by GeorgeWang on May 10, 2017

内容主要参考官方文档

1.类与结构体

swift中的类和OC不同,不用将声明和实现分开到.h.m文件,而是统一在一个代码结构就可以。此外,swift中的类和结构体比其他语言中类和结构的功能要更加相似。

1.1 类和结构体的区别

  • 相同点:

    • 用属性存储值
    • 方法提供功能入口
    • 都提供下标语法
    • 用初始化方法初始化他们的初始状态
    • 在默认实现的基础上可以扩展功能
    • 都可以选择性服从协议
  • 不同点(主要是类比结构体的优势):

    • 继承
    • 类型转化,在运行时可以判断及转换不同类型
    • 注销函数让类能够释放持有的资源
    • 引用计数允许多个变量引用同个实例

1.2 定义

  • 类定义以*class关键字开头,结构体以struct开头,如:class SomeClass {}

1.3 初始化

  • 类和结构体的初始化都使用括号语法,如:var instance = SomeClass()
  • 类和结构体都可以使用 . 语法来访问及设置变量,如同setget,值得一题的是,swift中作为类属性的结构体的属性,在赋值的时候不用一整个结构体赋值,可以单个属性赋值,如OC中view.frame要修改view.frame.size.height只能通过view.frame = newRect,而swift可以直接view.frame.size.height = newValue来修改
  • 结构体初始化的时候,可以在括号里面初始化属性值,如:let instance = SomeStruct(p1:value1, p2:value2);不过,类则不可以,除非自己定义初始化函数

1.4 存储类型区别

  • 结构体和枚举都是值类型
  • 类是引用类型,使用引用计数
  • swift中判断类实例是否相同和OC不一样,使用的是===!==操作符号,==!=只用于一些值判断,或者由用户自己定义操作符的规则,相当于OC中的isEqual:方法
  • 指针: C、C++、Objective-C都用带*号指针指向实例的地址空间,swift中的引用则不直接用带*号指针指向实例的地址空间,写法也和普通值类型没什么区别

1.5 如何选择类或者机构体

  • 首先是根据上文提到的类和结构体不同点来选择
  • 其次,在这些条件下使用结构体:
    • 自定义类型只是用来做简单的数据集合
    • 在做赋值操作的时候希望看到的是数据直接copy而不是实例引用
    • 封装数据结构中的属性是值类型而不是引用类型
    • 数据结构不需要使用到继承

在以上之外的条件下,一般使用类来封装数据结构,swift中绝大多数情况也应该这样做

1.6 值类型的复制

  • 在swift中使用数组、字典和集合的时候,都是赋值而不是引用,因为swift中这三种类型都是通过结构体实现的而不是像OC一样的类实现

2. 属性

swift中的属性可分为存储属性计算属性两种类型,存储属性可以在类和结构体中定义,而计算属性除了类和结构体,还可以在枚举中定义;我们可以通过添加对存储属性的观察来发现其变化,这个在基类和子类都可以添加。此外,属性还有常量变量之分,就是letvar

2.1 存储属性

  • 存储属性存储常量变量,前者只能在初始化的时候赋值,后者能够在需要的时候修改值
  • 需要注意的是,如果一个结构体实例赋给了一个常量,那么结构体的属性即使定义为变量也是不能修改的,因为一旦赋给了常量,那么属性也都是常量;但是类则不然,常量类的变量属性还是可以修改
  • 懒存储属性:直到使用时才初始化一个默认的值或引用,不过这一特性只能用于变量,因为常量必须在类或结构体初始化之前初始化;懒存储属性对于:1.需要依靠初始化之后外部条件来初始化 2.初始化消耗资源过高 的类型非常有用 懒存储属性通过关键字lazy来定义,比如:

      class DataManager {
      		lazy var importer = DataImporter()
      		var data = [String]()
      		// the DataManager class would provide data management functionality here
      } 类中data在初始化的时候就会初始化为一个空的数组,而需要耗费巨大I/O资源的**importer**则等到第一次使用的时候才通过**DataImporter()**初始化 需要**注意**的是,在多线程情况下,**懒存储**并不保证只初始化一次
    
  • 属性实例变量,在OC中我们在类的外部调用属性,在内部可以选择性地使用_开头的实例变量,属性提供了setter和getter,而实例变量则提供了存储单元;在swift中,属性实例变量没有了区别,都是用同样的表示法,实例变量没办法直接读取,必须通过属性来操作,这样子避免了属性实例变量的混淆

2.2 计算属性

  • 计算属性存储属性最大的不同在于计算变量不真正提供存储,他将内容存储到另一个存储变量中,并提供一个getter以及一个可选的setter

      struct Frame {
          CGPoint origin
          CGSize size
          var center : CGPoint {
              get {
                  return Point(origin.x+size.width/2, origin.y+size.height/2)
              }
              set (newValue) {
                  origin = Point(newValue.x-size.width/2, newValue.y-size.height/2)
              }
          }
      }   其中如果setter不提供参数,那么默认也会有`newValue`作为参数名
    
  • 计算属性如果只提供getter而不提供setter,那么该属性就是一个只读属性,并且这时候可以省略get关键字,直接返回值,如:

      struct Frame {
          CGPoint origin
          CGSize size
          var center : CGPoint {
              return Point(origin.x+size.width/2, origin.y+size.height/2)
          }
      }
    
  • 需要注意,所有的计算属性都应该是变量,因为常量在初始化的时候就已经确定了,后续操作没有修改的意义

2.3 属性观察

  • swift提供willsetdidset两种观察属性方法,可以用于除了懒加载之外的所有存储属性,以及覆盖超类的属性(不管是存储还是计算属性),对于当前类的非覆盖的计算属性,没必要使用观察方法,因为在setter中就可以做相应的操作
  • willset在setter之前调用,提供newValue作为参数值(可以自定义参数),didset在setter之后调用,提供oldValue作为参数值(可以自定义参数)
  • 如果在子类初始化时候初始化超类属性,那么该属性的willsetdidset会在超类的初始化之后执行,如果在超类初始化之前赋值,那么属性观察无效
  • willsetdidset中对当前属性设置新的值,不会再度调用观察方法

2.4 全局变量和本地变量

  • 全局变量和本地变量也可以作为计算变量以及为存储变量添加观察
  • 全局变量和常量是懒加载(compulate lazy),而且不用和懒加载属性一样使用lazy关键字;本地变量和常量则没有这个功能

2.5 类型属性

  • 类型属性属于该类型而不属于实例,就如类方法属于类而不属于类实例,类型属性也分存储属性和计算属性,其中存储属性需要我们生命的时候就给上默认值,因为它不像实例属性,可以在实例初始化的时候初始化
  • 类型存储属性都是懒加载的,并且不用使用lazy关键字
  • 类型属性生命与类、结构体、枚举的{}之内,并用staticclass关键字声明,其中class声明的类型属性可以被子类覆盖重写
  • 类型属性读写时,通过Type.property的方式,如SomeClass.typeProperty

3.方法

swift中类、结构体、枚举都可以定义方法,包括实例方法和类型方法(如:类方法),在类的定义方法,和OC大致相同;但是在结构体和枚举中swift就要强大很多,枚举先不说,OC中结构体是不支持方法的,但是swift不仅支持,而且支持实例方法和类型方法

3.1 实例方法

  • 实例方法定义和函数差不多,只不过是定义在类型的{}括号范围内,使用时要通过实例.语法
  • 和OC一样,方法中通过self来表示当前实例,不过一般不怎么用到self,因为实例方法内部调用实例的内容不用通过 实例.语法,除非方法的参数名称和类中某个属性名字相同,这时候参数优先,如果我们要使用类的属性,就必须通过 self.
  • 如果是结构体或枚举的实例方法,由于结构体和枚举是值类型,swift规定不能在实例方法中修改当前实例的值,除非在方法前面加上mutating声明该方法是可变的;不过,如果该实例是被定义为常量,那么尽管拥有可变方法,也不能调用
  • 在可变的值类型实例方法中,可以对self做赋值操作

3.2 类型方法

  • 类型方法在普通方法前面加上static关键字,在类里面,可以用class关键字,这样不仅是类方法,还可以在子类中重写
  • 类型方法使用,通过TypeName.functionName(...)的方式,类型方法中self也表示当前类(不是之前的类实例),如果在类型方法内部调用该类型的其他类型方法,可以通过self.otherFunName()otherFunName();而如果在实例方法中调用类型方法的话,则必须使用TypeName.functionName(...)的方式。

4. 继承(Inheritance)

  • swift中的类不像OC一样要求继承自NSObject,可以不用继承任何类
  • 子类可以覆盖任何父类的实例/类型方法、实例/类型属性、下标,使用override关键字,任何不使用此关键字的覆盖编译报错,同样编译器也会检查是否
  • 子类使用父类的方法、属性、下标,通过super常量,如super.someMethod()super.somePropertysuper[index]
  • 覆盖父类的属性,可以覆盖其settergetter,如果父类没有settergetter,子类写上了,这时候属性性质就有可能改变,比如父类没有getter,子类实现了,那么属性就变成计算属性;子类属性覆盖时可能是:reonly属性->readwrite属性、存储属性->计算属性,但不能是:readwrite属性->reonly属性;如果覆盖setter,那同时也必须要覆盖getter,如果不想改变父类getter的结果,那么直接返回super.someProperty就可以
  • 子类可以通过覆盖父类属性来添加属性观察,不过要遵守属性观察的基本原则,比如不能为reonly属性、常量属性添加观察,也不能同时在覆盖属性添加setter和属性观察
  • 如果不想属性、方法、下标被覆盖,可以添加final属性,甚至class也可以添加final属性,防止被继承,任何尝试覆盖或继承final的操作都不能编译通过

5.初始化

  • swift中class、struct、enum都可以定义初始化函数,不过swift的初始化函数并没有返回值,默认初始化函数格式如下:

      struct Fahrenheit {
      		var temperature: Double
      		init() {
      		temperature = 32.0
      		}
      }
    
  • 初始化方法可以自定义以传入参数,虽然初始化方法在形式上不和一般方法一样,但其参数依然有参数标签和参数名之分,同样参数标签可以用_省略;可选类型的属性在初始化方法中可以不初始化,他们默认是nil;常量属性要在初始化方法中初始化,因为在后续操作中都不能初始化或修改,此外,常量属性只能在当前类初始化,不能在子类中初始化
  • 如果没有自己实现初始化方法,系统会提供一个默认初始化方法,并将所有属性初始化为他们的默认值,可选属性初始化为nil
  • swift除了提供默认初始化方法,同样的,在没有自定义初始化方法的时候,swift还会为结构体提供成员初始化方法,用户可以直接调用并传入适当的参数值,如:

      struct Size {
      		var width = 0.0, height = 0.0
      }
      let twoByTwo = Size(width: 2.0, height: 2.0)
    

5.1 值类型的初始化委托

  • 初始化方法可以委托其他初始化方法做初始化工作,这就像是OC中参数少的方法逐级调用参数多的方法;但是由于值类型(结构体和枚举)不支持继承,因此值类型和引用类型(类)的初始化委托还有不同的地方,值类型的比较简单;
  • 在值类型初始化方法中,可以通过self.init调用其他初始化方法,但也仅限于内部调用,不能在其他方法中调用;同时,如果自定义初始化方法,那么系统提供的默认方法不能再调用
  • 自定义初始化方法推荐在扩展中实现

5.2 类的继承和初始化

  • 所有的存储类型属性在初始化方法中都必须初始化,包括从父类继承的属性;swift定义了两种类型的初始化方法来保证存储属性被初始化,分别是指定初始化便捷初始化
  • 指定初始化是首要的初始化方法,它初始化所有属性并调用父类方法沿着继承链往上初始化;类一般比较少指定初始化方法,多数情况只有一个
  • 便捷初始化是次要的,可以调用指定初始化方法并对部分参数设置默认值
  • 两种初始化的实现方式如下:

      // 指定初始化
      init(parameters) {
      		statements
      }
      // 便捷初始化,多了关键字 convenience
      convenience init(parameters) {
      		statements
      }
    
  • 指定初始化便捷初始化的使用有以下三个规则:
    • 指定初始化必须调用直接父类的指定初始化方法
    • 便捷初始化必须调用当前类的其他初始化方法
    • 便捷初始化必须在根本上调用制定初始化方法

    如下图所示: swift_init_designated_convinence

    下面是一个更复杂的调用关系,可以看得出指定初始化是继承链调用的一个“漏斗”

    swift_init_designated_convinence

  • 两阶段初始化:类的初始化一般分两阶段,第一阶段类会自动初始化所有存储属性,第二阶段就是自定义初始化了,这样保证每个存储属性使用之前都能被初始化,也防止属性被其他方法初始化为预期外值,编译器会对初始化方法做以下安全检查: * 当前类的指定初始化方法要确保在委托父类初始化方法之前初始化本类所有属性 * 当前类要在委托父类指定初始化之后初始化继承的属性,否则已初始化的值可能会被重写 * 当前类的便捷初始化方法要在委托其他初始化方法之后再为任何一个属性赋值,否则值可能会丢失 * 在第一阶段的初始化之前,不得调用任何实例方法、属性、或使用self

    两个阶段的具体工作如下:

    总的来说就是,第一阶段由当前类沿着继承链上溯去分配内存并初始化,第二阶段由根类下溯,执行自定义的初始化

  • swift的初始化方法不和OC一样,父类的初始化方法默认不会被子类继承,以防止某些情况下父类的初始化不适合子类。如果非要让父类的初始化方法有用,只能在子类中重写,并调用父类的该方法,记得在方法前面加上override关键字,即使是快捷方法覆盖指定方法也要。
  • 不过如果是想覆盖父类的便捷方法,由于子类便捷方法不能直接调用父类便捷方法(根据上文的规则),父类的便捷方法也没用了
  • 注意,可以在初始化时修改继承变量属性,不能修改继承的常量属性(原因可以思考一下,结合两阶段初始化和常量特性)
  • 自定继承初始化方法,在某些特殊情况下,子类还是会继承父类的初始化方法:当前类所定义的属性都有默认值,1.当前类没有定义指定初始化方法,会自动继承父类指定初始化方法;2.子类实现了父类所有指定初始化方法(包括1中自动继承的和自定义的),子类会自动继承父类快捷初始化方法

5.3 可失败的初始化(Failable Initializer)

  • 某些情况下,类、结构体和枚举都可能初始化失败,比如错误的参数、系统资源缺失,这时候可以根据情况给出可失败的处理
  • 可失败初始化,在init后面加上?,如init?(...),并在失败的时候返回nil,尽管成功的时候不需要返回值(严格来说这并不是返回,而是告诉系统这里失败了),不过类中不能存在除了?其他都相同的初始化方法(如参数名字和类型相同)
  • 枚举的rawValue初始化会自动提供失败处理
  • 结构体、枚举、类,在可失败的初始化方法中,值类型的可以调用其他值类型的可失败初始化,类可以调用父类的可失败初始化,一旦所调用的初始化方法失败了,那么当前的初始化也会马上停止;可失败初始化方法可以调用非可失败方法

  • 可失败初始化方法的重写
    • 你可以在子类覆盖父类的可失败方法,也可以用非可失败方法去覆盖可失败方法,不过这时候如果委托父类方法就要强制拆包,不过反过来就行不通
  • 除了可失败的初始化,还有不可失败的初始化,在init后方加上感叹号,如init!(...),可失败和不可失败之间可以互相委托调用,还可以从initinit!委托调用

5.4 必要的初始化

  • 通过在初始化方法前面加上required关键字,表示该初始化方法是必要的,所有子类都必须重写,而且依然需要加上required关键字,不过重写时候不用加上override

5.5 通过闭包或者函数为属性设置默认值

  • 在某些情况下,我们在设置属性默认值的时候需要一个计算过程,这时候可以通过一个闭包的方式,如:

      class SomeClass {
      		let someProperty: SomeType = {
      		// create a default value for someProperty inside this closure
      		// someValue must be of the same type as SomeType
      		return someValue
      		}()
      }
    

    指定初始化方法中,该闭包会执行

  • 留意以下上面例子闭包后面的(),这个括号表示立马执行该闭包,如果没有这个括号,那么默认值就是一个闭包而不是闭包返回的值了
  • 注意,由于闭包调用实在初始化方法中,此时当前类的其他属性可能还没有初始化,因此不能调用这些属性,即使它们有默认值,也不能使用self

6. 释放(deinitializer)

  • 有初始化也有释放,其方法名为deinit,在类实例被销毁之前调用,在这里可以销毁资源

分享到: