Showing Posts From
Swift
Swift进阶黄金之路(二)
- 01 Jun, 2020
Swift进阶黄金之路(一) 上期遗留一个问题:为什么 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中。 我们可以结合Swift的官方文档对rethrows再做一遍回顾:A function or method can be declared with the rethrows keyword to indicate that it throws an error only if one of its function parameters throws an error. These functions and methods are known as rethrowing functions and rethrowing methods. Rethrowing functions and methods must have at least one throwing function parameter.返回rethrows的函数要求至少有一个可抛出异常的函数式参数,而有以函数作为参数的函数就叫做高阶函数。 这期分两方面介绍Swift:特性修饰词和一些重要的Swift概念。特性修饰词 在Swift语法中有很多@符号,这些@符号在Swift4之前的版本大多是兼容OC的特性,Swift4及之后则出现越来越多搭配@符号的新特性。以@开头的修饰词,在官网中叫Attributes,在SwiftGG的翻译中叫特性,我并没有找到这一类被@修饰的符号的统称,就暂且叫他们特性修饰词吧,如果有清楚的小伙伴可以告知我。 从Swift5的发布来看(@dynamicCallable,@State),之后将会有更多的特性修饰词出现,在他们出来之前,我们有必要先了解下现有的一些特性修饰词以及它们的作用。 参考:Swift Attributes @available @available: 可用来标识计算属性、函数、类、协议、结构体、枚举等类型的生命周期。(依赖于特定的平台版本 或 Swift 版本)。它的后面一般跟至少两个参数,参数之间以逗号隔开。其中第一个参数是固定的,代表着平台和语言,可选值有以下这几个:iOS iOSApplicationExtension macOS macOSApplicationExtension watchOS watchOSApplicationExtension tvOS tvOSApplicationExtension swift可以使用*指代支持所有这些平台。 有一个我们常用的例子,当需要关闭ScrollView的自动调整inset功能时: // 指定该方法仅在iOS11及以上的系统设置 if #available(iOS 11.0, *) { scrollView.contentInsetAdjustmentBehavior = .never } else { automaticallyAdjustsScrollViewInsets = false }还有一种用法是放在函数、结构体、枚举、类或者协议的前面,表示当前类型仅适用于某一平台: @available(iOS 12.0, *) func adjustDarkMode() { /* code */ } @available(iOS 12.0, *) struct DarkModeConfig { /* code */ } @available(iOS 12.0, *) protocol DarkModeTheme { /* code */ }版本和平台的限定可以写多个: @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>?注意:作为条件语句的available前面是#,作为标记位时是@ 刚才说了,available后面参数至少要有两个,后面的可选参数这些:deprecated:从指定平台标记为过期,可以指定版本号obsoleted=版本号:从指定平台某个版本开始废弃(注意弃用的区别,deprecated是还可以继续使用,只不过是不推荐了,obsoleted是调用就会编译错误)该声明message=信息内容:给出一些附加信息unavailable:指定平台上是无效的renamed=新名字:重命名声明我们看几个例子,这个是Array里flatMap的函数说明: @available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value") public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]它的含义是针对swift语言,该方式在swift4.1版本之后标记为过期,对应该函数的新名字为compactMap(_:),如果我们在4.1之上的版本使用该函数会收到编译器的警告,即⚠️Please use compactMap(_:) for the case where closure returns an optional value。 在Realm库里,有一个销毁NotificationToken的方法,被标记为unavailable: extension RLMNotificationToken { @available(*, unavailable, renamed: "invalidate()") @nonobjc public func stop() { fatalError() } }标记为unavailable就不会被编译器联想到。这个主要是为升级用户的迁移做准备,从可用stop()的版本升上了,会红色报错,提示该方法不可用。因为有renamed,编译器会推荐你用invalidate(),点击fix就直接切换了。所以这两个标记参数常一起出现。 @discardableResult 带返回的函数如果没有处理返回值会被编译器警告⚠️。但有时我们就是不需要返回值的,这个时候我们可以让编译器忽略警告,就是在方法名前用@discardableResult声明一下。可以参考Alamofire中request的写法: @discardableResult public func request( _ url: URLConvertible, method: HTTPMethod = .get, parameters: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil) -> DataRequest { return SessionManager.default.request( url, method: method, parameters: parameters, encoding: encoding, headers: headers ) }@inlinable 这个关键词是可内联的声明,它来源于C语言中的inline。C中一般用于函数前,做内联函数,它的目的是防止当某一函数多次调用造成函数栈溢出的情况。因为声明为内联函数,会在编译时将该段函数调用用具体实现代替,这么做可以省去函数调用的时间。 内联函数常出现在系统库中,OC中的runtim中就有大量的inline使用: static inline id autorelease(id obj) { ASSERT(obj); ASSERT(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; }Swift中的@inlinable和C中的inline基本相同,它在标准库的定义中也广泛出现,可用于方法,计算属性,下标,便利构造方法或者deinit方法中。 例如Swift对Array中map函数的定义: @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]其实Array中声明的大部分函数前面都加了@inlinable,当应用某一处调用该方法时,编译器会将调用处用具体实现代码替换。 需要注意内联声明不能用于标记为private或者fileprivate的地方。 这很好理解,对私有方法的内联是没有意义的。内联的好处是运行时更快,因为它省略了从标准库调用map实现的步骤。但这个快也是有代价的,因为是编译时做替换,这增加了编译的开销,会相应的延长编译时间。 内联更多的是用于系统库的特性,目前我了解的Swift三方库中仅有CocoaLumberjack使用了@inlinable这个特性。 @warn_unqualified_access 通过命名我们可以推断出其大概含义:对“不合规”的访问进行警告。这是为了解决对于相同名称的函数,不同访问对象可能产生歧义的问题。 比如说,Swift 标准库中Array和Sequence均实现了min()方法,而系统库中也定义了min(::),对于可能存在的二义性问题,我们可以借助于@warn_unqualified_access。 extension Array where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Element? } extension Sequence where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Self.Element? }这个特性声明会由编译器在可能存在二义性的场景中对我们发出警告。这里有一个场景可以便于理解它的含义,我们自定义一个求Array中最小值的函数: extension Array where Element: Comparable { func minValue() -> Element? { return min() } }我们会收到编译器的警告:Use of 'min' treated as a reference to instance method in protocol 'Sequence', Use 'self.' to silence this warning。它告诉我们编译器推断我们当前使用的是Sequence中的min(),这与我们的想法是违背的。因为有这个@warn_unqualified_access限定,我们能及时的发现问题,并解决问题:self.min()。 @objc 把这个特性用到任何可以在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 setter 和 getter ),初始化器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。 用 objc 特性标记的类必须继承自一个 Objective-C 中定义的类。如果你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。如果一个类继承自另一个带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。 @objc还有一个用处是当你想在OC的代码中暴露一个不同的名字时,可以用这个特性,它可以用于类,函数,枚举,枚举成员,协议,getter,setter等。 // 当在OC代码中访问enabled的getter方法时,是通过isEnabled class ExampleClass: NSObject { @objc var enabled: Bool { @objc(isEnabled) get { // Return the appropriate value } } }这一特性还可以用于解决潜在的命名冲突问题,因为Swift有命名空间,常常不带前缀声明,而OC没有命名空间是需要带的,当在OC代码中引用Swift库,为了防止潜在的命名冲突,可以选择一个带前缀的名字供OC代码使用。 Charts作为一个在OC和Swift中都很常用的图标库,是需要较好的同时兼容两种语言的使用的,所以也可以看到里面有大量通过@objc标记对OC调用时的重命名代码: @objc(ChartAnimator) open class Animator: NSObject { }@objc(ChartComponentBase) open class ComponentBase: NSObject { }@objcMembers 因为Swift中定义的方法默认是不能被OC调用的,除非我们手动添加@objc标识。但如果一个类的方法属性较多,这样会很麻烦,于是有了这样一个标识符@objcMembers,它可以让整个类的属性方法都隐式添加@objc,不光如此对于类的子类、扩展、子类的扩展都也隐式的添加@objc,当然对于OC不支持的类型,仍然无法被OC调用: @objcMembers class MyClass : NSObject { func foo() { } // implicitly @objc func bar() -> (Int, Int) // not @objc, because tuple returns // aren't representable in Objective-C }extension MyClass { func baz() { } // implicitly @objc }class MySubClass : MyClass { func wibble() { } // implicitly @objc }extension MySubClass { func wobble() { } // implicitly @objc }参考:Swift3、4中的@objc、@objcMembers和dynamic @testable @testable是用于测试模块访问主target的一个关键词。 因为测试模块和主工程是两个不同的target,在swift中,每个target代表着不同的module,不同module之间访问代码需要public和open级别的关键词支撑。但是主工程并不是对外模块,为了测试修改访问权限是不应该的,所以有了@testable关键词。使用如下: import XCTest @testable import Projectclass ProjectTests: XCTestCase { /* code */ }这时测试模块就可以访问那些标记为internal或者public级别的类和成员了。 @frozen 和@unknown default frozen意为冻结,是为Swift5的ABI稳定准备的一个字段,意味向编译器保证之后不会做出改变。为什么需要这么做以及这么做有什么好处,他们和ABI稳定是息息相关的,内容有点多就不放这里了,之后会单独出一篇文章介绍,这里只介绍这两个字段的含义。 @frozen public enum ComparisonResult : Int { case orderedAscending = -1 case orderedSame = 0 case orderedDescending = 1 }@frozen public struct String {}extension AVPlayerItem { public enum Status : Int { case unknown = 0 case readyToPlay = 1 case failed = 2 } }ComparisonResult这个枚举值被标记为@frozen即使保证之后该枚举值不会再变。注意到String作为结构体也被标记为@frozen,意为String结构体的属性及属性顺序将不再变化。其实我们常用的类型像Int、Float、Array、Dictionary、Set等都已被“冻结”。需要说明的是冻结仅针对struct和enum这种值类型,因为他们在编译器就确定好了内存布局。对于class类型,不存在是否冻结的概念,可以想下为什么。 对于没有标记为frozen的枚举AVPlayerItem.Status,则认为该枚举值在之后的系统版本中可能变化。 对于可能变化的枚举,我们在列出所有case的时候还需要加上对@unknown default的判断,这一步会有编译器检查: switch currentItem.status { case .readyToPlay: /* code */ case .failed: /* code */ case .unknown: /* code */ @unknown default: fatalError("not supported") }@State、@Binding、@ObservedObject、@EnvironmentObject 这几个是SwiftUI中出现的特性修饰词,因为我对SwiftUI的了解不多,这里就不做解释了。附一篇文章供大家了解。 [译]理解 SwiftUI 里的属性装饰器@State, @Binding, @ObservedObject, @EnvironmentObject 几个重要关键词 lazy lazy是懒加载的关键词,当我们仅需要在使用时进行初始化操作就可以选用该关键词。举个例子: class Avatar { lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize) var largeImage: UIImage init(largeImage: UIImage) { self.largeImage = largeImage } }对于smallImage,我们声明了lazy,如果我们不去调用它是不会走后面的图片缩放计算的。但是如果没有lazy,因为是初始化方法,它会直接计算出smallImage的值。所以lazy很好的避免的不必要的计算。 另一个常用lazy的地方是对于UI属性的定义: lazy var dayLabel: UILabel = { let label = UILabel() label.text = self.todayText() return label }()这里使用的是一个闭包,当调用该属性时,执行闭包里面的内容,返回具体的label,完成初始化。 使用lazy你可能会发现它只能通过var初始而不能通过let,这是由 lazy 的具体实现细节决定的:它在没有值的情况下以某种方式被初始化,然后在被访问时改变自己的值,这就要求该属性是可变的。 另外我们可以在Sequences中使用lazy,在讲解它之前我们先看一个例子: func increment(x: Int) -> Int { print("Computing next value of \(x)") return x+1 }let array = Array(0..<1000) let incArray = array.map(increment) print("Result:") print(incArray[0], incArray[4])在执行print("Result:")之前,Computing next value of ...会被执行1000次,但实际上我们只需要0和4这两个index对应的值。 上面说了序列也可以使用lazy,使用的方式是: let array = Array(0..<1000) let incArray = array.lazy.map(increment) print("Result:") print(incArray[0], incArray[4])// Result: // 1 5在执行print("Result:")之前,并不会打印任何东西,只打印了我们用到的1和5。就是说这里的lazy可以延迟到我们取值时才去计算map里的结果。 我们看下这个lazy的定义: @inlinable public var lazy: LazySequence<Array<Element>> { get }它返回一个LazySequence的结构体,这个结构体里面包含了Array,而map的计算在LazySequence里又重新定义了一下: /// Returns a `LazyMapSequence` over this `Sequence`. The elements of /// the result are computed lazily, each time they are read, by /// calling `transform` function on a base element. @inlinable public func map<U>(_ transform: @escaping (Base.Element) -> U) -> LazyMapSequence<Base, U>这里完成了lazy序列的实现。LazySequence类型的lazy只能被用于map、flatMap、compactMap这样的高阶函数中。 参考: “懒”点儿好 纠错:参考文章中说:"这些类型(LazySequence)只能被用在 map,flatMap,filter这样的高阶函数中" 其实是没有filter的,因为filter是过滤函数,它需要完整遍历一遍序列才能完成过滤操作,是无法懒加载的,而且我查了LazySequence的定义,确实是没有filter函数的。 unowned weak Swift开发过程中我们会经常跟闭包打交道,而用到闭包就不可避免的遇到循环引用问题。在Swift处理循环引用可以使用unowned和weak这两个关键词。看下面两个例子: class Dog { var name: String init (name: String ) { self.name = name } deinit { print("\(name) is deinitialized") } }class Bone { // weak 修饰词 weak var owner: Dog? init(owner: Dog?) { self.owner = owner } deinit { print("bone is deinitialized" ) } }var lucky: Dog? = Dog(name: "Lucky") var bone: Bone? = Bone(owner: lucky!) lucky = nil // Lucky is deinitialized这里Dog和Bone是相互引用的关系,如果没有weak var owner: Dog?这里的weak声明,将不会打印Lucky is deinitialized。还有一种解决循环应用的方式是把weak替换为unowned关键词。weak相当于oc里面的weak,弱引用,不会增加循环计数。主体对象释放时被weak修饰的属性也会被释放,所以weak修饰对象就是optional。 unowned相当于oc里面的unsafe_unretained,它不会增加引用计数,即使它的引用对象释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果此时为无效引用,再去尝试访问它就会crash。这两者还有一个更常用的地方是在闭包里面: lazy var someClosure: () -> Void = { [weak self] in // 被weak修饰后self为optional,这里是判断self非空的操作 guard let self = self else { retrun } self.doSomethings() }这里如果是unowned修饰self的话,就不需要用guard做解包操作了。但是我们不能为了省略解包的操作就用unowned,也不能为了安全起见全部weak,弄清楚两者的适用场景非常重要。 根据苹果的建议:Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.当闭包和它捕获的实例总是相互引用,并且总是同时释放时,即相同的生命周期,我们应该用unowned,除此之外的场景就用weak。 img 参考:内存管理,WEAK 和 UNOWNED Unowned 还是 Weak?生命周期和性能对比 KeyPath KeyPath是键值路径,最开始是用于处理KVC和KVO问题,后来又做了更广泛的扩展。 // KVC问题,支持struct、class struct User { let name: String var age: Int }var user1 = User() user1.name = "ferry" user1.age = 18//使用KVC取值 let path: KeyPath = \User.name user1[keyPath: path] = "zhang" let name = user1[keyPath: path] print(name) //zhang// KVO的实现还是仅限于继承自NSObject的类型 // playItem为AVPlayerItem对象 playItem.observe(\.status, changeHandler: { (_, change) in /* code */ })这个KeyPath的定义是这样的: public class AnyKeyPath : Hashable, _AppendKeyPath {}/// A partially type-erased key path, from a concrete root type to any /// resulting value type. public class PartialKeyPath<Root> : AnyKeyPath {}/// A key path from a specific root type to a specific resulting value type. public class KeyPath<Root, Value> : PartialKeyPath<Root> {}定义一个KeyPath需要指定两个类型,根类型和对应的结果类型。对应上面示例中的path: let path: KeyPath<User, String> = \User.name根类型就是User,结果类型就是String。也可以不指定,因为编译器可以从\User.name推断出来。那为什么叫根类型的?可以注意到KeyPath遵循一个协议_AppendKeyPath,它里面定义了很多append的方法,KeyPath是多层可以追加的,就是如果属性是自定义的Address类型,形如: struct Address { var country: String = "" } let path: KeyPath<User, String> = \User.address.country这里根类型为User,次级类型是Address,结果类型是String。所以path的类型依然是KeyPath。 明白了这些我们可以用KeyPath做一些扩展: extension Sequence { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { return sorted { a, b in return a[keyPath: keyPath] < b[keyPath: keyPath] } } } // users is Array<User> let newUsers = users.sorted(by: \.age)这个自定义sorted函数实现了通过传入keyPath进行升序排列功能。 参考:The power of key paths in Swift some some是Swift5.1新增的特性。它的用法就是修饰在一个 protocol 前面,默认场景下 protocol 是没有具体类型信息的,但是用 some 修饰后,编译器会让 protocol 的实例类型对外透明。 可以通过一个例子理解这段话的含义,当我们尝试定义一个遵循Equatable协议的value时: // Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements var value: Equatable { return 1 }var value: Int { return 1 }编译器提示我们Equatable只能被用来做泛型的约束,它不是一个具体的类型,这里我们需要使用一个遵循Equatable的具体类型(Int)进行定义。但有时我们并不想指定具体的类型,这时就可以在协议名前加上some,让编译器自己去推断value的类型: var value: some Equatable { return 1 }在SwiftUI里some随处可见: struct ContentView: View { var body: some View { Text("Hello World") } }这里使用some就是因为View是一个协议,而不是具体类型。 当我们尝试欺骗编译器,每次随机返回不同的Equatable类型: var value: some Equatable { if Bool.random() { return 1 } else { return "1" } }聪明的编译器是会发现的,并警告我们Function declares an opaque return type, but the return statements in its body do not have matching underlying types。 参考:SwiftUI 的一些初步探索 (一)
Swift进阶黄金之路
- 10 May, 2020
Swift进阶黄金之路(二) 这篇是对一文鉴定是Swift的王者,还是青铜文章中问题的解答。这些问题仅仅是表层概念,属于知识点,在我看来即使都很清楚也并不能代表上了王者,如果非要用段位类比的话,黄金还是合理的😄。 Swift是一门上手容易,但是精通较难的语言。即使下面这些内容都不清楚也不妨碍你开发业务需求,但是了解之后它能够帮助我们写出更加Swifty的代码。一、 协议 Protocol ExpressibleByDictionaryLiteral ExpressibleByDictionaryLiteral是字典的字面量协议,该协议的完整写法为: public protocol ExpressibleByDictionaryLiteral { /// The key type of a dictionary literal. associatedtype Key /// The value type of a dictionary literal. associatedtype Value /// Creates an instance initialized with the given key-value pairs. init(dictionaryLiteral elements: (Self.Key, Self.Value)...) }首先字面量(Literal)的意思是:用于表达源代码中一个固定值的表示法(notation)。 举个例子,构造字典我们可以通过以下两种方式进行: // 方法一: var countryCodes = Dictionary<String, Any>() countryCodes["BR"] = "Brazil" countryCodes["GH"] = "Ghana" // 方法二: let countryCodes = ["BR": "Brazil", "GH": "Ghana"]第二种构造方式就是通过字面量方式进行构造的。 其实基础类型基本都是通过字面量进行构造的: let num: Int = 10 let flag: Bool = true let str: String = "Brazil" let array: [String] = ["Brazil", "Ghana"]而这些都有对应的字面量协议: ExpressibleByNilLiteral // nil字面量协议 ExpressibleByIntegerLiteral // 整数字面量协议 ExpressibleByFloatLiteral // 浮点数字面量协议 ExpressibleByBooleanLiteral // 布尔值字面量协议 ExpressibleByStringLiteral // 字符串字面量协议 ExpressibleByArrayLiteral // 数组字面量协议Sequence Sequence翻译过来就是序列,该协议的目的是一系列相同类型的值的集合,并且提供对这些值的迭代能力,这里的迭代可以理解为遍历,也即for-in的能力。可以看下该协议的定义: protocol Sequence { associatedtype Iterator: IteratorProtocol func makeIterator() -> Iterator }Sequence又引入了另一个协议IteratorProtocol,该协议就是为了提供序列的迭代能力。 public protocol IteratorProtocol { associatedtype Element public mutating func next() -> Self.Element? }我们通常用for-in实现数组的迭代: let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"] for animal in animals { print(animal) }这里的for-in会被编译器翻译成: var animalIterator = animals.makeIterator() while let animal = animalIterator.next() { print(animal) }Collection Collection译为集合,其继承于Sequence。 public protocol Collection : Sequence { associatedtype Index : Comparable var startIndex: Index { get } var endIndex: Index { get } var isEmpty: Bool { get } var count: Int { get } subscript(position: Index) -> Element { get } subscript(bounds: Range<Index>) -> SubSequence { get } }是一个元素可以反复遍历并且可以通过索引的下标访问的有限集合,注意Sequence可以是无限的,Collection必须是有限的。 Collection在Sequence的基础上扩展了下标访问、元素个数能特性。我们常用的集合类型Array,Dictionary,Set都遵循该协议。 CustomStringConvertible 这个协议表示自定义类型输出的样式。先来看下它的定义: public protocol CustomStringConvertible { var description: String { get } }只有一个description的属性。它的使用很简单: struct Point: CustomStringConvertible { let x: Int, y: Int var description: String { return "(\(x), \(y))" } }let p = Point(x: 21, y: 30) print(p) // (21, 30) //String(describing: <#T##CustomStringConvertible#>) let s = String(describing: p) print(s) // (21, 30)如果不实现CustomStringConvertible,直接打印对象,系统会根据默认设置进行输出。我们可以通过CustomStringConvertible对这一输出行为进行设置,还有一个协议是CustomDebugStringConvertible: public protocol CustomDebugStringConvertible { var debugDescription: String { get } }跟CustomStringConvertible用法一样,对应debugPrint的输出。 Hashable 我们常用的Dictionary,Set均实现了Hashable协议。Hash的目的是为了将查找集合某一元素的时间复杂度降低到O(1),为了实现这一目的需要将集合元素与存储地址之间建议一种尽可能一一对应的关系。 我们再看Hashable`协议的定义: public protocol Hashable : Equatable { var hashValue: Int { get } func hash(into hasher: inout Hasher) }public protocol Equatable { static func == (lhs: Self, rhs: Self) -> Bool }注意到func hash(into hasher: inout Hasher),Swift 4.2 通过引入 Hasher 类型并采用新的通用哈希函数进一步优化 Hashable。 如果你要自定义类型实现 Hashable 的方式,可以重写 hash(into:) 方法而不是 hashValue。hash(into:) 通过传递了一个 Hasher 引用对象,然后通过这个对象调用 combine(_:) 来添加类型的必要状态信息。 // Swift >= 4.2 struct Color: Hashable { let red: UInt8 let green: UInt8 let blue: UInt8 // Synthesized by compiler func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) } // Default implementation from protocol extension var hashValue: Int { var hasher = Hasher() self.hash(into: &hasher) return hasher.finalize() } }参考:Hashable / Hasher Codable Codable是可Decodable和Encodable的类型别名。它能够将程序内部的数据结构序列化成可交换数据,也能够将通用数据格式反序列化为内部使用的数据结构,大大提升对象和其表示之间互相转换的体验。处理的问题就是我们经常遇到的JSON转模型,和模型转JSON。 public typealias Codable = Decodable & Encodablepublic protocol Decodable { init(from decoder: Decoder) throws } public protocol Encodable { func encode(to encoder: Encoder) throws }这里只举一个简单的解码过程: //json数据 { "id": "1283984", "name": "Mike", "age": 18 } // 定义对象 struct Person: Codable{ var id: String var name: String var age: Int } // json为网络接口返回的Data类型数据 let mike = try! JSONDecoder().decode(Person.self, from: json) print(mike) //输出:Student(id: "1283984", name: "Mike", age: 18)是不是非常简单,Codable还支持各种自定义解编码过程,完全可以取代SwiftyJSON,HandyJSON等编解码库。 Comparable 这个是用于实现比较功能的协议,它的定义如下: public protocol Comparable : Equatable { static func < (lhs: Self, rhs: Self) -> Bool static func <= (lhs: Self, rhs: Self) -> Bool static func >= (lhs: Self, rhs: Self) -> Bool static func > (lhs: Self, rhs: Self) -> Bool }其继承于Equatable,即判等的协议。可以很清楚的理解实现了各种比较的定义就具有了比较的功能。这个不做比较。 RangeReplaceableCollection RangeReplaceableCollection支持用另一个集合的元素替换元素的任意子范围的集合。 看下它的定义: public protocol RangeReplaceableCollection : Collection where Self.SubSequence : RangeReplaceableCollection { associatedtype SubSequence mutating func append(_ newElement: Self.Element) mutating func insert<S>(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element /* 拼接、插入、删除、替换的方法,他们都具有对组元素的操作能力 */ override subscript(bounds: Self.Index) -> Self.Element { get } override subscript(bounds: Range<Self.Index>) -> Self.SubSequence { get } }举个例子,Array支持该协议,我们可以进行如下操作: var bugs = ["Aphid", "Damselfly"] bugs.append("Earwig") bugs.insert(contentsOf: ["Bumblebee", "Cicada"], at: 1) print(bugs) // Prints "["Aphid", "Bumblebee", "Cicada", "Damselfly", "Earwig"]"这里附一张Swift中Array遵循的协议关系图,有助于大家理解上面讲解的几个协议之间的关系:图像来源:https://swiftdoc.org/v3.1/type/array/hierarchy/ 二、@propertyWrapper阅读以下代码,print 输出什么@propertyWrapper struct Wrapper<T> { var wrappedValue: T var projectedValue: Wrapper<T> { return self } func foo() { print("Foo") } } struct HasWrapper { @Wrapper var x = 0 func foo() { print(x) // 0 print(_x) // Wrapper<Int>(wrappedValue: 0) print($x) // Wrapper<Int>(wrappedValue: 0) } }这段代码看似要考察对@propertyWrapper的理解,但是有很多无用内容,导致代码很奇怪。 @propertyWrapper的意思就是属性包装,它可以将一系列相似的属性方法进行统一处理。举个例子,如果我们需要在UserDefaults中加一个是否首次启动的值,正常可以这样处理: extension UserDefaults { enum Keys { static let isFirstLaunch = "isFirstLaunch" } var isFirstLaunch: Bool { get { return bool(forKey: Keys.isFirstLaunch) } set { set(newValue, forKey: Keys.isFirstLaunch) } } }如果我们需要加入很多这样属性的话,就需要写大量的get 、set方法。而@propertyWrapper的作用就是为属性的这种设置提供一个模板写法,以下是使用属性包装的写法。 @propertyWrapper struct UserDefaultWrapper<T> { private let key: String private let defaultValue: T init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } }class UserDefaults { @UserDefaultWrapper(key: Keys.isFirstLaunch, defaultValue: false) var isFirstLaunch: Bool }@propertyWrapper约束的对象必须要定义wrappedValue属性,因为该对象包裹的属性会走到wrappedValue的实现。 回到实例代码,定义了wrappedValue却并没有添加任何实现,这是允许的。所以访问x的时候其实是访问Wrapper的wrappedValue,因为没有给出任何实现所以直接打印出0。而_x和$x对应的就是Wrapper自身。 参考:Swift Property Wrappers 三、关键字 public open public open为权限关键词。对于一个严格的项目来说,精确的最小化访问控制级别对于代码的维护来说相当重要的。完整的权限关键词,按权限大小排序如下: open > public > internal > fileprivate > privateopen权限最大,允许外部module访问,继承,重写。 public允许外部module访问,但不允许继承,重写。 internal为默认关键词,在同一个module内可以共用。 fileprivate表示代码可以在当前文件中被访问,而不做类型限定。 private表示代码只能在当前作用域或者同一文件中同一类型的作用域中被使用。这些权限关键词可以修饰,属性,方法和类型。需要注意:当一个类型的某一属性要用public修饰时,该类型至少要用public(或者open)权限的关键词修复。可以理解为数据访问是分层的,我们为了获取某一属性或方法需要先获取该类型,所以外层(类型)的访问权限要满足大于等于内层(类型、方法、属性)权限。 参考:Swift AccessControl static class final 原文中final跟权限关键词放在一起了,其实是不合理的,就将其放到这里来讨论。 static静态变量关键词,来源于C语言。 在Swift中常用语以下场景: // 仅用于类名前,表示该类不能被继承。仅支持class类型 final class Manager { // 单例的声明 static let shared = Manager() // 实例属性,可被重写 var name: String = "Ferry" // 实例属性,不可被重写 final var lastName: String = "Zhang" // 类属性,不可被重写 static var address: String = "Beijing" // 类属性,可被重写。注意只能作为计算属性,而不能作为存储属性 class var code: String { return "0122" } // 实例函数,可被重写 func download() { /* code... */ } // 实例函数,不可被重写 final func download() { /* code... */ } // 类函数,可被重写 class func removeCache() { /* code... */ } // 类函数,不可被重写 static func download() { /* code... */ } }struct Manager { // 单例的声明 static let shared = Manager() // 类属性 static var name: String = "Ferry" // 类函数 static func download() { /* code... */ } }struct和enum因为不能被继承,所以也就无法使用class和final关键词,仅能通过static关键词进行限定 mutating inout mutating用于修饰会改变该类型的函数之前,基本都用于struct对象的修改。看下面例子: struct Point { var x: CGFloat var y: CGFloat // 因为该方法改变了struct的属性值(x),所以必须要加上mutating mutating func moveRight(offset: CGFloat) { x += offset } func normalSwap(a: CGFloat, b: CGFloat) { let temp = a a = b b = temp } // 将两个值交换,需传入对象地址。注意inout需要加载类型名前 func inoutSwap(a: inout CGFloat, b: inout CGFloat) { let temp = a a = b b = temp } }var location1: CGFloat = 10 var location2: CGFloat = -10var point = Point.init(x: 0, y: 0) point.moveRight(offset: location1) print(point) //Point(x: 10.0, y: 0.0)point.normalSwap(a: location1, b: location2) print(location1) //10 print(location2) //-10 // 注意需带取址符& point.inoutSwap(a: &location1, b: &location2) print(location1) //-10 print(location2) //10inout需要传入取值符,所以它的改变会导致该对象跟着变动。可以再回看上面说的Hashable的一个协议实现: func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) }只有使用inout才能修改传入的hasher的值。 infix operator infix operator即为中缀操作符,还有prefix、postfix后缀操作符。 它的作用是自定义操作符。比如Python里可以用**进行幂运算,但是Swift里面,我们就可以利用自定义操作符来定义一个用**实现的幂运算。 // 定义中缀操作符 infix operator ** // 实现该操作符的逻辑,中缀需要两个参数 func ** (left: Double, right: Double) -> Double { return pow(left, right) } let number = 2 ** 3 print(value) //8同理我们还可以定义前缀和后缀操作符: //定义阶乘操作,后缀操作符 postfix operator ~! postfix func ~! (value: Int) -> Int { func factorial(_ value: Int) -> Int { if value <= 1 { return 1 } return value * factorial(value - 1) } return factorial(value) } //定义输出操作,前缀操作符 prefix operator << prefix func << (value: Any) { print(value) }let number1 = 4~! print(number1) // 24<<number1 // 24 <<"zhangferry" // zhangferry前缀和后缀仅需要一个操作数,所以只有一个参数即可。 关于操作符的更多内容可以查看这里:Swift Operators。 注意,因为该文章较早,其中对于操作符的一些定义已经改变。 @dynamicMemberLookup,@dynamicCallable 这两个关键词我确实没有用过,看到dynamic可以知道这两个特性是为了让Swift具有动态性。 @dynamicMemberLookup中文叫动态查找成员。在使用@dynamicMemberLookup标记了对象后(对象、结构体、枚举、protocol),实现了subscript(dynamicMember member: String)方法后我们就可以访问到对象不存在的属性。如果访问到的属性不存在,就会调用到实现的 subscript(dynamicMember member: String)方法,key 作为 member 传入这个方法。 举个例子: @dynamicMemberLookup struct Person { subscript(dynamicMember member: String) -> String { let properties = ["nickname": "Zhuo", "city": "Hangzhou"] return properties[member, default: "undefined"] } } //执行以下代码 let p = Person() print(p.city) //Hangzhou print(p.nickname) //Zhuo print(p.name) //undefined我们没有定义Person的city、nickname,name属性,却可以用点语法去尝试访问它。如果没有@dynamicMemberLookup这种写法会被编译器检查出来并报错,但是加了该关键词编译器就不会管它是不是存在都予以通过。 @dynamicCallable struct Person { // 实现方法一 func dynamicallyCall(withArguments: [String]) { for item in withArguments { print(item) } } // 实现方法二 func dynamicallyCall(withKeywordArguments: KeyValuePairs<String, String>){ for (key, value) in withKeywordArguments { print("\(key) --- \(value)") } } } let p = Person() p("zhangsan") // 等于 p.dynamicallyCall(withArguments: ["zhangsan"]) p("zhangsan", "20", "男") // 等于 p.dynamicallyCall(withArguments: ["zhangsan", "20", "男"]) p(name: "zhangsan") // 等于 p.dynamicallyCall(withKeywordArguments: ["name": "zhangsan"]) p(name: "zhangsan", age:"20", sex: "男") // 等于 p.dynamicallyCall(withKeywordArguments: ["name": "zhangsan", "age": "20", "sex": "男"])@dynamicCallable可以理解成动态调用,当为某一类型做此声明时,需要实现dynamicallyCall(withArguments:)或者dynamicallyCall(withKeywordArguments:)。编译器将允许你调用并为定义的方法。 一个动态查找成员变量,一个动态方法调用,带上这两个特性Swift就可以变成彻头彻尾的动态语言了。所以作为静态语言的Swift也是可以具有动态特性的。 更多关于这两个动态标记的讨论可以看卓同学的这篇:细说 Swift 4.2 新特性:Dynamic Member Lookup where where一般用作条件限定。它可以用在for-in、swith、do-catch中: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] for item in numbers where item % 2 == 1 { print("odd: \(item)") // 将输出1,3,5,7,9等数 }numbers.forEach { (item) in switch item { case let x where x % 2 == 0: print("even: \(x)") // 将输出2,4,6,8等数 default: break } }where也可以用于类型限定。 我们可以扩展一个字典的merge函数,它可以将两个字典进行合并,对于相同的Key值以要合并的字典为准。并且该方法我只想针对Key和Value都是String类型的字典使用,就可以这么做: // 这里的Key Value来自于Dictionary中定义的泛型 extension Dictionary where Key == String, Value == String { //同一个key操作覆盖旧值 func merge(other: Dictionary) -> Dictionary { return self.merging(other) { _, new in new } } }@autoclosure @autoclosure 是使用在闭包类型之前,做的事情就是把一句表达式自动地封装成一个闭包 (closure)。 比如我们有一个方法接受一个闭包,当闭包执行的结果为 true 的时候进行打印,分别使用普通闭包和加上autoclosure的闭包实现: func logIfTrueNormal(predicate: () -> Bool) { if predicate() { print("True") } } // 注意@autoclosure加到闭包的前面 func logIfTrueAutoclosure(predicate: @autoclosure () -> Bool) { if predicate() { print("True") } } // 调用方式 logIfTrueNormal(predicate: {3 > 1}) logIfTrueAutoclosure(predicate: 3 > 1)编译器会将logIfTrueAutoclosure函数参数中的3 > 1这个表达式转成{3 > 1}这种尾随闭包样式。 那这种写法有什么用处呢?我们可以从一个示例中体会一下,在Swift系统提供的几个短路运算符(即表达式左边如果已经确定结果,右边将不再运算)中均采用了@autoclosure标记的闭包。那??运算符举例,它的实现是这样的: public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T { switch optional { case .some(let value): return value case .none: return try defaultValue() } } // 使用 var name: String? = "ferry" let currentName = name ?? getDefaultName()因为使用了@autoclosure标记闭包,所以??的defaultValue参数我们可以使用表达式,又因为是闭包,所以当name非空时,直接返回了该值,不会调用getDefaultName()函数,减少计算。 参考:@AUTOCLOSURE 和 ??,注意因为Swift版本问题,实例代码无法运行。 @escaping @escaping也是闭包修饰词,用它标记的闭包被称为逃逸闭包,还有一个关键词是@noescape,用它修饰的闭包叫做非逃逸闭包。在Swift3及之后的版本,闭包默认为非逃逸闭包,在这之前默认闭包为逃逸闭包。 这两者的区别主要在于声明周期的不同,当闭包作为参数时,如果其声明周期与函数一致就是非逃逸闭包,如果声明周期大于函数就是逃逸闭包。结合示例来理解: // 非逃逸闭包 func logIfTrueNormal(predicate: () -> Bool) { if predicate() { print("True") } } // 逃逸闭包 func logIfTrueEscaping(predicate: @escaping () -> Bool) { DispatchQueue.main.async { if predicate() { print("True") } } }第二个函数的闭包为逃逸闭包是因为其是异步调用,在函数退出时,该闭包还存在,声明周期长于函数。 如果你无法判断出应该使用逃逸还是非逃逸闭包,也无需担心,因为编译器会帮你做出判断。第二个函数,如果我们不声明逃逸闭包编译器会报错,警告我们:Escaping closure captures non-escaping parameter 'predicate'。当然我们还是应该理解两者的区别。 四、高阶函数 Filter, Map, Reduce, flatmap, compactMap 这几个高阶函数都是对数组对象使用的,我们通过示例去了解他们吧: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] // filter 过滤 let odd = numbers.filter { (number) -> Bool in return number % 2 == 1 } print(odd) // [1, 3, 5, 7, 9]//map 转换 let maps = odd.map { (number) -> String in return "\(number)" } print(maps) // ["1", "3", "5", "7", "9"]// reduce 累计运算 let result = odd.reduce(0, +) print(result) // 25// flatMap 1.数组展开 let numberList = [[1, 2, 3], [4, 5], [[6]]] let flatMapNumber = numberList.flatMap { (value) in return value } print(flatMapNumber) // [1, 2, 3, 4, 5, [6]]// flatMap 2.过滤数组中的nil let country = ["cn", "us", nil, "en"] let flatMap = country.flatMap { (value) in return value } print(flatMap) //["cn", "us", "en"]// compactMap 过滤数组中的nil let compactMap = country.compactMap { (value) in return value } print(compactMap) // ["cn", "us", "en"]filter,reduce其实很好理解,map、flatMap、compactMap刚开始接触时确实容易搞混,这个需要多加使用和练习。 注意到flatMap有两种用法,一种是展开数组,将二维数组降为一维数组,一种是过滤数组中的nil。在Swift4.1版本已经将flatMap过滤数组中nil的函数标位deprecated,所以我们过滤数组中nil的操作应该使用compactMap函数。 参考:Swift 烧脑体操(四) - map 和 flatMap 五、几个Swift中的概念 柯里化什么意思 柯里化指的是从一个多参数函数变成一连串单参数函数的变换,这是实现函数式编程的重要手段,举个例子: // 该函数返回类型为(Int) -> Bool func greaterThan(_ comparer: Int) -> (Int) -> Bool { return { number in return number > comparer } } // 定义一个greaterThan10的函数 let greaterThan10 = greaterThan(10) greaterThan10(13) // => true greaterThan10(9) // => false所以柯里化也可以理解为批量生成一系列相似的函数。 参考:柯里化 (CURRYING) POP 与 OOP的区别 OOP(object-oriented programming)面向对象编程: 在面向对象编程世界里,一切皆为对象,它的核心思想是继承、封装、多态。 POP(protocol-oriented programming)面向协议编程: 面向协议编程则主要通过协议,又或叫做接口对一系列操作进行定义。面向协议也有继承封装多态,只不过这些不是针对对象建立的。 为什么Swift演变成了一门面向协议的编程语言。这是因为面向对象存在以下几个问题: 1、动态派发的安全性(这应该是OC的困境,在Swift中Xcode是不可能让这种问题编译通过的) 2、横切关注点(Cross-Cutting Concerns)问题。面向对象无法描述两个不同事物具有某个相同特性这一点。 3、菱形问题(比如C++中)。C++可以多继承,在多继承中,两个父类实现了相同的方法,子类无法确定继承哪个父类的此方法,由于多继承的拓扑结构是一个菱形,所以这个问题有被叫做菱形缺陷(Diamond Problem)。 参考文章: Swift 中的面向协议编程:是否优于面向对象编程? 面向协议编程与 Cocoa 的邂逅 (上) Any 与AnyObject 区别 AnyObject: 是一个协议,所有class都遵守该协议,常用语跟OC对象的数据转换。 Any:它可以代表任何型別的类(class)、结构体 (struct)、枚举 (enum),包括函式和可选型,基本上可以说是任何东西。 rethrows 和 throws 有什么区别呢? throws是处理错误用的,可以看一个往沙盒写入文件的例子: // 写入的方法定义 public func write(to url: URL, options: Data.WritingOptions = []) throws // 调用 do { let data = Data() try data.write(to: localUrl) } catch let error { print(error.localizedDescription) }将一个会有错误抛出的函数末尾加上throws,则该方法调用时需要使用try语句进行调用,用于提示当前函数是有抛错风险的,其中catch句柄是可以忽略的。 rethrows与throws并没有太多不同,它们都是标记了一个方法应该抛出错误。但是 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中(想一下为什么是高阶函数?下期给出答案)。 查看map的方法声明,我们能同时看到 throws,rethrows: @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]不知道你们第一次见到map函数本体的时候会不会疑惑,为什么map里的闭包需要抛出错误?为什么我们调用的时候并没有用try语法也可以正常通过? 其实是这样的,transform是需要我们定义的闭包,它有可能抛出异常,也可能不抛出异常。Swift作为类型安全的语言就需要保证在有异常的时候需要使用try去调用,在没有异常的时候要正常调用,那怎么兼容这两种情况呢,这就是rethrows的作用了。 func squareOf(x: Int) -> Int {return x * x}func divideTenBy(x: Int) throws -> Double { guard x != 0 else { throw CalculationError.DivideByZero } return 10.0 / Double(x) }let theNumbers = [10, 20, 30] let squareResult = theNumbers.map(squareOf(x:)) // [100, 400, 9000]do { let divideResult = try theNumbers.map(divideTenBy(x:)) } catch let error { print(error) }当我们直接写let divideResult = theNumbers.map(divideTenBy(x:))时,编译器会报错:Call can throw but is not marked with 'try'。这样就实现了根据情况去决定是否需要用try-catch去捕获map里的异常了。 参考:错误和异常处理 break return continue fallthough 在语句中的含义(switch、while、for) 这个比较简单,只说相对特别的示例吧,在Swift的switch语句,会在每个case结束的时候自动退出该switch判断,如果我们想不退出,继续进行下一个case的判断,可以加上fallthough。