SwiftTips之LanguageAPI
继令你极度舒适的Swift集合类高阶函数之后,把很久之前Swift知识进行了梳理并总结成文。这些Swift知识点大多是一些细节,容易忽略但使用效果又极佳,其中包括语言基础、内存、指针、OC差异、优雅奇点、开发环境等方面。其中包含一部分总结了喵神书中的点,特此注明。
if case let
首先,一个用到数据绑定的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 = aViewaView == bView // true
aView === bView // true
多重可选类型
var optString: String? = "abc"
var aOptString: String?? = optString
var literalOptString: String?? = "abc"print(aOptString) // Optional(Optional("string"))
print(literalOptString) // Optional(Optional("string"))
有值的多重可选类型aOptString
和literalOptString
是等效的。
var optNil: String? = nil
var aOptNil: String?? = optNil
var literalOptNil: String?? = nilprint(aOptNil) // Optional(nil)
print(literalOptNil) // nil
为nil
的多重可选类型aOptNil
和literalOptNil
是不一样的类型。这说明,多重可选类型的分层逻辑还是很严谨的,它能明确定位nil
究竟在哪一层。
class和 static的区别
有一个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、.self、Self
.Type
: 当前类的元类型(Meta)。
.self
: 静态获取当前类型或者实例的本身(包括class
、struct
、enum
、protocol
)。
Self
: 不是一个特定的类型,遵循当前协议的类型或者当前类及其子类。
struct S {static var classProperty = ""var instanceProperty = ""
}
protocol P {}
.Type
和.self
我认为下面的例子将.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.instanceProperty
classForCoder
也是一个获取类型的方法,但最好不要使用。它是Foundation
框架下的NSObject
属性,并不是swift
的属性。在Swift开发中,尽可能保持Swift化
。
UIImageView.classForCoder() // 不推荐
UIImageView.self // 推荐
Self
Swift不能在协议中定义泛型进行限制,所以在声明或者实现协议时,Self
就可以来代指实现这个协议本身的类型。
protocol SomeProcotol {func someFunc() -> Self
}
另外,用在类中,只可以用作方法的返回值(其他位置不可以使用)。
Self
表示当前类及其子类,这里仅限于 class。
class SS {func some() -> Self { return self }
}
SS().some()
swift中动态和静态地获取类型
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也不会触发。
Protocol的调用逻辑
protocol AProtocol {func method1() -> String
}extension AProtocol {func method1() -> String {return "在Protocol中的实现"}func method2() -> String {return "在Protocol中的实现"}
}struct AStruct: AProtocol {}struct BStruct: AProtocol {func method1() -> String {return "在实际类中的实现"}func method2() -> String {return "在实际类中的实现"}
}
协议方法调用者为实际类型的情况
如果实际类型实现了协议,那么实际类型中协议的实现将被调用。
如果实际类型中没有实现协议,那么协议扩展中的默认实现将被调用。
AStruct().method1() // 在Protocol中的实现
BStruct().method1() // 在实际类中的实现
协议方法调用者为被推断为协议类型的情况:
如果方法在协议中进行了声明,且类型中实现了协议,那么类型中的实现将被调用。
如果方法没有在协议中声明,或者在类型中没有实现,那么协议扩展中的默认实现被调用。
BStruct().method1() // 在实际类中的实现
BStruct().method2() // 在实际类中的实现let aProtocol = BStruct() as AProtocol
aProtocol.method1() // 在实际类中的实现
aProtocol.method2() // 在Protocol中的实现
Objective-C协议的默认实现
Swift可以在扩展中实现协议,从而进行默认调用,而OC中并没有这种方法。所以,孙源曾经封装了一个库来实现类似功能:ProtocolKit 。
下标语法
struct House {var peoples: [String]subscript(index: Int) -> String {set {peoples[index] = newValue}get {return peoples[index]}}subscript(people: String) -> Int? {return peoples.firstIndex(of: people)}
}let peoples = ["Jordan", "Duncan", "YaoMing", "James", "Wade"]
var nbaHouse = House(peoples: peoples)
nbaHouse[1] // "Duncan"
nbaHouse["James"] // 3nbaHouse[2] // "YaoMing"
nbaHouse[2] = "Iverson" // "Iverson"
nbaHouse.peoples // ["Jordan", "Duncan", "Iverson", "James", "Wade"]
减少容器类的类型损失
我们想要不同类型的元素放入一个容器中,比如定义容器中元素类型 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) -> Bool - 2.判等与 nil比较的类型
func ~=(lhs: _OptionalNilComparisonType, rhs: T?) -> Bool - 3.判等一个范围输入和某个特定值
func ~=(pattern: I, value: I.Bound) -> Bool
Swift的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 有一组非常方便的接口,用来将字面量转换为特定的类型。
- ExpressibleByArrayLiteral
- ExpressibleByBooleanLiteral
- ExpressibleByDictionaryLiteral
- ExpressibleByFloatLiteral
- ExpressibleByIntegerLiteral
- ExpressibleByStringLiteral
- ExpressibleByUnicodeScalarLiteral
- ExpressibleByExtendedGraphemeClusterLiteral
Int
型的字面量语法
/**public protocol ExpressibleByIntegerLiteral {associatedtype IntegerLiteralType : _ExpressibleByBuiltinIntegerLiteralinit(integerLiteral value: Self.IntegerLiteralType)}
*/
ExpressibleByIntegerLiteral
是关于Int
型的字面量语法的协议。
struct AgeStage: ExpressibleByIntegerLiteral {let age: Intinit(age: Int) {self.age = age}init(integerLiteral value: Int) {self.init(age: value)}
}let age18: AgeStage = 18
age18.age // 18
字符串类型的字面量语法
/**public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteralinit(stringLiteral value: Self.StringLiteralType)}*/
ExpressibleByStringLiteral
是关于字符串类型的字面量语法的协议。但因为ExpressibleByStringLiteral
是有继承的关系的,所以其实需要遵循三个协议内容来实现字面量语法。
class Cow: ExpressibleByStringLiteral {let name: Stringinit(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: nonehigherThan: MultiplicationPrecedence
}infix operator +: AdditionPrecedence
infix operator ++: MultiplicationPrecedence
infix operator +*: MulAddPrecedencestruct Seat {var row = 0var column = 0static func + (left: Seat, right: Seat) -> Seat {let row = left.row + right.rowlet column = left.column + right.columnreturn Seat(row: row, column: column)}static func ++ (left: Seat, right: Seat) -> Seat {let row = left.row + right.row * 2let column = left.column + right.column * 2return Seat(row: row, column: column)}static func +* (left: Seat, right: Seat) -> Seat {let row = left.row * left.row + right.row * right.rowlet column = left.column * left.column + right.column * right.columnreturn 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: NSRegularExpressioninit(_ 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: nonehigherThan: DefaultPrecedence
}infix operator =~: MatchPrecedencefunc >> (pattern: String, content: String) -> Bool {do {return try RegexHelper(pattern).match(content)} catch _ {return false}
}if zhPattern >> "DCSnail哈哈哈" {print("纯中文字符串通过")
}
//
集合协议Sequence和IteratorProtocol
想修改或者实现一个自定义的集合类型,就需要用到Sequence
协议,以及IteratorProtocol
协议。它们的API如下:
/**public protocol IteratorProtocol {associatedtype Elementmutating func next() -> Self.Element?}
public protocol Sequence {associatedtype Element where Self.Element == Self.Iterator.Elementassociatedtype Iterator : IteratorProtocol__consuming func makeIterator() -> Self.Iteratorvar underestimatedCount: Int { get }func withContiguousStorageIfAvailable<R>(_ body: (UnsafeBufferPointer<Self.Element>) throws -> R) rethrows -> R?}*/
比如要自定义一个反向迭代的数组,需要编写一个反向的迭代器,并植入Sequence
协议中。
struct CustomReverseIterator<T>: IteratorProtocol {typealias Element = Tvar array: [Element]var currentIndex: Intinit(_ array: [Element]) {self.array = arraycurrentIndex = array.count - 1}mutating func next() -> Element? {guard currentIndex >= 0 else {return nil}let int = array[currentIndex]currentIndex -= 1return 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
// Antelope
Codable
Codable
是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 & Encodable
JSON数据如下,数据模型对应的类型为定义为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
}
Encodable、Decodable
Decodable:JSON -> 模型
let decoder = JSONDecoder()
do {// 字典使用Type.self; 数组使用[Type].selflet 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 = nameself.age = age}// Decodableinit(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)}// Encodablefunc 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 namecase age}
CodingKeys
当然在实际开发中,没有这么简单,可能还有数据模型中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: Stringvar createTime: Datevar state: CompanyStatevar special: Floatvar employees: [Fish]enum CodingKeys: String, CodingKey {case companyName = "company_name"case createTime = "create_time"case statecase special = "worth"case employees}enum CompanyState: String, Codable {case open = "open"case closecase 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
Equatable、Hashable、Comparable
Equatable
首先,这三者都是协议,它们之间的关系是,Hashable
和Comparable
都是继承自Equatable
协议。
实现Equatable
协议后,就可以用==
符号来判断两个对象是否相等了。
public protocol Equatable {static func == (lhs: Self, rhs: Self) -> Bool
}
这里先不用代码说明,因为下面Hashable
协议中会提到。
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: Stringlet age: Intinit(name: String, age: Int) {self.name = nameself.age = age}// Hashablefunc 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// }// Equatablestatic 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: Stringlet age: Intinit(name: String, age: Int) {self.name = nameself.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
遵循Comparable
协议,当然也需要满足Equatable
协议。实现对应的协议方法后就可以使用<
、<=
、>=
、>
等符号进行比较了。
public protocol Comparable : Equatable {static func < (lhs: Self, rhs: Self) -> Boolstatic func <= (lhs: Self, rhs: Self) -> Boolstatic func >= (lhs: Self, rhs: Self) -> Boolstatic func > (lhs: Self, rhs: Self) -> Bool
}
当为Tiger
自定义比较条件后,就会感到Swift这些协议真是太便捷了。
struct Tiger: Comparable {let name: Stringlet age: Intinit(name: String, age: Int) {self.name = nameself.age = age}// Comparablestatic 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}// Equatablestatic 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
KeyPath
Key-Path表达式
\TypeName.path
这个Key-Path表达式
会在编译期生成一个KeyPath类
的实例。而这个KeyPath
实例指向某个类型的属性或者下标,KeyPath类
可配合下标subscript(keyPath:)
进行使用。
不明白?上代码。看看KeyPath类
是如何指向一个类型的属性的。
struct Person {var name: Stringvar 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 Golet greetings = ["hello", "hola", "bonjour", "안녕"]
greetings[keyPath: \[String].[1]] // holavar 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字符串表达式
#keyPath(TypeName.property)
这个Key-Path字符串表达式
会在编译期转化为字符串字面量并生成一个字符串。Key-Path字符串表达式
可配合Objective-C中的setValue:forKey:
进行使用。
class Cat: NSObject {@objc var age: Intinit(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
KVO
在Swift中当然还是有的,对于继承自NSObjc
的类,可以随意使用KVO
。使用的API也很简单:
var view = UIView()
let observer: NSKeyValueObservation = view.observe(\.frame, options: [.new]) { v, changed inprint(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) inprint(pa)print(changed.newValue)
}
panda.name = "panda"
// Optional("panda")
Swift中的KVO
需要依赖的东西比原来多,需要属性有dynamic
和objc
进行修饰。
大多数情况下,我们想要观察的类包含这两个修饰,当然会损失一部分性能。并且有时候我们很可能也无法修改想要观察的类的源码,还需要继承这个类并且将需要观察的属性使用 dynamic
和objc
进行重写。
所以,在纯Swift
中还是尽量Swift化,用属性的set/get机制
来实现相同的效果,这才是最佳方式。
相关资料
Swift基础知识碎片
令你极度舒适的Swift集合类高阶函数
Swift关键字总结
SwiftTips之LanguageAPI相关推荐
- Swift-Tips之重复字符串
method: init(repeating:count:) let str = String(repeating: "测试", count: 10) print(str) //测 ...
- Swift-Tips之rounded(_:)
rounded(_:) 使用指定的舍入规则将该值取整. 代码示例 let nums = [3.0, 3.001, 3.5, 3.999, -3.0, -3.001, -3.5, -3.999] let ...
- 线程安全: 互斥锁和自旋锁(10种)
无并发,不编程.提到多线程就很难绕开锁?. iOS开发中较常见的两类锁: 1. 互斥锁: 同一时刻只能有一个线程获得互斥锁,其余线程处于挂起状态. 2. 自旋锁: 当某个线程获得自旋锁后,别的线程会一 ...
- iOS开发月报#10|201904
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布. 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue. Tips 关于 ...
- swift 字符串转int_Swift Tips Streamline 如何假装写过 Swift
作者 Nemocdz,腾讯 iOS 工程师.这篇文章转载至他的博客,可以点击阅读原文来查看他的博客.本文总结了笔者日常使用 Swift 的一些小 Tips. 文章较长,预计阅读时间 15分钟 Safe ...
- Messagepack Java实例
接前文,上个博客笔者详解了Messagepack的基础原理,详见链接:http://www.cnblogs.com/cnxieyang/p/8323698.html 本文将介绍Messagepack- ...
- Swift tips 017 - Speeding up Swift package tests
代码截图 代码出处: Swift Tips 017 by John Sundell[1] 小笔记 这段代码在说什么 Swift Package Manager(Swift 包管理器,一般简称 Swif ...
最新文章
- jmeter中没有sampler_JMeter 接口自动化测试篇 29
- linux 下取进程占用 cpu/内存 最高的前10个进程
- spring开发_BeanFactoryPostProcessor_容器后处理器
- 谢文: 三网融合还是三网凑合(转一篇好文)
- Codeforces Gym 100650B 	Countdown (离线)
- [GWCTF 2019]babyvm
- python多线程多进程多协程_python 多进程、多线程、协程
- perl malformed JSON string, neither tag, array, object, number, string or atom, at character offset
- linux awk列数据处理工具使用示例
- Java调用.Net的web service的几种方式
- tomcat的缺少tcnative-1.dll的解决
- WinDbg学习笔记(二)--字符串访问断点
- MySQL常见问题的解决,root用户密码忘记,不是内部或外部命令,修改数据库和表的字符编码,命令行客户端的字符集问题
- IE8兼容性问题的解决方案
- imageset matlab,如何以imageSet或imageDataStore的形式向MATLAB中的BagOfFeatures()函數提供輸入?...
- bcscale php,【PHP开发】bcscale timezone charset的设定说明
- JavaScript学习(十一)—selected属性、checked属性、class属性的操作
- 测试python安装成功_Python在Windows上安装配置测试
- 关于http的一点常识
- Java基础2一基础语法