> 文章列表 > Swift5编程总结

Swift5编程总结

Swift5编程总结

Swift5总结

关于Swift

Swift通过采用现代编程模式来定义大多数常见的编程错误:

  • 变量在使用前始终初始化。
  • 检查数组索引是否存在越界错误。
  • 检查整数是否溢出。
  • Optionals确保nil明确处理值。
  • 内存自动管理。
  • 错误处理允许从意外故障中受控恢复。

Swift初见

简单的值

常量或者变量的类型必须和赋给它们的值一样。然而,不用明确地声明类型。当通过一个值来声明变量和常量时,编译器会自动推断其类型。

如果初始值没有提供足够的信息(或者没有初始值),那需要在变量后面声明类型,用冒号分割。

值永远不会隐式转换为其他类型。如果需要将值转换为其他类型,请显式创建所需类型的实例。

处理可选值:

  • 使用if-let、guard-let解包
  • 使用??运算符提供默认值
  • 使用!运算符强制解包

在操作(比如方法、属性和子脚本)之前加 ?。如果 ? 之前的值是 nil,? 后面的东西都会被忽略,并且整个表达式返回 nil

switch 语句中匹配到的 case 语句之后,就会退出 switch 语句,并不会继续向下运行,所以不需要在每个子句结尾写 break。

字典是无序集合,迭代字典的键和值时顺序不确定。

闭包

简写闭包

  • 已知类型的闭包可以省略参数类型和返回类型
  • 单一语句隐式返回该条语句的值可以省略return关键字
  • 可以使用($+编号)来引用闭包的参数
  • 当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在圆括号后面。
  • 当闭包是函数的唯一参数时,可以省略圆括号

对象和类

不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用 willSet 和 didSet。写入的代码会在属性值发生改变时调用,但不包含构造器中发生值改变的情况。

处理变量的可选值时,可以在操作(比如方法、属性和子脚本)之前加 ?。如果 ? 之前的值是 nil,? 后面的东西都会被忽略,并且整个表达式返回 nil。否则,可选值会被解包,之后的所有代码都会按照解包后的值运行。在这两种情况下,整个表达式的值也是一个可选值。

枚举和结构体

如果枚举成员的实例有原始值,那么这些值是在声明的时候就已经决定了,这意味着不同枚举实例的枚举成员总会有一个相同的原始值。当然我们也可以为枚举成员设定关联值,关联值是在创建实例时决定的。这意味着同一枚举成员不同实例的关联值可以不相同。可以把关联值想象成枚举成员实例的存储属性。

协议和扩展

mutating 关键字用来标记一个会修改结构体的方法。类的声明不需要标记任何方法,因为类中的方法通常可以修改类属性(类的性质)。

可以像使用其他命名类型一样使用协议名,但是当处理类型是协议的值时,协议外定义的方法不可用。

错误处理

使用 throw 来抛出一个错误和使用 throws 来表示一个可以抛出错误的函数。

defer 代码块来表示在函数返回前,函数中最后执行的代码。无论函数是否会抛出错误,这段代码都将执行。

泛型

在类型名后面使用 where 来指定对类型的一系列需求。

用try?将结果转换为可选的。如果函数抛出错误,则丢弃特定错误,结果为nil。否则,结果是一个包含函数返回的值的可选值。

基础

Swift和OC的比较:

  • 包含C和OC的基础数据类型
  • 常量更加强大方便
  • 提供高阶数据类型元组可以创建或者传递一组数据
  • 提供可选(Optional)类型处理值缺失的情况,比nil更具有表现力
  • 类型安全,清楚的知道值的类型

常量和变量

一般来说很少需要写类型注解。如果在声明常量或者变量的时候赋了一个初始值,Swift 可以推断出这个常量或者变量的类型。

分号

在同一行内写多条独立的语句的情况下必须要用分号。

类型安全和类型推断

  • 类型安全的语言可以清楚地知道代码要处理的值的类型。
  • 编译代码时进行类型检查(type checks),并把不匹配的类型标记为错误。

因为有类型推断,和 C 或者 Objective-C 比起来 Swift 很少需要声明类型。

数值类型转换

结合数字类常量和变量不同于结合数字类字面量。字面量 3 可以直接和字面量 0.14159 相加,因为数字字面量本身没有明确的类型。它们的类型只在编译器需要求值的时候被推测。

类型别名

类型别名(type aliases)就是给现有类型定义另一个名字。可以使用 typealias 关键字来定义类型别名。

可选值解包

  • 使用!强制解包
  • 可选绑定,在 if 、guard 和 while 语句中如果有值就把值赋给一个临时常量或者变量
  • 把想要用作可选的类型的后面的问号(String?)改成感叹号(String!)来声明一个隐式解析可选类型(在隐式解包可选类型没有值的时候尝试取值,会触发运行时错误。和在没有值的普通可选类型后面加一个惊叹号一样)

Swift 的 nil 和 Objective-C 中的 nil 并不一样。在 Objective-C 中,nil 是一个指向不存在对象的指针。在 Swift 中,nil 不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 nil,不只是对象类型。

错误处理

通过在函数的声明中包含关键字throws来表示它可以引发错误,throw 关键词在函数中抛出错误。当调用可以抛出错误的函数时,将try关键字添加到表达式中。

一个 do 语句创建了一个新的包含作用域,使得错误能被传播到一个或多个 catch 从句。

断言和先决条件

断言和先决条件可以强制检查数据和程序状态,使得程序可预测的中止(不是系统强制的,被动的中止),并帮助使这个问题更容易调试。

  • 断言仅在调试环境运行,在生产环境中,断言的条件将不会进行计算。这个意味着在开发阶段可以使用很多断言,但是这些断言在生产环境中不会产生任何影响。
  • 先决条件则在调试环境和生产环境中运行。

使用 unchecked 模式(-Ounchecked)编译代码,先决条件将不会进行检查,编译器假设所有的先决条件总是为 true(真)。fatalError(_:file:line:) 函数总是中断执行,无论怎么进行优化设定。

基本运算符

