接着上一篇
枚举
枚举语法
使用enum
关键词来创建枚举并且把它们的整个定义放在一对大括号内:
1 | enum SomeEnumeration { |
例子:
1 | enum CompassPoint { |
注意每一个 case
来定义一个新的枚举成员。如果出现在同一行上,需要用逗号隔开:
1 | enum Planet { |
和 oc 不同,这里的枚举值不会被完全的隐式赋值为 0,1,2,3(但是如果你给定了其中一个的值,其他的值可以被隐式地推断出来)。这些枚举成员本身就是完备的值,比如最上面的这些值的类型是已经明确定义好的 CompassPoint
类型。
使用方式:
1 | var directionToHead = CompassPoint.West |
directionToHead
的类型可以在它被CompassPoint
的某个值初始化时推断出来。一旦directionToHead
被声明为CompassPoint
类型,你可以使用更简短的点语法将其设置为另一个CompassPoint
的值:
1 | directionToHead = .East |
使用 switch 枚举
可以使用 switch
匹配枚举值:
1 | directionToHead = .South |
关联值
有些时候枚举值会需要存储一些关联值以方便使用,比如:
1 | enum Barcode { |
表示 UPCA
具有 (Int,Int,Int,Int)
的关联值,QRCode
具有 String
的关联值。使用:
1 | var productBarcode = Barcode.UPCA(8, 85909, 51226, 3) |
类型推断为 Barcode
,关联值只是附加信息,便于存储一些必要信息。比如在使用 switch 语句时,可以将关联值提取出来,就可以在执行语句中使用。可以在switch
的 case 分支代码中提取每个关联值作为一个常量(用let
前缀)或者作为一个变量(用var
前缀)来使用:
1 | switch productBarcode { |
为了简洁,可以将let
或者var
提取出来:
1 | switch productBarcode { |
原始值
原始值的定义和 oc 中枚举的效果很像。可以为每个枚举成员定义一个默认值。这些默认值的类型必须相同:
1 | enum ASCIIControlCharacter: Character { |
其中,将枚举类型定义为字符串类型。原始值还可以是字符,任意整形或浮点型值。
注意,原始值和关联值是不同的。原始值是定义枚举时被预先填充的值。对于一个特定的枚举成员,原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。
关联值和原始值不能同时混合使用
原始值的隐式赋值
使用整数或者字符串作为原始值枚举时,不需要显式赋值,Swift 会自动赋值:
1 | enum Planet: Int { |
在上面的例子中,Plant.Mercury
的显式原始值为1
,Planet.Venus
的隐式原始值为2
。
当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称:
1 | enum CompassPoint: String { |
上面例子中,CompassPoint.South
拥有隐式原始值South
,即就是其本生。可以使用枚举成员的rawValue
属性可以访问该枚举成员的原始值:
1 | let earthsOrder = Planet.Earth.rawValue |
这里拿出了 rawValue
,那么 earthsOrder
和sunsetDirection
就是明确的值了,而不是枚举类型。
使用原始值初始化枚举实例
如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做rawValue
的参数,参数类型即为原始值类型,返回值则是枚举成员或nil
。你可以使用这个初始化方法来创建一个新的枚举实例。
比如利用原始值7
创建了枚举成员Uranus
:
1 | let possiblePlanet = Planet(rawValue: 7) |
原始值构造器总是返回一个可选的枚举成员,因为可能没有对应的枚举类型。在上面的例子中,possiblePlanet
是Planet?
类型。
类和结构体
定义语法
使用 class
和 struct
分别表示类和结构体。示例如下:
1 | struct Resolution { |
这里面可以注意一点。就是对于变量进行了初始化。这样有什么好处呢?可以使 swift 进行类型推断出当前变量的类型。在 swift 中,每个变量的类型都必须是确定的。
类和结构体实例
创建类和结构体实例的语法相似:
1 | let someResolution = Resolution() |
通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。更多构造过程在后面将会更详细的讨论。
属性访问
通过 .
语法,可以访问实例的属性。与 oc 不同的是,Swift 允许直接设置结构体属性的子属性。反正通过 .
什么都能拿到就是了。
结构体类型的成员逐一构造器
所有结构体(特指结构体)都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:
1 | let vga = Resolution(width:640, height: 480) |
与结构体不同,类实例没有默认的成员逐一构造器。详细见后面。
结构体和枚举是值类型
什么是值类型?就是我们所说的值传递和引用传递中的值传递。指在被赋给一个变量常量或者传递给一个函数的时候,值会被拷贝。
所有的基本类型都是值拷贝这和以前毫无异议。但是在 Swift 中,字符串、数组和字典也是值拷贝,这和 oc 中很不相同。这意味着数组和字典中的所有元素都会被拷贝一份。其实是因为它们底层都是以结构体的形式实现的。
1 | let hd = Resolution(width: 1920, height: 1080) |
例如上面的例子,cinema
和 hd
相同,但其实在内存中是两个不同的对象。修改其中一个的值不会改变另一个实例相应属性的值。枚举同样。
这里只是数组对象会被创建一个新的,但是数组里的对象都是引用类型。
类是引用类型
这点没啥不同的。就是不同常量或者变量指向内存上的相同地址。
恒等运算符
Swift 中内建了两个恒等运算符:
- 等价于(
===
) - 不等价于 (
!==
)
注意这和“等于” ==
有什么区别呢?
- “等价于”表示两个类类型(注意只能用在类类型中)的常量或者变量引用同一个类实例。
- “等于”表示两个实例的值“相等”或“相同”。
也就是说 ===
为 true
,那么两个类实例必然指向同一块内存地址。==
为 true
则只要类内属性相同即可。(oc 中的 ==
就是这里的 ===
,比较的是指针地址。
一般对象相等都是比较地址,即 ===
。如果要使用 ==
必须要实现 Equatable
协议。在 Equatable
里声明了这个操作符的接口方法:
1 | protocol Equatable { |
实现它:
1 | class MyClass: Equatable { |
类和结构体的选择
结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者适用不同的任务。
其实大部分数据构造都是用类,而非结构体。只有在数据结构非常简单的时候用结构体。
字符串、数组、字典的赋值和复制行为
上面也说过了 Swift 中的 String
,Array
和Dictionary
类型均以结构体的形式实现。这意味着被赋值给新的常量或变量,或者被传入函数或方法中时,它们的值会被拷贝。
Objective-C 中NSArray
和NSDictionary
类型均以类的形式实现,而并非结构体。它们在被赋值或者被传入函数或方法时,不会发生值拷贝,而是传递现有实例的引用。
不过不用担心值拷贝会影响新能,Swift 中有优化。
属性
存储属性
类和结构体中用 var
或者 let
修饰的就是存储属性:
1 | struct FixedLengthRange { |
常量结构体的存储属性
如果创建了一个结构体的实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使有属性被声明为变量也不行:
1 | let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4) |
因为 rangeOfFourItems
被声明成了常量(用 let
关键字),即使 firstValue
是一个变量属性,也无法再修改它了。
这种行为是由于结构体(struct)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。属于引用类型的类(class)则不一样。把一个引用类型的实例赋给一个常量后,仍然可以修改该实例的变量属性。
因此数组字典等如果设置为
let
,那么数组字典中的项,值类型无法给内容,引用类型无法改地址
延迟存储属性
指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy
来标识。注意,延迟存储属性必须被声明为 var
。
1 | class DataImporter { |
上面这个类中,DataImporter
是一个很费时的操作。所以设置为 lazy
,只有在第一次访问到的时候才会被创建。
计算属性
计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。其实就是每次点到这个属性的时候都会再计算一遍,适用于会根据其他值变化的属性,这样就不用每次用到的时候专门调用一个处理方法了。而是由系统直接调用了 getter 方法
1 | struct Point { |
上面这个矩形的类,通过 origin
和 size
来计算出 center
。其中 setter 方法的 newCenter
由于类型推断,默认为 Point
类型,就不用再写明类型了。
这里要强调一点。计算属性的 set 方法在 init 方法中是不会被调用的。如果你在初始化方法中给计算属性赋值了,那么这个计算属性直接就等于这个值,而不是再调用 setter 方法。
便捷 setter 声明
由于 setter
函数必然要传入一个新值,所以 Swift 定义了一个默认名称 newValue
。所以可以采取简略的形式:
1 | struct AlternativeRect { |
只读计算属性
只有 getter 没有 setter 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
如果是只读计算属性,那么连 get
关键字都可以扔掉了:
1 | struct Cuboid { |
属性观察器
可以监听属性值的变化(可以为除了延迟存储属性之外的其他存储属性添加属性观察器,因为可以通过 setter 方法直接监控)。
提供了两个属性观察器:
willSet
新值被设置前调用didSet
新值被设置后调用
willSet
接受新的属性值作为常量传入,可以自己指定这个参数的名称。如果不指定,默认名称为 newValue
。
didSet
将旧的属性值传入,不接受自定义参数名,默认参数名为 oldValue
。如果在 didSet
方法中再次对该属性赋值,那么新值会覆盖旧的值。
1 | class StepCounter { |
如果将属性通过 in-out 方式传入函数,willSet
和 didSet
也会调用。这是因为 in-out 参数采用了拷入拷出模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。
这个来实现 kvo
全局变量和局部变量
全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。
全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要标记lazy
修饰符。
局部范围的常量或变量从不延迟计算。
类型属性
实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。类似于静态变量或常量。
跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy
修饰符。
类型属性语法
在 Swift 中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。
使用关键字 static
来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class
来支持子类对父类的实现进行重写。下面的例子演示了存储型和计算型类型属性的语法:
1 | struct SomeStructure { |
使用了 class
,子类才能知道这个计算型属性是可以重写的,重写的时候要加上 override
表示:
1 | class OverridedClass: SomeClass { |
其实计算型属性就相当于是个方法。
获取和设置类型属性的值
类型属性通过类型本身访问:
1 | print(SomeStructure.storedTypeProperty) |
方法
类、结构体、枚举都可以定义实例、类型方法(这和 oc 中不同,oc 只能在类中定义方法)。
实例方法
实例方法的语法和函数完全一致:
1 | class Counter { |
和调用属性一样,用点语法调用实例方法:
1 | let counter = Counter() |
self 属性
每个实例都有一个隐式的属性叫做 self
。我们可以在实例方法中,通过 self
获取当前实例。
可以改写上面的 incremnet()
方法:
1 | func increment() { |
实际使用中,并不太需要 self
,因为没必要。那么什么时候是必须的呢?
1 | struct Point { |
比如上面的代码,实例方法的入参名和实例属性相同,这个时候必须要用 self
以示区分。
实例方法中修改值类型
结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改(也就是说一般创建好之后,结构体和枚举一般就不让修改了)。
如果你想在该实例方法中修改结构体和枚举的属性,那么需要在该方法前加一个可变标记,就可以改变它的值了。这个方法做的任何改变都会在方法执行结束时写回到原始结构中。
要使用可变
方法,将关键字mutating
放到方法的func
关键字之前就可以了:
1 | struct Point { |
注意,不能在结构体类型的常量上调用可变方法,因为其属性不能被改变,即使属性是变量属性。这一点在属性一章中已经讲过了。
可变方法中给 self 赋值
可变方法还能够赋给隐含属性self
一个全新的实例。上面Point
的例子可以用下面的方式改写:
1 | struct Point { |
枚举的可变方法可以把self
设置为同一枚举类型中不同的成员:
1 | enum TriStateSwitch { |
上面的例子中定义了一个三态开关的枚举。每次调用next()
方法时,开关在不同的电源状态(Off
,Low
,High
)之间循环切换。
类型方法
实例方法是被某个类型的实例调用的方法。你也可以定义在类型本身上调用的方法,这种方法就叫做类型方法(Type Methods)。在方法的func
关键字之前加上关键字static
,来指定类型方法。类还可以用关键字class
来允许子类重写父类的方法实现。
在 Objective-C 中,你只能为 Objective-C 的类类型(classes)定义类型方法(type-level methods)。在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。
用点语法调用类型方法:
1 | class SomeClass { |
类型方法和其他语言的静态方法无二,不多说了。
下标
下标是访问集合列表中元素的跨界方式。可以通过下标索引,设置和获取值,而省略调用相应的存取方法。你可以定义有多个入参的下标满足自定义类型的需求。
下标语法
下标允许你通过在实例名称后面的方括号中传入一个或者多个索引值来对实例进行存取。语法类似于实例方法语法和计算型属性语法的混合。与定义实例方法类似,定义下标使用subscript
关键字,指定一个或多个输入参数和返回类型。下标可以设定为读写或只读。这种行为由 getter 和 setter 实现,有点类似计算型属性:
1 | subscript(index: Int) -> Int { |
newValue
的类型和下标的返回类型相同。如同计算型属性,可以不指定 setter 的参数(newValue
)。如果不指定参数,setter 会提供一个名为newValue
的默认参数。
如同只读计算型属性,可以省略只读下标的get
关键字:
1 | subscript(index: Int) -> Int { |
下面看一个例子:
1 | struct TimesTable { |
下标选项
一个类或结构体可以根据自身需要提供多个下标实现,使用下标时将通过入参的数量和类型进行区分,自动匹配合适的下标,这就是下标的重载。
虽然接受单一入参的下标是最常见的,但也可以根据情况定义接受多个入参的下标。例如下例定义了一个Matrix
结构体,用于表示一个Double
类型的二维矩阵。Matrix
结构体的下标接受两个整型参数:
1 | struct Matrix { |
使用:
1 | var matrix = Matrix(rows: 2, columns: 2) |
其中使用了断言,断言在下标越界时触发。