结构体和类
结构体和类对比
* Swift 中结构体和类有很多共同点。两者都可以:
* 定义属性用于存储值
* 定义方法用于提供功能
* 定义下标操作用于通过下标语法访问它们的值
* 定义构造器用于设置初始值
* 通过扩展以增加默认实现之外的功能
* 遵循协议以提供某种标准功能
与结构体相比,类还有如下的附加功能:
* 继承允许一个类继承另一个类的特征
* 类型转换允许在运行时检查和解释一个类实例的类型
* 析构器允许一个类实例释放任何其所被分配的资源
* 引用计数允许对一个类的多次引用
判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符:
* 相同(===)判断内存地址是否相同
* 不相同(!==)
类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着你的大多数自定义数据类型都会是结构体和枚举。
在结构和类之间进行选择
* 默认情况下使用结构体, 结构体的优势。
* 当您需要 Objective-C 互操作性时使用类。Anyobject桥接oc,必须用oc类型的接口处理数据; 自定义模型继承Objective-C framework。
* 如果需要控制要建模的数据的标识符使用类。(共享实例和本地数据库对象)
* 使用结构和协议对继承和共享行为进行建模。优先使用协议继承,因为协议允许类、结构体和枚举。类只能兼容其他类。
* 不需要控制标识时使用结构体(数据是从远程服务器获取)
Choosing Between Structures and Classes 百度搜索之后重新看一遍,现在代码量太少读不通这篇文章。特别是Use Structures When You Don't Control Identity这段文字。
值语义的特性
值语义特性之一 值类型都有且仅有一个持有者;将值类型作为函数形式参数可以让代码天然地具有线程安全特性,不可改变的东西是可以在线程之间安全共享的。
值语义特性之一 值类型都有且仅有一个持有者; 结构体的隐式setter方法会自动被标记为mutating(值语义)而改变一个结构体变量的属性, 相当于重新为结构体实例赋值 | 生成一个新的结构体代替原来的结构体,所以let常量的结构体不能被改变。即内存的改变在原地进行的行为就是写时复制,结构体属性被改变的一瞬间是唯一的。
值语义的特性之一 mutating(inout &) self ,将隐式的self标记inout。inout(标记必须是可变的实参变量)将可变实参标记为可变形参(函数形参默认不可变self),返回时将旧的值self覆盖。这种方法一般不以ed和ing结尾结尾,无法对字面量和常量(不可变的形式参数)结构体进行修改。
常见错误: error: cannot use mutating getter(不能在形式参数上使用var) on immutable value: ‘函数形式参数’ is a 'let' constant.
技术总结: 当你要在写时复制的值类型中使用一些类似Foundation数据类型或者Class类型的时候需要手动复制他们的实例或者同时为它们定义一个有限制的使用接口,进而保持值语义,保证值类型复制过程中不同类实例之间是不可变的(高效的深拷贝)。
写时复制的陷阱: 避免间接访问产生的不必要的复制,Swift字典和集合、其他类型(包括结构体和Class)的下标访问(只有swift数组除外)会间接产生不必要的复制,直接访问值类型的时候可以获取写时复制的优化。在不需要值语义的前提下,你可以将值类型改写成Class类型,避免间接访问产生不必要的复制 (苹果期望将Array的地址器技术应用到字典和集合中,地址器技术未来可能会文档化)。
构造过程具体在playground中演示
构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例使用前有个过程是必须的,它包括设置实例中每个存储属性的初始值二段式构造过程阶段一和执行其他必须的设置二段式构造过程阶段二或构造过程代理。
你要通过定义构造器来实现构造过程,它就像用来创建特定类型新实例的特殊方法。与 Objective-C 中的构造器不同,Swift 的构造器没有返回值。它们的主要任务是保证某种类型的新实例在第一次使用前完成正确的初始化。
默认构造器
如果结构体或类为所有属性提供了默认值,又没有提供任何自定义的构造器,那么 Swift 会自动给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。
结构体的逐一成员构造器
结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能会获得逐一成员构造器。
逐一成员构造器是用来初始化结构体新实例里成员属性的快捷方法。新实例的属性初始值可以通过名字传入逐一成员构造器中。
值类型的构造器代理代理都是指调用方法
构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理(调用),它能避免多个构造器间的代码重复。构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类(请参考 继承)。这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。这些责任将在后续章节 类的继承和构造过程 中介绍。
对于值类型,你可以使用 self.init 在自定义的构造器中引用相同类型中的其它构造器。并且你只能在构造器内部调用 self.init。
如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器<包括class类型,也无法访问默认构造器>(如果是结构体,还将无法访问逐一成员构造器)。此约束可避免因为使用自动(默认或逐一成员)初始化器而意外绕过更复杂的自定义初始化器的情况,其中复杂初始化器提供的额外重要的设置。
类的继承和构造过程
类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。
Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们被称为指定构造器和便利构造器。
指定构造器和便利构造器
类的指定构造器的写法跟值类型简单构造器一样。
便利构造器也采用相同样式的写法,但需要在 init 关键字之前放置 convenience 关键字,并使用空格将它们俩分开。
1⃣️指定构造器是Class类型中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续往上进行。许多类通过继承了父类中的指定构造器而满足了这个条件 ———————— 普遍每一个类都必须至少拥有一个指定构造器。2⃣️只在必要的时候为类提供便利构造器,通过使用便利构造器来快捷调用某个指定构造器,可以为便利构造器的部分形参提供默认值。创建一个特殊用途或特定输入值的实例,能够节省更多开发时间并让类的构造过程更清晰明了。
类类型的构造器代理代理都是指调用方法
为了简化指定构造器和便利构造器之间的调用关系,Swift 构造器之间的代理调用遵循以下三条规则:
规则 1
指定构造器必须调用其直接父类的的指定构造器。
规则 2
便利构造器必须调用同类中定义的其它构造器(包括指定构造器和其他便利构造器)。
规则 3
便利构造器最后必须调用指定构造器。
一个更方便记忆的方法是:
指定构造器必须总是向上代理
便利构造器必须总是横向代理
这些规则可以通过下面图例来说明:
两段式构造过程
Swift 中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值1。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性2。
两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。
Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程不出错地完成:
安全检查 1
指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。阶段1最后一步为了满足这一规则,指定构造器必须保证它所在类的属性在它往上代理之前先完成初始化。
安全检查 2
指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
安全检查 3
便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。如果没这么做,便利构造器赋予的新值将被该类的指定构造器所覆盖。
安全检查 4
构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。
类的实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,类的实例才是有效的,才能访问属性和调用方法。
以下是基于上述安全检查的两段式构造过程展示:
阶段 1
* 类的某个指定构造器或便利构造器被调用。
* 完成类的新实例内存的分配,但此时内存还没有被初始化。
* 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
* 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
* 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
* 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。安全检查 1此时阶段 1 完成。
阶段 2
* 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。子类可以在构造过程修改继承来的变量属性,但是不能修改继承来的常量属性。
* 最终,继承链中任意的便利构造器有机会自定义实例和使用 self。
Tip: ⓵如果子类的构造器没有在阶段 2 过程中做自定义操作,②并且父类有一个无参数的指定构造器,😍你可以在所有子类的存储属性赋值之后隐式调用(省略) super.init() 。
下图展示了在假定的子类和父类之间的构造阶段 1:
以下展示了相同构造过程的阶段 2:
构造器的继承和重写:
跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有被完全初始化或被错误初始化。父类的构造器仅会在安全和适当的某些情况下被继承。具体内容请参考后续章节 构造器的自动继承。
当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上 override 修饰符。
当你重写一个父类的指定构造器时,你总是需要写 override 修饰符,即使是为了实现子类的便利构造器。相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文 类的构造器代理规则 有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加 override 修饰符。
Tip: ⓵如果子类的构造器没有在阶段 2 过程中做自定义操作,②并且父类有一个无参数的指定构造器,😍你可以在所有子类的存储属性赋值之后隐式调用(省略) super.init() 。
构造器的自动继承:
如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。
假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用:
规则 1
如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
规则 2
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。
即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。
注意
子类可以将父类的指定构造器实现为便利构造器来满足规则 2。
指定构造器和便利构造器实践:
下图展示了这三个类的构造器链, 代码见playground文件:
可失败构造器
有时,定义一个构造器可失败的类,结构体或者枚举是很有用的。这里所指的“失败” 指的是,如给构造器传入无效的形参,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。其语法为在 init 关键字后面添加问号(init?)。
注意
可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。
可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过 return nil 语句来表明可失败构造器在何种情况下应该 “失败”。严格来说,构造器都不支持返回值,因此而不要用关键字 return 来表明构造成功。
1⃣️枚举类型的可失败构造器||2⃣️带原始值的枚举类型的可失败构造器
你可以通过一个带一个或多个形参的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的形参无法匹配任何枚举成员,则构造失败。
带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:),该可失败构造器有一个合适的原始值类型的 rawValue 形参,选择找到的相匹配的枚举成员,找不到则构造失败。
构造失败的传递
类、结构体、枚举的可失败构造器可以横向代理到它们自己其他的可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。
无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。
注意
可失败构造器也可以代理到其它的不可失败构造器。通过这种方式,你另外需要增加一个可能的失败状态到现有的构造过程中。
重写一个可失败构造器
如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。
注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。
注意
你可以用非可失败构造器重写可失败构造器,但反过来却不行。
init! 可失败构造器
通常来说我们通过在 init 关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以通过在 init 后面添加感叹号的方式来定义一个可失败构造器(init!),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。
你可以在 init? 中代理到 init!,反之亦然。你也可以用 init? 重写 init!,反之亦然。你还可以用 init 代理到 init!,不过,一旦 init! 构造失败,则会触发一个断言。
必要构造器
required 修饰符表明所有该类的子类都必须实现该构造器
子类重写父类的必要构造器时,必须添加上 required 修饰符,但不需要添加 override 修饰符。
如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。
通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些自定义或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变量,作为属性的默认值。
注意
如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的 self 属性,或者调用任何实例方法。
析构过程
析构器只适用于类类型,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字 deinit 来标示,类似于构造器要用 init 来标示。
析构过程原理
Swift 会自动释放不再需要的实例以释放资源。如 自动引用计数 章节中所讲述,Swift 通过自动引用计数(ARC) 处理实例的内存管理。通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前手动去关闭该文件。
在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数和圆括号
析构器是在实例释放发生前被自动调用的。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。
因为直到实例的析构器被调用后,实例才会被释放,所以析构器可以访问实例的所有属性,并且可以根据那些属性可以修改它的行为(比如查找一个需要被关闭的文件)。