继令你极度舒适的Swift集合类高阶函数之后,把很久之前Swift知识进行了梳理并总结成文。这些Swift知识点大多是一些细节,容易忽略但使用效果又极佳,其中包括语言基础、内存、指针、OC差异、优雅奇点、开发环境等方面。其中包含一部分总结了喵神书中的点,特此注明。
首先,一个用到数据绑定的switch语法是这样的。
let someTuple = (66, 99) switch someTuple { case (100, _): print("左侧100, 不在乎右侧") case (66, let right): print("左侧66, 右侧\(right)") case let (left, 88): print("左侧\(left), 右侧为88") case let (left, right): print("其它: \(left) + \(right)") }if case let其实和switch是有关系的,正如下面代码中的,两种表达式的效果是相同的。所以if case let其实就相当于switch case let的逻辑分支。
switch someTuple { case let (left, 99): print("左侧\(left), 右侧99") default: print("右侧非99") } // 与上面的代码效果一致 if case let (left, 99) = someTuple { print("左侧\(left), 右侧99") }除了if case let,类似的表达方式还有guard case let。
guard case let (left, 99) = someTuple else { fatalError("右侧非99") }以及for case let。
let tupleArray = [(11, 22), (33, 55), (77, 77), (88, 88)] // 而下面两种表达方式的效果是相同的 for (left, right) in tupleArray { print("\(left) + \(right)") } for case let (left, right) in tupleArray { print("\(left) + \(right)") } // 而且还可以配合 where使用 for case let (left, _) in tupleArray where left < 50 { print("左侧\(left)小于50") }当然你可以说这种写法很鸡肋,因为没有必要多敲两个代码。但它的真正价值在于你使用有关联值的枚举时,这并不是元组可以代替的。
enum BirthDay { case time(year: Int, month: Int, day: Int) } let birth1 = BirthDay.time(year: 2000, month: 1, day: 2) let birth2 = BirthDay.time(year: 2010, month: 5, day: 20) let birth3 = BirthDay.time(year: 2020, month: 10, day: 30) let birthArr = [birth1, birth2, birth3] for case let BirthDay.time(_, month, day) in birthArr where month <= 6 { print("他是前半年的生日\(month)月\(day)日") }==表示值相同。值类型和引用类型都可以比较。
let string1 = "string" let string2 = "string" string1 == string2 // true===表示两个引用类型引用自相同的实例,即引用了同一块内存区域。只能比较于引用类型。
let aView = UIView() let bView = aView aView == bView // true aView === bView // true有值的多重可选类型aOptString和literalOptString是等效的。
var optNil: String? = nil var aOptNil: String?? = optNil var literalOptNil: String?? = nil print(aOptNil) // Optional(nil) print(literalOptNil) // nil为nil的多重可选类型aOptNil和literalOptNil是不一样的类型。这说明,多重可选类型的分层逻辑还是很严谨的,它能明确定位nil究竟在哪一层。
有一个A类,还有一个B类继承于A类。下面代码中介绍了,关于父类和子类中使用static和class关键字的场景。
class A { // class修饰 class func aClassMethod() {} class var aClassProperty: String { return "a" } // class var aClassSaveProperty = "" // Error: Class stored properties not supported in classes; did you mean 'static'? // static修饰 static func aStaticMethod() {} static var aStaticProperty: String { return "ap" } } class B: A { // class修饰 override class func aClassMethod() {} override class var aClassProperty: String { return "b" } // static修饰 // Error: Cannot override static method // static func aStaticMethod() {} // Error: Cannot override static property // override static var aStaticProperty: String { // return "bp" // } }在类中static和class关键字都可以修饰方法和属性, 但有一些本质的不同:
1.应用类型 static修饰,表示静态方法或静态属性,可以用于所有类型 class,struct,enum。 class修饰,表示类方法或类属性,只可以用于 class中。2.属性类型 static修饰的属性可以是计算属性也可以是存储属性。 class修饰的属性只能是计算属性。3.继承重写 static修饰的类方法和属性是可以继承重写的。 class修饰的类方法和类属性无法在子类中重写,相当于final class。.Type: 当前类的元类型(Meta)。 .self: 静态获取当前类型或者实例的本身(包括class、struct、enum、protocol)。 Self: 不是一个特定的类型,遵循当前协议的类型或者当前类及其子类。
struct S { static var classProperty = "" var instanceProperty = "" } protocol P {}我认为下面的例子将.Type和.self的区别表述的非常清楚了。
type(of: S()) // S // S()实例, 其类型是 S type(of: S().self) // S // S类型的本身, 其类型是 S.Type type(of: S.self) // S.Type // S类型的元类, 其类型是 S.Type.Type type(of: S.Type.self) // S.Type.Type // P协议的本身, 其类型是 P.Protocol type(of: P.self) // P.Protocol // P协议的元类, 其类型是 P.Type.Protocol type(of: P.Type.self) // P.Type.Protocol // intType的值是 Int, 类型是 Int.Type let intType: Int.Type = Int.self intType // Int在效果上,.self在类型后相当于取得类型本身,在实例后相当于取得这个实例本身。
S.classProperty S().instanceProperty在语法上,.self是可以省略不写的,所以下面的代码与上面的效果一致。
S.self.classProperty S().self.instancePropertyclassForCoder也是一个获取类型的方法,但最好不要使用。它是Foundation框架下的NSObject属性,并不是swift的属性。在Swift开发中,尽可能保持Swift化。
UIImageView.classForCoder() // 不推荐 UIImageView.self // 推荐Swift不能在协议中定义泛型进行限制,所以在声明或者实现协议时,Self就可以来代指实现这个协议本身的类型。
protocol SomeProcotol { func someFunc() -> Self }另外,用在类中,只可以用作方法的返回值(其他位置不可以使用)。 Self表示当前类及其子类,这里仅限于 class。
class SS { func some() -> Self { return self } } SS().some()type(of: someInstance): 动态获取当前实例的类型. .dynamicType: Deprecated, instead of type(of:) someInstance.self: 静态获取类型 is: 静态获取类型
class BaseClass { class func printClassName() { print("BaseClass") } } class SubClass: BaseClass { override class func printClassName() { print("SubClass") } } let someInstance: BaseClass = SubClass()someInstance是一个指定为BaseClass的SubClass实例对象,这在OC中称为多态。
但Swift默认情况下是不采用动态派发,而是静态的,所以函数的调用是在编译时期决定。比如is条件语句、.self,都是静态获取类型的。
someInstance is SubClass // True someInstance is BaseClass // True BaseClass.self is BaseClass.Type // True获取一个对象的动态类型,可以通过 type(of:)。
type(of: someInstance) == SubClass.self // True type(of: someInstance) == BaseClass.self // False这里所说的函数嵌套与柯里化中所提到的函数分层调用不是一个概念,而是在函数中继续定义函数。 来看下面一个函数:
func generateObjec(type: Int) -> String { if 0 == type { return zeroType() } else if 1 == type { return oneType() } else { return defaultType() } } func zeroType() -> String { return "Zero" } func oneType() -> String { return "One" } func defaultType() -> String { return "Two" }如果使用函数嵌套将会如下效果:
func generateObjec(type: Int) -> String { func zeroType() -> String { return "Zero" } func oneType() -> String { return "One" } func defaultType() -> String { return "Two" } if 0 == type { return zeroType() } else if 1 == type { return oneType() } else { return defaultType() } }函数嵌套在你函数主体内容过长,且本模块的功能与外部逻辑没有任何关系时,能发挥非常大的作用。它会使你的单个函数不在冗长,且将它们分成几个小型的模块,且定义在主函数之内,并不影响外部的关系。
所以这样的访问权限和这样的模块化会提高代码可读性和维护性。这个Tip在之前文章Swift基础知识碎片中也曾聊过。
在类ObserverA和ObserverB中,观察属性的使用逻辑和重写逻辑如下:
class ObserverA { var number :Int { get { print("get") return 1 } set { print("set") } } } class ObserverB: ObserverA { override var number: Int { willSet { print("willSet") } didSet { print("didSet") } } } let obseverB = ObserverB() obseverB.number = 0 // 打印顺序: // get // willSet // set // didSet总结如下:
1.初始化方法对属性的设定,以及在 willSet和 didSet中对属性的再次设定都不会再次触发属性观察。2.在 swift中的计算属性只是提供 set和 get两种方法,当你添加 willSet及 didSet方法时会报错。所以在同一个类型中,属性观察和计算属性是不能同时共存的。但我们可以通过继承重写计算属性来实现属性观察的目的。3.当触发 didSet的时候,会自动触发一次 get,这是因为 didSet中会用到 oldValue,而这个值需要在整个 set动作之前进行获取并存储待用,否则将无法确保正确性。如果我们不实现 didSet的话,那次 get也不会触发。如果实际类型实现了协议,那么实际类型中协议的实现将被调用。 如果实际类型中没有实现协议,那么协议扩展中的默认实现将被调用。
AStruct().method1() // 在Protocol中的实现 BStruct().method1() // 在实际类中的实现如果方法在协议中进行了声明,且类型中实现了协议,那么类型中的实现将被调用。 如果方法没有在协议中声明,或者在类型中没有实现,那么协议扩展中的默认实现被调用。
BStruct().method1() // 在实际类中的实现 BStruct().method2() // 在实际类中的实现 let aProtocol = BStruct() as AProtocol aProtocol.method1() // 在实际类中的实现 aProtocol.method2() // 在Protocol中的实现Swift可以在扩展中实现协议,从而进行默认调用,而OC中并没有这种方法。所以,孙源曾经封装了一个库来实现类似功能:ProtocolKit 。
我们想要不同类型的元素放入一个容器中,比如定义容器中元素类型 Any或者 Anyobject,但这样的转换会造成部分信息的损失。
let mixed: [Any] = [1, "two", true] let any = mixed[0]我们想要放入一个容器中的元素或多或少会有某些共同点,这就使得用协议来规定。这种方法虽然也损失了一部分类型信息,但是相对于Any或者Anyobject还是改善很多。
let mixed2: [CustomStringConvertible] = [1, "two", true] for obj in mixed2 { print(obj.description) }另一种做法是使用enum可以嵌套值的特点,将相关信息封装进enum中。这个方法绝对无懈可击。
enum MixedWrap { case IntValue(Int) case StringValue(String) case BoolValue(Bool) } let mixed3 = [MixedWrap.IntValue(1), MixedWrap.StringValue("two"), MixedWrap.BoolValue(true)] for value in mixed3 { switch value { case let .IntValue(i): print(i) case let .StringValue(s): print(s) case let .BoolValue(b): print(b) } }在 Swift 中,使用 ~= 来表示模式匹配的运算符。~=操作符有下面三种API:
1.判等类型是否相同 func ~=(a: T, b: T) -> Bool2.判等与 nil比较的类型 func ~=(lhs: _OptionalNilComparisonType, rhs: T?) -> Bool3.判等一个范围输入和某个特定值 func ~=(pattern: I, value: I.Bound) -> BoolSwift的switch就是使用了~=运算符进行模式匹配,case指定的模式作为左参数输入,而等待匹配的被switch的元素作为运算符的右侧参数。只不过这个调用是由 Swift隐式完成的。
switch "snail" { case "snail": print("相等") default: break } let num: Int? = nil switch num { case nil: print("nil") default: print("\(num!)") } let x = 5 switch x { case 0...10: print("Bound之内") default: print("Bound之外") }再来看我们如何进行自定义switch的模式匹配。
func ~=(left: String, right: String) -> Bool { // 自定义为包含条件 return right.contains(left) } let content = "1234567890" switch content { case "123": print("包含数字") case "abc": print("包含字母") default: print("不包含") } // 包含数字Swift 有一组非常方便的接口,用来将字面量转换为特定的类型。
ExpressibleByArrayLiteralExpressibleByBooleanLiteralExpressibleByDictionaryLiteralExpressibleByFloatLiteralExpressibleByIntegerLiteralExpressibleByStringLiteralExpressibleByUnicodeScalarLiteralExpressibleByExtendedGraphemeClusterLiteralExpressibleByIntegerLiteral是关于Int型的字面量语法的协议。
struct AgeStage: ExpressibleByIntegerLiteral { let age: Int init(age: Int) { self.age = age } init(integerLiteral value: Int) { self.init(age: value) } } let age18: AgeStage = 18 age18.age // 18ExpressibleByStringLiteral是关于字符串类型的字面量语法的协议。但因为ExpressibleByStringLiteral是有继承的关系的,所以其实需要遵循三个协议内容来实现字面量语法。
class Cow: ExpressibleByStringLiteral { let name: String init(name value: String) { self.name = value } required convenience init(stringLiteral value: String) { self.init(name: value) } required convenience init(extendedGraphemeClusterLiteral value: String) { self.init(name: value) } required convenience init(unicodeScalarLiteral value: String) { self.init(name: value) } } let cow: Cow = "Mar~Mar~" cow.name // Mar~Mar~为Seat类自定义运算符+、++、+*,来提升代码的简洁度。并为它们设定了运算的优先级,来确保运算过程的正确性。
precedencegroup MulAddPrecedence { associativity: none higherThan: MultiplicationPrecedence } infix operator +: AdditionPrecedence infix operator ++: MultiplicationPrecedence infix operator +*: MulAddPrecedence struct Seat { var row = 0 var column = 0 static func + (left: Seat, right: Seat) -> Seat { let row = left.row + right.row let column = left.column + right.column return Seat(row: row, column: column) } static func ++ (left: Seat, right: Seat) -> Seat { let row = left.row + right.row * 2 let column = left.column + right.column * 2 return Seat(row: row, column: column) } static func +* (left: Seat, right: Seat) -> Seat { let row = left.row * left.row + right.row * right.row let column = left.column * left.column + right.column * right.column return Seat(row: row, column: column) } } let seat1 = Seat(row: 2, column: 3) let seat2 = Seat(row: 4, column: 7) let seat3 = Seat(row: 5, column: 10) seat1 + seat2 + seat3 // row 11, column 20 seat1 ++ seat2 ++ seat3 // row 20, column 37 seat1 +* (seat2 +* seat3) // row 1685, column 22210 //seat1 +* seat2 +* seat3 // Error在 seat1 +* seat2 +* seat3这行代码,会报错:Adjacent operators are in non-associative precedence group ‘MulAddPrecedence’。这是由于其优先级MulAddPrecedence的结合性为none,即没有定义。这时候需要将其改为left或者right。
另外,关于运算符和优先级的一些说明可以看这里,precedenceGroup、precedence、associativity。
在Swift中没有正则表达式的API,但可以使用OC中的 NSRegularExpression配合Swift中的特性来使用。下面代码中封装了一个正则工具:
struct RegexHelper { let regex: NSRegularExpression init(_ pattern: String) throws { try regex = NSRegularExpression(pattern: pattern, options: .caseInsensitive) } func match(_ content: String) -> Bool { let matches = regex.matches(in: content, options: [], range: NSMakeRange(0, content.utf16.count)) return !matches.isEmpty } } let zhPattern = "^[\u{4e00}-\u{9fa5}]{0,}$" let matcher = try RegexHelper(zhPattern) if matcher.match("哈哈哈哈哈哈") { print("纯中文字符串通过") } //纯中文字符串通过再进一步封装,自定义一个运算符结合起来,更加简便。
precedencegroup MatchPrecedence { associativity: none higherThan: DefaultPrecedence } infix operator =~: MatchPrecedence func >> (pattern: String, content: String) -> Bool { do { return try RegexHelper(pattern).match(content) } catch _ { return false } } if zhPattern >> "DCSnail哈哈哈" { print("纯中文字符串通过") } //想修改或者实现一个自定义的集合类型,就需要用到Sequence协议,以及IteratorProtocol协议。它们的API如下:
/** public protocol IteratorProtocol { associatedtype Element mutating func next() -> Self.Element? } public protocol Sequence { associatedtype Element where Self.Element == Self.Iterator.Element associatedtype Iterator : IteratorProtocol __consuming func makeIterator() -> Self.Iterator var underestimatedCount: Int { get } func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<Self.Element>) throws -> R) rethrows -> R? } */比如要自定义一个反向迭代的数组,需要编写一个反向的迭代器,并植入Sequence协议中。
struct CustomReverseIterator<T>: IteratorProtocol { typealias Element = T var array: [Element] var currentIndex: Int init(_ array: [Element]) { self.array = array currentIndex = array.count - 1 } mutating func next() -> Element? { guard currentIndex >= 0 else { return nil } let int = array[currentIndex] currentIndex -= 1 return int } } struct CustomReverseSequence<T>: Sequence { var array: [T] init(_ array: [T]) { self.array = array } typealias intrator = CustomReverseIterator<T> func makeIterator() -> intrator { return CustomReverseIterator(self.array) } } let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"] let sequence = CustomReverseSequence(animals) for animal in sequence { print(animal) } // Dolphin // Camel // Butterfly // AntelopeCodable是Swift中关于序列化和反序列化的标准协议。在官方API中Decodable 协议和Encodable协议的别名称作Codable。
public protocol Decodable { init(from decoder: Decoder) throws } public protocol Encodable { func encode(to encoder: Encoder) throws } public typealias Codable = Decodable & EncodableJSON数据如下,数据模型对应的类型为定义为Fish,在Swift中通过Codble协议可以很轻易地完成JOSN和model之间的转化。
let fishJson = """ [{ "name": "SmallFish", "age": 1, },{ "name": "BigFish", "age": 3, }] """.data(using: .utf8)! struct Fish: Codable { var name = "" var age = 0 }Decodable:JSON -> 模型
let decoder = JSONDecoder() do { // 字典使用Type.self; 数组使用[Type].self let fishs = try decoder.decode([Fish].self, from: fishJson) print(fishs) // [__lldb_expr_1.Fish(name: "SmallFish", age: 1), __lldb_expr_1.Fish(name: "BigFish", age: 3)] } catch { print(error) }Encodable:模型 -> JSON
let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted do { let data = try encoder.encode(Fish(name: "OldFish", age: 5)) print(String(data: data, encoding: .utf8)!) // { "name" : "OldFish", "age" : 5 } } catch { print(error) }进入正题,在如上代码中Fish类仅仅写了一个遵循Codable的代码,Swift是如何进行转化的呢?其实,结构体Fish类遵循Codable的代码后,会自动实现以下内容:
init(name: String, age: Int) { self.name = name self.age = age } // Decodable init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let name = try container.decode(String.self, forKey: .name) let age = try container.decode(Int.self, forKey: .age) self.init(name: name, age: age) } // Encodable func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(age, forKey: .age) } enum CodingKeys: String, CodingKey { case name case age }当然在实际开发中,没有这么简单,可能还有数据模型中key的转化、value的转化等。下面中举例了companyJson和接收类型Company的关系。
let companyJson = """ { "company_name": "****科技有限公司", "create_time": 1556676900, "state": "suspend", "worth": "---", "employees": [{ "name": "SmallFish", "age": 1, },{ "name": "BigFish", "age": 3, }], } """.data(using: .utf8)! struct Company: Codable { var companyName: String var createTime: Date var state: CompanyState var special: Float var employees: [Fish] enum CodingKeys: String, CodingKey { case companyName = "company_name" case createTime = "create_time" case state case special = "worth" case employees } enum CompanyState: String, Codable { case open = "open" case close case suspend } }key的转化:通过对 CodingKeys枚举进行自定义,来实现 key的对应转化。上面代码中自定义的转化为company_name->companyName、create_time->createTime、worth->special。
还可以通过设置key的策略,系统自动转化划线转驼峰。所以这个属性设置后,可以省略company_name和create_time。当设置decoder的keyDecodingStrategy属性为.convertFromSnakeCase后就会自动转化为companyName和createTime了。
value的转化:通过将state定义为关联值为字符串的枚举类型CompanyState,以实现value从字符串向枚举的自主转化,以及整体的Codable。这里极大的体现了Swift中枚举的灵活性,利用其关联值的特性进行自主转化。
但是比如,json中的某个value为字符串类型,想要在model中转化为Int类型,这就需要自己实现Codable的协议了,自己定义转化了。当然你实现后,也会覆盖系统的自动实现。
// date的转化策略, 由时间戳直接转化为 date decoder.dateDecodingStrategy = .secondsSince1970 // 其它的转化策略... // special指定 infinity、-infinity、nan 三个特殊值的转化 decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "+++", negativeInfinity: "---", nan: "***") do { let company = try decoder.decode(Company.self, from: companyJson) print(company) // Company(companyName: "****科技有限公司", createTime: 2019-05-01 02:15:00 +0000, state: __lldb_expr_1.Company.CompanyState.suspend, special: -inf, employees: [__lldb_expr_1.Fish(name: "SmallFish", age: 1), __lldb_expr_1.Fish(name: "BigFish", age: 3)]) } catch { print(error) }补充:NaN是Not a Number的简写,可以用来表示某些未被定义的或者出现了错误的运算。
let a = 0.0 / 0.0 // nan let b = sqrt(-1.0) // nan let c = 0.0 * Double.infinity // nan a.isNaN // true首先,这三者都是协议,它们之间的关系是,Hashable和Comparable都是继承自Equatable协议。
实现Equatable协议后,就可以用==符号来判断两个对象是否相等了。
public protocol Equatable { static func == (lhs: Self, rhs: Self) -> Bool }这里先不用代码说明,因为下面Hashable协议中会提到。
遵循Hashable协议,也必须满足Equatable协议。因为Hashable协议继承自Equatable协议。实现了Hashable协议就可以使用哈希值了。
public protocol Hashable : Equatable { var hashValue: Int { get } func hash(into hasher: inout Hasher) }官方文档中指出,hashValue is deprecated as a Hashable requirement.To conform to Hashable,implement the hash(into:) requirement instead。如今Hashable协议,已不再要求必须实现hashValue了,由hash(into:)代替。
对于class来说,Hashable协议实现是这样的:
class Duck: Hashable { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } // Hashable func hash(into hasher: inout Hasher) { // 需要注意的是不同的类比较哈希的话, combine的顺序需要保证一致 hasher.combine(name) hasher.combine(age) } // hashValue 已不再要求必须实现了, 由上面hash(into:)代替 // var hashValue: Int { // return self.name.hashValue ^ self.age.hashValue // } // Equatable static func == (lhs: Duck, rhs: Duck) -> Bool { if lhs.name == rhs.name && lhs.age == rhs.age{ return true } return false } } let duck = Duck(name: "Duck", age: 2) let duck2 = Duck(name: "Duck", age: 2) // 哈希值 duck.hashValue // -7196963873420679324 duck2.hashValue // -7196963873420679324 // 手动实现的Equatable duck == duck2 // true如果你自定义的类型满足下面条件,编译器会自动实现Hashable和Equatable,只需遵循即可。
在struct中,存储属性如果都遵循Hashable协议。在enum中,关联值如果都遵循Hashable协议。但是对于class来说是不会自动合成的,需要手动实现.
struct Animal: Hashable { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } } let animal = Animal(name: "Animal", age: 3) let animal2 = Animal(name: "Animal", age: 3) // 哈希值 animal.hashValue // 8606943294433516990 animal2.hashValue // 8606943294433516990 // 自动实现的Equatable animal == animal2 // true关于哈希值,需要说明的是,除非我们正在开发一个哈希散列的数据结构,否则我们不应该直接依赖系统所实现的哈希值来做其他操作。
首先哈希的定义是单向的,对于相等的对象或值,我们可以期待它们拥有相同的哈希,但是反过来并不一定成立。其次,某些对象的哈希值有可能随着系统环境或者时间的变化而改变。
因此你也不应该依赖于哈希值来构建一些需要确定对象唯一性的功能,在绝大部分情况下,你将会得到错误的结果。
遵循Comparable协议,当然也需要满足Equatable协议。实现对应的协议方法后就可以使用<、<=、>=、>等符号进行比较了。
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 }当为Tiger自定义比较条件后,就会感到Swift这些协议真是太便捷了。
struct Tiger: Comparable { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } // Comparable static func < (lhs: Tiger, rhs: Tiger) -> Bool { if lhs.age < rhs.age { return true } return false } static func > (lhs: Tiger, rhs: Tiger) -> Bool { if lhs.age > rhs.age { return true } return false } // Equatable static func == (lhs: Tiger, rhs: Tiger) -> Bool { if lhs.age == rhs.age{ return true } return false } } let tiger = Tiger(name: "Tiger", age: 4) let tiger2 = Tiger(name: "Tiger", age: 5) tiger > tiger2 // false这个Key-Path表达式会在编译期生成一个KeyPath类的实例。而这个KeyPath实例指向某个类型的属性或者下标,KeyPath类可配合下标subscript(keyPath:)进行使用。
不明白?上代码。看看KeyPath类是如何指向一个类型的属性的。
struct Person { var name: String var dog: [Dog] } struct Dog { var name: String } var titi = Dog(name: "Titi") var james = Person(name: "James", dog: [titi]) // 创建 KeyPath类的实例 let nameKeyPath = \Person.name james[keyPath: nameKeyPath] = "James Wade" // 可省略类型自主推断 james[keyPath: \.name] // James Wade通过KeyPath类也可以实现存取属性是数组的情况,需要注意的是Key-Path表达式中所使用的的下标必须满足Hashable协议标准。另外,还支持字典类型。
james.dog[keyPath: \[Dog].[0].name] = "Titi Go" james.dog // Titi Go let greetings = ["hello", "hola", "bonjour", "안녕"] greetings[keyPath: \[String].[1]] // hola var keyIntDic = ["first": [1, 2, 3], "second": [4, 5, 6]] keyIntDic[keyPath: \[String: [Int]].["first"]] = [0, 0, 0] keyIntDic // ["second": [4, 5, 6], "first": [0, 0, 0]]Key-Path表达式可以引用self,KeyPath实例指向当前实例自身。
var compoundValue = (a: 1, b: 2) compoundValue[keyPath: \.self] = (a: 10, b: 20) compoundValue // (a 10, b 20)这个Key-Path字符串表达式会在编译期转化为字符串字面量并生成一个字符串。Key-Path字符串表达式可配合Objective-C中的setValue:forKey:进行使用。
class Cat: NSObject { @objc var age: Int init(age: Int) { self.age = age } func agekeyPath() -> String { return #keyPath(age) } } let kitty = Cat(age: 2) // 在编译时,被字符串字面量所取代 let ageKeyPath = #keyPath(Cat.age) kitty.setValue(3, forKey: ageKeyPath) kitty.value(forKey: ageKeyPath) // 只有在类的内部key-Path字符串表达式才可以省略类名, 其它情况都不可省略 kitty.value(forKey: kitty.agekeyPath())因为需要支持Objective-C中setValue:forKey:,所以属性必须使用@objc修饰。又因为属性需要@objc修饰,所以当前类型必须是class,且必须继承于任何一个OC类。所以,Key-Path字符串表达式还是偏OC的,我还是那句话,尽量做到Swift化。
KVO在Swift中当然还是有的,对于继承自NSObjc的类,可以随意使用KVO。使用的API也很简单:
var view = UIView() let observer: NSKeyValueObservation = view.observe(\.frame, options: [.new]) { v, changed in print(v) print(changed.newValue) } view.frame = CGRect(x: 0, y: 0, width: 10, height: 10) // <UIView: 0x7f9660d08c90; frame = (0 0; 10 10); layer = <CALayer: 0x6000035da920>> // Optional((0.0, 0.0, 10.0, 10.0))但对于非NSObject的类就需要@objc dynamic来修饰之后,才能使用KVO。因为这是属于OC的东西。
class Panda: NSObject { @objc dynamic var name = "" } var panda = Panda() panda.observe(\.name, options: [.new]) { (pa, changed) in print(pa) print(changed.newValue) } panda.name = "panda" // Optional("panda")Swift中的KVO需要依赖的东西比原来多,需要属性有dynamic和objc进行修饰。 大多数情况下,我们想要观察的类包含这两个修饰,当然会损失一部分性能。并且有时候我们很可能也无法修改想要观察的类的源码,还需要继承这个类并且将需要观察的属性使用 dynamic和objc进行重写。
所以,在纯Swift中还是尽量Swift化,用属性的set/get机制来实现相同的效果,这才是最佳方式。
Swift基础知识碎片 令你极度舒适的Swift集合类高阶函数 Swift关键字总结