Functions
Functions
三个特征:
- 可以被赋值给变量, 可以当作参数传递(in / out)
- 只能赋值函数名, 不能带参数, (将来可能允许)
- 能捕捉上下文变量
- 能用
func关键字创建, 也可以用closure表达式创建(即{})- 可以理解为
function literals, 就像1和hello分别是Int和String的literal - 同时也是一个匿名函数
- 可以理解为
func counterFunc() -> (Int) -> String {
var counter = 0
func innerFunc(i: Int) -> String {
counter += i // counter is captured
return "Running total: \(counter)"
}
return innerFunc
}
let f = counterFunc()
f(3) // Running total: 3
f(4) // Running total: 7
let g = counterFunc()
g(2) // Running total: 2
g(2) // Running total: 4
f(2) // Running total: 9
f捕获了创建当时的上下文里counter的值, 这个counter在counterFuncreturn后会一直存在, 直到f被销毁.- 但是重新调用
counterFunc会创建新的counter - In programming terminology, a combination of a
functionand an environment ofcaptured variablesis called a closure.
关于closure的几个约定:
- 如果你需要传递一个函数作为参数, 没必要先把函数赋值给一个变量, 直接传递闭包即可
- 如果闭包的参数类型是确定(可推断的)的, 可以省略类型声明, 入参出差均适用
- 如果函数体只有一个表达式, 那么它的返回值即为闭包的返回值
- 如果闭包的参数只有一个, 可以省略参数名, 只用
$0表示第一个参数, 以此类推 - 如果函数的最后一个参数是一个闭包, 可以把闭包表达式放在函数参数列表的后面(从括号里挪出来)
- 如果函数只有一个参数, 并且这个参数是一个闭包, 那么可以省略括号
对照下面的例子看一下更直观:
[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } ) // 1. 直接传递闭包
[1, 2, 3].map( { i in return i * 2 } ) // 2. 类型推定
[1, 2, 3].map( { i in i * 2 } ) // 3. 返回值推定
[1, 2, 3].map( { $0 * 2 } ) // 4. 参数名省略
[1, 2, 3].map() { $0 * 2 } // 5. 闭包表达式挪到后面
[1, 2, 3].map { $0 * 2 } // 6. 参数只有一个, 省略括号
关于第4点, 注意两点:
- 使用位置参数如
$0, 其实是省略掉了整个参数一节 - 而如果你的实现体根本不需要这些参数, 那么应该显式声明我知道参数的存在, 但不关心
(0..<3).map { _ in Int.random(in: 1..<100) } // [3, 63, 60]
关于类型推断, 下例中仍然能推断出入参为Int出参为Bool, 神不神奇:
let isEven = { $0 % 2 == 0 }
但是如果你的入参不是一个Int, 则需要显式声明类型了:
let isEvenAlt = { (i: Int8) -> Bool in i % 2 == 0 }
原因:
protocol ExpressibleByIntegerLiteral {
associatedtype IntegerLiteralType
/// Create an instance initialized to `value`.
init(integerLiteral value: IntegerLiteralType)
}
/// The default type for an otherwise-unconstrained integer literal.
typealias IntegerLiteralType = Int
这个协议定义了数字字面量会被转化为什么类型. 如果你改为UInt32, 那么整数的字面量就会被推断为UInt32类型, 而不是Int了.
参考下别的解决从字典量推断类型的方法, 这些属于语法许可, 但是比较偏门, 因为使用closure的场景多数是有明确的上下文的, 而不是强行给一个上下文让其推断类型.
let isEvenAlt2: (Int8) -> Bool = { $0 % 2 == 0 }
let isEvenAlt3 = { $0 % 2 == 0 } as (Int8) -> Bool
Flexibility through Functions
记一个按字典排序的知识点:
let animals = ["elephant", "zebra", "dog"]
animals.sorted { lhs, rhs in
let l = lhs.reversed()
let r = rhs.reversed()
return l.lexicographicallyPrecedes(r) // 字典顺序
}
// ["zebra", "dog", "elephant"]
lexicographicallyPrecedes也叫电话簿排序, 它依次从两边的元素里取出一个值相比, 直到碰到不等的为止, 所以显然, 它更大的作用在于能为两个数组排序, 比如排序[1,2,3,5]和[1,3,4,5], 就是比较第一个不相等的元素(2和3)
Compare this with the way sorting works in Objective-C. If you want to sort an array
using Foundation, you’re met with a long list of different options:
- there are sort methods that take a selector,
- block,
- or function pointer as a comparison predicate,
- or you can pass in an array of
NSSortDescriptorsto define the sorting criteria. All of these provide a lot offlexibilityand power, but at the cost ofcomplexity.
Support for
selectorsanddynamic dispatchis still there in Swift, but the Swift standard library favors a morefunction-basedapproach instead.
看一个OC方法栈的例子, 使用三个排序规则:
let lastDescriptor = NSSortDescriptor(key: #keyPath(Person.last),
ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))
let firstDescriptor = NSSortDescriptor(key: #keyPath(Person.first),
ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))
let yearDescriptor = NSSortDescriptor(key: #keyPath(Person.yearOfBirth),
ascending: true)
let descriptors = [lastDescriptor, firstDescriptor, yearDescriptor]
(people as NSArray).sortedArray(using: descriptors)
上述使用到了两个Objective-C的Runtime特性:
keyPath: 不同于Swift的原生的keyPath, 它只是一个字符串key-value coding:(KVC), which looks up the value of a key at runtime.* `selector`也同样是一个字符串, 在`Runtime`里找到对应的函数
关于Swfit的改写请详阅原文.
Functions as Delegates
protocol AlertViewDelegate: AnyObject {
func buttonTapped(atIndex: Int)
}
这种定法是class-only protocol, 因为它可以被class实现, 并且成为别人的weak属性, 永远不会被强持有, 有效避免循环引用.
weak var delegate: AlertViewDelegate?
func fire() {
delegate?.buttonTapped(atIndex: 0)
}
但是如果需要被Struct实现的话, 应该怎么声明呢?
protocol AlertViewDelegate {
mutating func buttonTapped(atIndex: Int)
}
但是丧失了weak的特性, 如果你用一个object实现了这个协议并设为某个类的代理, 那么这个类将强持有这个object. 比如你用一个结构体来记录点击的次数:
struct TapLogger: AlertViewDelegate {
var taps: [Int] = []
mutating func buttonTapped(atIndex index: Int) {
taps.append(index)
}
}
实际使用会碰到两个问题:
- 你tap了几次之后, 会发现
taps仍然为空 - struct的delegate甚至丢失了类型牲:
// 需要强转一下才能使用代理的方法
if let theLogger = alert.delegate as? TapLogger {
print(theLogger.taps)
}
显然不是一种好的实践.
Functions Instead of Delegates
即使用var buttonTapped: ((_ buttonIndex: Int) -> ())?这样的行为去替换掉代理(特别是只有一个方法的代理).
class ViewController {
let alert: AlertView
init() {
alert = AlertView(buttons: ["OK", "Cancel"])
alert.buttonTapped = self.buttonTapped(atIndex:)
}
func buttonTapped(atIndex index: Int) {
print("Button tapped: \(index)")
}
}
按钮事件的回调, 不再到代理里去找, 而是一个函数, 但上面这种写法显然造成了循环引用, self持有alert, alert持有self的引用, 所以需要使用weak:
alert.buttonTapped = { [weak self] index in
self?.buttonTapped(atIndex: index)
}
对任何实例的属性或方法的引用都会造成持有, 所以使用的时候需要特别注意.
inout Parameters and Mutating Methods
- 以
&形式使用的intout参数可能会让人误以为是引用, 但实际上并不是, 它只是把参数的值拷贝了一份, 然后把拷贝的值传给函数, 函数修改了拷贝的值, 然后返回给调用者. - 你只能传递能被赋值的变量, 比如
array[0]可以, 但2+2不可以- 同样,
let声明的变量也不可以 - 有
getter和setter才行, 所以计算属性也是不可以的 - 书上定义为
lvalue和rvalue, 即写在等号左边还是右边的值, 显然, 左边的能被赋值.
- 同样,
var point = Point(x: 0, y: 0)
increment(value: &point.x)
point // Point(x: 1, y: 0)
这样也是可以的, 所以&只是个语法而已, 不是代表地址.
编译器可能会优化为引用传递而不是复制, 但我们不应依赖这一点.
此外, 嵌套的inout是没问题的, 但escaping是不可以的(因为你不知道这个函数什么时候被调用, 所以不能保证inout参数的值返回的时候数据源还在不在).
& 不意味 inout 的情况
& 除了在将变量传递给 inout 以外,还可以⽤来将变量转换为⼀个不安全的指针。(这就是我们最初误以为的那个场景:)
func incref(pointer: UnsafeMutablePointer<Int>) -> () -> Int {
// 将指针复制存储在闭包中
return {
pointer.pointee += 1
return pointer.pointee
}
}
因为Swift会将Array隐式地退化为指针, 所以会造成这样的调用:
let fun: () -> Int
do {
var array = [0]
fun = incref(pointer: &array)
}
fun()
- array在出了
do后就不存在了, 这个地址就是个时指针 - 对野指针指向的对象加1后果也是不可知的.
Properities
computed properties
computed properties其实就是一个方法, 加上语法支持让其表现得像访问一个property一样.- Apple鼓励你在定义
computed properties的时候注释上它的时间复杂度, 因为调用者很可能误以为它就是一个属性, 那样复杂度只是O(1), 调用者可能就会频繁调用, 实际上它可能是 O(n) 的复杂度(比如遍历数组后得到的结果), 这样就会造成性能问题.
Observe Changes
属性观察者必须在声明⼀个属性的时候就被定义,你⽆法在扩展⾥进⾏追加。所以,这不是⼀个提供给类型⽤⼾的⼯具,它是专⻔为类型的设计者⽽设计的。 不过,你可以在⼦类中重写⼀个属性,来添加观察者。下⾯就是⼀个例⼦:
class Robot {
enum State {
case stopped, movingForward, turningRight, turningLeft
}
var state = State.stopped
}
class ObservableRobot: Robot {
override var state: State {
willSet {
print("状态从 \(state) 迁移到 \(newValue)")
}
}
}
var robot = ObservableRobot()
robot.state = .movingForward // 状态从 stopped 迁移到 movingForward
Lazy Stored Properties
class GPSTrackViewController: UIViewController {
var track: GPSTrack = GPSTrack()
lazy var preview: UIImage = {
for point in track.record {
// Do some expensive computation.
}
return UIImage(/* ... */)
}()
}
- lazy本质上是执行一个闭包, 只不过编译器会把闭包的执行放到第一次访问的时候, 而不是在初始化的时候.
- 所以它是一个执行闭包后的结果, 而不是一个闭包, 所以后续访问这个属性的时候就是一个确定的值了
- 所以lazy不是一个计算属性, 它的值在第一次确认后就不会再变了.
- 存储变量和Lazy变量都不能在Extension里定义(就是因为需要存储空间)
- 第一次访问会赋值, 所以lazy是一个
mutating operation- 所以如果一个结构体含有lazy属性, 那么这个结构体将不能被
let定义(除非你没用到lazy属性) - 但是没有任何线程保护, 在多线程下可能会被初始化多次
- 所以如果一个结构体含有lazy属性, 那么这个结构体将不能被
var point = Point(x: 3, y: 4)
point.distanceFromOrigin // 5.0 // sqrt(3*3+4*4)
point.x += 10
point.distanceFromOrigin // 5.0 // lazy不是计算属性
Subscripts
Custom Subscripts
extension Collection {
subscript(indices indexList: Index...) -> [Element] {
var result: [Element] = []
for index in indexList {
result.append(self[index])
}
return result
}
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
Array("abcdefghijklmnopqrstuvwxyz")[indices: 7, 4, 11, 11, 14]
// ["h", "e", "l", "l", "o"]
[1,2,3][safe: 4] // nil
其中, Index...表⽰indexList 是⼀个可变参数 (variadic parameter)
Advanced Subscripts
假定你有一个值为Any的字典, 你想逐层访问到最内层的值一般是不可行的, 因为你第一层取出来后就已经不知道类型了:
var japan: [String: Any] = [
"name": "Japan",
"capital": "Tokyo",
"population": 126_440_000,
"coordinates": [
"latitude": 35.0,
"longitude": 139.0
]
]
// Error: Type 'Any' has no subscript members.
japan["coordinates"]?["latitude"] = 36.0
// 强转是可以, 但是它就不能被赋值了
// Error: Cannot assign to immutable expression.
(japan["coordinates"] as? [String: Double])?["latitude"] = 36.0
考虑做一个新型的下标, 支持传入类型, 这样在取出的时候就有类型了:
extension Dictionary {
subscript<Result>(key: Key, as type: Result.Type) -> Result? {
get {
return self[key] as? Result
}
set {
// Delete existing value if caller passed nil.
guard let value = newValue else {
self[key] = nil
return
}
// Ignore if types don't match.
guard let value2 = value as? Value else {
return
}
self[key] = value2
}
}
}
// 直接使用走的不是自定义下标
japan["coordinates"] // Optional(["longitude": 139.0, "latitude": 36.0])
// 自定义下标, 带类型转换
japan["coordinates", as: [String: Double].self]?["latitude"] = 36.0
注意, 方法里的第二个参数
as type在方法体里并没有用到, 但是约束了方法声明里的<Result>泛型是什么, 方法体里用的也是Result而不是type, 这是一种非常奇怪的用法, 要特别留意一下.
Key Paths
- Swift 4 added the concept of
key pathsto the language. - A key path is an
uninvoked reference(未调用引用) to a property, analogous to an unapplied method reference(未应用的方法引用). - Key-path expressions start with a backslash, e.g.
\String.count.- 如果类型能被推断, 可以省略类型, 如
\.count
- 如果类型能被推断, 可以省略类型, 如
- The compiler automatically generates a new
[keyPath:]subscript for all types.* `"hello"[keyPath: \.count]`等同于`"hello".count` - Key Path可以拼接:
nameKeyPath.appending(path: \.count) - Key path除了表达属性, 也可以表达下标:
var bart = Person(name: "Bart Simpson", address: simpsonResidence)
let people = [lisa, bart]
people[keyPath: \.[1].name] // Bart Simpson
前面写过一个排序器, 为了自定义用哪个键排序, 还让用记传一个closure进来, 有了keyPath就简单多了:
func sortDescriptor<Root, Value>(key: KeyPath<Root, Value>)
-> SortDescriptor<Root> where Value: Comparable {
return { $0[keyPath: key] < $1[keyPath: key] }
}
// Usage
let streetSDKeyPath: SortDescriptor<Person> =
sortDescriptor(key: \.address.street)
Witable Key Paths和双向绑定
演示一个实现属性绑定的例子:d
extension NSObjectProtocol where Self: NSObject {
func observe<A, Other>(_ keyPath: KeyPath<Self, A>,
writeTo other: Other,
_ otherKeyPath: ReferenceWritableKeyPath<Other, A>)
-> NSKeyValueObservation
where A: Equatable, Other: NSObjectProtocol
{
return observe(keyPath, options: .new) { _, change in
guard let newValue = change.newValue,
other[keyPath: otherKeyPath] != newValue else {
return // prevent endless feedback loop
}
other[keyPath: otherKeyPath] = newValue
}
}
}
- 扩展的是
NSObjectProtocol而不是NSObject ReferenceWritableKeyPath保证了可以对let声明的变量进行写操作
有了observe(_:writeTo:_)方法, 我们就可以绑定属性了:
extension NSObjectProtocol where Self: NSObject {
func bind<A, Other>(_ keyPath: ReferenceWritableKeyPath<Self,A>,
to other: Other,
_ otherKeyPath: ReferenceWritableKeyPath<Other,A>)
-> (NSKeyValueObservation, NSKeyValueObservation)
where A: Equatable, Other: NSObject
{
// 双向绑定
let one = observe(keyPath, writeTo: other, otherKeyPath)
let two = other.observe(otherKeyPath, writeTo: self, keyPath)
return (one,two)
}
}
使用:
final class Sample: NSObject {
@objc dynamic var name: String = ""
}
class MyObj: NSObject {
@objc dynamic var test: String = ""
}
let sample = Sample()
let other = MyObj()
let observation = sample.bind(\Sample.name, to: other, \.test)
sample.name = "NEW"
other.test // NEW
other.test = "HI"
sample.name // HI
Autoclosures
配合Interview看看.
- In the standard library, functions like
assertandfatalErroralso useautoclosures
之前碰到
autoclosure的时候我有疑问有什么作用, 把closure写成表达式, 为什么不直接传个表达式过去, 现在明白了, 延迟执行没讲到核心, 而是可以不执行, 比如说在assert里, 如果条件为false, 那么后面的autoclosure就不会执行了, 这可以避免一些不必要的计算.
@escaping (Private)
-
we need to use an
explicit selfin thecompletion handlerof a network request, -
whereas we don’t need to be explicit about self in closures passed to
maporfilter. -
要不要显式
self取决于这个闭包是存起来稍后使用(escaping), 还是在作用域范围内同步使用. 这是刻意设计的提醒你这里可能会出现循环引用. -
不标记为
escaping的闭包, 编译器是不会允许你保存或者传递的(比如传给调用者) -
closures that never leave a function’s local scope are
non-escaping. -
A non-escaping closure can’t create a
permanent referencecycle because it’s automatically destroyed when the function it’s defined in returns. -
`