不成熟的思考
苹果,过去使用OC作为开发语言,OC是基于C的,与Java、JS等等其他常见语言有显著区别,但是又从语言上支持了OO模式,但由于OC和其他基于VM的语言语法上差别较大,因此一般不会有人拿它们来做对比;事实上,OC很多方面的确和同是OO的Java不一样,这就和C++和Java不一样差不多,虽然同属面向对象语言,但它们的底层差别实在是太大了。
如今,苹果搞的Swift,看起来和Java、JS更相似了,但我们还是要明白,Swift是更贴近C的语言,而不是Java,Swift的泛型、结构体等特性,和Java完全不一样,不要代入Java的视角来看待Swift,不然会有很多地方无法理解。
比如,从Java的视角来看,是无论如何无法理解Swift的Any的,Java中一切都是对象,泛型必须是Object及其子类,但是Swift的泛型更像是C++的泛型模板,Any可以匹配class和基础类型。要想深入理解这两种语言的差异,还得对Swift的运行时有一定的了解才可以吧。
XCode
- 通过XCode的Help可以方便的查询所有类型的文档;
- 按住option点击变量可以查看变量推断出来的类型;
基本语法 - 变量、常量、类型
Swift使用var表示变量,使用let表示常量,每一个语句后面可以不用写分号,变量在声明时如果能够推导出类型也不用写类型标注,在Swift中,类型标注的语法和Kotlin、TS差不多,都是冒号后面跟具体类型。
var number = 3 // 变量
let three = 3 // 常量
var number: Int = 3 // 可以显式写明类型
可空类型
Swift和其他语言的显著区别是可空类型,空由nil表示,如果一个变量可能是nil,就必须被声明为可空的,并且在使用时必须利用特殊的操作符检查是否为nil,这种特性几乎是次世代语言的标配,被称作 null-safety 。在声明类型时,可以在类型后面加上问号 ? 表示是一个可空类型。
Swift中的可空类型是一个包装类型Optional,在使用前必须需要解包装,不能直接把可空类型当成实际类型使用。其实,所有的解包装用的操作符,都是Optional类型重写的,会做特定检查并返回,包括下面这些:
- 强制展开 - forced unwrapping,! 操作符,将Optional断言为非nil,转换为普通类型使用;
- 隐式展开可空类型 - implicitly unwrapped optional,在声明时,通过在类型后面加上惊叹号,可以将变量声明为隐式的可空类型,也就是说,变量可以被赋值为nil,但使用时却可以直接被当成普通类型使用,是一种破坏null-safety的写法,没什么特别的理由的话就避免这种写法;
- 可空链式调用- optional chaining, ?. 操作符,如果操作符左侧Optional包装的值为nil,那么整个表达式就会直接返回nil,如果不为nil,那么?.就会调用在包装的对象上定义的方法;
- nil合并运算符 - nil coalescing operator, ?? 操作符,该操作符在左侧Optional包装的对象为nil时返回右侧表达式的值,在optional不为nil时返回Optional包装的非nil对象;
另外,还有一些语法糖(吐槽一下,swift的语法糖非得这么诡异吗?):
- 可空实例绑定 - Optional Binding,利用 if let notOptional = optional 语法,当optioanl不为nil时判断为真,例子参考下面的 “再来谈谈if”;
数字类型
整数
Swift中基础的整数类型是Int,长度和C的int一样,由机器字长决定,有的iOS设备上Int还是32位的,不过如今大部分苹果的机器上Int都是64位的。可以使用Int.min和Int.max取得Int的最大最小值。
和C类似,Swift还提供了Int8, Int16, Int32, Int64这些具体指定长度的整数类型。
在Int前面加个U,就是无符号整数UInt,UInt同样有UInt8等等。
Swift中,编译器会在编译期检查整型溢出,并将其标记为Error,如果我们在做特定的底层操作,就是想允许溢出,那就得使用“溢出操作符”:&+,&-,&*。Swift的设计哲学是尽可能的阻止使用者犯错,而不小心的整数溢出正是常见的错误。
Int是个比较特殊的类型,因为编译器总是会把一个整数字面量推断为Int类型,如果你想要一个UInt,就必须显式写出类型。
在Swift中,没有强制类型转换操作符,即使是在多个整型类型间转换,也得使用构造函数,例如:
let a: Int8 = 14
Int = Int(a)
浮点数
Swift中Double类型是编译器偏爱的浮点数类型,任何小数字面量都会被编译器推导为Double类型,此外,Swift和其他语言一样,也支持标准的Float类型。
使用浮点数时,不要使用 == 做判断,而是要将两个值的差的绝对值和某个足够小的数做比较,因为浮点数天生就是不完全精确的。
字符串
Swift中字符串的类型是String。Swift语言的设计非常注重效率,字符串本质上是一种特殊的集合类型,而swift对于集合类型默认提供了极其优化的API,如果不专门学习一下这些API,就没办法正常使用Swift的集合类型。
字符串String是由一个个的Character类型的字符组成的,每个Character字符代表一个Unicode字符,可以使用String的characters属性访问Character数组。
Swift的字符串API用起来比其他语言都要困难得多,原因是Swift对Unicode标准支持得比较完善。
String的索引不可以直接用数字Int类型表示,而是必须用String.CharacterView.Index类型表示,String#startIndex方法可以返回字符串的起始索引,要构造特定位置的索引,必须结合起始索引对象一块完成:
let end = myStr.index(myStr.startIndex, offsetBy: 4)
不过,Swift支持另外一种看起来和下标类似的操作,就是区间操作,索引类型支持区间,可以通过 … 操作符将两个索引构造成区间,并根据区间取出子字符串:
let range = str.startIndex … str.index(str.startIndex, offsetBy: 4)
let substr = str[range]
Swift与Unicode
Unicode目前是21位的标量,在Swift中使用这样的写法可以直接表示Unicode标量: “\u{1F60#}” ,也就是\u{
- 扩展字形簇与标准等价簇 - Unicode中存在所谓的扩展字形簇(extended grapheme cluster),是由多个Unicode标量组合来表示的人来单个可读字符,如带重读音标的字母,但是同时Unicode还单独定义了很多单个Unicode标量来专门表示常见的扩展字形,因此存在两个字符串的Unicode表示不一致,但却实际上能够等同的情况,这种相等性是Unicode规范要求的,称作标准等价簇。扩展字形簇不论由多少个Unicode字符组成,它们的长度都必须被当作1,以避免出现两个字符串长度不同但却相等的矛盾情形。
- 由于Swift字符串中存储的是Unicode,而Unicode又比较复杂,存在上面提到的问题,因此如果我们想要获得字符串的第n个字符,就必须要从字符串的开头进行遍历,将所有的扩展字形簇合并当成单个字符,因此在Swift中查找字符串的第n个字符并不是直接根据下标定位的,正因如此Swift的字符串不支持下标访问。
- 这块和Java等其他语言不一样,其他语言没考虑这么多,例如Java使用Unicode16,也就是UTF16存储字符串,那么一个包含音标的字符串abc,其中c带音标, 长度就会大于4,并且这些其他语言没遵循Unicode规定的标准等价簇,也就是说两个字形一样的字符串,其长度是不想等的,并且equals判断也不想等,和Swift不一样,但因此它们的字符串处理要简单得多。
- 出于上面讨论的原因,Swift的字符串API才会比别的语言用起来困难些。
字符串插值
Swift的字符串插值语法比较特殊:
let hello = “heelo”
let world = “world”
let echo = “(hello), (world)”
相等判断
Swift区分 == 和 ===,其中前者相当于Java中的equals,后者相当于Java中的 ==,即前者比较对象值,后者比较对象引用。
Swift中,使用叹号表示非,没有not之类的关键字。
条件语句
if/else
Swift中,if语句不需要写小括号,其他的和别的语言差不多:
if population < 10000 {
…
} else {
…
}
三目运算符
swift的三目运算符和Java的没区别。
switch
Swift中的switch非常灵活,语法也有点诡异,它可以做到很多if做不到的事情,事实上有些事情可能我们觉得明明应该由if来做,但是吧,Swift中,就必须得用switch,所以留意一下这块吧。
swift的switch语句和其他语言差别比较大,下面总结一下
- swift规定每一个switch匹配后面都必须至少有一条可执行语句,因此对于空逻辑需要使用break,这纯粹是个占位用的语句,因为swift中默认在匹配代码执行完后就会跳出,如果想和C或者Java那样继续执行,就得使用专门的 fallthrough 关键字:
- swift规定每个switch语句必须确保匹配所有可能的值,也就是说如果不能通过case完全枚举所有值,就必须得写default;
- swift中,很奇怪的一点是,if语句里不好做区间判断,得通过switch来做,case后面写 a…b,即可判断是否处于区间内;
- 另一个初看没什么用的特性是值绑定,单独用只能用于替换default,看起来有点多此一举,这个特性需要在switch where中体现出价值,语法是 case let xxx where xxx > 100,这样xxx就会引用本次匹配到的值。
- swift提供了元组,元组经常和switch配合使用,元组可以不给每个位置的元素指定名字,这样写:let err = (code, msg),也可以指定,这样写:let err = (code: code, errorMessage: msg),如果switch判断的是一个元组,在case里面直接写要匹配的元组即可,甚至可以写 _ 作为元组中特定位置的通配符。
- swift中,可以写if-case语句,算是对if能力的补全,在if-case中可以像写switch中的某一个case一样,利用区间判断,以及值绑定和类似于where的语句(只不过这次不用写where了,真实不统一的抽象美)。
```swift
// 普通值switch
switch aValue {
case someCondition:
case otherCondition:break
case 1000…2000:break
default:fallthrough
}break
// 元组switch switch err { case (200, _): … case (500, ‘error’): … case default: break } // if-case语句,区间和数字的相等比较就是在验证区间是否包含数字 if case 3…10 = age, age >= 21 { // … }
<a name="vbro4"></a>
#### 再来谈谈if
Swift中的if是个比较高糖的语法,支持的写法包括以下这些,并且支持用逗号分隔写成一个if表达式:<br />if-case<br />if-let<br />if-where,这个就是标准的if
```swift
if case a = b, let h = opt, h > 10 {
print(a)
}
循环
for
基本的for-in循环,注意i不需要事先声明,它的类型是根据循环条件推断出来的:
for i in 1…5 {
// …
}
此外,swift还支持在for-in循环中利用where子句控制是否执行循环体代码:
for i in 1…5 where i == 3 {
// …
}
while
最基本的while循环:
var i = 0
while i < 6 {
// …
i += 1
}
swift还支持repeat-while循环,对应其他语言的do-while循环,确保至少执行一次循环体内代码,语法如下:
repeat {
// …
} while i < 6
循环控制语句
和其他语言一样,swift也支持使用continue、break关键字。
swift中的区间
Swift中,使用 … 操作符可以为支持区间的类型创建区间,区间本身也是一种类型,… 创建出来的也是区间类型对象,所有可以使用 … 的地方,都可以使用区间类型对象来替代,但有时候语法上看起来会比较奇怪:
let a = 1...3
let b = 2
if case a = b, b > 1 {
print("true")
}
此外,Swift还支持半闭区间,使用 ..< 可以创建半闭区间对象。
容器
数组
Swift中的数组类型为Array,Array为泛型类,类型声明需要指明泛型类型,如Array
常用数组能力包括:
- append(_:),数组末尾添加元素
- remove(at:),移除指定位置元素
- count属性,获取数组元素总数
- range下标,获取数组的指定范围子数组
- 索引下标,获取或修改指定元素
- 运算符,将另一个数组所有内容添加到数组末尾
- insert(_:at:),在指定位置插入添加元素
- == 运算符,判断另个数组是否相同,相同判断要求顺序一致
- for-in循环
Swift中,使用var声明的数组是可变数组,使用let声明的是不可变数组,不可变,不仅仅是引用本身不可变,包括被引用的对象内容也是不可变的,这个点又和其他语言不一样了,是Swift提供的语法糖,让let语意更加强大。
字典
Swift中的字典类型是Dictionary,同样是泛型类,声明时需指定key和value的泛型类型,如Dictionary
常用字典能力包括:
- count属性
- key下标,获取、修改或添加字典中的元素
- updateValue(_:forKey:),更新指定的key,并返回原本key上绑定的value
- removeValue(forKey:),删除指定的元素,并返回原本key对应的value
- 此外,将指定的元素直接通过key下标语法修改为nil,作用也和删除是一样的,key-value对会被移除,而不是将value置为nil;
- for-in循环,字典针对for-in返回key-value二元组,可以方便的遍历字典
- keys属性,返回可迭代的key集合,使用Array(dict.keys)即可创建由字典所有key组成的数组
和数组类似,使用let声明的字典不可变。
此外,字典的key必须是可散列的。
集合
集合的类型是Set,语意和其他语言一样,就是无序不重复集合,由于用的较少,没有像数组和字典那样的语法糖,没有集合字面量,要使用集合,必须创建Set实例,然后通过方法来操作;创建集合只要使用无参构造函数即可:Set
虽然没有字面量,但使用集合的可变参构造函数填充初始元素也不麻烦:Set
常用集合能力包括:
- insert(_:)
- for-in
- contains(_:)
- union(_:)
- intersection(_:)
- isDisjoint(with:),判断是否两个集合不相交
函数
基本函数定义
Swift中的函数以关键字 func 标记,最基本的函数如下,注意返回值类型是使用 -> 标记的,如果要支持直接返回nil,将返回类型标记为可空类型即可:
func printHello(name: String) -> String? {
let helloStr = "Hello, \(name)"
print(helloStr)
if helloStr.length > 10 {
return helloStr
} else {
return
}
}
// 直接使用函数名称调用函数
printHello(name: "Tommy")
正如上面的例子那样,调用函数时,参数需要使用参数名标记,这是为了更好的可读性,这一点和大部分语言都不一样。
为了进一步提升可读性,swift还支持分别指定调用函数时的参数名称(常常称作形参)和在函数内部使用的函数名称(实参),写法是在定义函数时,写由空格分隔的两个参数名称,前面的是形参,后面的就是实参,例如:
func printHello(to name: String) {……}
特别的,如果希望调用时不需要指定参数名称,可以把形参指定为 ,例如:
func printHello( name: String) {……}
像上面的例子把to这个介词作为形参,有助于进一步提升可读性;不过考虑一下有多个参数的函数,此时需要写多个to吗?事实上,swift建议如果介词需要作用于多个参数,那么介词就应该写在函数名的末尾,不过这仅仅是个建议。
变长参数
variadic,变长参数,通过在参数类型声明后面加 … 来使用。在函数体内,变长参数被当成数组。
func printNames(names: String…) {……}
默认参数值
函数参数可以有默认值,有默认值的参数必须被放在所有无默认值的函数之后,这样调用时才能够不必指定这些参数的值,定义默认值很简单,直接在参数的类型声明后面写等号即可,和变量定义一样:
func printName(firstName: String, lastName: String = “Tom”) {……}
in-out参数
在C++中,如果参数传递的是引用,那么在函数内对于参数所做的修改都会反映到调用函数时传入的那个值上,如果传的不是引用,那么实际传入函数的是通过原入参的copy构造函数新构造出来的对象,因此对这个对象的修改不会影响到调用时的参数。在编译器的层面,引用是通过传递地址被支持的。
在swift中,变量类型有引用类型和值类型的区分,如果是引用类型,那么其实inout没意义,对引用类型的修改一定会反映到引用的对象上;但是如果传递的是值类型,就可以在声明参数类型时使用inout进行修饰,这样就好像以C++的引用形式传递这样的值类型一样,函数内的实参是对象本身,而不是一个复制。
嵌套函数
在函数内可以定义函数,这样定义的内层嵌套函数只在外层函数作用域内可用,并且内层函数可以访问到外层函数作用域内的变量,这种语言特性常常被称作“闭包”。
谈到闭包,就要考虑一下异步过程,如果将内层函数作为异步回调函数,会怎么样?后面会有专门讨论闭包的部分,不用太担心。
当一个函数比较复杂,需要定义一些仅对于该函数有意义的过程时,这种语言特性或许会比较有用。
多返回值
由于swift支持元组,因此在函数中返回多个值是很方便的,不像java需要专门定义一些类型,只要将返回类型声明为命名元组即可,例如:
func test() -> (name: String, age: Int)
利用guard提前返回
swift提供了guard关键字,专门用来做sanity check,利用guard可以检查可空变量并在非空时进行赋值,当检查结果是空时,guard要求必须从当前函数return或者throw:
guard var <变量> = <Optional表达式> else {
// ....
return
}
函数类型
Swift中,函数本身也是值,函数值有自己的类型:函数类型。相比普通的类型,函数类型的类型声明会比较复杂,因为函数的签名由入参和返回值组成,函数类型声明必须要明确这些:
// 函数类型声明:(<入参>) -> (<返回值>)
// 一个接受String作为参数,什么都不返回的类型:
(String) -> ()
// 一个不需要参数,返回元组的类型:
() -> (String, Int)
// 声明定义一个函数类型变量:
func defaultCallback(info: String) -> Bool {......}
var callback: (String) -> (Bool) = defaultCallback
Void
Swfit中一切函数都有返回值,没有返回值的函数实际上返回的是Void类型,Void类型的定义是:
public typealias Void = ()
也就是说,函数签名中的 (String) -> (),相当于是(String) -> Void,这两个表示是等价的,Void就是空元组类型。如果不写返回类型,Swift编译器就会自动的插入这种类型作为返回类型。
知道了这一点,还可以写出些没什么鸟用的代码,除了可以迷惑同行外没啥意义:
var vo: (Void, Void) = ((), ())
闭包
上面介绍了函数,函数就是一种有名字的闭包。在swift中,相比函数,闭包的定义更加紧凑,闭包类似于函数,但省去了命名和声明。
当一个函数的参数类型是一个函数类型时,就把这种函数叫做接受一个闭包的函数,被作为参数传入的函数就是闭包。例如,func test(callback: (String) -> ()) -> Void,这里的callback就是闭包。当使用test函数时,可以将一个符合函数类型的函数名称传进去,也可以直接用更简洁的闭包表达式语法创建一个匿名函数传进去:
test(callback: {
(info: String) -> Void in
// ……
})
上面的闭包表达式是:{(<参数声明>) -> <返回类型> in <函数体>}
由于使用闭包时,参数和返回值的类型都已经确定,因此一般在声明参数时都不必写出类型,只要写有意义的参数名称即可,也就是说上面的 : String 部分和 -> Void 部分是推荐省略的,经过简化,可以写成:
test(callback: {info in
// ……
})
Swift还允许进一步简化写法,但这种简化就开始损害可读性了,闭包表达式的in关键字及其前面的参数名称也可以省略,直接利用 $0, $1 代表第n个参数。
此外,特别的,当闭包函数体只有一行表达式时,可以省略return关键字,此时这一行表达式的值就被当作被return的值。
最后,Swift还支持将闭包写在参数圆括号外面,如果函数参数是函数的最后一个参数,就可以将闭包写在圆括号外,特别的,如果函数参数只接受一个函数参数,那么圆括号也可以不写,这种写法正是时下流行的DSL写法:
test {
// do something with $0
}
这种写法被称作“尾部闭包语法,trailing closure syntax”。
捕获变量
闭包和内嵌函数都可以引用外层函数作用域内的变量。
和Java的匿名内部类或者Lambda不一样,Swift的闭包捕获特性和JS/Dart更为贴近,这暗示着Swift的函数作用域实现并不是基于栈的,而是链式作用域,每个函数都关联着一个作用域对象,从而允许在函数返回后依然能够访问函数定义域内定义的局部变量,如果是Java和C这样的基于栈的作用域,那么函数一旦返回,作用域也就被销毁,被挪作它用,不存在了。
此外,函数是引用类型,闭包可以看作是匿名的函数,闭包和函数都是引用类型,正因如此捕获变量才能够正常工作。
枚举
基本的枚举定义,注意枚举值是使用case定义的,而且swift中的枚举值命名和普通变量一样都是小写字母开头的驼峰命名法:
// 基础枚举类型定义
enum TextAlign {
case left
case right
case center
}
// 枚举类型变量定义
var align = TextAlign.center
// 一旦变量类型是枚举,赋值和做相等判断时就可以省略类型
// 赋值
align = .left
// if
if align == .right {
....
}
// switch
switch align {
case .left:
...
case .right:
...
...
}
原始值
Swift的枚举类型值没有直接和一个Int值关联。为了让Swift枚举表现出和C/C++类似的行为,和一个整数关联,就要用下面这种写法,然后使用枚举值的rawValue属性:
// 注意这里的写法,和声明类时的继承写法一样
enum TextAlign: Int {
case left
case right
case center
case justify
}
// 也可以显式指定原始值
enum TextAlign: Int {
case left = 10
case right = 20
case center = 30
case justify = 40
}
// 然后就可以使用rawValue来引用Int原始值了
TextAlign.left.rawValue
// 也可以将原始值转化为枚举值
if let align = TextAlign(rawValue: 20) {
// ...转化成功
} else {
// ...转化失败
}
Swift里的枚举可以有任意类型的原始值,只要声明时指定好即可,其中Int和String作为原始值时可以不显式指定原始值,默认分别是从0开始的整数,或者枚举值名称字符串。
方法
枚举类型可以定义方法,和普通的类方法相比区别不大。具体参考后面介绍的结构体/类的方法定义。
关联值
Swift的枚举可以定义关联值,关联值是由每个枚举值定义的,在创建枚举值时需提供关联值,关联值可以在实例方法中使用,但需要使用专门的switch-case-let写法进行模式匹配,将关联值绑定到局部变量上。
这是Swift枚举所特有的语言特性,和Java、C++都不一样,这也暗示Swift中的枚举值并不是唯一实例(享元实例),由于能够创建多个和不同值关联的同类枚举值,Swift的同类枚举值一定不可能是指向唯一实例的,从这个角度来看,Swift的枚举和传统的Java、C++的枚举语义完全不一样,倒是更像Kotlin的sealed密封类。(不过说真的,看到现在,感觉swift这语言这些breaking级别的创新有点太多了,而且还和其他语言的非常传统的关键字绑在一起,强行改变这些关键字的语义,没什么道理。。)
// 声明关联值类型
enum ShapeDimensions {
case square(side: Double)
case rect(width: Double, length: Double)
// 为了使用关联值,需要首先确定枚举值的类型,为此需要使用case-let写法
func area() -> Double {
switch self {
case let .square(side: side):
return side * side
...
}
}
}
// 定义枚举值时,需提供关联值
var squareShape = ShapeDimensions.square(side: 10.0)
递归枚举与indirect
有时候我们希望特定枚举值的关联值是枚举类型自身,但这时候Swift编译器会抱怨说不能这么干,为什么?
因为Swift的枚举是值类型,值类型必须在定义时就明确该类型对象所占用的内存大小,为了确定枚举类型占用的内存大小,编译器需要知道每个枚举值的大小,以最大枚举值内存大小为准,如果枚举值的关联值是枚举类型自身,编译器就会陷入:为了确定枚举类型A的大小,需要首先确定关联值类型 枚举类型A 的大小,这样就陷入递归出不来了。
打破递归的方式很简单,就是将这样的关联值作为指针,或者说引用来存储,指针/引用本质上是指向另一个对象的内存地址,占用的内存大小就是机器字长,不必关心指向的内存实际有多大,但这种方式相比上面描述的值对象存储方式而言,内存的空间利用率差了点,没那么紧凑,属于使用空间换取灵活性的方案。
Swift中,我们接触不到指针,但Swift是考虑到这种case的,因此提供了indirect关键字,indirect可以用于修饰枚举类型或枚举类型值,Swift会在计算被修饰的类型所占存储空间时,为其固定分配一个字长的空间,用于存放地址,实际对象会另外分配空间存储,该地址指向实际的对象位置。这样,Swift就能够在编译时避免陷入递归了,直接为indirect类型分配一个字长就OK。
struct和class
Swift中的struct是值类型,class是引用类型。虽然有种C++的既视感,但这俩关键词和C++对应的关键词语义存在不同,Swift的关键字命名感觉有点碰瓷的意思。。。让有一些经验的程序员看着很亲切,然后再给你个surprise。
Swift的基础类型都是值类型,例如Int,String,如果查看它们的文档就能看到不太显眼的struct标记。在使用库提供的类型时,最好留意一下它们是值类型还是引用类型,它们在使用时完全区分不出来,但又表现出不同的复制、赋值语义,Swift的这种设计还是有点容易留坑的,只能使用方多注意。
结构体
结构体是值类型,在赋值时采用的语义是浅拷贝。一般结构体被用作简单的信息容器。结构体只能组合不能继承。
struct Town {
// 实例属性定义
var population = 300
var number = 10
// 实例方法定义
func printDesc() {
print("population: \(population)")
}
// 对于值类型,凡是会修改属性的方法都必须被声明为mutating的
mutating func change(value: Int) {
number = value
}
// struct使用static定义类型方法,这一点和类不同
static func ...
}
类
类是引用类型,赋值时不会发生拷贝。
基础的类定义和上面的结构体一样,把struct换成class即可;但是类有一些结构体没有的能力:
- 继承
- 声明类时,在类名后使用冒号定义父类
- 重写父类的非final方法时,使用override func声明方法
- 希望禁止子类重写时,使用final关键字修饰方法
- 类型方法
- 使用class关键字定义类型方法:class func …,由class修饰的类型方法可以由子类重写
- 这个特性是C++和Java没有的,这也说明swift的class类型方法调用是在运行时才确定的
- 也可以使用static关键字定义不可被重写的类型方法,此时static相当于final class
- 使用class关键字定义类型方法:class func …,由class修饰的类型方法可以由子类重写
关于类型方法
类型方法能够在类型对象上表现出来多态,如下:
class A {
class var a: String {
return "hello"
}
class func testStatic() {
print(a)
}
}
class B : A {
override class var a: String {
return "child"
}
}
// 打印出来的是 child
B.testStatic()
关于mutating
值类型定义的方法如果要修改值对象属性,必须由mutating标记,为什么?
原因主要是Swift的实例方法类型,这是一个接受一个对象类型参数,该参数会被作为实例方法的self,并返回可作用于该参数的函数的方法。
考虑一下这样一个场景:
struct Person {
var firstName = "hello"
mutating func changeTo(_ firstName: String) {
firstName = "hello"
}
}
// 类型为:(inout Person) -> (String) -> (),如果没有mutating,就不会有inout修饰,
// 在这种场景下导致错误值类型复制的行为,传入的self其实是原Person实例的复制,为了避免这种
// 错误,Swift规定所有的修改值类型实例属性的方法都必须是mutating的
var changeToMethod = Person.changeTo
属性
enum、struct、class都可以定义属性。
- 使用var定义读写属性,使用let定义只读属性,只读属性就是常量;
- 使用lazy定义惰性存储属性,lazy属性需要指定一个属性值计算方法,当属性第一次被访问时会使用该方法计算出来属性值并进行设置,后面访问属性就和正常的读写属性一样了;
- 使用lazy var xxx:
= <闭包>的写法来定义惰性存储属性; - 什么时候会需要使用惰性存储属性?如果需要按照某种规则来对属性值进行计算,为什么不直接使用计算属性?这个的确是个问题,Big Nerd Ranch的这本Swift权威指南也并没明说,只是说如果一个属性的值依赖于其他属性的话,使用惰性属性可能会比较方便,惰性属性闭包里可以使用self,从self中可以访问所有非惰性属性,并且能够确保这些属性都是已经完成初始化的。
- 使用lazy var xxx:
- 计算属性是定义了读取/存储方法的属性,这样的属性不一定实际对应着存储字段;
- var xxx:
{ get {…} set(newValue) {…} },其中set方法的newValue可以不指定,这样Swift会自动的给我们提供newValue变量来代表被设置的值 - 如果只需要提供读取方法,就可以省略掉 get { },直接写成var xxx:
{ … return…}
- var xxx:
- 属性观察:使用willSet和didSet来观察存储属性的变化,注意计算属性不能被观察,不过这不是什么问题,因为可以自行为计算属性编写逻辑,自行通知属性变化事件;
- var a = 123 { didSet() {…} },didSet可以指定参数名称,如果像这样不指定默认参数名称是oldValue,类似的willSet默认参数名称是newValue;
类型属性
定义在类型上,而不是实例对象上的属性是类型属性(type property)。
值类型的类型,也就是struct的类型属性以static修饰。
class的类型属性可以使用static或者class修饰,使用static修饰的属性不可以被子类覆盖,使用class修饰的可以被子类覆盖,但class修饰的类型属性只能是计算属性,毕竟如果是存储属性的话,是没道理能够被子类覆盖的,计算属性本质上是一对get和set方法,方法才能够被覆盖。
class A {
class var a: String {
return "hello"
}
}
class B : A {
override class var a: String {
return "child"
}
}
访问控制
访问控制围绕两个概念:模块、源文件。
Swift的import作用是引入模块。
Swift的访问控制提供了五个级别:
- open,对于模块内和引入了模块的代码完全可见,并且可以被模块外的类继承
- public,对模块内、和引入了模块的代码可见,但模块外不可以被继承,只能在模块内被继承
- internal,默认访问控制,仅对模块内可见
- filaprivate,仅对所在源文件可见
- private,仅对所在作用于内可见
分别控制属性读写可见行
swift允许分别控制读写的可见性,但是读取的可见行必须大于写入的可见性:
internal private(set) var aaa = “a”
由于internal是默认的可见性,因此可以省略internal:
private(set) var aaa = “a”
类型初始化
基础语法:
init(…) {…},注意初始化方法必须使用init修饰,不用func,也没有方法名。
如果不定义初始化方法,Swift也会帮我们定义一个默认的无参初始化方法,这个默认无参初始化方法内会为所有指定了初始值的属性赋上初值。
struct的成员初始化方法
使用struct的默认初始化方法时,也可以为类型实例属性指定初始值,如下这种使用默认初始化方法指定初始值的方式叫做成员初始化方法(memberwise initializer):
var a = A()(member1: …, member2: …, …)
使用这种方式指定的初始值会覆盖在类定义中为属性指定的初值。由于struct存在这样的由编译器自动生成的初始化方法,因此在swift中,struct即使不写初始化方法,成员属性也可以不指定默认值,因为总是可以在创建实例时通过这种方式为其指定值。
struct的自定义初始化方法
一旦提供了自定义的初始化方法,swift的默认初始化方法就不可用了。
自定义初始化方法和前面的基础语法一样,swift中使用self代表当前实例对象,当参数和属性名字相同时只需使用self引用属性即可。swift要求自定义初始化方法中完成所有属性的初始化,只读属性也可以在初始化方法中被指定值,前面虽然管它们叫常量,但实际上这里的常量语意和通常不一样,指的是在对象生命周期内为常量,而不是在应用整个声明周期内为常量。
可以在一个struct中定义多个初始化方法,使用self.init可以调用其他初始化方法,这种做法被称作委托初始化(initializer delegation)。
class初始化
和struct不同,class不写初始化方法时,虽然也会由编译器生成默认初始化方法,但class没有struct那样的成员初始化方法,正因如此在不写class初始化方法时,swift要求每一个属性都必须被指定默认初始值。如果定义了初始化方法,那么就可以不用为每个属性指定默认值了,此时需要确保初始化方法为每个属性都完成初始化。
子类在特定情况下会继承父类初始化方法:
- 子类为所有新增的属性提供了默认值(因此只需确保父类成员初始化,实例就是完整的)
- 子类没有定义任何初始化方法,就会继承父类的指定初始化方法
- 子类实现了父类所有指定初始化方法(显式指定或隐式继承),就会继承父类便捷初始化方法
class的指定初始化方法
指定初始化方法是不加任何修饰的init初始化方法,指定初始化方法必须确保为所有属性设置好初值。可以使用super.init调用父类的指定初始化方法,调用父类初始化方法前,需要确保本类型新引入的所有属性都已经被设置好初始值。
一个类可以有多个指定初始化方法。
class的便捷初始化方法
便捷初始化方法中无需确保所有属性被赋值,它们总是继续调用另一个便捷初始化方法或者调用指定初始化方法,最终必须确保指定初始化方法被调用。
使用convenience init标记便捷初始化方法。
class的必需初始化方法
可以使用required标记指定初始化方法,这样子类必需要定义一个参数和required完全一样的初始化方法。(不过我没看出来这有什么用,恶心子类吗?)
class的反初始化
只有引用类型存在反初始化,反初始化是类型实例在被清除出内存前调用的方法,使用deinit定义,必须是一个无参的方法:
deinit {…}
在反初始化方法被调用时,实例还是有效的,可以访问实例的任意属性。
可失败初始化
初始化可以失败,这时候需要以某种方式通知调用者。
可失败初始化这个特性相比其他语言比较少见,实际上这个特性让swift能够避免其他语言的不完整对象的问题,当初始化时我们可以检查参数是否有效、依赖的外部资源是否可用,如果不可用,就直接令初始化失败,而不是像其他语言那样依然能够初始化成功,但要通过其他途径检查对象状态来确定对象是否正确的完成了初始化。
定义初始化方法时,使用 init? 表示初始化方法可能失败,当初始化失败时会返回nil,因此init?初始化方法返回类型为Optional。也可以将可能失败的初始化方法用 init! 标记,这样虽然调用者不必考虑nil了,但类型安全性会受到损害,不要这么干。
当需要令初始化失败时,直接在初始化方法中返回nil即可。
值类型和引用类型
值类型在赋值时有复制语意,引用类型没有;
使用let声明的变量是不可变的,对于值类型而言,变量代表的是整个值,因此值类型的所有属性都是不可变的;对于引用而言,变量仅仅是一个引用,并不代表被引用的对象,因此let声明的引用的属性能够被修改。
混用值类型和引用类型时需要小心,一般来说不太希望将引用类型定义成值类型的属性,因为值类型的意义就在于复制语意,将引用类型混入值类型破坏了复制语意,会导致使用值类型的用户极其意外,意外到不得不花几个小时在毫无疑义的调试上,然后感慨这B真菜和这语言真有病。
swift提供了 == 和 ===,前者比较值,后者比较内存地址。值类型不能使用 === 进行比较,因为它们本来就不是对象引用,内存地址比较没有意义。类似的,引用类型默认也不能使用==比较,如果想要比较引用类型的值,必须让引用类型实现equatable协议。
Swift中的基础类型、集合类型、数据类型都是struct。其中,集合类型是值类型而不是引用类型会让很多有其他语言的用户吃一壶,并质疑Swift的集合在作为参数传递时岂不是会导致内存利用效率问题,但实际上,Swift还使用了非常精巧的COW,Copy-On-Write技术,来消除内存复制。
协议
Swift的协议(protocol)概念上和Java中的接口相似,协议用于定义类型需要满足的接口,类型可以实现协议要求的接口,称作符合(conform)这个协议。
// 协议的定义
protocol DataSource {
var rows: Int { get }
func getDetail(row: Int) -> String
}
// 定义符合协议的class或struct,如果有多个协议,需要使用 , 分隔,意思是这个实体类符合所有这些协议
struct CustomDataSource : DataSource {
// 需要在这里提供rows和getDetail实现
var rows: Int {
return 3
}
func getDetail(row: Int) -> String {
return "test"
}
}
// 定义有父类,并且符合协议的class,父类需要放在第一个
class Child : SuperClass, DataSource {
// ....
}
组合VS继承
如果一个类需要符合多个协议,可以在定义类时使用 ,将多个协议连起来:
class Test : Proto1 , Proto2 {….}
另外还有两种思路,一种是如果Proto2本身就是对Proto1的扩展,那么Proto2就应该继承自Proto1:
protocol Proto2 : Proto1 {….}
另外一种是虽然Proto1和Proto2之间没有自然的继承关系,但是将两个协议组合在一起是有意义的,此时可以声明一个新的协议来继承自Proto1和Proto2:
protocol Proto3 : Proto1, Proto2 { }
要求参数符合多个协议
方法声明中,可以将多个协议通过 & 连接起来,表示希望参数同时符合多个协议,这样在方法体中就可以把这个参数当成符合其中任何一种协议的对象来使用了。
mutating需要在协议中声明
出于和前面介绍过的mutating原理相同的考虑,一个可以修改self的协议方法必须在协议定义时就声明为mutating的,这样使用协议类型作为类型参数的方法才能够在编译时被加上inout关键字修饰。
错误处理
基础的错误处理语法如下:
// 定义可能抛出异常的方法
// 先定义Error,Error必须符合Swift.Error协议,这是一个空协议,只要声明符合即可
enum Error: Swift.Error {
case invalidInput(String)
}
func test() throws -> [String] {
// ...
throw Error.invalidInput("err")
// ...
}
// 下面是调用可能抛出异常的方法的语法
// swift使用 do - try - catch来捕获异常
do {
var strs = try test()
// ...
} catch Error.invalidInput(let info) {
// 这里虽然无法取到Error对象,但可以通过类似swich-let的方式取到error的关联值
print("\(info)")
} catch {
// swift会自动将被抛出的Error绑定到常量error上
print("\(error)")
}
此外,在一个已经声明了throws的方法中,可以不写do catch直接使用try关键字来调用可能抛出异常的方法,此时如果真的出现异常,异常会被这个方法继续抛出;
为了提供简化异常处理的可能性,swift还提供 try! 和 try?
- try! - 使用带叹号的try表示坚信不会抛出异常,或者说如果真的抛出了异常,应用也绝对没有任何办法恢复,此时不如直接触发应用崩溃;
- try? - 使用带问号的try,可以不用写异常处理do-catch,try? 表达式的类型为原返回类型的可空类型,此时如果出现异常,try? 表达式的结果就是nil,通常使用guard搭配try?使用;
guard let token = try? getToken() else {
print("err")
return
}
swift错误处理哲学
- swift不规定抛出的异常类型,并且调用可能抛出异常的方法时,必须要提供默认catch,以兼容所有可能的错误,这样做可以对扩展开放,方法能够在不影响调用方的情况下进行扩展;
- swift要求所有可能抛出异常的方法都利用throws进行标记,不提供像Java那样的UncheckedError;
扩展
swift允许扩展已有类型。扩展其实相当于是将某个特定类型的一系列helper方法直接关联到特定类型上的语法糖,因此扩展虽然能够定义各种方法和计算属性,但不能定义新的存储属性。
一般来说,不会直接在原来的类型名称上定义扩展,通常会先使用typealias定义类型别名,然后利用extension <别名> {…}定义扩展,并且后面都使用类型别名配合定义的扩展使用,这样有助于提升可读性,明确语意。不过,直接在原始类型上定义扩展也是OK的,而且由于别名和原类型是等价的,定义扩展时使用别名还是原类型名在功能上是没区别的,利用别名定义的扩展,在原类型名上也可用。
扩展能够:
- 添加计算属性
- 添加新初始化方法,对于struct类型而言,在扩展中定义辅助的初始化方法不影响成员初始化方法;
- 添加新方法
- 使类型符合新协议
- 添加嵌入类型
extension <Type> : <Protocol> {
// 定义计算属性
var aaa: String {
return ""
}
// 定义初始化方法,初始化方法必须转发到原初始化方法
init(...) {
self.init(...)
}
// 还可以定义嵌套类型
enum Err {...}
// 如果定义的是struct扩展方法,并且方法会修改self,那么必须要声明mutating
mutating func change() {...}
}
泛型
基础语法:
struct Stack<Element> {
var items: Element = [Element]()
// ...
}
使用上面这种写法,在类或结构体body中不能调用任何Element类型的方法,甚至连判断相等也做不到,因为Element是什么类型是不确定的,Swift的泛型并不像C++的模板那样自由灵活。如果想要在类中使用泛型类型的方法或属性,就要为泛型加上类型约束。
存在两种约束:一种是类型必须是某个类的子类,一种是类型必须符合某个或某组协议。在声明泛型类型时,可以利用
关联协议
协议不可以直接利用上面那种语法定义,不过协议中可以定义关联类型,关联类型是在使用协议时,由使用方通过typealias指定的,或者是在某个类实现协议时,由实现方通过where指定的。
protocol myPto {
// associatedtype关键字用于定义关联类型
associatedtype Element
func next() -> Element?
}
// 符合类型的类需要利用typealias为关联类型指定类型别名,以明确关联类型的具体类型
class Test<T> : myPto {
typealias Element = T
func next() -> Element {
// 返回一个T类型的
}
// 在泛型类中使用另一个泛型类时,需要指定另一个泛型类的泛型类型,可以使用where来进行指定
func add<S: Sequence>(_ sequence: S)
where S.Iterator.Element = Element {
// 这里就可以把sequence当成Sequence<Element>类型来使用了
}
}
参数多态
swift的泛型本质上是一种编译时多态。
协议扩展
可以为协议定义扩展,这样相当于是基于协议接口定义了一组Helper方法,关联到了这个协议类型上。
内存管理
swift的struct和enum都是值类型,值类型的内存管理比较简单,赋值会导致值类型的浅拷贝,导致分配足够存放值类型所有成员的空间,并将所有成员复制过去。
引用类型则使用引用计数进行内存管理,引用计数一旦为0,对象内存就会被释放。使用引用计数意味着内存管理效率会比较高(相比GC),但是语言必须提供一些机制避免循环引用。
使用弱引用避免循环引用
使用weak修饰属性,以避免循环引用,被weak修饰的属性必须是var,并且是可空类型。
避免在闭包中的循环引用
使用闭包捕获语法将self引用以弱引用捕获,避免对self的强引用捕获:
{
[weak self]
…
}
非逃逸闭包
逃逸闭包指的是在定义闭包的函数结束后,还可能会被调用的闭包,这样的闭包如果使用到了self,就必须捕获一个对self的引用;非逃逸闭包是确保在定义闭包的函数结束前就被调用,并且函数结束后不会再被调用的闭包,非逃逸闭包不必捕获任何引用,因为能够确保闭包被调用时函数没有结束,上下文中的变量一定是可用的,可以直接使用函数上下文中的self。
swift中,通过实例属性存储的闭包是逃逸闭包,而通过方法参数传递的闭包是非逃逸闭包,要是像规定一个方法的闭包参数必须是逃逸闭包,以在方法中将闭包赋值给属性,就必须使用@escaping关键字来修饰方法属性。