一、闭包
- 1.1、闭包表达式(Closure Expression)
在 Swift 里面可以通过函数func
定义一个函数,也可以通过闭包表达式定义一个函数
func sum(_ v1:Int,_ v2:Int) -> Int{ return v1+v2 } sum(1,2) // 3
- 闭包的格式
{ 参数列表 -> 返回值类型 in 具体的代码 }
- 闭包的具体举例
var fn = { (_ v1:Int,_ v2:Int) -> Int in return v1+v2 } fn(2,3) // 5
提示:上面仅仅是给闭包定义了一个变量,如果不写变量和下面的意思一样
{ (_ v1:Int,_ v2:Int) -> Int in return v1+v2 }(2,3)
- 1.2、闭包表达式的简写
基础的闭包
var fn = { (v1:Int,v2:Int) -> Int in return v1+v2 }
- 简写的代码
func exec(v1:Int,v2:Int,fn:(Int,Int)->Int){ print(fn(v1,v2)) }
提示:传进去一个闭包
- 调用 exec 函数方式如下:等效,结果都是 20
方式一
exec(1, 2, fn: fn)
- 方式二
exec(v1: 10, v2: 10) { (v3, v4) -> Int in return v3 + v4 }
- 方式三
exec(v1: 10, v2: 10, fn: { v3,v4 in return v3+v4 })
- 方式四
exec(v1: 10, v2: 10, fn: { v3,v4 in v3+v4 })
- 方式五
exec(v1: 10, v2: 10, fn: {$0 + $1})
- 方式六
exec(v1: 10, v2: 10, fn: +)
- 1.3、尾随闭包
- 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
- 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
func exec(v1:Int,v2:Int,fn:(Int,Int)->Int){ print(fn(v1,v2)) } exec(v1: 6, v2: 7, fn: {$0 + $1})
- 结果:13,让两个参数相加,我们还可以:
-
、*
等等 - 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的写法,那就不需要再函数名后面写圆括号
func exec(fn:(Int,Int)->Int){ print(fn(2,5)) }
- 调用方式如下,结果为 7
exec(fn: {$0 + $1}) exec(){$0 + $1} exec{$0 + $1}
- 1.4、数组的排序
func cmp(i1:Int,i2:Int) -> Bool { // 大的排到前面 return i1 > i2 } // 返回 false: i1 排在 i2 后面,也就是 i1 的值小于 i2 // 返回 true: i1 排在 i2 前面,也就是 i1 的值大于 i2
- 1.5、忽略参数
func exec(fn:(Int,Int)->Int){ print(fn(1,5)) } exec{_,_ in 11} // 11
- 1.6、闭包(Closure):的实质是一段有具体功能的代码块。
- 闭包:一个函数和他捕获的变量/常量环境组合起来,称为闭包。闭包的核心是在其使用的局部变量/常量 会被额外的复制或者引用,使这些变量脱离其作用域后依然有效。
- 一般指定义在函数内部的函数
- 一般它捕获的是外层函数的局部变量/常量
// 返回的 plus 与 num 形成了闭包 func getFn() -> Fn{ var num = 0 func plus(_ i:Int) -> Int{ num += I return num } return plus } var fn1 = getFn() var fn2 = getFn() fn1(1) fn2(2)
- 可以把闭包想象成一个类的实例对象
- 存储在堆空间
- 捕获的局部变量/常量就是对象的成员(存储属性)
- 组成闭包的函数就是类内部定义的方法
class Closure{ var num = 2 func plus(_ i:Int) -> Int { num += I return num } } var cs1 = Closure() var cs2 = Closure() cs1.plus(2) // 4 cs2.plus(2) // 4 cs1.plus(3) // 7 cs2.plus(3) // 7
- 1.7、自动闭包自动闭包的条件:自动闭包参数使用有严格的条件是首先此闭包不能有参数,其次在调用函数传参的时,此闭包的实现只能由一句表达式组成,闭包的返回值即为此表达式的值,自动闭包由
@autoclosure
来声明,如下 例三
- 例一:如果第1个数大于0,返回第一个数。否则返回第2个数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int { return v1 > 0 ? v1 : v2 } getFirstPositive(10, 20)
- 例二:改成函数类型的参数,可以让v2延迟加载
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } getFirstPositive(-4) { 20 }
- 例三:为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } getFirstPositive(-4, 20)
- 结论
@autoclosure
会自动将 20 封装成闭包 { 20 }@autoclosure
只支持() -> T
格式的参数@autoclosure
并非只支持最后1个参数- 空合并运算符
??
使用了@autoclosure
技术 - 有
@autoclosure
、无@autoclosure
,构成了 函数重载
- 1.8、逃逸闭包和非逃逸闭包
- 逃逸闭包:是指函数内的闭包在函数执行结束后在函数外依然可以使用
- 非逃逸闭包:是指在函数的声明周期结束后,闭包也将会被销毁。换句话说,非逃逸闭包只能在函数内部使用,在函数外部不能使用。默认情况下函数中的闭包都为非逃逸闭包,这样做的优点是可以提高代码的性能,节省内存消耗,开发者可以根据实际需求将闭包参数声明为逃逸闭包。
提示:
- 非逃逸闭包也不可以作为返回值返回,如果这么做,编译器会抛出一个错误。
- 将闭包声明为非逃逸型,需要使用
@noescape
修饰。需要注意的是,在最新的Xcode版本里面已经不需要再使用,参数默认都是非逃逸的,如下代码
只有一个闭包的函数,将此闭包声明为非逃逸的,此闭包既不可最为返回值也不可赋值给外部变量 在xcode10.1中会有警告,这个关键字可以忽略
- 逃逸类型的闭包通常用于异步操作中,例如一个请求完成后要执行闭包回调,需要使用逃逸类型。
二、属性
- 2.1、Swift 里面跟实例相关的属性可以分为2大类
- 存储属性(Sored Property)
- 类似于成员变量这个概念
- 存储在实例的内存中
- 结构体和类可以定义存储属性
- 枚举不可以定义存储属性
提示:
- 关于存储属性,在Swift里面有个明确的规定,在创建类和结构体的时候,必须为所有的存储属性设置一个合适的初始值。
struct Circle { /// 存储属性 var radius:Double } var circle = Circle(radius: 2)
- 可以在初始化器里面为存储属性设置一个初始值
struct Circle { /// 存储属性 var radius:Double init () { radius = 2 } } var circle = Circle()
- 可以分配一个默认的属性值作为属性定义的一部分。如在定义属性的时候直接给一个 值,
var radius:Int = 2
struct Circle { /// 存储属性 var radius:Double = 2.0 } var circle = Circle()
- 计算属性(Computed Property)
- 本质就是方法(函数)
- 不占用实例的内存
- 枚举、结构体、类 都可以定义计算属性
提示:
- set传入的新值默认叫做 newValue,也可以自定义
- 定义计算属性只能用 var ,不能用 let(代表常量,值是一成不变的)
- 计算属性的值是可能发生变化的(即使是只读计算属性)
- 有
set
方法的话必须有get
方法,有get
方法可以没有set
方法
- 2.2、以结构体举例
struct Circle { /// 存储属性 var radius:Double /// 计算属性 var diameter:Double { set { radius = newValue / 2.0 } get { radius * 2.0 } } } var circle = Circle(radius: 2) circle.diameter = 20; //结果是 8 个字节 print("占用的内存大小=\(MemoryLayout.stride(ofValue: circle))")
提示:计算属性是不占内存的,原因是它的set 和 get 类似于两个函数,如下代码,由此可以看出是不占用内存的
func setDiameter (newValue:Double) { radius = newValue / 2.0 } func getDiameter (newValue:Double) -> Double { radius * 2.0 }
- 2.3、枚举原始值 rawvalue 原理
enum TestEnum : Int { case test1 = 1, test2 = 2, test3 = 3 var rawValue: Int { switch self { case .test1: return 8 case .test2: return 8 case .test3: return 10 } } } print(TestEnum.test3.rawValue) // 结果 10
枚举原始值
rawValue
的本质是:只读计算属性,查看汇编里面只有get
方法
- 2.4、延迟存储属性(Lazy Stored Property)
- 使用
lazy
可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
class Car { init() { print("Car init") } func run() { print("Car is running!") } } class Person { lazy var car = Car() init() { print("Person init") } func goOut() { car.run() } } // 使用 let person = Person() print("------") person.goOut()
- 打印结果如下:我们可以看到在
let person = Person()
执行的时候lazy var car = Car()
没有立马执行,而是在用到 car 的时候才被调用的,由此可以看出在存储属性前面加上 lazy 是不会立马执行的,在用到的时候才会被执行
Person init ------ Car init Car is running!
提示
lazy
属性必须是var
,不能是letlet
必须在实例的初始化方法完成之前就拥有值- 如果 多条线程 同时第一次访问
lazy
属性,是无法保证属性只被初始化1次,这个是线程不安全的
lazy var image:UIImage = { //图片url let imgUrl = "http://www.uw3c.com/images/index/logo_monkey.png"/ let url : NSURL = NSURL(string:imgUrl)!// 转换网络URL let data : NSData = NSData(contentsOf:url as URL)! return UIImage(data: data as Data)! }()
- 2.5、延迟属性的注意点
- 当一个结构体包含一个延迟属性的时候,只有 var 才能访问延迟属性,因为延迟属性初始化时需要改变结构体的内存
解释:如果实例定义为 let,那么节结构体的内存就固定了,不能被修改,那么延迟属性的本质是修改结构体的内存,编译器就会直接报错
struct Point1 { var x = 1 var y = 2 lazy var z = 3 } // 使用 let point = Point1() point.z
- 2.6、属性观察器
struct Circle { /// 存储属性 var radius:Double { willSet { print("willSet",newValue) } didSet { print("didSet",oldValue,radius) } } init() { self.radius = 1.0 print("Circle init") } } var circle = Circle() circle.radius = 10 print(circle.radius) 打印结果如下 Circle init willSet 10.0 didSet 1.0 10.0 10.0
提示:
willSet
会传递新值,默认值叫newValue
didSet
会传递旧值,默认值叫oldValue
- 在初始化器中设置属性值不会触发
willSet
和didSet
- 在属性定义时设置初始值也不会触发
willSet
和didSet
- 2.7、全局变量 和 局部变量
- 属性观察器、计算属性的功能,同样可以应用在全局变量,局部变量身上
- 全局变量
var num:Int { get { return 10 } set { print("setNum",newValue) } } num = 12 // setNum 12 print(num) // 10
- 局部变量
fun test() { var age:Int = 10 { willSet { print("willSet",newValue) } didSet { print("didSet",oldValue,radius) } } age = 11 // 打印:willSet 11 // didSet 10 11 } // 调用方法 test()
- 2.8、inout 本质的探究
struct Shape { var width:Int var side:Int { willSet { print("willSet - side",newValue) } didSet { print("willSet - side",oldValue,side) } } var girth:Int { set { width = newValue / side print("setGirth",newValue) } get { print("getGirth") return width * side } } func show() -> Void { print("width = \(width), side = \(side), girth = \(girth)") } } func test(_ num: inout Int) -> Void { num = 20 } var s = Shape(width: 10, side: 4) test(&s.width) s.show() print("----------") test(&s.side) s.show() print("----------") test(&s.girth) s.show() 打印结果是 getGirth width = 20,side = 4,width = 80 ---------- willSet - side 20 willSet - side 4 20 getGirth width = 20,side = 20,width = 400 ---------- getGirth setGirth 20 getGirth width = 1,side = 20,width = 20
- 如果实参有物理内存地址,且没有设置属性观察器:直接将实参的内存地址传入函数(实参进行引用传递)
- 如果实参是计算属性 或者 设置了属性观察器:采取了 Copy In Copy Out 的做法
- 调用函数时,先复制实参的值,产生副本 【get】
- 将副本的内存地址传入函数 (副本进行引用传递),在函数的内部可以修改副本的值
- 函数返回后,再将副本的值覆盖实参的值 【set】
- 总结:inout 的本质是引用传递 (地址传递)
- 2.9、类型属性(Type Property)严格来说属性可以分为:实例属性 和 类型属性
- 实例属性(Instance Property):只能通过实例去访问
- 存储实例属性(Stored Instance Property):存储在实例内存中,每个实例都有一份内存
- 计算实例属性(Computed Instance Property)
- 类型属性(Type Property):只能通过类去访问
- 存储类属性(Stored Instance Property):整个程序运行中,就只有一份内存(类似于全局变量)
- 计算类属性(Computed Instance Property)
- 可以通过
static
定义类型属性,如果是类可以使用关键字class
struct Car { static var count = 0 init() { Car.count += 1 } } let c1 = car() let c2 = car() let c3 = car() print(Car.count) // 结果是 3
- 总结:类属性细节
- 不同于 存储实例属性,你必须给 存储类型属性 设定初始值,因为类型没有像实例那样的init初始化器来初始化存储属性
- 存储类型属性默认就是lazy,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次
- 存储类型属性可以是 let
提示:枚举类型也可以定义类型属性(存储类型属性、计算类型属性)
- 类存储属性的使用-- 单利
class Person { public static let share = Person() // 类存储属性,在这个程序中只有一份内存 private init() {} }
三、方法
- 3.1、类、枚举、结构体 都可以定义 实例方法、类型方法
- 实例方法(Instance Method):通过实例对象调用
- 类型方法(Type Method):通过 类型调用,用 class/static关键字定义
struct Car { static var count = 0 init() { Car.count += 1 } // 在函数(方法) 前面加上 class/static关键字 就可以变成类方法,用类名来调用 static func getCount() -> Int { count } } let c1 = car() let c2 = car() let c3 = car() print(Car.getCount()) // 结果是 3
提示:self
- 在实例方法里面代表实例对象
- 在类方法里面代表类型
上代码代码static func getCount()
里面的count
等价于self.count
、Car.self.count
、Car.count
- 3.2、mutating 的使用
- 结构体和枚举都是值类型,默认情况下,值类型的属性不能被自身的实例方法修改
- 解决办法:在 func 前面加上 mutaing 可以允许这种修改行为
struct Point { var x = 0.0,y = 0.0 mutating func moveBy(deltaX:Double,deltaY:Double) { x += deltaX y += deltaY // 等效于上面的代码 //self = Point2(x: x + deltaX, y: y + deltaY) } } enum StateSwitch { case low,middle,high mutating func next() { switch self { case .low: self = .middle case .middle: self = .high case .high: self = .low } } }
- 3.3、@discardableResult 消除方法返回值没使用的警告,如下
struct Point { var x = 0.0,y = 0.0 @discardableResult mutating func moveBy(deltaX:Double,deltaY:Double) -> Double { x += deltaX y += deltaY return x + y } } var point = Point() point.moveBy(deltaX: 2.0, deltaY: 2.0)
提示
:如果 函数moveBy
不加@discardableResult
,point.moveBy(deltaX: 2.0, deltaY: 2.0) 会提提示Result of call to 'moveBy(deltaX:deltaY:)' is unused
四、下标(subscrip)
- 4.1、使用 subscrip 可以给任意类型(枚举、结构体、类)增加下标功能,有些地方翻译为:下标脚本
- 4.2、subscrip的语法类似于实例方法、计算属性,本质就是方法 (函数)
struct Point { var x = 0.0,y = 0.0 subscript(index: Int) -> Double { set { if index == 0 { x = newValue } else if index == 1 { y = newValue } } get { if index == 0 { return x } else if index == 1 { return y } return 0 } } } var p = Point5() p[0] = 1.1 p[1] = 2.2 print(p.x) // 1.1 print(p.y) // 2.2 print(p[0]) // 1.1 print(p[1]) // 2.2
提示:
- subscript 中定义的返回值类型决定了:get 方法的返回值 和 set 方法中 newValue 的类型
- subscript 可以接受多个参数,并且类型任意
- 4.3、subscript 下标细节
- 细节一:subscript 下标可以没有 set 方法,但必须有 get 方法,如果只有get方法可以把
get
关键字去掉,如下
struct Point { var x = 0.0,y = 0.0 subscript(index: Int) -> Double { if index == 0 { return x } else if index == 1 { return y } return 0 } }
- 细节二:可以设置参数标签
struct Point { var x = 0.0,y = 0.0 subscript(index i: Int) -> Double { if i == 0 { return x } else if i == 1 { return y } return 0 } } var p = Point6() p.y = 22.0 // 访问的时候必须加上参数标签 print(p[index: 1])
- 细节三:下标可以是 类型方法
class Sum { static subscript(v1:Int,v2:Int) -> Int { return v1 + v2 } } print(Sum[10,20]) // 打印结果是 30
- 4.4、接收多个参数的下标
class Grid { var data = [ [0, 1, 2], [3, 4, 5], [6, 7, 8] ] subscript(row: Int, column: Int) -> Int { set { guard row >= 0 && row < 3 && column >= 0 && column < 3 else { return } data[row][column] = newValue } get { guard row >= 0 && row < 3 && column >= 0 && column < 3 else { return 0 } return data[row][column] } } } var grid = Grid() grid[0, 1] = 77 // 第1行第2列 grid[1, 2] = 88 // 第2行第3列 grid[2, 0] = 99 // 第3行第1列 print(grid.data)
- 打印结果如下:
[[0, 77, 2], [3, 4, 88], [99, 7, 8]]