简介
- Swift 语言由苹果公司在2014年推出,用来撰写 macOS 和 iOS 应用程序
Swift 语言学习路线及重难点
- 常量与变量
- 数据类型
- 运算符
- 元组
- 逻辑分支
- 循环
- 字符/字符串
- 数组/集合
- 字典
- 可选型
- 类型转换(转化符号is和as)
- 函数
- 闭包
- 枚举
- 结构体
- 类
- 属性与方法
- 构造与析构函数
- 结构体与类
- 协议 protocol
- 扩展 extension
- 泛型
- 异常 和 Result
- 元类型、.self 与 Self
- @objc关键字
- where关键字
- Key Path
- Codable协议
- 访问权限
- 混合开发
- 命名空间
- 学习参考
常量与变量
什么是常量和变量
- 常量:使用
let定义,定义后不可修改 - 变量:使用
var定义,定义后可以修改
常量和变量的使用注意
- 在开发中,建议先定义常量,如需修改再修改为变量(更加安全)
- 声明为常量不可修改的意思是 指针不可以再指向其他对象,但是可以通过指针拿到对象,修改其中属性
数据类型
- Swift中的数据类型有:整型/浮点型/Bool型/元组/枚举/结构体/类等。
类型推断
- Swift是强类型语言(类型安全),是一种总是强制类型定义的语言,要求所有变量都必须先定义后使用
- 注意:
- 定义一个标识符时有直接进行赋值,标识符后面的类型可以省略
- Swift有类型推导,会自动根据后面的赋值来决定前面的标识符的数据类型
运算符
常用的运算符
- +、-、*、/、% ——
算数运算符 - =、+=、-=、*=、/=、%= ——
赋值运算符 - >、>=、<、<=、==、!= ——
比较运算符 区间运算符- 半开半闭区间:
0..<10表示0~9 - 闭区间:
0...10表示0~10
- 半开半闭区间:
- 与
&&或||非!——逻辑运算符 ~=——包含运算符(1...100).contains(42)和1...100 ~= 42效果相同
三目运算符和空合运算符??
Swift 中在进行基本运算时必须保证数据的类型一致,否则会报错。
- 只有相同类型的数据才可以进行运算
- Swift 中没有隐式转换,数据类型的转换必须显示进行:
转换类型(待转换的变量/常量)如Int(Double)
元组
- 元祖是 Swift 中新增的一种数据类型
- 可以用于定义一组数据,用
()括起来,多个值用,隔开 - 组成元组的数据可以称为
元素
// 元组的常见写法
var one = ("李四", 30, 1.75)
var two = (name:"李四", age:30, height:1.75)
let (errorCode, errorInfo) = (404, "Not Found")
逻辑分支
if
// if 的使用
if a > 9 {
print(a)
}
// guard 的使用
guard 条件表达式 else {
// guard是Swift2.0新增的语法,跳转语句一般是return、break、continue、throw
}
语句组
switch
- Swift 对
switch进行了大大的增强,使其拥有其他语言中没有的特性 switch后可以不跟(),但必须有:。- 一个
case判断,可以判断多个值,以,隔开 case后可以跟break和fallthrough,如需case穿透,使用关键字fallthrough- 支持区间判断和多种数据类型、浮点型、字符串类型等
case后如果是多行语句可以不加{}。
循环
for循环
// for in 循环
for i in 0..<10 {
print(i)
}
// 特殊写法 如不需要用下标i
for _ in 0...10 {
print("hello swift")
}
- 序列函数
stride- 常见的区间运算符主要是
...和..<,通过它们可以构造遍历的内容区间。 - 可以通过
stride返回一个序列,作用类似于区间运算符
- 常见的区间运算符主要是
// from: 开始 through: 结束(包含)by: 间隔,类似于 1...11
for i in stride(from: 1, through: 11, by: 2) {}
// from: 开始 to: 结束(不包含) by: 间隔,类似于 1..<11
for i in stride(from: 1, to:11, by: 2) {}
// 构造序列
let seq = stride(from: 0, to: 2, by: 1)
// 迭代器,可以逐一获取序列的值
var i = seq.makeIterator()
i.next() // 0
i.next() // 1
i.next() // nil
i.next() // nil
while 和 repeat while 循环
var a = 0
while a < 10 {
// a++已经在Swift3之后淘汰
a = a + 1
}
var b = 0
repeat {
// 循环体至少执行1次
b = b + 1
} while b < 20
字符Character
- 一种数据类型,类型关键字为
Character - 字符串可以理解为多个字符的集合,字符是组成字符串的基本单位
- 字符的值只能包含一个值
- 字符的值必须包含在
""双引号里**(单引号不行)**
转义字符
有一些特殊的字符,通常以 \ 开始,有着特殊的意义
\0空白符\\反斜杠 \\t制表符\n换行符\r回车符\'\"单引号,双引号\u{}用unicode码创建字符
其他方法
Character大部分情况下都要与 String 配合使用。下面是 Swift5 新增的内容,较实用:
let content: String = "🐍 ABC123😁\n45"
var number = 0 // 数字
var letter = 0 // 字母
var newline = 0 // 换行
var whitespace = 0 // 空格
var symbol = 0 // 表情
for str in content {
number += str.isNumber ? 1 : 0 // 判断数字 = 5
letter += str.isLetter ? 1 : 0 // 判断字母 = 3
newline += str.isNewline ? 1 : 0 // 判断换行 = 1
whitespace += str.isWhitespace ? 1 : 0 // 判断空格 = 2
symbol += str.isSymbol ? 1 : 0 // 判断表情 = 2
}
字符集合CharacterSet
- 在 Swift 中,有一个与
Character概念相关的数据类型CharacterSet,可以称之为数据集合,它适用于操作Unicode字符,在开发中经常使用。 - 常见的
CharacterSet分类CharacterSet.whitespaces空格CharacterSet.whitespacesAndNewlines空格和换行CharacterSet.lowercaseLetters小写英文字母CharacterSet.uppercaseLetters大写英文字母CharacterSet.letters所有英文字母,包含大小写CharacterSet.decimalDigits0~9 数字CharacterSet.alphanumerics字母和数字的组合inverted相反的字符集(取反操作)
- 应用
// 自定义 trim 函数,去除空格(或者特定字符)
public func trim(str: String) -> String {
return str.trimmingCharacters(in: CharacterSet.whitespaces)
}
// 验证密码是不是只有数字组成
public fun judgePwd(str: String) -> Bool {
return str.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil ? true : false
}
judgePwd(str: "1234") // true
judgePwd(str: "1234A") // false
// URL编码
func urlValidate(url: String) -> URL {
let url = URL(string: url.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed) ?? "")
return url!
}
字符串
String是一个结构体,NSString是OC对象,String性能更高String支持直接遍历
字符串常用操作
// 1、长度
let str = "12345678"
let len = str.count // 8
let len2 = (str as NSString).length // 8
// 2、拼接 使用 + 或 append
// append,返回Void,会改变原先字符串的值。appending,返回String,会产生一个新字符串,原先值不会改变。
let str = "abc" + "def"
// 3、删除
// remove,会改变原字符串的值,返回被删除的那个Character或Void。drop,不会改变原字符串的值,返回删除后的Substring。
var str: String = "ABC12345"
let a = str.removeFirst() // A
let b = str.removeLast() // 5
print(str) // BC1234
var str2: String = "ABC12345"
let c = str2.dropFirst() // BC12345
let d = str2.dropLast() // ABC1234
print(str2) // ABC12345
// 4、遍历
// 方式一:字符遍历
for char in str {
print(char)
}
// 方式二:元组遍历
for (index, value) in str.enumerated() {
print("\(index) --- \(value)")
}
// 5、大写或小写
str.lowercased().uppercased() // 小写 再 大写
str.capitalized // 单词首字母大写
// 6、判断开始于结束
str.hasPrefix("A") // 是否以A开始,返回Bool值
str.hasSuffix("N") // 是否以N结束
// 7、含有字符串
str.contains("cd")
// 8、串分割
// components,分隔符可以是字符或者字符串。split,分隔符只能是字符
let str1 = "abcdefg$$aaaa$$bbbb$$ccc"
let desc1 = str1.components(separatedBy: "$$")
let str2 = "abcdefg$aaaa$bbbb$ccc"
let desc2 = str2.components(separatedBy: "$") // components方式
let desc3 = str2.split(separator: "$") // split方式
let str3 = "abcdefg$aaaa$$$$bbbb$ccc"
let desc4 = str3.components(separatedBy: "$")
// 忽略多个 $ 因为omittingEmptySubsequences参数默认为true
let desc5 = str3.split(separator: "$")
// 与components方式一致
let desc6 = str3.split(separator: "$", omittingEmptySubsequences: false)
// 9、替换
let desc1 = str1.replacingOccurrences(of:"?", with:"**")
// 10、子串
str.prefix(5) // 截取前5个字符
str.suffix(5) // 截取后5个字符
str.index(str.startIndex, offsetBy: -2)
let sub1 = str[0..<5] // 从位置0开始到5结束获取字符串
// 截取第 1 个字符到第 3 个字符范围的字符串
let index3 = str.index(str.startIndex, offsetBy: 3)
let sub3 = str[str.startIndex..<index3]
// 11、格式化
String(format: "%.2f", 3.1415926) // 3.14
let value = String(format: "%02d", 1) // 01
// 12、String转unicode
var myString = "好"
for scalar in myString.unicodeScalars {
print(String(scalar.value, radix: 166)) // 597d
}
// 13、区间连接
// 可以通过 ... 或者 ..< 来连接两个字符串。一个常见的使用场景就是检查某个字符是否是合法字符。
let str = "Hello Swift"
let content: ClosedRange<String> = "A"..."Z"
for c in str {
if content.contains(String(c)) {
print("\(c)是大写字母")
}
}
// 输出:H是大写字母 S是大写字母
- Swift 5.0 新推出的
Raw String- Swift 5.0 对字符串增加了一个新特性,使用
#包裹字符串。
- Swift 5.0 对字符串增加了一个新特性,使用
// Swift 5.0 之前
let str = "字符串中有\\转义字符反斜杠\\"
// Swift 5.0,如果字符串声明被 # 号包裹,\ 不需要转义
let str = #"字符串中有\转义字符反斜杠\"#
// 假如字符串中有 # 怎么办?用两个 # 包裹字符串
let str = ##"字符串中有#转义字符反斜杠\"##
**
length与count**
在 Objective-C 中,获取字符串长度用的是length返回的是基于UTF-16的长度,而 Swift 中获取字符串长度用的是count返回的是Unicode字符个数。二者在纯文本字符串中没有区别,但在包含Emoji字符串时就有区别了,所以在混合编程中获取富文本字符长度时要格外注意这个问题。如果在 Swift 中想要获取和 OC 一样的长度值,可以使用String.utf16.count。
数组
- 数组是一堆有序的由相同类型元素构成的集合类型。
- 数组中的元素可重复出现。
- Swift用
Array表示数组,是一个泛型集合类型。 Array是一个结构体,可以放普通类型(Int,Double,Bool等)
// 定义
let array1: [String] = []
let array2: [String] = [String]()
var array3: Array<String> = ["zhangsan", "lisi", "wangwu"]
// 创建重复元素的数组
let array = Array(repeating: "A", count: 10)
// 基本操作
array.count // 获取长度
array.isEmpty // 判空
array.append("l") // 添加数据
array.insert("wo", at:0) // 插入元素
array.dropFirst() // 删除元素 二种remove和drop
array[0] = "fangqi" // 修改元素
array.reverse() // 倒序
array.contains("zs") // 包含数据
array.firstIndex(of: "zs") // 索引 获取数组中第一个符合元素的索引
// 数组合并,只有相同类型的数组才能合并
var array = ["zhangsan", "lisi", "wangwu"]
var array1 = ["zhaoliu", "wangqi"]
var array2 = array + array1 // 数组合并直接用 + 即可
// Any任意类型,虽不报错,但是不建议一个数组中存放多种类型的数据
var array3: [Any] = [2, 3.0, "zhangsan"]
var array4: [Any] = ["lisi", true]
var array5: [Any] = array3 + array4
// 普通遍历
for i in 0..<array.count {
print(array[i])
}
// for in 方式遍历
for item in array {
print(item)
}
// 设置遍历的区间
for item in array[0..<2] {
print(item)
}
// 枚举方式遍历
for (index, name) in array.enumerated() {
print(index)
print(name)
}
// 迭代器
array.makeIterator()
集合Set
- 与数组的概念差不多,主要区别:
- 元素无序
- 元素不能重复
- Swift 用
Set表示集合,也是一个泛型集合类型
// set无序,所以不能像数组那样通过下标获取数据
set.first
Set(set.prefix(2)) // 获取set的前2个值
Set(set.suffix(2)) // 获取set的最后2个值
set.insert("man") // 插入数据
Set提供了方便的运算
- 并集
- 交集
- 交集的补集
- 补集
- 子集与真子集(如果A是B的子集且B不是A的子集,那么A叫做B的真子集)
- 超集与真超集
- 判断是否有交集
对象重复问题 Hashable
- 集合中重复是否元素是靠一个叫
hash值的东西决定的。 - 当一个类型遵守
Hashable协议并且其属性也都遵守Hashable协议的时候,Swift会帮我们自动计算这个对象的hash值 - 如果需要手动处理对象的
hash值,需要利用hash()与==()两个函数。
// 添加两个方法:一个用于生成自定义哈希值,另一个用于检查两个 是否相同。例:
struct Student: Hashable {
var id: Int
var name: String
var birthday = Date()
init(id: Int, name: String) {
self.id = id
self.name = name
}
// 第一个需要实现的方法是用id和name生成哈希值
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
}
// 第二个需要实现的方法是运算符重载"==",比较id和name是否相等
static func ==(lhs:Student, rhs: Student) -> Bool {
return lhs.id == rhs.id && lhs.name == rhs.name
}
}
// 如果两个对象经人为操作后相等,然后插入到Set中,则Set中最终的保留的数据第一个,因为第二次插入直接会失败。
// 上面案例对 class 一样适用
案例二
- 如果插入的是同一个实例,只是修改了其中属性,则
struct与class会出现不同的结果 struct- 不论修改实例的哪个属性,原来的实例不会变,会多出一条新的数据。
class- 修改任一属性,这个属性恰恰会影响 hash 值,则程序会随机崩溃。
- 崩溃信息:
Fatal error: Duplicate elements of type 'Student' were found in a Set. This usually means either that the type violates Hashable's requirements, or that members of such a set were mutated after insertion.
- 结论:将引用类型的实例插入到集合Set后,不能修改任何影响其哈希值或相等性测试的属性,也就是修改
hash()和==()函数里面的属性是不可以的。
字典
- 字典是由键值
key:value对组成的集合类型。 - 字典中的元素之间是无序的。
- 键集合是不能有重复元素的,而值集合是可以重复的。
- Swift 中的字典类型是
Dictionary,也是一个泛型集合类型。 - Dictionary 是一个结构体,可以放普通类型(Int, Double, Bool等)。
- 用
[]表示一个字典,里面的元素由,隔开。
// Swift中任意类型用Any表示,如下定义字典
var dict: [String:Any] = ["name":"张三", "age":18]
// 基本操作
dict.count // 获取长度
dict.isEmpty // 判空
dict["height"] = 1.82 // 添加数据
dict["name"] = "lisi" // 修改字典 或使用 dict.updateValue("lisi", forKey:"name")
dict.removeValue(forKey: "height") // 删除字段
// 合并
var dict1: [String: String] = ["name": "zhangsan", "age": "20"]
var dict2: [String: String] = ["height:" "1.8", "phoneNum": "1883"]
for (key, value) in dict2 { // 字典合并不能像数组那样直接用+
dict1[key] = value
}
// 遍历
for value in dict.values {}
for key in dict.keys {}
for (key, value) in dict {}
for (index, value) in dict.enumerated() {} // 枚举方式遍历
dict.makeIterator() // 迭代器
可选型 Optional
- 可选类型(Optional)的取值为:有值 | nil
// 定义可选类型
let name: String? = nil
// 取出可选类型的值 ! 强制解包(显示解包)
print(name!) // 如果可选类型为nil,会报错
// 可选绑定(隐式解包)
if let str = name {
print(str) // 此时输出就是str的值,而不是Optional
}
// 或使用guard取出可选类型的值(必须在函数范围内使用)
guard let str = name else {
return
}
print(str)
// while let 表示一个当遇到 nil 时终止的循环
let seq = stride(from: 0, to: 11, by: 1)
var i = seq.makeIterator()
while let i = i.next() {
print(i)
}
// 空合运算符
var name: String?
name = "lisi"
// 如果name有值,就强制解包并返回,如果没有值,就返回右边的值
let result = name ?? "zhangsan"
类型转换
Swift 是一种强类型语言,在类型转换的时候,必须要显示的表达出来。类型转换主要包含基本类型之间的转换,基本类型与其他类型的转换,类之间的转换等。
基本类型转换
- 基本数据类型转换如
Double转Int,只需用数据类型(待转类型)转换即可。
基本类型与字符串转换
- 字符串采用插值方式直接将基本类型转换成字符串。
var age = 10
var str = "小明今年\(age)岁"
NSLocalizedString中的插值(Swift5)
let label: UILabel = UILabel()
let quantity = 10
// Swift 5 之前这样写是错误的,因为插值发生在翻译之前
label.text = NSLocalizedString("You have \(quantity) apples", comment: "Number of apples")
- 字符串转换成基本类型,返回的是可选型,因为不能保证字符串的内容是可以转换的。
AnyObject、Any
AnyObject:代表任何类类型的对象实例。Any:比AnyObject范围更广,代表除函数外任何类型的实例,不仅包含普通对象,还包含基本类型。
类型判断与转化 is 和 as
is:使用类型检查操作符is来检查一个变量/常量是否属于一个特定的数据类型,如果属于该检查操作符返回true。as:类型转换操作符有3种,as用于向上类型转换,as? 或 as!用于向下类型转换。- 如果不确定向下转换类型是否能够成功,使用
as?返回一个目标类型的可选型。如果转换失败,返回nil - 如果确定向下转换类型会成功时,使用
as!直接转换成目标类型,如果转换失败,直接崩溃。
- 如果不确定向下转换类型是否能够成功,使用
// 定义数组
let array: [Any] = [12, "zhangsan"]
// 取出数组中最后一个元素
let objcLast = array.last!
// is 判断元素是否是一个Int类型
if objcLast is Int {
print("是Int类型")
}
// as? 将Any转成可选类型,通过判断可选类型是否有值,来决定是否转化成功了
let name = objcLast as? String
print(name) // 结果:Optional("zhangsan")
// as! 将Any转成具体的类型,如果不是该类型,那么程序会崩溃
let name2 = objcLast as! String
print(name2) // 结果:zhangsan
函数
func 函数名(参数列表) -> 返回值类型 {
return 返回值
}
函数的使用注意
- 函数参数没有用
var和let修饰,但它是常量,不能在函数内修改 - 每个函数的形式参数都包含 形式参数标签 和 形式参数名 两部分
- 某些情况,如果没传入具体的参数,可以使用 默认参数
- 可变参数,可接受不确定数量的参数,必须有相同的类型
- 默认函数参数是 值传递 ,如想改变外面变量,使用
inout关键字 - 函数的嵌套,不推荐该写法
函数类型
- 函数是 引用类型
- 每个函数都有属于自己的类型,由函数的 参数类型 和 返回类型 组成
- 有了函数类型,就可以把函数类型像
Int、Double、Array来用,比如函数类型(Int, Int) -> Int
在 iOS 开发中,如果一个函数有返回值,但是在调用的地方没有用 变/常量 接收,则编译器会给出一个警告,提醒函数的结果没有使用。此时有 2 种方式去除警告:
- 使用通配符
_来接收函数的返回值。 - 在函数前面加上
@discardableResult。
函数重载
- 函数重载需要两个条件
- 函数名相同
- 函数形式参数个数或形式参数类型或形式参数标签不同
- 返回值类型与函数重载无关
内联函数(了解略)
闭包
// 闭包表达式
{ (parameters) -> (return type) in
statements
}
- 闭包表达式由一对
{}开始与结束 - 由
in关键字将闭包分割成两部分:参数与返回值、闭包体 - 闭包形参不能提供默认值
闭包参数名称缩写
- Swift 提供了参数名称的缩写功能,直接通过
$0, $1, $2来顺序调用闭包的参数。 - 在闭包表达式中使用参数名称缩写后,闭包可以进一步简化。如下案例:
// 从数组中筛选出合适的数组组成新的数组
func getList(score: [Int], op: (Int) -> Bool) -> [Int] {
var newScore: [Int] = [Int]()
for item in score {
if op(item) {
newScore.append(item)
}
}
return newScore
}
let array = getList(score: [65,75,85,95], op: { (num: Int) -> Bool in return num>80 })
// 简写一:省略 -> 与返回值类型
let array1 = getList(score: [65,75,85,95], op: { (num: Int) in return num>80 })
// 简写二:省略参数类型和括号
let array2 = getList(score: [65,75,85,95], op: { num in return num>80 })
// 简写三:省略 return 关键字
let array3 = getList(score: [65,75,85,95], op: { num in num>80 })
// 简写四:参数名称缩写,省略参数声明和 in,改为$0
let array4 = getList(score: [65,75,85,95], op: { $0>80 })
捕获列表
- 闭包可以在上下文环境中捕获常量、变量、并在自己的作用域内使用
- 可以使用 捕获列表 显示的控制如何在闭包中捕获值。捕获列表是在参数列表之前,以
,分隔的[表达式列表]。如果使用捕获列表,则必须使用in关键字,即使省略了参数名,参数类型和返回类型。 - 捕获列表 中的属性使用的是
拷贝,否则使用的是引用。
var vehicle = "Car"
var animal = "Cat"
let closure = { [vehicle] in // 会产生一个vehicle的拷贝
print("Vehicle is \(vehicle)") // Vehicle is Car
print("Animal is \(animal)") // Animal is Dog
}
vehicle = "Airplane" // 这里修改不会影响 捕获列表 中的属性值
animal = "Dog"
closure()
** 三种类型(尾随闭包、逃逸闭包、自动闭包) **
尾随闭包
- 是一个写在函数括号之后的闭包表达式,函数支持将其做为最后一个参数调用
- 闭包是函数的最后一个参数。
- 调用时,函数的
)可以前置到倒数第二个参数末尾,后面的参数直接使用{ // 执行代码 },形式参数标签也随之省略 - 只有一个参数,且该参数是闭包时,函数的
()可以省略。 - 将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。
// 尾随闭包
func doSomething(info: String, closure: (String) -> Void) {
clousre(info)
}
// 不使用尾随闭包进行函数调用
doSomething(info: "Hello", closure: { s in print(s) })
// 使用尾随闭包进行函数调用
doSomething(info: "World") { s in
print(s)
}
逃逸闭包 @escaping
- 闭包作为一个参数传递给一个函数,且闭包在函数执行完之后再调用。
- 可以在函数中将逃逸闭包赋值给一个类的成员变量或者保存在一个数组中以备后续调用。
- 声明闭包作为形参的函数时,可以在形参的类型之前写上
@escaping来明确闭包是允许逃逸的。 - 逃逸闭包常用于异步回调。
- 捕获了
inout参数的闭包不允许逃逸
// 逃逸闭包:闭包可以超出函数的范围来调用
// 存放没有参数、没有返回值的闭包
var closureArray: [()->Void] = [()->Void]()
// 定义一个函数,接收一个非逃逸闭包为参数
func nonEscapeClosure(closure: ()->Void) {
// closureArray.append(closure) // error、非逃逸闭包这样操作会报错
closure()
}
// 定义一个函数,接收一个逃逸闭包为参数,将闭包并存储到一个数组里面去,并没有调用
func escapeClosure(closure: @escaping ()->Void) {
closureArray.append(closure)
}
var x = 10
print(x) // 10
nonEscapeClosure {
x = 100
}
print(x) // 100,因为闭包在函数里面执行了
escapeClosure {
x = 200
}
print(x) // 100,因为闭包逃逸了,没有在函数里面执行
closureArray.first?()
print(x) // 200,在函数外面调用了闭包
自动闭包 @autoclosure
- 一种自动创建的闭包,用于包装函数参数的表达式
- 不接受任何参数,被调用时会返回被包装在其中的表达式的值
- 在形参的类型之前加上
@autoclosure关键字标识是一个自动闭包
// 自动闭包
func printIfTrue(predicate: @autoclosure ()->Bool) {
if predicate() {
print("is true")
}
}
// Swift 将会把 2 > 1 这个表达式自动转换为 () -> Bool
printIfTrue(predicate: 2>1)
Swift中闭包在官方系统库中的应用函数
- sort —— 排序
var array: [String] = ["Animal", "Baby", "Apple", "Google", "Aunt"]
// 这种默认升序
array.sorted()
// 如果需要降序
array.sort { (str1, str2) -> Bool in
return str1 > str2
}
- forEach —— 遍历
var array: [String] = ["Animal", "Baby", "Apple", "Google", "Aunt"]
// 遍历
array.forEach { (str) in
print(str)
}
- filter —— 筛选
var array: [String] = ["Animal", "Baby", "Apple", "Google", "Aunt"]
// 筛选
let a = array.filter { (str) -> Bool in
str.starts(with: "A")
}
- map —— 变换
var array: [String] = ["Animal", "Baby", "Apple", "Google", "Aunt"]
// 闭包返回一个变换后的元素,接着组成一个新数组
let a = array.map { (str) -> String in
"Hello " + str
}
- reduce —— 合归
var sum:[Int] = [11, 22, 33, 44]
var total = sum.reduce(0) { (result, num) -> Int in
return result + num
}
- allSatisfy —— 条件符合
// 判断数组的所有元素是否全部大于85
let scores = [86, 88, 95, 92]
// 检查序列中的所有元素是否满足条件,返回Bool
let passed = scores.allSatisfy { $0 > 85 }
- compactMap —— 转换
let arr: Array = [1, 2, 34, 5, 6, 7, 8, 12, 45, 6. 9]
// 返回操作的新数组(并不是筛选),数组,字典都可以使用
let compact = arr.compactMap({ $0%2 == 0})
- mapValues —— 转换value
let dic = ["first":1, "second":2, "three":3, "four":4]
// 字典中的函数,对字典的value执行操作,返回改变value后新的字典
let mapValues = dic.mapValues({ $0 + 2 })
- compactMapValues —— 上面两个的合并
let dic = ["first":1, "second":2, "three":3, "four":4, "five":"abc"]
// 将上述两个方法的功能合并在一起,返回一个对value操作后的新字典,并且自动过滤不符合条件的键值对
let newDic = dic.compactMapValues({Int($0)})
first(where:) —— 筛选第一个符合条件的 last(where:) —— 筛选最后一个符合条件
var array: [String] = ["Animal", "Baby", "Apple", "Google", "Aunt"]
let elementF = array.first(where: { $0.hasPrefix("A") })
let elementL = array.last(where: { $0.hasPrefix("A") })
removeAll(where:) —— 删除
// 高效根据条件删除,比filter内存效率高,指定不想要的东西
var array: [String] = ["Animal", "Baby", "Apple", "Google", "Aunt"]
array.removeAll(where: { $0.hasPrefix("A") })
循环引用
- 循环引用指的是
某个类与闭包相互引用,导致内存无法释放的问题 - 循环引用问题必须解决,如下方案:
- 使用
weak,对当前控制器使用弱引用。weak var weakSelf = self - 和 方案1 类似,可以直接写在闭包中,在闭包
in之前加上[weak self],这样闭包中self都是弱引用 - 和 方案2 一样,只是关键字
unowned替换weak。unowned表示即使它原来引用的对象被释放了,仍然会保持对被已经释放了的对象一个无效的引用,它不能是Optional值,也不会被指向nil。
- 使用
枚举
- 枚举是一等数据类型,它定义了一组数据,并提供了一种安全的方式来使用这些数据。
- 枚举中可以像类和结构体一样增加 属性和方法。
- 枚举中的每个数据称之为枚举成员。
- 枚举不必给每一个枚举成员提供一个值。
- 枚举可以提供的值类型有:字符串、字符、整型值,浮点值等。
- 使用
enum关键字定义枚举,并把定义放在一对{}内,用case关键字定义枚举成员值。
枚举定义
enum Sex {
case male
case female
}
枚举类型推断
- 如果枚举类型确定了,在访问值的时候可以用
.值来访问。
枚举成员类型
- 在定义枚举时可以设置枚举成员的类型,但设置后不一定要给成员赋值;如果给成员赋值了就必须设置枚举成员的类型。
- 直接在枚举后面跟上
: 类型即可。 如:enum Sex: Int
枚举原始值
- Swift 中的枚举默认是没有原始值的,但是可以在定义时告诉系统让枚举有原始值。
- 注意
- 原始值区分大小写。
- 通过
rawValue可以获取原始值。 - 通过
rawValue返回的枚举是一个可选型,因为原始值对应的枚举不一定存在。 - 如果想指定第一个元素的原始值后,后面元素原始值默认
+1,枚举一定是Int类型。
enum Planet: Int {
case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus
}
let planet: Planet = .Mars
print(planet.rawValue) // 4
// p是一个可选型
let p = Planet(rawValue: 3)
if let p = p {
switch p {
case .Mercury:
print("mercury")
case .Venus:
print("Venus")
}
}
枚举遍历
Swift 4.2 中对枚举扩充了一个功能。可以通过在枚举名字后面写 : CaseIterable 来允许枚举被遍历。Swift 会暴露一个包含对应枚举类型所有情况的集合名为 allCases,通过它可以将枚举的所有情况包进一个集合中。
enum Method: CaseIterable {
case Add, Sub, Mul, Div
}
for method in Method.allCases {
print(method)
}
枚举关联值
- Swift 中的枚举可以设置关联值,将枚举的成员值跟其他的类型关联存储。
- 在需要枚举值有不同类型的时候会很有意义,例如成绩,既可以表示为具体得数字,也可以表示为优良中差的字符串形式。
- 如何表示关联值:在成员值后面加上
(类型)。
// 定义带有关联值的枚举
enum Result {
case number(Double)
case words(String)
}
// 使用
var result: Result = .number(98.5)
result = .words("优")
- 关联值与
switch配合使用- 此时
case后面需要var或let修饰。 - 枚举的关联值也需要提供一个临时的占位符。
- 如果是
var修饰,则占位符为变量。 - 如果是
let修饰,则占位符为常量。(推荐)
- 此时
switch result {
case let .number(n):
print(n)
case let .words(w):
print(w)
}
递归枚举
递归枚举的意思是枚举的成员类型正好是当前枚举。需要使用关键字 indirect 来表示枚举或者成员可递归
// 标记整个枚举是递归枚举
indirect enum Food {
case beef
case bread
case hamburger(Food, Food) // 汉堡包也是食物,但他需要牛肉和面包
}
// 仅标记存在递归的枚举成员
// 推荐写法:仅标记枚举成员,能够减少不必要的消耗。
enum Food {
case beef
case bread
indirect case hamburger(Food, Food)
}
枚举的可变参数
Swift 5 之后,枚举定义时的关联值为可变参数的,由之前的 ... 改为数组。
enum ABC {
// case abc(argv: Int...) // Swift 5 以前这样定义
case abc(argv: [Int])
}
func getABC() -> ABC {
return .abc(argv: [0, 1, 2, 3])
}
@unknown 发出警告
Swift 5 为枚举新增了一个关键字 @unknown,主要是为了在 switch case 使用枚举时,如果不是所有情况都使用,default 前面加上该关键字可以发出警告,提醒开发者这里没有使用枚举的所有情况。
- 枚举的值如果没有全部列入
case,则程序直接报错。 - 如果加入
default程序完全正确,这样其实是不好的,忽略了枚举的其他可能性。 - 引入
@unknown,会发出警告,提醒你没有使用枚举的所有情况,但程序还是可以运行。
结构体 struct
- 结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合
- 结构体是值类型(包括枚举)它在代码传递中总是会被拷贝
- 结构体既可以定义属性又可以定义方法
- 常用的结构体比如:
CGRect、CGSize、CGPoint
实例化
- 实例化结构体最简单的是在结构体名字后面写上
(),任何属性都被初始化为它们的默认值。 - 所有的结构体都有一个自动生成的
成员构造函数来实例化结构体,可以使用它来初始化所有的成员属性。 - Swift 5.1以后,如果结构体的属性有默认值,那么该属性在实例化的时候变成可选。
Swift 中哪些是结构体
- Swift 中所有内建类型如
Int,Double,Bool都是用结构体来实现的。 - Swift 中的
String,Array,Set和Dictionary也都是用结构体来实现的。 - 这意味着他们都是值类型,所以它们在赋值或者传递时,会进行拷贝,这里与OC中有明显区别。
类
- Swift 虽然推荐面向协议编程(POP),但其也是一门面向对象编程语言(OOP)。
- 面向对象的基础是类,类产生了对象(类的实例)。
- 类也是由一系列具有相同类型或不同类型的数据和行为构成的数据集合。
- 类中既可以定义属性又可以定义方法。
- 类是 引用类型
- 使用
class关键字定义类 - 和结构体不同 默认没有 成员构造函数。
引用类型
- 不同于值类型,引用类型被赋值到一个常量或者变量,或者被传递到一个函数时它不会被拷贝,而是指向同一个实例对象。
- 内部使用引用计数器管理内存(ARC)。
特征运算符
- 因为类是引用类型,可能有很多常量和变量都是引用到了同一个类的实例。有时需要找出两个常量或变量是否引用自同一个实例,Swift提供了两个特征运算符来检查两个常量或者变量是否引用相同的实例。
- 相同于
=== - 不同于
!==
- 相同于
继承
- 一个类可以从另一个类继承方法、属性和其他特征。
- 当两个类形成继承关系,继承的类就是子类,被继承的就是父类。
- 继承的目的是为了代码复用(
Do Not Repeat Yourself)。
重写
- 重写的属性和方法前需要加上
override关键字 override执行时 Swift 编译器会检查重写的类的父类(或父类的父类)是否存在匹配的属性与方法,没有会报错。- 重写
方法/属性 - 防止重写
- 可以通过
final标记class、func、let/var阻止子类的重写
- 可以通过
多态
在面向对象编程里,继承的同时会产生另外一个问题,那就是多态。多态指的是同一操作作用于不同的对象,可以产生不同结果。
- 本质:父类型的引用指向子类型。
- 前提:继承 + 重写
- 优点:把不同的子类对象都当做父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,写出通用的程序,以适应需求的不断变化,提高代码的扩展性。
属性与方法
Swift 中属性有多种,主要分为:存储属性、计算属性、延迟加载属性、类型属性和全局属性。
存储属性
- 结构体与类可以定义存储属性,枚举不可以定义存储属性。
- 可以提供默认值,也可以在构造函数中进行初始化。
- 可以是
var或者let。 - 存储在实例对象的内存中。
class Student {
// 存储属性
var age: Int = 0
var name: String?
var chineseScore: Double = 0.0
var mathScore: Double = 0.0
}
计算属性
- 枚举、结构体和类都可以定义计算属性。
- 不占用实例对象的内存。
- 必须是
var,不能是let。 - 不直接存储值,但需要提供一个
get和一个可选的set方法间接计算而来。 - 通过
get方法获取值,set方法设置值,且在set方法中默认提供一个名为newValue的变量表示传进来的设置值。 - 如果只提供
get,而不提供set,则该属性为只读属性,测试可以省略get{}。
class Student {
// 计算属性
var averageScore: Double {
get {
return (chineseScore + mathScore) / 2
}
// 以下会发生死循环,因为在set内部进行赋值又会调用set方法
// 可以采用赋值给另外一个属性来防止死循环
// newValue是系统分配的变量名,内部存储着新值
set {
self.averageScore = newValue
}
}
// 只读属性
var grade: String {
return "一年级"
}
}
延迟加载属性
- 延迟加载属性又称为 懒加载属性。
- 设计思想:希望该属性在使用时才真正加载到内存中。
- 使用
lazy关键字定义某个属性需要延迟加载。 - 必须是
var,不能是let。 - 只在第一次访问时初始化一次,但如果有多条线程同时第一次访问,无法保证属性只被初始化一次。
- 本质:在第一次访问的时候执行闭包,将闭包的返回值赋值给属性。
// 格式
lazy var 变量: 类型 = { 创建变量代码 }()
class Student {
// 延迟加载属性
lazy var teachers: [String] = { () -> [String] in
return ["Mr Zhang", "Mr Li", "Mr Yang"]
}()
}
LazySequence
- Sequence 是一个协议,之前学习的Array,Set和Dictionary都直接或间接遵守了 Collection协议,Collection又继承自 Sequence协议。
LazySequence也遵守了 Sequence 协议。 LazySequence同时遵守了LazySequenceProtocol协议,用于处理map, filter等操作的惰性求值,定义如下:
// LazySequenceProtocol
extension LazySequenceProtocol where Self.Elements: LazySequenceProtocol {
@inlinable public var lazy: Self.Elements { get }
}
// LazySequence
extension LazySequence: LazySequenceProtocol {
public typealias Elements = Base
@inlinable public var elements: LazySequence<Base>.Elements { get }
}
LazySequence的应用场景是当 Sequence 元素占用内存过大或元素过多时进行相关操作,因为此时一次性将其全部加载到内存是不合理的。- 案例
let array = [1, 2, 3, 4, 5, 6, 7]
// 普通方式
let result = array.filter { element in
print("filtering \(element)")
return element.isMultiple(of: 2)
}.map { element -> Int in
print("mapping \(element)")
return element * 2
}
print("等所有元素执行完filter和map才能使用")
result.forEach {
print("forEach printing \($0)")
}
// lazy方式
let result2 = array.lazy.filter { element in
print("filtering \(element)")
return element.isMultiple(of: 2)
}.map { element -> Int in
print("mapping \(element)")
return element * 2
}
print("只有使用元素时才执行filter和map")
result2.forEach {
print("forEach printing \($0)")
}
类型属性
- 属性的设置和修改,需要通过类型而不是对象来完成。
- 类型属性使用
static来修饰。 - 枚举、结构体、类都可以定义类型属性。
class Student {
// 类型属性
static var courseCount: Int = 5
}
// 设置类型属性的值
Student.courseCount = 6
// 取出类型属性的值
print(Student.courseCount)
存储类型与计算类型属性
- 类型属性也分为存储类型属性 与 计算类型属性。
- 对于存储类型属性,必须设置初始值。
- 存储类型属性默认是延迟加载的,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次。
几种属性的区别
- 存储属性:实实在在存储常量和变量的值。
- 计算属性:不直接赋值,而是通过计算得来,它提供
get和set方法间接访问和设置值。 - 类型属性:本质是一个全局属性,在类型里限定了其作用域,用关键字
static修饰。 - 延迟加载属性:用关键字
lazy修饰,必须进行初始化即在花括号{}后面要加上()。 - 全局属性:类型外面的属性,作用域全局。
- 总结
- 存储属性,最先被初始化。
- 构造方法,仅次于存储属性调用,可以在这里对存储属性进行赋值。
- 延迟加载属性、类型属性、全局属性都是在第一次使用的时候初始化一次,以后调用都不再初始化。
- 当懒加载属性是基于一个存储属性计算的时候,切勿使用懒加载属性,采用计算属性。
属性观察器
- Swift 通过属性观察器来监听和响应属性值的变化。
- 可以为非 lazy 的 var 存储属性和类型属性设置属性观察器。
- 对于计算属性,不需要定义属性观察器,因为计算属性在
set里就可以获取到属性的变化。 - 定义观察者
willSet:在属性值被存储之前设置。此时新属性值作为一个常量参数被传入。该参数名默认为newValue,可以自定义didSet:在新属性值被存储后立即调用。与willSet相同,此时传入是属性的旧值,默认参数名为oldValue,可以自定义willSet与didSet只有在属性改变时才会调用,在构造函数中进行初始化时不会调用。
class Student {
// 这里用可选型,否则需要用构造函数初始化
var name: String? {
// 属性即将改变,还未改变时会调用的方法
// 可以给 newValue 自定义名称
willSet(new) {
print("willSet---")
// 在该方法中有一个默认的系统属性 newValue,用于存储新值
if let new = new {
print(new)
}
}
// 属性值已经改变了,会调用的方法
didSet {
print("didSet---")
// 在该方法中有一个默认的系统属性 oldValue,用于存储旧值
// oldValue 需要在整个set动作之前进行获取并存储待用,所以存在get操作
if let oldValue = oldValue {
print(oldValue)
}
}
}
}
属性继承与重写
- 属性继承:子类可以继承父类的属性,包括存储属性、计算属性和类型属性,还可以继承父类的属性观察器。
- 属性重写
- 无论继承的是存储属性还是计算属性,子类都可以通过提供
get和set对属性进行重写。 - 不可以将继承来的可读可写的属性重写为只读属性。
- 如果重写时提供了
set方法,一定要提供get方法。
- 无论继承的是存储属性还是计算属性,子类都可以通过提供
- 属性观察器重写
- 无论父类有没有为该属性添加属性观察器,子类都可以添加属性观察器。
- 如果父类已经添加了属性观察器,当属性发生变化时,父类与子类都会得到通知。
self属性
- 类中隐含一个特殊的
self属性,可以使用self访问当前类中的属性和方法。 - 在类中可以不写
self,但在闭包内访问时必须带上self。 self是什么?其本质是当前该类的实例化对象。
方法(实例方法)
- 实例方法(又称对象方法)属于特定类实例、结构体实例或者枚举实例的函数。
- 通过实例化类型的对象来调用。
方法(mutating实例方法)
- 值类型默认情况下,不能在实例方法中修改属性的值。如果需要修改,可以在函数前加上
mutating来实现。
struct Student {
var name: String = "zhangsan"
func say(info: String) {
print(info)
}
mutating func eat(food: String) {
// 修改属性的值
self.name = "lisi"
// 调用当前里面定义的方法
self.say(info: food)
}
}
var stu = Student()
stu.eat(food: "米饭")
类型方法
- 通过类型而不是对象来调用的方法。
- 类型方法可以有两个关键字修饰:
- 在函数前加上
static关键字,能在类和结构体中使用。 - 在函数前加上
class关键字,只能在类中使用。
- 在函数前加上
可调用类型 callAsFunction
- 这是 Swift 5.2 增加的一个新特性
- 如果一个属性是通过类型中名为
callAsFunction的方法实现的,那么可以直接通过语法该类型的实例()获取该值。
class和static总结
- 相同点
- 二者都可以修饰方法,
static修饰的方法叫做静态方法,class修饰的叫做类方法。 - 二者都可以修饰计算属性。
- 二者都可以修饰方法,
- 不同点
class不能修饰存储属性,static可以修饰存储属性,static修饰的存储属性称为静态变量。class修饰的计算属性可以被重写,static修饰的不能被重写。class修饰的类方法可以被重写,static修饰的静态方法不能被重写。class修饰的类方法被重写时,可以使用static让方法变为静态方法。class修饰的计算属性被重写时,可以使用static让其变为静态属性,但它的子类就不能被重写了。class只能在类中使用,但是static可以在类,结构体,或者枚举中使用。
构造与析构函数
说明
- 枚举、结构体、类都可以有构造函数,但只有类有 析构函数。
- 构造函数又分为
默认构造函数、成员构造函数和自定义构造函数。 - 类的构造函数又分为指定构造函数和便利构造函数。
介绍
- 构造函数用于初始化一个枚举、结构体、类的实例(创建对象)。
- 构造函数不需要手动调用,默认情况下创建实例时,必然会调用一个构造函数。
- 即便没有编写任何构造函数,编译器也会提供一个默认构造函数。
- 构造函数的名字为关键字
init,且没有func修饰。 - 如果类是继承自
NSObject,可以对父类的构造函数进行重写。 - 注意点
- 结构体中的属性可以没有默认值,也不需要在构造函数中初始化。
- 类中的属性有三种必须满足三种情况之一
- 有默认值
- 可选型
- 在构造函数中初始化
默认构造函数
- 没有形参的实例方法,使用
init关键字,没有func修饰
成员构造函数
- 结构体类型中的一种特殊构造函数。
- 如果结构体类型中没有定义任何构造函数,它会自动获得一个成员构造函数用于初始化结构体中的所有属性。
自定义构造函数
- 希望在创建一个对象时手动给属性赋值(属性的值是在外面传进去的)
- 自定义构造函数和默认构造函数可以同时存在
- 结构体的自定义构造函数与类相似,但如果属性都有默认值,在 Swift 5.1 后结构体的自定义构造函数就失去意义。如下:
// 没有构造函数
struct Size {
var width = 0.0
var height = 0.0
}
// 此时下面都是正确的
var size = Size()
var size2 = Size(width: 1.2, height: 1.2)
var size3 = Size(width: 1.2)
var size4 = Size(height: 1.2)
// 自定义构造函数
struct Size {
var width = 0.0
var height = 0.0
init(height: Double) {
self.width = 1.2
self.height = height
}
}
// 如果定义了如上构造函数,下面是错误的
var size = Size()
var size2 = Size(width: 1.2, height: 1.2)
var size3 = Size(width: 1.2)
// 只有下面是正确的
var size4 = Size(height: 1.2)
构造函数委托
- 构造函数可以调用其他构造函数来执行部分实例的初始化。这个过程,就是所谓的
构造函数委托。 - 构造函数委托对于值类型和类类型是不同的。
类的指定与便利构造函数
- 所有类的存储属性 - 包括从它的父类继承的所有属性都必须在初始化期间分配初始值。
- Swift 为类定义了两种构造函数以确保所有存储属性都能初始化,它们是
指定构造函数和便利构造函数。- 指定构造函数是类的主要构造函数。指定构造函数可以初始化所有类引用的属性,并且调用合适的父类构造函数来继续这个初始化过程给父类链。
- 一个类通常都有这样的一个指定构造函数,它的参数与存储属性一一对应。
- 便利构造函数并不是必须的,它需要借助于指定构造函数完成存储属性的初始化。
- 便利构造函数需要在
init之前加上convenience关键字。
// 类的指定构造函数
init(parameters) {
statements
}
// 便捷构造函数需用 convenience 修饰符放到 init 关键字前
convenience init(parameters) {
statements
self.init(parameters)
}
- 指定构造函数必须总是向上委托
- 便捷构造函数必须总是横向委托
构造函数的继承与重写
- 在Swift中,子类的构造函数有两种来源,首先是自己拥有的构造函数,其次是从父类中继承过来的构造函数。但是,并不是所有父类构造函数都能被子类继承。子类继承父类的构造函数是有条件的,遵守以下2个规则:
- 规则1 - 如果子类没有定义任何
指定构造函数,它会自动继承父类所有指定构造函数。 - 规则2 - 如果子类提供了
所有父类指定构造函数的实现,那么它会自动继承所有父类便利构造函数。
- 规则1 - 如果子类没有定义任何
- 如果一个子类中任意的构造函数和父类的便利构造函数一模一样,不算重写。
可失败的构造函数
必要构造函数
- 在类的构造函数前添加
required修饰符来表明它是一个必要构造函数。 - 当子类重写父类的必要构造函数时,必须在子类的构造函数前也要添加
required修饰符以确保当其他类继承该子类时,该构造函数同为必要构造函数。 - 在重写父类的必要构造函数时,不需要添加
override修饰符。
析构函数
- Swift 会自动释放不再需要的实例以释放资源
- Swift 通过自动引用计数(
ARC)处理实例的内存管理 - 当引用计数为 0 时,系统会自动调用析构函数(不可以手动调用)
- 通常在析构函数中释放一些资源(如移除通知等操作)
- Swift 通过自动引用计数(
- 语法
deinit {
// 执行析构过程
}
单例模式
- 单例模式是一个很常见的设计模式。对于一些希望能在全局方便访问的实例或者在 App 的生命周期中应该只存在一个的对象,一般都会使用单例来存储和访问。
- 保证一个类仅有一个实例,且该实例必须自己创建并提供一个访问它的全局访问点。
- iOS 开发中的
UIApplication, Bundle, NotificationCenter, FileManager, UserDefaults, URLCache等都使用了单例
class SharedManager {
// 类型属性
static let shared = SharedManager()
// 关键点:私有构造函数覆盖默认的,这样就不能通过 init 来构造实例
private init() { }
}
// 使用单例
let manager = SharedManager.shared
结构体与类
结构体和类都可以用来定义数据模型,结构体总是通过值来传递,而类总是通过引用来传递,所以经常会有这样的描述:struct 是值类型,class 是引用类型
相同点
- 可以定义属性
- 可以定义方法
- 可以定义构造函数(
init函数) - 可以使用扩展
- 可以遵守协议
不同点
- 属性的初始化不同:结构体会自动生成一个成员构造函数,而类必须显示创建一个指定构造函数。(属性既不是可选型又没有初始值)
- 修改属性的函数不同:修改结构体属性的函数前需要有
mutating关键字,类不需要。 - 结构体不能继承,类可以继承。
- 结构体是值类型,分配在栈中;类是引用类型,分配在堆中。
- 类有判等运算
===,而结构体没有。 - 类有析构函数
denit,而结构体没有。 - 赋值导致的结果不同:结构体赋值的时候,会
copy一份完整相同的内容给另一个变量(开辟了新的内存地址,这种方式也称之为深拷贝),而类在赋值的时候,不会进行copy,只是增加了原变量内存地址的引用而已(没有开辟新的内存地址,这种方式也称之为浅拷贝)。
使用建议
- 如果不需要继承的话,尽量选择结构体。
- 如果一定要选择类,把它声明为
final(除非需要被继承),可以提升性能。 - 不管是结构体还是类,尽可能把属性定义为常量。
- 在需要与 Objective-C 相互操作时使用类。
- 需要控制身份时使用类,不需要时使用结构体。
- 使用结构体和协议来建立继承和共享关系。(面向协议编程,开发中优先采用
结构体+协议而不是类+继承)
第 5 点解读
Swift 中的类因为是引用类型,具有内置的标识概念。这意味着当两个不同的类实例即使它们的存储属性值完全相同,用===判断仍然是false,也就是不同的实例。这也意味着在应用程序中共享类实例时,对该实例所做的更改会影响其他使用该实例的地方。当需要实例具有这种标识概念时使用类。
结构体作为数据模型
优点
- 不会内存泄漏:由于值类型没有引用计数,所以不会有循环引用导致内存泄漏的问题。
- 访问速度快:值类型以栈的形式分配,而不是用堆,因此访问速度比类快。(堆和栈可以理解为内存分配中的不同功能区域,虽然都属于内存的一部分,但存放的内容不一样)
- 线程安全:值类型是线程安全的。
为什么
struct比class访问速度快?
- 分配和释放:堆在分配和释放时都要调用相应的函数,这些都会花费一定的时间,而栈却不需要。
- 访问时间:访问堆的内容,需要两次访问内存,第一次得取得指针,第二次才是真正的数据,而栈只需访问一次。
copy-on-write
值类型的拷贝会产生额外的开销,但是 Swift 对其做了优化:在没有必要拷贝的时候,值类型的拷贝的动作是不会发生的,只有当值类型的内容发生改变时,值类型才会真正进行拷贝,这种技术称之为 copy-on-write。
为什么内建类型设计为值类型?
值类型和引用类型相比,最大优势可以高效的使用内存,值类型在栈上操作,引用类型在堆上操作,栈上操作仅仅是单个指针的移动,而堆上操作牵涉到合并、位移和重链接,Swift 这样设计减少了堆上内存分配和回收次数,并使用 copy-on-write 将值类型与拷贝开销降到最低。
协议 protocol
协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现即遵循协议
扩展 extension
- 为现有的类、结构体、枚举类型、协议添加新功能。扩展和Objective-C中的分类类似
- Swift中使用
extension关键字实现扩展
面向协议编程
针对某个需要实现的功能,可以使用协议定义出接口,然后利用协议扩展提供默认的实现,需要这个功能,只需要声明遵守这个协议即可,遵守某个协议的对象调用协议声明的方法时,如果遵守者本身没有提供实现,协议扩展提供的默认实现会被调用
泛型
类型约束 和 关联类型
- 关联类型通过
associatedtype关键字指定
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
}
protocol SomeProtocol {
associatedtype Element: Equatable
func method1(element: Element)
}
异常
// 1、定义异常
enum FileReadError: Error {
case FileIsNull
case FileNotFound
}
// 2、让方法抛出异常
func readFileContent(filePath: String) throws -> String {
if filePath == "" {
throw FileReadError.FileIsNull
}
if filePath != "/User/Desktop/123.plist" {
throw FileReadError.FileNotFound
}
return "123"
}
// 处理异常
do {
let result = try readFileContent(filePath: "abc")
} catch {
print(error) // 有一个隐藏参数 error
}
// defer关键字
Result
- 在Swift5中,新增了一个枚举类型
Result,使我们能够更简单、更清晰处理复杂代码中的错误
// 使用Result处理异常如下
func readFileContent(filePath: String) -> Result<String, FileReadError> {
if filePath == "" {
return .failure(.FileIsNull)
}
if filePath != "/User/Desktop/123.plist" {
return .failure(.FileNotFound)
}
return .success("123")
}
// 调用
let result = readFileContent(filePath: "")
switch result {
case .failure(let error)
print(error)
case .success(let content)
print(content)
}
元类型、.self 与 Self
- 获取对象类型:
type(of: )语法 - 元类型:可以理解为类型的类型,可以通过
类型.Type定义,可以修饰变量或常量,如何得到这种类型?需要通过类型.self Self大写在定义协议的时候用的频率很高,用于协议中限制相关的类型
@objc关键字
出于安全的考虑,需将暴露给Objective-C使用的如类、属性和方法的声明前面加上@objc
#selector中调用的方法需要在方法前声明@objc- 协议的方法可选时,协议和可选方法前要用
@objc声明 - 用weak修饰的协议时 ,协议前面要用
@objc声明 - 类上加
@objcMembers,则其及子类、扩展里的属性和方法都会隐式的加上@objc,如果部分不想加,可以用@nonobjc修饰 - 扩展前加上
@objc,那么里面的方法都会隐式加上@objc
where关键字
where 关键字的含义和数据库中差不多,用于条件筛选,在Swift中哪些地方用到,如下总结
- Switch case 分支
- for 循环
- protocol 协议
- Generic 泛型
- do catch 异常处理
Key Path
- 类似OC中的KVC
- 用于间接获取/设置值
- 类必须继承自NSObject,否则不能用
- 哪些属性可以通过KeyPath操作,就需要在前面加上
@objc
// Swift 3 之前
stu.value(forKey: "name")
stu.setValue("lisi", forKey: "name")
// Swift 3
stu.value(forKey: #keyPath(Student.sex))
stu.setValue("女", forKey: #keyPath(Student.sex))
// Swift 4
stu[keyPath: \Student.sex]
stu[keyPath: \Student.sex] = "女"
Codable协议
- JSON转Model,以前可以利用
KVC、NSJSONSerialization实现 - Swift 4之后推荐使用
Codable协议,可以通过编码和解码实现互转
访问权限
open和public:允许被定义模块中任意源文件访问,也可以被另一模块源文件通过导入该定义模块来访问internal:(默认)允许被定义模块中的任意源文件访问,但不能被该模块之外的任何源文件访问fileprivate:使用限制于当前定义的原文件中private:使用限制于封闭声明中,比fileprivate更严格
注意
- 访问权限可以修饰 类、方法、属性等
- 在Swift4中,private的属性作用域扩大到extension中,也就是说在extension中访问属性可以是fileprivate或private修饰的
学习参考
- 持续关注Swift之后发布的新版本,了解新特性,关注SwiftUI等
学习网址