与C和Objective-C中的赋值运算符不同,Swift中的赋值运算符本身不返回值。通过使if x = y无效,Swift可以帮助避免代码中的这类错误。

元组的比较从左到右进行,一次一个值,直到比较找到两个不相等的值。Swift标准库包含的元组比较运算符,元组必须少于7个元素。要将七个或更多元素的元组进行比较,您必须自己实现比较运算符。

空合运算符(a ?? b)如果可选值a包含一个值则进行解包,如果是nil返回一个默认b值。表达式a始终是可选类型。表达式b必须与存储在a中的类型匹配。

字符和字符串

多行字符串字面量 - 由三个双引号括起来的字符序列。

  • 多行字符串字面量中,如果想换行,以便加强代码的可读性,但是又不想在多行字符串字面量中出现换行符的话,在这些行的末尾写一个反斜杠(\\)作为续行符。
  • 要创建以换行符开头或结尾的多行字符串字面量,请将空行写为第一行或最后一行。
  • 可以缩进多行字符串。在右引号标记(“”")之前的空格告诉Swift在所有其他行之前要忽略那些空格(如果有行的缩进空格不够会报错)。

将字符串放在引号(")中并用数字符号(#)括起来。例如,打印字符串文字 #“Line 1 \\nLine 2”# 会打印换行符转义序列(\\n)而不是给文字换行。

如果需要字符串文字中字符的特殊效果,请匹配转义字符(\\)后面添加与起始位置个数相匹配的 # 符。 例如,如果字符串是 #“Line 1 \\nLine 2”# 并且想要换行,则可以使用 #“Line 1 #nLine 2”# 来代替。

  • 字符串可以通过加法运算符(+)相加在一起(或称“连接”)创建一个新的字符串
  • 也可以通过加法赋值运算符(+=)将一个字符串添加到一个已经存在字符串变量上
  • 可以用 append() 方法将一个字符附加到一个字符串变量的尾部

插值字符串中写在括号中的表达式不能包含非转义反斜杠(\\),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。

可扩展的字形群可以由多个 Unicode 标量组成,使用可拓展的字符群集作为 Character 值来连接或改变字符串时,并不一定会更改字符串的字符数量。

count 属性必须遍历全部的 Unicode 标量,来确定字符串的字符数量。

通过 count 属性返回的字符数量并不总是与包含相同字符的 NSString 的 length 属性相同。NSString 的 length 属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。

可以使用 startIndex 和 endIndex 属性或者 index(before:) 、index(after:) 和 index(_:offsetBy:) 方法操作字符串的索引。

endIndex 属性不能作为一个字符串的有效下标。如果 String 是空串,startIndex 和 endIndex 是相等的。

获取越界索引对应的 Character,将引发一个运行时错误。

使用 insert(:at:)、insert(contentsOf:at:)、remove(at:) 和 removeSubrange(😃 方法操作字符串。

String 和 SubString 的区别在于性能优化上,SubString 可以重用原 String 的内存空间,或者另一个 SubString 的内存空间(String 也有同样的优化,但如果两个 String 共享内存的话,它们就会相等)。这一优化意味着在修改 String 和 SubString 之前都不需要消耗性能去复制内存。

只要可扩展的字形群集有同样的语言意义和外观则认为它们标准相等,即使它们是由不同的 Unicode 标量构成。

集合类型

Swift 中的数组、集合和字典必须明确其中保存的键和值类型,这样就可以避免插入一个错误数据类型的值。同理,对于获取到的值也可以放心,其数据类型是确定的。Swift 的数组、集合和字典类型被实现为泛型。

如果把数组、集合或字典分配成常量,那么它就是不可变的,它的大小和内容都不能被改变。

相同的值可以多次出现在一个数组的不同位置中。Swift 的 Array 类型被桥接到 Foundation 中的 NSArray 类。

数组的enumerated()方法返回一个由索引值和数据值组成的元组数组。

当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。和数组不同的是,集合没有等价的简化形式。

一个集合类型不能从数组字面量中被直接推断出来,因此 Set 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果想使用一个数组字面量构造一个集合并且与该数组字面量中的所有元素类型相同,那么无须写出集合(元素)的具体类型。

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]

Swift 的所有基本类型(比如 String、Int、Double 和 Bool)默认都是可哈希化的,可以作为集合值的类型或者字典键的类型。没有关联值的枚举成员值(在 枚举 有讲述)默认也是可哈希化的。

一个集合类型不能从数组字面量中被直接推断出来,因此 Set 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果想使用一个数组字面量构造一个集合并且与该数组字面量中的所有元素类型相同,那么无须写出集合的具体类型。

Set 类型没有确定的顺序,为了按照特定顺序来遍历一个集合中的值可以使用 sorted() 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符 < 对元素进行比较的结果来确定。

  • intersection(_😃 方法根据两个集合的交集创建一个新的集合。

  • symmetricDifference(_😃 方法根据两个集合不相交的值创建一个新的集合。

  • union(_😃 方法根据两个集合的所有值创建一个新的集合。

  • subtracting(_😃 方法根据不在另一个集合中的值创建一个新的集合。

  • “是否相等”运算符(==)来判断两个集合包含的值是否全部相同。

  • isSubset(of:) 方法来判断一个集合中的所有值是否也被包含在另外一个集合中。

  • isSuperset(of:) 方法来判断一个集合是否包含另一个集合中所有的值。

  • isStrictSubset(of:) 或者 isStrictSuperset(of:) 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。

  • isDisjoint(with:) 方法来判断两个集合是否不含有相同的值(是否没有交集)。

字典是一种无序的集合,它存储的是键值对之间的关系,其所有键的值需要是相同的类型,所有值的类型也需要相同。一个字典的 Key 类型必须遵循 Hashable 协议,就像 Set 的值类型。

字典的updateValue(_:forKey:) 方法会返回对应值类型的可选类型。

Swift 的 Dictionary 是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 keys 或 values 属性使用 sorted() 方法。

控制流

使用 stride(from:to:by:) 函数跳过不需要的标记。可以在闭区间使用 stride(from:through:by:) 起到同样作用。

当一个 switch 分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让 switch 分支达到被忽略的效果。应该使用 break 来忽略某个分支。

fallthrough 关键字不会检查它下一个将会落入执行的 case 中的匹配条件。fallthrough 简单地使代码继续连接到下一个 case 中的代码,这和 C 语言标准中的 switch 语句特性是一样的。

使用标签(statement label)来标记一个循环体或者条件语句,对于一个条件语句,可以使用 break 加标签的方式,来结束这个被标记的语句。对于一个循环语句,可以使用 break 或者 continue 加标签,来结束或者继续这条被标记语句的执行。

不同于 if 语句,一个 guard 语句总是有一个 else 从句,如果条件不为真则执行 else 从句中的代码。

guard 语句的 else 分支必须转移控制以退出 guard 语句出现的代码段。可以用控制转移语句如 return、break、continue 或者 throw 做这件事,或者调用一个不返回的方法或函数,例如 fatalError()。

相比于可以实现同样功能的 if 语句,按需使用 guard 语句会提升我们代码的可读性。它可以使代码连贯的被执行而不需要将它包在 else 块中,它可以使在紧邻条件判断的地方,处理违规的情况。

函数

输入输出参数不能有默认值,而且可变参数不能用 inout 标记。

闭包

隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字

尾随闭包改写在方法圆括号的外面,如果闭包表达式是函数或方法的唯一参数,当使用尾随闭包时,可以把 () 省略掉。

尾随闭包语法,优雅地在函数后封装了闭包的具体功能,而不再需要将整个闭包包裹在 map(_😃 方法的括号内。

逃逸闭包,必须显式地引用 self;非逃逸闭包,可以隐式引用 self。

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。函数通过将参数标记为 @autoclosure 来接收一个自动闭包。

自动闭包能够延迟求值,因为直到调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得能控制代码的执行时机。

枚举

枚举定义了一个全新的类型,像 Swift 中其他类型一样。

令枚举遵循 CaseIterable 协议。Swift 会生成一个 allCases 属性,用于表示一个包含枚举所有成员的集合。有关联值或原始值的枚举不能遵守 CaseIterable 协议

如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,可以只在成员名称前标注一个 let 或者 var。

原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。

在枚举类型开头加上 indirect 关键字来表明它的所有成员都是可递归的。

结构体和类

Swift 中结构体和类有很多共同点。两者都可以:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作用于通过下标语法访问它们的值
  • 定义构造器用于设置初始值
  • 通过扩展以增加默认实现之外的功能
  • 遵循协议以提供某种标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着大多数自定义数据类型都会是结构体和枚举。

结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。类没有默认的成员逐一构造器。

Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。

标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而在代码中看起来就像是立即发生了复制。

属性

结构体实例赋值给一个常量,则无法修改该实例的任何属性,即使被声明为可变属性也不行。引用类型的类则不一样。把一个引用类型的实例赋给一个常量后,依然可以修改该实例的可变属性。

必须将延时加载属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。

如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。

可以为除了延时加载存储属性之外的其他存储属性添加属性观察器,也可以在子类中通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSet 和 didSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。

带有观察器的属性通过 in-out 方式传入参数,willSet 和 didSet 也会调用。这是因为 in-out 参数采用了拷入拷出内存模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。

属性包装器

全局的常量或变量都是延迟计算的,跟延时加载存储属性相似,不同的地方在于,全局的常量或变量不需要标记 lazy 修饰符。

跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。

方法

在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,不仅能选择是否要定义一个类/结构体/枚举,还能灵活地在创建的类型(类/结构体/枚举)上定义方法。

实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。实例方法只能被它所属的类的某个特定实例调用。实例方法不能脱离于现存的实例而被调用。

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。如果确实需要在某个特定的方法中修改结构体或者枚举的属性,可以为这个方法选择可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的 self 属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。

在方法的 func 关键字之前加上关键字 static,来指定类型方法。类还可以用关键字 class 来指定,从而允许子类重写父类该方法的实现。

在 Objective-C 中,只能为 Objective-C 的类类型(classes)定义类型方法(type-level methods)。在 Swift 中,可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。

下标

下标可以定义在类、结构体和枚举中,是访问集合、列表或序列中元素的快捷方式。

下标可以接受任意数量的入参,并且这些入参可以是任意类型。下标的返回值也可以是任意类型。下标可以使用可变参数,但是不能使用 in-out 参数以及不能提供默认参数。

一个类或结构体可以根据自身需要提供多个下标实现,使用下标时将通过入参的数量和类型进行区分,自动匹配合适的下标。它通常被称为下标的重载。

继承

Swift 中的类并不是从一个通用的基类继承而来的。如果不为自己定义的类指定一个超类的话,这个类就会自动成为基类。

可以重写继承来的实例属性或类型属性,提供自己定制的 getter 和 setter,或添加属性观察器,使重写的属性可以观察到底层的属性值什么时候发生改变。

可以提供定制的 getter(或 setter)来重写任何一个继承来的属性,无论这个属性是存储型还是计算型属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。在重写一个属性时,必须将它的名字和类型都写出来。这样才能使编译器去检查重写的属性是与超类中同名同类型的属性相匹配的。

可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 getter 和 setter 即可。但是不可以将一个继承来的读写属性重写为一个只读属性。

如果在重写属性中提供了 setter,那么也一定要提供 getter。如果不想在重写版本中的 getter 里修改继承来的属性值,可以直接通过 super.someProperty 来返回继承来的值。

不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供 willSet 或 didSet 实现也是不恰当。不可以同时提供重写的 setter 和重写的属性观察器。如果想观察属性值的变化,并且已经为那个属性提供了定制的 setter,那么在 setter 中就可以观察到任何值变化了。

通过把方法,属性或下标标记为 final 来防止它们被重写,只需要在声明关键字前加上 final 修饰符即可。

通过在关键字 class 前添加 final 修饰符(final class)来将整个类标记为 final 。这样的类是不可被继承的,试图继承这样的类会导致编译报错。

构造过程

在新实例使用前有个过程是必须的,它包括设置实例中每个存储属性的初始值和执行其他必须的设置或构造过程。

与 Objective-C 中的构造器不同,Swift 的构造器没有返回值。它们的主要任务是保证某种类型的新实例在第一次使用前完成正确的初始化。

当为存储型属性分配默认值或者在构造器中为设置初始值时,它们的值是被直接设置的,不会触发任何属性观察者。

如果一个属性总是使用相同的初始值,那么为其设置一个默认值比每次都在构造器中赋值要好。两种方法的最终结果是一样的,只不过使用默认值让属性的初始化和声明结合得更紧密。它能让构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型;同时,它也能充分利用默认构造器、构造器继承等特性。

自定义的类型有一个逻辑上允许值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时机可以赋值为空——都需要将它声明为 可选类型。可选类型的属性将自动初始化为 nil,表示这个属性是特意在构造过程设置为空。

可以在构造过程中的任意时间点给常量属性赋值,一旦常量属性被赋值,它将永远不可更改。

对于类的实例来说,常量属性(没有初始化)只能在定义它的类的构造过程中修改,不能在子类中修改。

如果结构体或类为所有属性提供了默认值,又没有提供任何自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。调用一个逐一成员构造器(memberwise initializer)时,可以省略任何一个有默认值的属性。

如果结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能会获得逐一成员构造器。

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能避免多个构造器间的代码重复。

如果为某个值类型定义了一个自定义的构造器,将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这种限制避免了在一个更复杂的构造器中做了额外的重要设置,但有人不小心使用自动生成的构造器而导致错误的情况。假如希望默认构造器、逐一成员构造器以及自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。

类的继承和构造过程

类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。

指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续往上进行。每一个类都必须至少拥有一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。

便利构造器是类中比较次要的、辅助型的构造器。只在必要的时候为类提供便利构造器,便利构造器也采用相同样式的写法,但需要在 init 关键字之前放置 convenience 关键字,并使用空格将它们俩分开。

Swift 构造器之间的代理调用遵循以下三条规则:

  • 指定构造器必须调用其直接父类的的指定构造器
  • 便利构造器必须调用同类中定义的其它构造器。
  • 便利构造器最后必须调用指定构造器。

一个更方便记忆的方法是:指定构造器必须总是向上代理,便利构造器必须总是横向代理。

Swift 中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。

安全检查1:指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
安全检查2:指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。如果没这么做(编译报错),指定构造器赋予的新值将被父类中的构造器所覆盖。
安全检查3:便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。如果没这么做(编译报错),便利构造器赋予的新值将被该类的指定构造器所覆盖。
安全检查4:构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

阶段1

  • 类的某个指定构造器或便利构造器被调用。
  • 完成类的新实例内存的分配,但此时内存还没有被初始化。
  • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
  • 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
  • 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
  • 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。

阶段2

  • 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
  • 最终,继承链中任意的便利构造器有机会自定义实例和使用 self。

Swift 中的子类默认情况下不会继承父类的构造器,Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类的新实例时没有完全或错误地被初始化。

可以在子类中提供这些构造器的自定义实现,当在编写一个和父类中指定构造器相匹配的子类构造器时,实际上是在重写父类的这个指定构造器。因此,必须在定义子类构造器时带上 override 修饰符。即使重写的是系统自动提供的默认构造器,也需要带上 override 修饰符。相反,如果编写一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器,因此,严格意义上来讲,子类并未对父类构造器提供重写。最后的结果就是,在子类中“重写”一个父类便利构造器时,不需要加 override 修饰符。

构造器自动继承规则:

  • 如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
  • 如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。

子类可以将父类的指定构造器实现为便利构造器。

如果代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

当用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。

可以用非可失败构造器重写可失败构造器,但反过来却不行。

在 init 关键字后添加问号的方式(init?)来定义一个可失败构造器,但也可以通过在 init 后面添加感叹号的方式来定义一个可失败构造器(init!),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器。在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符。如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。

如果某个存储型属性的默认值需要一些自定义或设置,可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着不能在闭包里访问其它属性,即使这些属性有默认值。同样,也不能使用隐式的 self 属性,或者调用任何实例方法。

析构过程

析构器是在实例释放发生前被自动调用的。不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。

可选链

错误处理

Swift 中有 4 种处理错误的方式:

  • 可以把函数抛出的错误传递给调用此函数的代码
  • 用 do-catch 语句处理错误
  • 将错误作为可选类型处理
  • 或者断言此错误根本不会发生。

只有 throwing 函数可以传递错误。任何在某个非 throwing 函数内部抛出的错误只能在函数内部处理。

使用 try? 通过将错误转换成一个可选值来处理错误。如果是在计算 try? 表达式时抛出错误,该表达式的结果就为 nil。

在表达式前面写 try! 来禁用错误传递,这会把调用包装在一个不会有错误抛出的运行时断言中。如果真的抛出了错误,会得到一个运行时错误。

使用 defer 语句在即将离开当前代码块时执行一系列语句。该语句能执行一些必要的清理工作,不管是以何种方式离开当前代码块的——无论是由于抛出错误而离开,或是由于诸如 return、break 的语句。

defer 语句将代码的执行延迟到当前的作用域退出之前。延迟执行的语句不能包含任何控制转移语句,例如 break、return 语句,或是抛出一个错误。延迟执行的操作会按照它们声明的顺序从后往前执行——也就是说,第一条 defer 语句中的代码最后才执行,第二条 defer 语句中的代码倒数第二个执行,以此类推。最后一条语句会第一个执行。

类型转换

Swift 为不确定类型提供了两种特殊的类型别名:

  • Any 可以表示任何类型,包括函数类型。
  • AnyObject 可以表示任何类类型的实例。

Any 类型可以表示所有类型的值,包括可选类型。Swift 会在用 Any 类型来表示一个可选值的时候,给一个警告。如果确实想使用 Any 类型来承载可选值,可以使用 as 操作符显式转换为 Any

扩展

扩展和 Objective-C 的分类很相似。(与 Objective-C 分类不同的是,Swift 扩展是没有名字的。)

Swift 中的扩展可以:

  • 添加计算型实例属性和计算型类属性
  • 定义实例方法和类方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使已经存在的类型遵循(conform)一个协议

扩展可以给一个类型添加新的功能,但是不能重写已经存在的功能。

扩展可以给一个类添加新的便利构造器,但是它们不能给类添加新的指定构造器或者析构器。指定构造器和析构器必须始终由类的原始实现提供。

如果使用扩展给一个值类型添加构造器只是用于给所有的存储属性提供默认值,并且没有定义任何自定义构造器,那么可以在该值类型扩展的构造器中使用默认构造器和成员构造器。

协议

协议 规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。

协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。

协议还指定属性是可读的还是可读可写的。如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法提供默认参数。

实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。

在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,都必须为构造器实现标上 required 修饰符。

如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。

如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required 和 override 修饰符。

协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。

通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型准守(以及非结构体或者非枚举的类型)。

协议组合使用 SomeProtocol & AnotherProtocol 的形式。可以列举任意数量的协议,用和符号(&)分开。除了协议列表,协议组合也能包含类类型,这允许标明一个需要的父类。协议组合行为就和定义的临时局部协议一样拥有构成中所有协议的需求。协议组合不定义任何新的协议类型。

使用 类型转换 中描述的 is 和 as 操作符来检查协议一致性,即是否遵循某协议,并且可以转换到指定的协议类型。检查和转换协议的语法与检查和转换类型是完全一样的:

  • is 用来检查实例是否遵循某个协议,若遵循则返回 true,否则返回 false;
  • as? 返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回 nil;
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。

在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上 @objc 属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。

通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。通过协议扩展,所有遵循协议的类型,都能自动获得这个扩展所增加的方法实现。

协议扩展可以为遵循协议的类型增加实现,但不能声明该协议继承自另一个协议。协议的继承只能在协议声明处进行指定。

通过协议扩展来为协议要求的方法、计算属性提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。

通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。

可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述。

泛型

泛型代码能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。避免编写重复的代码,而是用一种清晰抽象的方式来表达代码的意图。

始终使用大写字母开头的驼峰命名法(例如 T 和 MyTypeParameter)来为类型参数命名,以表明它们是占位类型,而不是一个值。

当对泛型类型进行扩展时,并不需要提供类型参数列表作为定义的一部分。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

当自定义泛型类型时,可以定义自己的类型约束,这些约束将提供更为强大的泛型编程能力。像 可哈希(hashable) 这种抽象概念根据它们的概念特征来描述类型,而不是它们的具体类型。

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

可以利用扩展让一个已存在的类型遵循一个协议,这包括使用了关联类型协议。

可以在协议里给关联类型添加约束来要求遵循的类型满足约束。

类型约束 能够为泛型函数、下标、类型的类型参数定义一些强制要求。

对关联类型添加约束通常是非常有用的。可以通过定义一个泛型 where 子句来实现。通过泛型 where 子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。可以通过将 where 关键字紧跟在类型参数列表后面来定义 where 子句,where 子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。可以在函数体或者类型的大括号之前添加 where 子句。

可以使用泛型 where 子句作为扩展的一部分。

可以在关联类型后面加上具有泛型 where 的字句。

下标可以是泛型,它们能够包含泛型 where 子句。可以在 subscript 后用尖括号来写占位符类型,还可以在下标代码块花括号前写 where 子句。

不透明类型

具有不透明返回类型的函数或方法会隐藏返回值的类型信息。函数不再提供具体的类型作为返回类型,而是根据它支持的协议来描述返回值。在处理模块和调用代码之间的关系时,隐藏类型信息非常有用,因为返回的底层数据类型仍然可以保持私有。而且不同于返回协议类型,不透明类型能保证类型一致性 —— 编译器能获取到类型信息,同时模块使用者却不能获取到。

可以认为不透明类型和泛型相反。泛型允许调用一个方法时,为这个方法的形参和返回值指定一个与实现无关的类型。不透明类型允许函数实现时,选择一个与调用代码无关的返回类型。

函数中有多个地方返回了不透明类型,那么所有可能的返回值都必须是同一类型。即使对于泛型函数,不透明返回类型可以使用泛型参数,但仍需保证返回类型唯一。

返回类型始终唯一的要求,并不会影响在返回的不透明类型中使用泛型,只是在返回的底层类型中使用了泛型参数。

不透明类型作为函数返回值,看起来和返回协议类型非常相似,但这两者有一个主要区别,就在于是否需要保证类型一致性。一个不透明类型只能对应一个具体的类型,即便函数调用者并不能知道是哪一种类型;协议类型可以同时对应多个类型,只要它们都遵循同一协议。总的来说,协议类型更具灵活性,底层类型可以存储更多样的值,而不透明类型对这些底层类型有更强的限定。

将协议类型作为函数的返回类型能更加灵活,函数只要返回遵循协议的类型即可。然而,更具灵活性导致牺牲了对返回值执行某些操作的能力。为什么不能使用 == 运算符 —— 它依赖于具体的类型信息,而这正是使用协议类型所无法提供的。

相比之下,不透明类型则保留了底层类型的唯一性。Swift 能够推断出关联类型,这个特点使得作为函数返回值,不透明类型比协议类型有更大的使用场景。

不能将有一个关联类型的协议作为方法的返回类型,也不能将它用于对泛型返回类型的约束,因为函数体之外并没有暴露足够多的信息来推断泛型类型。

自动引用计数

引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。

每当创建一个新的类实例时,ARC 会分配一块内存来储存该实例的信息。内存中会包含实例的类型信息,以及这个实例所关联的任何存储属性的值。当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。这确保了不再被使用的实例,不会一直占用内存空间。

当 ARC 回收并释放了正在被使用中的实例后,该实例的属性和方法将不能再被访问和调用。实际上,如果试图访问这个实例,应用程序很可能会崩溃。无论将实例赋值给属性、常量或变量,它们都会创建此实例的强引用。之所以称之为“强”引用,是因为它会将实例牢牢地保持住,只要强引用还在,实例是不允许被销毁的。

弱引用和无主引用允许循环引用中的一个实例引用另一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。

当其他的实例有更短的生命周期时,使用弱引用,也就是说,当其他实例析构在先时。相比之下,当其他实例有相同的或者更长生命周期时,请使用无主引用。

当 ARC 设置弱引用为 nil 时,属性观察不会被触发。

声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其弱引用赋值为 nil。并且因为弱引用需要在运行时允许被赋值为 nil,所以它们会被定义为可选类型变量,而不是常量。

声明属性或者变量时,在前面加上关键字 unowned 表示这是一个无主引用。无主引用通常都被期望拥有值。不过 ARC 无法在实例被销毁后将无主引用设为 nil,因为非可选类型的变量不允许被赋值为 nil。

使用无主引用,必须确保引用始终指向一个未销毁的实例。如果试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。

对于需要禁用运行时的安全检查的情况(例如,出于性能方面的原因),Swift 还提供了不安全的无主引用。与所有不安全的操作一样,需要负责检查代码以确保其安全性。 可以通过 unowned(unsafe) 来声明不安全无主引用。如果试图在实例被销毁后,访问该实例的不安全无主引用,程序会尝试访问该实例之前所在的内存地址,这是一个不安全的操作。

  • 两个属性的值都允许为 nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。
  • 一个属性的值允许为 nil,而另一个属性的值不允许为 nil,这也可能会产生循环强引用。这种场景最适合通过无主引用来解决。
  • 两个属性都必须有值,并且初始化完成后永远不会为 nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解包可选值属性(利用隐式解包可选值默认为nil,从而满足了类的构造器的两个构造阶段的要求)。

使用隐式解包可选值值意味着满足了类的构造器的两个构造阶段的要求。属性在初始化完成后,能像非可选值一样使用和存取,同时还避免了循环强引用。

闭包和类相似,都是引用类型。

在默认的闭包中可以使用 self,因为只有当初始化完成以及 self 确实存在后,才能访问 lazy 属性。

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。捕获列表中的每一项都由一对元素组成,一个元素是 weak 或 unowned 关键字,另一个元素是类实例的引用(例如 self)或初始化过的变量(如 delegate = self.delegate)。这些项在方括号中用逗号分开。

Swift 有如下要求:只要在闭包内使用 self 的成员,就要用 self.someProperty 或者 self.someMethod()(而不只是 someProperty 或 someMethod())。这提醒可能会一不小心就捕获了 self。

在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为 无主引用。相反,在被捕获的引用可能会变为 nil 时,将闭包内的捕获定义为 弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil。这使可以在闭包体内检查它们是否存在。如果被捕获的引用绝对不会变为 nil,应该用无主引用,而不是弱引用。

内存安全

默认情况下,Swift 会阻止代码里不安全的行为。Swift 会保证变量在使用之前就完成初始化,在内存被回收之后就无法被访问,并且数组的索引会做越界检查。

Swift 也保证同时访问同一块内存时不会冲突,通过约束代码里对于存储地址的写操作,去获取那一块内存的访问独占权。而如果代码确实存在冲突,那在编译时或者运行时就会得到错误。

内存访问的冲突会发生在代码尝试同时访问同一个存储地址的时侯。同一个存储地址的多个访问同时发生会造成不可预计或不一致的行为。在 Swift 里,有很多修改值的行为都会持续好几行代码,在修改值的过程中进行访问是有可能发生的。

内存访问冲突时,要考虑内存访问上下文中的这三个性质:访问是读还是写,访问的时长,以及被访问的存储地址。特别是,冲突会发生在有两个访问符合下列的情况:

  • 至少有一个是写访问
  • 它们访问的是同一个存储地址
  • 它们的访问在时间线上部分重叠

如果一个访问不可能在其访问期间被其它代码访问,那么就是一个瞬时访问。正常来说,两个瞬时访问是不可能同时发生的。大多数内存访问都是瞬时的。

有几种被称为长期访问的内存访问方式,会在别的代码执行时持续进行。瞬时访问和长期访问的区别在于别的代码有没有可能在访问期间同时访问,也就是在时间线上的重叠。一个长期访问可以被别的长期访问或瞬时访问重叠。重叠的访问主要出现在使用 in-out 参数的函数和方法或者结构体的 mutating 方法里。

一个函数会对它所有的 in-out 参数进行长期写访问。in-out 参数的写访问会在所有非 in-out 参数处理完之后开始,直到函数执行完毕为止。如果有多个 in-out 参数,则写访问开始的顺序与参数的顺序一致。长期访问的存在会造成一个结果,不能在访问以 in-out 形式传入后的原变量,即使作用域原则和访问权限允许——任何访问原变量的行为都会造成冲突。

值类型,修改值的任何一部分都是对于整个值的修改,意味着其中一个属性的读或写访问都需要访问整一个值。

即使有些代码违反了访问独占权的原则,也是内存安全的,所以如果编译器可以保证这种非专属的访问是安全的,那 Swift 就会允许这种行为的代码运行。特别是当遵循下面的原则时,它可以保证结构体属性的重叠访问是安全的:

  • 访问的是实例的存储属性,而不是计算属性或类的属性
  • 结构体是局部变量的值,而非全局变量
  • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了

访问控制

在 Swift 中,Xcode 的每个 target(例如框架或应用程序)都被当作独立的模块处理。如果为了实现某个通用的功能,或者是为了封装一些常用方法而将代码打包成独立的框架,这个框架就是 Swift 中的一个模块。当它被导入到某个应用程序或者其他框架时,框架的内容都将属于这个独立的模块。

源文件 就是 Swift 模块中的源代码文件(实际上,源文件属于一个应用程序或框架)。尽管一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数等的定义。

Swift 为代码中的实体提供了五种不同的访问级别。这些访问级别不仅与源文件中定义的实体相关,同时也与源文件所属的模块相关:

  • open 和 public 级别可以让实体被同一模块源文件中的所有实体访问,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,会使用 open 或 public 级别来指定框架的外部接口。open 和 public 的区别在后面会提到。
  • internal 级别让实体被同一模块源文件中的任何实体访问,但是不能被模块外的实体访问。通常情况下,如果某个接口只在应用程序或框架内部使用,就可以将其设置为 internal 级别。
  • fileprivate 限制实体只能在其定义的文件内部访问。如果功能的部分实现细节只需要在文件内使用时,可以使用 fileprivate 来将其隐藏。
  • private 限制实体只能在其定义的作用域,以及同一文件内的 extension 访问。如果功能的部分细节只需要在当前作用域内使用时,可以使用 private 来将其隐藏。

open 为最高访问级别(限制最少),private 为最低访问级别(限制最多)。

open 只能作用于类和类的成员,它和 public 的区别主要在于 open 限定的类和成员能够在模块外能被继承和重写。将类的访问级别显示指定为 open 表明已经设计好了类的代码,并且充分考虑过这个类在其他模块中用作父类时的影响。

Swift 中的访问级别遵循一个基本原则:实体不能定义在具有更低访问级别(更严格)的实体中。例如:

  • 一个 public 的变量,其类型的访问级别不能是 internal,fileprivate 或是 private。因为无法保证变量的类型在使用变量的地方也具有访问权限。
  • 函数的访问级别不能高于它的参数类型和返回类型的访问级别。因为这样就会出现函数可以在任何地方被访问,但是它的参数类型和返回类型却不可以的情况。

不显式的指定访问级别,那么将都有一个 internal 的默认访问级别,(有一些例外情况,稍后说明)。因此,多数情况下不需要显示指定实体的访问级别。

框架的内部实现仍然可以使用默认的访问级别 internal,当需要对框架内部其它部分隐藏细节时可以使用 private 或 fileprivate。对于框架的对外 API 部分,就需要将它们设置为 open 或 public 了。

一个类型的访问级别会影响到类型成员(属性、方法、构造器、下标)的默认访问级别。如果将类型指定为 private 或者 fileprivate 级别,那么该类型的所有成员的默认访问级别也会变成 private 或者 fileprivate 级别。如果类型指定为 internal 或 public(或者不明确指定访问级别,而使用默认的 internal ),那么该类型的所有成员的默认访问级别将是 internal。一个 public 类型的所有成员的访问级别默认为 internal 级别,而不是 public 级别。这样做的好处是,在定义公共接口的时候,可以明确地选择哪些接口是需要公开的,哪些是内部使用的,避免不小心将内部使用的接口公开。

元组的访问级别将由元组中访问级别最严格的类型来决定。例如一个包含两种不同类型的元组,其中一个类型为 internal,另一个类型为 private,那么这个元组的访问级别为 private。元组不同于类、结构体、枚举、函数那样有单独的定义。一个元组的访问级别由元组中元素的访问级别来决定的,不能被显示指定。

函数的访问级别根据访问级别最严格的参数类型或返回类型的访问级别来决定。但是,如果这种访问级别不符合函数定义所在环境的默认访问级别,那么就需要明确地指定该函数的访问级别。

枚举成员的访问级别和该枚举类型相同,不能为枚举成员单独指定不同的访问级别。枚举定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。

嵌套类型的访问级别和包含它的类型的访问级别相同,嵌套类型是 public 的情况除外。在一个 public 的类型中定义嵌套类型,那么嵌套类型自动拥有 internal 的访问级别。如果想让嵌套类型拥有 public 访问级别,那么必须显式指定该嵌套类型的访问级别为 public。

可以继承同一模块中的所有有访问权限的类,也可以继承不同模块中被 open 修饰的类。一个子类的访问级别不得高于父类的访问级别。例如,父类的访问级别是 internal,子类的访问级别就不能是 public。在同一模块中,可以在符合当前访问级别的条件下重写任意类成员(方法、属性、构造器、下标等)。在不同模块中,可以重写类中被 open 修饰的成员。可以通过重写给所继承类的成员提供更高的访问级别。甚至可以在子类中,用子类成员去访问访问级别更低的父类成员,只要这一操作在相应访问级别的限制范围内。

常量、变量、属性不能拥有比它们的类型更高的访问级别。例如,不能定义一个 public 级别的属性,但是它的类型却是 private 级别的。同样,下标也不能拥有比索引类型或返回类型更高的访问级别。

常量、变量、属性、下标的 Getters 和 Setters 的访问级别和它们所属类型的访问级别相同。Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 var 或 subscript 关键字之前,可以通过 fileprivate(set),private(set) 或 internal(set) 为它们的写入权限指定更低的访问级别。这个规则同时适用于存储型属性和计算型属性。即使不明确指定存储型属性的 Getter 和 Setter,Swift 也会隐式地为其创建 Getter 和 Setter,用于访问该属性的存储内容。使用 fileprivate(set),private(set) 和 internal(set) 可以改变 Setter 的访问级别,这对计算型属性也同样适用。

可以在必要时为 Getter 和 Setter 显式指定访问级别。可以结合 public 和 private(set) 修饰符把 Getter 的访问级别设置为 public,而 Setter 的访问级别设置为 private。

自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一的例外是 必要构造器,它的访问级别必须和所属类型的访问级别相同。如同函数或方法的参数,构造器参数的访问级别也不能低于构造器本身的访问级别。

默认构造器的访问级别与所属类型的访问级别相同,除非类型的访问级别是 public。如果一个类型被指定为 public 级别,那么默认构造器的访问级别将为 internal。如果希望一个 public 级别的类型也能在其他模块中使用这种无参数的默认构造器,只能自己提供一个 public 访问级别的无参数构造器。

如果结构体中任意存储型属性的访问级别为 private,那么该结构体默认的成员逐一构造器的访问级别就是 private。否则,这种构造器的访问级别依然是 internal。如同前面提到的默认构造器,如果希望一个 public 级别的结构体也能在其他模块中使用其默认的成员逐一构造器,依然只能自己提供一个 public 访问级别的成员逐一构造器。

如果想为一个协议类型明确地指定访问级别,在声明协议时指定即可。这将限制该协议只能在适当的访问级别范围内被遵循。协议中的每个方法或属性都必须具有和该协议相同的访问级别。不能将协议中的方法或属性设置为其他访问级别。这样才能确保该协议的所有方法或属性对于任意遵循者都可用。

如果定义了一个继承自其他协议的新协议,那么新协议拥有的访问级别最高也只能和被继承协议的访问级别相同。

一个类型可以遵循比它级别更低的协议。遵循协议时的上下文级别是类型和协议中级别最小的那个。

Extension 的新增成员具有和原始类型成员一致的访问级别。可以通过修饰语重新指定 extension 的默认访问级别(例如,private),从而给该 extension 中的所有成员指定一个新的默认访问级别。这个新的默认访问级别仍然可以被单独成员指定的访问级别所覆盖。

使用 extension 来遵循协议的话,就不能显式地声明 extension 的访问级别。extension 每个 protocol 要求的实现都默认使用 protocol 的访问级别。

扩展同一文件内的类,结构体或者枚举,extension 里的代码会表现得跟声明在原类型里的一模一样:

  • 在类型的声明里声明一个私有成员,在同一文件的 extension 里访问。
  • 在 extension 里声明一个私有成员,在同一文件的另一个 extension 里访问。
  • 在 extension 里声明一个私有成员,在同一文件的类型声明里访问。

泛型类型或泛型函数的访问级别取决于泛型类型或泛型函数本身的访问级别,还需结合类型参数的类型约束的访问级别,根据这些访问级别中的最低访问级别来确定。

定义的任何类型别名都会被当作不同的类型,以便于进行访问控制。类型别名的访问级别不可高于其表示的类型的访问级别。也适用于为满足协议遵循而将类型别名用于关联类型的情况。

高级运算符

  • 按位取反运算符(~)对一个数值的全部比特位进行取反
  • 按位与运算符(&) 对两个数的比特位进行合并。它返回一个新的数,只有当两个数的对应位都为 1 的时候,新数的对应位才为 1
  • 按位或运算符(|)可以对两个数的比特位进行比较。它返回一个新的数,只要两个数的对应位中有任意一个为 1 时,新数的对应位就为 1
  • 按位异或运算符,或称“排外的或运算符”(^),可以对两个数的比特位进行比较。它返回一个新的数,当两个数的对应位不相同时,新数的对应位就为 1,并且对应位相同时则为 0
  • 按位左移运算符(<<) 和 按位右移运算符(>>)可以对一个数的所有位进行指定位数的左移和右移

对一个数进行按位左移或按位右移,相当于对这个数进行乘以 2 或除以 2 的运算。将一个整数左移一位,等价于将这个数乘以 2,同样地,将一个整数右移一位,等价于将这个数除以 2。

对无符号整数进行移位的规则如下:

  • 已存在的位按指定的位数进行左移和右移。
  • 任何因移动而超出整型存储范围的位都会被丢弃。
  • 用 0 来填充移位后产生的空白位。

有符号整数使用第 1 个比特位(通常被称为符号位)来表示这个数的正负。符号位为 0 代表正数,为 1 代表负数。

负数的存储方式略有不同。它存储 2 的 n 次方减去其实际值的绝对值,这里的 n 是数值位的位数。一个 8 比特位的数有 7 个比特位是数值位,所以是 2 的 7 次方,即 128。

负数的表示通常被称为二进制补码。用这种方法来表示负数乍看起来有点奇怪,但它有几个优点:

  • 如果想负数进行加法运算,只需要对这两个数的全部 8 个比特位执行标准的二进制相加(包括符号位),并且将计算结果中超出 8 位的数值丢弃。
  • 使用二进制补码可以使负数的按位左移和右移运算得到跟正数同样的效果,即每向左移一位就将自身的数值乘以 2,每向右一位就将自身的数值除以 2。要达到此目的,对有符号整数的右移有一个额外的规则:当对有符号整数进行按位右移运算时,遵循与无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用 0。

Swift 提供的三个溢出运算符来让系统支持整数溢出运算。这些运算符都是以 & 开头的:溢出加法 &+、溢出减法 &-、溢出乘法 &*。

运算符的优先级使得一些运算符优先于其他运算符;它们会先被执行。结合性定义了相同优先级的运算符是如何结合的,也就是说,是与左边结合为一组,还是与右边结合为一组。可以将其理解为“它们是与左边的表达式结合的”,或者“它们是与右边的表达式结合的”。

类和结构体可以为现有的运算符提供自定义的实现。这通常被称为运算符重载。运算符函数必须使用 static。

复合赋值运算符将赋值运算符(=)与其它运算符进行结合。在实现的时候,需要把运算符的左参数设置成 inout 类型,因为这个参数的值会在运算符函数内直接被修改。

不能对默认的赋值运算符(=)进行重载。只有复合赋值运算符可以被重载。同样地,也无法对三元条件运算符 (a ? b : c) 进行重载。

可以使用 Swift 提供的等价运算符默认实现。Swift 为以下数种准守 Equatable 协议的自定义类型提供等价运算符的默认实现:

  • 只拥有存储属性,并且它们全都遵循 Equatable 协议的结构体
  • 只拥有关联类型,并且它们全都遵循 Equatable 协议的枚举
  • 没有关联类型的枚举

除了实现标准运算符,在 Swift 中还可以声明和实现自定义运算符。使用 operator 关键字在全局作用域内进行定义,同时还要指定 prefix、infix 或者 postfix 修饰符.

每个自定义中缀运算符都属于某个优先级组。优先级组指定了这个运算符相对于其他中缀运算符的优先级和结合性。没有明确放入某个优先级组的自定义中缀运算符将会被放到一个默认的优先级组内,其优先级高于三元运算符。

当定义前缀与后缀运算符的时候,没有指定优先级。如果对同一个值同时使用前缀与后缀运算符,则后缀运算符会先参与运算。