本編是Swifter - Swift 開發(fā)者必備 Tips 閱讀筆記
一、柯里化(Currying), 百度百科
柯里化(Currying)是把
接受多個參數(shù)的函數(shù)
變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù)
,并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)
的技術(shù)例如, 現(xiàn)在有如下函數(shù), 作用是每次傳入一個數(shù), 返回這個數(shù)+1的數(shù)字
func addOne(num: Int) -> Int {
return num + 1
}
- 如果我們需要一個得到數(shù)字 +2, +3.... 等等的函數(shù), 就需要分別定義
func addTwo(num: Int) -> Int {
return num + 2
}
func addThree(num: Int) -> Int {
return num + 3
}
- 我們可以將這類方法簡化為, 傳入兩個參數(shù), 第一個參數(shù)是需要增加的數(shù)量, 第二個是基數(shù)
func addTo(adder: Int, num: Int) -> Int {
return num + adder
}
-
addTo
函數(shù)可以進行柯里化, 接收一個單一參數(shù)adder
, 返回一個接受余下參數(shù)的新函數(shù)
, 并且這個新函數(shù)
需要返回原有函數(shù)的結(jié)果
func addTo(adder: Int) -> (Int) -> Int {
func addNum(num: Int) -> Int {
return num + adder
}
return addNum
}
- 最終簡化為:
func addTo(adder: Int) -> (Int) -> Int {
return {
num in
return num + adder
}
}
通過將需要傳入兩個參數(shù)的
addTo
函數(shù), 簡化為只需要傳入一個參數(shù)的addTo
函數(shù)的過程, 就是柯里化addTo
返回的函數(shù), 能夠接收余下參數(shù), 并且能夠返回最終結(jié)果這時我們想要獲取 +2, +3的方法, 只需要如下處理
let addTwo = addTo(2)
print(addTwo(1)) // 打印: 3
print(addTwo(2)) // 打印: 4
print(addTwo(3)) // 打印: 5
let addThree = addTo(3)
print(addThree(1)) // 打印: 4
print(addThree(2)) // 打印: 5
print(addThree(3)) // 打印: 6
- 柯里化是一種量產(chǎn)相似方法的好辦法,可以通過柯里化一個方法模板來避免寫出很多重復(fù)代碼,也方便了今后維護。
柯里化應(yīng)用實例
- Swift中, 可以使用如下的方式定義一個方法的管理器
struct Control {
let target: AnyObject
let action: Selector
func performAction() {
target.perform(action)
}
}
-
Control
的實例持有target
, 以及target
的某一個方法action
, 就可以使用調(diào)用performAction
來調(diào)用這個方法
class SomeClass: UIView {
var control: Control?
override init(frame: CGRect) {
super.init(frame: frame)
self.control = aControl(target: self, action: NSSelectorFromString("say"))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.control?.performAction()
}
@objc func say() {
print("啦啦啦")
}
}
上面的代碼中,
SomeClass
類有一個control
屬性, 在初始化時, 使control
持有了say
方法, 當(dāng)點擊SomeClass
的實例時, 使用control
觸發(fā)say
方法這種寫法針對只有一個調(diào)用事件時還算可以, 如果有多個調(diào)用事件, 就需要寫很多個
control
屬性, 這時就會使代碼顯得很亂我們可以通過柯里化, 對這種寫法進行優(yōu)化
首先, 我們定義一個包裝
target-action
的結(jié)構(gòu)體, 這個結(jié)構(gòu)體遵從TargetAction
協(xié)議
protocol TargetAction {
func performAction()
}
struct TargetActionWrapper<T: AnyObject>: TargetAction {
weak var target: T?
let action: (T) -> () -> ()
func performAction() -> () {
if let t = target {
action(t)()
}
}
}
- 接著定義事件觸發(fā)時機的枚舉類型
enum ControlEvent {
case touchesBegan
case touchesEnded
// ...
}
- 最后定義一個控制容器, 用來存放多個
TargetActionWrapper
類型的對象, 并管理這些對象
class Control: UIView {
var actions = [ControlEvent: TargetAction]()
func setTarget<T: AnyObject>(target: T, action: @escaping (T) -> () -> (), controlEvent: ControlEvent) {
actions[controlEvent] = TargetActionWrapper(target: target, action: action)
}
func removeTargetForControlEvent(controlEvent: ControlEvent) {
actions[controlEvent] = nil
}
func performActionForControlEvent(controlEvent: ControlEvent) {
actions[controlEvent]?.performAction()
}
}
- 我們重寫
SomClass
類
class SomeClass: UIView {
var control: Control = Control()
override init(frame: CGRect) {
super.init(frame: frame)
self.control.setTarget(target: self, action: SomeClass.touchesBegan, controlEvent: .touchesBegan)
self.control.setTarget(target: self, action: SomeClass.touchesEnded, controlEvent: .touchesEnded)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.control.performActionForControlEvent(controlEvent: .touchesBegan)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.control.performActionForControlEvent(controlEvent: .touchesEnded)
}
func touchesBegan() {
print("開始點擊")
}
func touchesEnded() {
print("結(jié)束點擊")
}
}
二、將protocol的方法聲明為mutating
- 對于值類型(結(jié)構(gòu)體和枚舉), 想要在實例方法中修改成員變量, 就需要在方法前加上
mutating
關(guān)鍵字
protocol Vehicle
{
var numberOfWheels: Int {get}
var color: UIColor {get set}
mutating func changeColor()
}
struct MyCar: Vehicle {
let numberOfWheels = 4
var color = UIColor.blue
mutating func changeColor() {
// 因為 `color` 的類型是 `UIColor`,這里直接寫 .red 就足以推斷類型了
color = .red
}
}
- 另外,在使用
class
來實現(xiàn)帶有mutating
的方法的協(xié)議時,具體實現(xiàn)的前面是不需要加mutating
修飾的,因為class
可以隨意更改自己的成員變量。所以說在協(xié)議里用mutating
修飾方法,對于class
的實現(xiàn)是完全透明,可以當(dāng)作不存在的。
三、Sequence
1、迭代器, 百度百科
迭代器是一個對象,它的工作是遍歷并選擇序列中的對象,它提供了一種訪問一個容器對象中的各個元素,而又不必暴露該對象內(nèi)部細節(jié)的方法。通過迭代器,開發(fā)人員不需要了解容器底層的結(jié)構(gòu),就可以實現(xiàn)對容器的遍歷。由于創(chuàng)建迭代器的代價小,因此迭代器通常被稱為輕量級的容器。
Swift中的迭代器需要遵從
IteratorProtocol
協(xié)議
public protocol IteratorProtocol {
associatedtype Element
// 返回下一個元素, 如果沒有返回 nil
// 通過反復(fù)調(diào)用, 可以獲取所有的元素, 一旦元素的序列已經(jīng)耗盡, 所有后續(xù)的調(diào)用返回 nil
public mutating func next() -> Self.Element?
}
- 以數(shù)組的迭代器為例, 可以使用數(shù)組的
makeIterator()
方法, 獲取數(shù)組的迭代器
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
var animalIterator = animals.makeIterator()
while let animal = animalIterator.next() {
print(animal)
}
// 打印結(jié)果:
Antelope
Butterfly
Camel
Dolphin
此時已經(jīng)通過迭代器遍歷了數(shù)組中的所有元素
如果再次調(diào)用
animalIterator.next()
方法, 只會返回nil
print(animalIterator.next())
// 打印結(jié)果: nil
迭代器只能通過
next()
方法獲取下一個元素, 一直遍歷到最后為止, 而無法返回到一開始的位置重新遍歷我們也可以自定義迭代器, 只需準守
IteratorProtocol
協(xié)議
struct CustomIterator<T>: IteratorProtocol {
typealias Element = T
var array: [Element]
var currentIndex = 0
init(_ array: [Element]) {
self.array = array
}
mutating func next() -> Element? {
if currentIndex < array.count {
let int = array[currentIndex]
currentIndex += 1
return int
}else {
return nil;
}
}
}
- 通過數(shù)組創(chuàng)建一個
CustomIterator
迭代器對象
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
var custom = CustomIterator(animals)
- 遍歷剛創(chuàng)建的結(jié)構(gòu)體
while let animal = custom.next() {
print(animal)
}
// 打印:
Antelope
Butterfly
Camel
Dolphin
- 再次調(diào)用
custom.next()
, 將會返回nil
print(custom.next())
// 返回: nil
- 上面自定義的迭代器是正向的遍歷集合中的元素, 我們還可以定義一個反向遍歷的迭代器
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
}
}
2、Sequence
在Swift中, 所有想要實現(xiàn)
for-in
循環(huán)的對象, 都必須繼承Sequence
協(xié)議遵守
Sequence
協(xié)議的最簡單的類型, 需要實現(xiàn)以下方法
public protocol Sequence {
// 關(guān)聯(lián)類型, 必須遵守了IteratorProtocol協(xié)議
associatedtype Iterator : IteratorProtocol
// 獲取一個迭代器
public func makeIterator() -> Self.Iterator
}
for-in
循環(huán)每一次都會調(diào)用序列的makeIterator()
方法, 來獲取一個新的迭代器, 然后開始遍歷現(xiàn)在我們自定義一個可倒序遍歷的序列
struct CustomReverseSequence<T>: Sequence {
var array: [T]
init(_ array: [T]) {
self.array = array
}
typealias intrator = CustomReverseIterator<T>
func makeIterator() -> intrator {
return CustomReverseIterator(self.array)
}
}
- 我們通過數(shù)組創(chuàng)建一個
CustomReverseSequence
的實例
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let sequence = CustomReverseSequence(animals)
- 此時, 我們可以使用
for-in
循環(huán), 來遍歷sequence
對象
for animal in sequence {
print(animal)
}
// 打印:
Dolphin
Camel
Butterfly
Antelope
- 這段代碼等價于
var iterator = sequence.makeIterator()
while let animal = iterator.next() {
print(animal)
}
- 有一點比較方便的是,
Sequence
擴展中, 實現(xiàn)了map
、filter
和reduce
這些方法, 我們可以直接調(diào)用, 下面以map
為例
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let sequence = CustomReverseSequence(animals)
let newSequence = sequence.map {
return $0 + "123"
}
print(newSequence)
// 打印: ["Dolphin123", "Camel123", "Butterfly123", "Antelope123"]
四、多元組(Tuple)
元組可以將多種類型的值, 包裝起來, 并做為返回值使用
例如OC版本的
CGRect
, 有一個輔助方法CGRectDivide
, 可以講一個CGRect
實例切割成兩部分
/*
CGRectDivide(CGRect rect, CGRect *slice, CGRect *remainder,
CGFloat amount, CGRectEdge edge)
*/
CGRect rect = CGRectMake(0, 0, 100, 100);
CGRect small;
CGRect large;
CGRectDivide(rect, &small, &large, 20, CGRectMinXEdge);
上面的代碼將
{0,0,100,100}
的 rect 分割為兩部分,分別是{0,0,20,100}
的small
和{20,0,80,100}
的large
。由于 C 系語言的單一返回,我們不得不通過傳入指針的方式讓方法來填充需要的部分,可以說使用起來既不直觀,又很麻煩。而在Swift中, 可以使用元組來返回多個值
extension CGRect {
//...
func divided(atDistance: CGFloat, from fromEdge: CGRectEdge)
-> (slice: CGRect, remainder: CGRect)
//...
}
- 在使用的時候, 對比OC會方便很多, 也更容易理解
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let (small, large) = rect.divided(atDistance: 20, from: .minXEdge)
- 上面的代碼, 就是使用元組做為返回值的一個例子, 除此之外, 還可以使用元組簡化交換兩個變量的值的寫法
- 在OC中, 想要交換兩個變量的值, 需要使用如下類似的寫法, 用一個常量做為中轉(zhuǎn)
func swapMe1<T>( a: inout T, b: inout T) {
let temp = a
a = b
b = temp
}
- 而使用Swift的元組, 就可以如下處理
func swapMe2<T>(a: inout T, b: inout T) {
(a, b) = (b, a)
}
五、@autoclosure 和 ??
1、自動閉包 @autoclosure
- 自動閉包是一種自動創(chuàng)建的閉包,用于包裝傳遞給函數(shù)作為參數(shù)的表達式。這種閉包不接受任何參數(shù),當(dāng)它被調(diào)用的時候,會返回被包裝在其中的表達式的值。
- 簡單地說, 就是把一句表達式封裝成一個閉包, 并將表達式的結(jié)果作為返回值
- 比如有如下函數(shù),
logIfTrue
接收一個()->Bool
類型的閉包
func logIfTrue(_ predicate: () -> Bool) {
if predicate() {
print("true")
}
}
- 當(dāng)我們調(diào)用
logIfTrue
方法時
logIfTrue { () -> Bool in
return 2 > 1
}
- 可以簡化為
logIfTrue {2 > 1}
- 但是不管哪一種方式, 要么是書法起來十分麻煩, 要么是表達上不太清晰, 所以蘋果提供了
@autoclosure
關(guān)鍵字, 可以將logIfTrue
函數(shù)的定義改寫為下面的樣子
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
if predicate() {
print("true")
}
}
- 這時候, 再次調(diào)用
logIfTrue
函數(shù)
logIfTrue (2 > 1)
- Swift將會把
2 > 1
這個表達式自動轉(zhuǎn)換為() -> Bool
類型的閉包, 這樣我們就得到了一個寫法簡單, 表意清除的式子
2、??
- 在Swift中, 有一個非常有用的操作符, 可以用來快速地對
nil
進行條件判斷, 那就是??
。這個操作符可以判斷輸入并在當(dāng)左側(cè)的值是非nil
的Optional值時返回其value, 當(dāng)左側(cè)是nil
時返回右側(cè)的值, 比如:
var a: Int?
var b = 10
var c = a ?? b
這個例子, 我們沒有給
a
賦值, 所以最終會將b
的值賦值給c
, 即c = 10
我們可以點進
??
的定義中查看
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T?) -> T?
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T
在這里, 我們的輸入滿足的是后者, 雖然表面上看
b
只是一個Int
, 但是在使用的時候, 被自動封裝成了() -> Int
類型的閉包此時, 推測一下
??
的具體實現(xiàn)
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
switch optional {
case .some(let value):
return value
case .none:
return defaultValue()
}
}
- 這里將
defaultValue
參數(shù)定義為了() -> T
類型的自動閉包, 而不是T
類型 - 如果我們直接使用
T
做為defaultValue
參數(shù)的類型, 那么在使用??
操作符之前就需要準備一個默認值。 - 正常情況下, 這樣不會有大問題, 但是如果這個默認值是通過一系列復(fù)雜計算得到的話, 可能會造成資源浪費, 因為如果
optional
不是nil
的話, 這個默認是根本不會被用到, 而會直接返回optional
解包后的值 - 使用
autoclosure
就避免了這樣的開銷, 方法會將默認值的計算推遲到optional
判定為nil
之后
@autoclosure并不支持帶有輸入?yún)?shù)的寫法, 也就是說只有形如
() -> T
的參數(shù)才能使用這個特性簡化
六、@escaping
- Swift中我們可以定義一個接受函數(shù)做為參數(shù)的函數(shù), 而在調(diào)用時, 使用閉包的方式來傳遞這個參數(shù)是常見手段
- 定義函數(shù)
func doWork(block: () -> ()) {
block()
}
- 調(diào)用函數(shù)
doWork {
print("work")
}
- 這種最簡單的形式的閉包其實還默認隱藏了一個假設(shè), 那就是參數(shù)
block
的內(nèi)容會在doWork
返回前就完成, 也就是說, 對于block
的調(diào)用是同步行為。 - 如果我們改變一下代碼, 將
block
放到一個Dispatch
中去, 讓他在doWork
返回后被調(diào)用的話, 我們就需要在block
的類型前加上@escaping
標記來表名這個閉包是會逃逸出該方法的
func doWorkAsync(block: @escaping: () -> ()) {
DispatchQueue.main.async {
block()
}
}
- 在使用閉包調(diào)用這個兩個方法時,也會有一些行為的不同。我們知道,閉包是可以捕獲其中的變量的。對于
doWork
參數(shù)里這樣的沒有逃逸行為的閉包,因為閉包的作用域不會超過函數(shù)本身,所以我們不需要擔(dān)心在閉包內(nèi)持有self
等。 - 而接受
@escaping
的doWorkAsync
則有所不同。由于需要確保閉包內(nèi)的成員依然有效,如果在閉包內(nèi)引用了self
及其成員的話,Swift 將強制我們明確地寫出self
。對比下面的兩個用例的不同之處:
class S {
var foo = "foo"
func method1() {
doWork {
print(foo)
}
foo = "bar"
}
func method2() {
doWorkAsync {
print(self.foo)
}
foo = "bar"
}
}
S().method1() // foo
S().method2() // bar
- 如果我們不希望在閉包中持有
self
, 可以使用[weak self]
的方式來表達:
func method3() {
doWorkAsync {
[weak self] in
print(self?.foo ?? "nil")
}
foo = "bar"
}
S().method3() // nil
- 因為
S()
沒有被強引用, 所以作用域結(jié)束后就會銷毀, 此時才會去調(diào)用doWorkAsync
中的block
, 這時self
已經(jīng)銷毀, 所以才會打印nil
如果你在協(xié)議或者父類中定義了一個接受
@escaping
為參數(shù)的方法, 那么在實際協(xié)議和類型或者是這個父類的子類時, 對應(yīng)的方法也必須被聲明為@escaping
, 否則兩個方法會被認為擁有不同的函數(shù)簽名protocol P { func work(b: @escaping ()->()) } // 可以編譯 class C: P { func work(b: @escaping () -> ()) { DispatchQueue.main.async { print("in C") b() } } } // 而這樣是無法編譯通過的: class C1: P { func work(b: () -> ()) { // ... } }
七、Optional Chaining
- 在使用可選鏈的時候, 可以讓我們擺脫很多不必要的判斷和取值
- 比如有下面的一段代碼
class Toy {
let name: String
init(name: String) {
self.name = name
}
}
class Pet {
var toy: Toy?
}
class Child {
var pet: Pet?
}
- 我們可以使用可選鏈, 調(diào)用一個孩子的寵物的玩具的名字
let xiaoming = Child()
let toyName = xiaoming.pet?.toy?.name
- 在實際的使用中, 我們大多數(shù)情況下可能更希望使用可選綁定來直接取值:
if let toyName = xiaoming.pet?.toy?.name {
// 小明有寵物, 并且寵物擁有玩具
}
上面的代碼是對屬性進行可選判斷, 除此之外, 還可以對方法進行可選綁定, 判斷方法是否調(diào)用成功
為
Toy
定義一個擴展
extension Toy {
func play() {
// 寵物玩玩具
}
}
- 還是使用上面的例子, 如果孩子有玩具, 就玩
xiaoming.pet?.toy?.play()
我們知道沒有返回值的方法具有隱式的返回類型
Void
, 等價于()
, 或者說空的元組所以我們可以使用可選鏈來判斷, 孩子的寵物是否真的調(diào)用
play()
方法
if let result: () = xiaoming.pet?.toy?.play() {
// 玩玩具
}else{
// 沒有玩具可以玩
}
- 除了例子中的小明, 可能還會有小紅, 小李等其他孩子, 這個時候如果每個孩子都判斷他們各自的寵物是否玩玩具, 就需要分別寫
if let result: () = xiaohong.pet?.toy?.play() {
// 玩玩具
}else{
// 沒有玩具可以玩
}
if let result: () = xiaoli.pet?.toy?.play() {
// 玩玩具
}else{
// 沒有玩具可以玩
}
- 這個時候我們就會想著將
xiaoming.pet?.toy?.play()
這一段代碼抽象出來, 做成一個閉包方便使用。傳入一個Child
對象, 如果小朋友有寵物并且寵物有玩具的話, 就去玩, 于是很可能你會寫出這樣的代碼:
這是錯誤的代碼
let playClosure = { (child: Child) -> () in child.pet?.toy?.play() }
- 這樣的代碼是沒有意義的, 因為這段代碼等價于
let playClosure = { (child: Child) -> () in child.pet?.toy?.play() return () }
- 我們需要將
child.pet?.toy?.play()
的返回值做為閉包playClosure
的返回值, 所以我們需要如下代碼
let playClosure = { (child: Child) -> ()? in
child.pet?.toy?.play()
}
- 上面的代碼, 有一個隱式的
return
, 實際代碼如下
let playClosure = { (child: Child) -> ()? in
return child.pet?.toy?.play()
}
- 這時我們可以使用可選綁定來判斷方法是否調(diào)用成功
if let toyName = playClosure(xiaoming) {
print("玩玩具~")
}else {
print("沒有玩具玩~")
}
if let toyName = playClosure(xiaohong) {
print("玩玩具~")
}else {
print("沒有玩具玩~")
}
if let toyName = playClosure(xiaoli) {
print("玩玩具~")
}else {
print("沒有玩具玩~")
}
八、操作符
與Objective-C不同, Swift支持重載操作符這樣的特性, 最常見的使用方式可能就是定義一些簡便的計算
比如我們需要定義一個表示二位向量的數(shù)據(jù)結(jié)構(gòu)
struct Vector2D {
var x = 0.0
var y = 0.0
}
- 一個很簡單的需求是像個
Vector2D
相加
let v1 = Vector2D(x: 1, y: 2)
let v2 = Vector2D(x: 3, y: 4)
let v3 = Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)
// v3為 Vector2D(x: 4.0, y: 6.0)
- 這樣的計算很簡單, 如果只做一次兩次很不錯, 但是一般情況下我們會進行很多這樣的操作, 這樣的話, 我們可能更愿意定義一個
Vector2D
相加的操作, 讓代碼簡化清晰 - 對于兩個向量的相加, 我們可以重載加號(
+
)操作符:
func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
- 這樣, 上面的
v3
以及之后的所有表示兩個向量相加的操作就全部可以用加號來表示了
let v4 = v1 + v2
// v4為 Vector2D(x: 4.0, y: 6.0)
- 類似的, 我們還可以為
Vector2D
定義像-
(減號, 兩個向量相減),-
(負號, 單個向量x和y同時取負)等等這樣的運算符
// - 減號
func - (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x - right.x, y: left.y - right.y)
}
// - 負號
prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
- 調(diào)用時, 直接使用操作符即可
let v5 = v1 - v2
print(v5)
// v5 為 Vector2D(x: -2.0, y: -2.0)
let v6 = -v1
print(v6)
// v6 為 Vector2D(x: -1.0, y: -2.0)
- 上面的加號, 減號和負號都是已經(jīng)存在于Swift中的運算符了, 我們所做的只是變換他的參數(shù)進行重載
- 如果我們想要定義一個全新的運算符的話, 要做的事會多一件
- 比如
點積運算
就是一個矢量運算中很常用的運算符, 它表示兩個向量對應(yīng)坐標的乘積的和。 - 根據(jù)定義以及參考重載運算符的方法, 我們選取
+*
來表示這個運算的話, 不難寫出:
func +* (letf: Vector2D, right: Vector2D) -> Double {
return letf.x * right.x + letf.y * right.y
}
- 但是編譯器會給我們一個錯誤:
Operator implementation without matching operator declaration
- 這是因為
+*
與之前的+
,-
等操作符不同,+
,-
這樣的操作符是Swift中已經(jīng)有定義聲明的, 如果我們想要添加新的操作符的話, 需要先對其進行生命, 告訴編譯器這個符號其實是一個操作符。添加如下代碼:
precedencegroup DotProductPrecedence {
associativity: none
higherThan: MultiplicationPrecedence
}
infix operator +*: DotProductPrecedence
precedencegroup
定義一個操作符優(yōu)先級別。操作符優(yōu)先級的定義和類型聲明有些類似, 一個操作符需要屬于某個特定的優(yōu)先級。Swift標準庫中已經(jīng)定義了一些常用的運算優(yōu)先級組, 比如加法優(yōu)先級組(
AdditionPrecedence
)和乘法優(yōu)先級組(MultiplicationPrecedence
)等。如果沒有合適你的運算符的優(yōu)先級組, 你就需要向我們在例子中做的這樣, 自己指定結(jié)合律方式和優(yōu)先級順序了
associativity
定義了結(jié)合律, 即多個同類的操作符順序出現(xiàn)時的計算順序。比較常見的加法和減法都是left, 就是說多個加法同時出現(xiàn)時按照從左往右的順序計算。
點乘的結(jié)果是一個Double
, 不會再和其他點乘結(jié)合使用, 所以這里是none
higherThan
運算的優(yōu)先級, 點積運算是優(yōu)先于乘法運算的。除了
higherThan
, 也支持使用loverThan
來指定優(yōu)先級低于某個其他組。
infix
表示要定義的是一個中位操作符, 即前后都是輸入; 其他的修飾子還包括
prefix
和postfix
- 有了這些之后, 點積運算符的定義如下
precedencegroup DotProductPrecedence {
associativity: none
higherThan: MultiplicationPrecedence
}
infix operator +*: DotProductPrecedence
func +* (letf: Vector2D, right: Vector2D) -> Double {
return letf.x * right.x + letf.y * right.y
}
- 我們可以很簡單地進行向量的點擊運算了:
let result = 3 * 5 + v1 +* v2
print(result)
// 打印: 26.0
最后需要多提一點的是,Swift 的操作符是不能定義在局部域中的,因為至少會希望在能在全局范圍使用你的操作符,否則操作符也就失去意義了。另外,來自不同 module 的操作符是有可能沖突的,這對于庫開發(fā)者來說是需要特別注意的地方。如果庫中的操作符沖突的話,使用者是無法像解決類型名沖突那樣通過指定庫名字來進行調(diào)用的。因此在重載或者自定義操作符時,應(yīng)當(dāng)盡量將其作為其他某個方法的 "簡便寫法",而避免在其中實現(xiàn)大量邏輯或者提供獨一無二的功能。這樣即使出現(xiàn)了沖突,使用者也還可以通過方法名調(diào)用的方式使用你的庫。運算符的命名也應(yīng)當(dāng)盡量明了,避免歧義和可能的誤解。因為一個不被公認的操作符是存在沖突風(fēng)險和理解難度的,所以我們不應(yīng)該濫用這個特性。在使用重載或者自定義操作符時,請先再三權(quán)衡斟酌,你或者你的用戶是否真的需要這個操作符。
九、func的參數(shù)修飾
- 在聲明一個Swift 的方法的時候, 我們一般不去指定參數(shù)前面的修飾符, 而是直接聲明參數(shù)
func incrementor(variable: Int) -> Int {
return variable + 1
}
- 如果我們想要對增加后的變量做點什么,又不想引入一個新的變量的話,很可能會寫出這樣的代碼:
這是錯誤的代碼
func incrementor(variable: Int) -> Int { variable += 1 print(variable) return variable }
- 之所以會有錯誤, 是因為Swift是一門討厭變化的語言, 所有有可能的地方, 都被默認認為是不可變的, 也就是用
let
進行聲明的 - 現(xiàn)在我們?nèi)绻胫辉诤瘮?shù)內(nèi)部對這樣的輸入值進行修改的話, 只能顯示地在函數(shù)內(nèi)部進行使用
var
進行賦值以后再操作了:
func incrementor2(variable: Int) -> Int {
var num = variable
num += 1
return num
}
- 有些時候我們會希望在方法內(nèi)部直接修改輸入的值, 這時候我們可以使用
inout
來對參數(shù)進行修飾
func incrementor(variable: inout Int) {
variable += 1
}
- 因為在函數(shù)內(nèi)部就更改了值,所以也不需要返回了。調(diào)用也要改變?yōu)橄鄳?yīng)的形式,在前面加上 & 符號:
var luckyNumber = 7
incrementor(variable: &luckyNumber)
print(luckyNumber)
// luckyNumber = 8
- 最后,要注意的是參數(shù)的修飾是具有傳遞限制的,就是說對于跨越層級的調(diào)用,我們需要保證同一參數(shù)的修飾是統(tǒng)一的。舉個例子,比如我們想擴展一下上面的方法,實現(xiàn)一個可以累加任意數(shù)字的 +N器 的話,可以寫成這樣:
func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) {
func incrementor(variable: inout Int) -> () {
variable += addNumber;
}
return incrementor;
}
- 外層的
makeIncrementor
的返回里也需要在參數(shù)的類型前面明確指出修飾詞,以符合內(nèi)部的定義,否則將無法編譯通過。
十、字面量表達
1、字面量
- 所謂字面量, 就是指像特定的數(shù)字, 字符串或者是布爾值這樣, 能夠直截了當(dāng)?shù)闹赋鲎约旱念愋筒樽兞窟M行賦值的值
- 比如在下面中的
3
、hello world
以及true
就稱為字面量
let aNumber = 3
let aString = "hello world"
let aBool = true
- 在Swift中,
Array
和Dictionary
在使用簡單的描述賦值的時候, 使用的也是字面量, 比如:
let anArray = [1, 2, 3]
let aDictionary = ["key1" : "value1", "key2" : "vakue2"]
2、使用字面量創(chuàng)建指定類型
- Swift為我們提供了一組協(xié)議, 使用字面量表達特定的類型
- 這些協(xié)議包括了各個原生的字面量, 在開發(fā)中可能經(jīng)常用的有以下幾種:
ExpressibleByArrayLiteral
ExpressibleByBooleanLiteral
ExpressibleByDictionaryLiteral
ExpressibleByFloatLiteral
ExpressibleByNilLiteral
ExpressibleByIntegerLiteral
ExpressibleByStringLiteral
- 對于實現(xiàn)了上面協(xié)議的類型, 在提供字面量賦值的時候, 就可以簡單地按照協(xié)議方法中定義的規(guī)則"無縫對應(yīng)"的通過賦值的方式將值表達為對應(yīng)類型
- 所有的字面量表達協(xié)議都定義了一個
typealias
和對應(yīng)的init
方法。拿ExpressibleByBooleanLiteral
舉個例子:
public protocol ExpressibleByBooleanLiteral {
associatedtype BooleanLiteralType : _ExpressibleByBuiltinBooleanLiteral
public init(booleanLiteral value: Self.BooleanLiteralType)
}
- 在這個協(xié)議中, Swift的標準庫已經(jīng)定義了
BooleanLiteralType
的類型是Bool
, 如果在類型中將BooleanLiteralType
設(shè)置為其他類型, 就會報錯
/// The default type for an otherwise-unconstrained boolean literal
typealias BooleanLiteralType = Bool
- 舉一個例子: 我們可以自定義
Int
的分類, 并遵守ExpressibleByBooleanLiteral
協(xié)議
extension Int: ExpressibleByBooleanLiteral {
public typealias BooleanLiteralType = Bool
public init(booleanLiteral value: Int.BooleanLiteralType) {
self = value ? 1 : 0
}
}
- 這時, 我們可以給一個整形變量賦值
true
:
let num1: Int = true
let num2: Int = false
print(num1, num2) // 打印: 1 0
3、ExpressibleByStringLiteral
-
ExpressibleByStringLiteral
協(xié)議稍微有些不同, 情況要復(fù)雜一些。 - 如果類型遵守
ExpressibleByStringLiteral
協(xié)議, 那么同時必須遵守下面的兩個協(xié)議
ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByUnicodeScalarLiteral
這兩個協(xié)議我們在日常項目中基本上不會使用,它們對應(yīng)字符簇和字符的字面量表達。雖然復(fù)雜一些,但是形式上還是一致的,只不過在實現(xiàn)
ExpressibleByStringLiteral
時我們需要將這三個init
方法都進行實現(xiàn)。還是以例子來說明,比如我們有個
Person
類,里面有這個人的名字:
class Person {
let name: String
init(name value: String) {
self.name = value
}
}
- 如果想要通過
String
賦值來生成Person
對象的話,可以改寫這個類:
class Person: ExpressibleByStringLiteral {
let name: String
init(name value: String) {
self.name = value
}
required init(stringLiteral value: String) {
self.name = value
}
required init(extendedGraphemeClusterLiteral value: String) {
self.name = value
}
required init(unicodeScalarLiteral value: String) {
self.name = value
}
}
- 在所有的協(xié)議定義的
init
前面我們都加上了required
關(guān)鍵字,這是由初始化方法的完備性需求所決定的,這個類的子類都需要保證能夠做類似的字面量表達,以確保類型安全。
注意: 如果是結(jié)構(gòu)體和枚舉, 就不需要加
required
, 這是因為值類型無法被繼承, 就不會有子類
- 在上面的例子里有很多重復(fù)的對
self.name
賦值的代碼,這是我們所不樂見的。一個改善的方式是在這些初始化方法中去調(diào)用原來的init(name value: String)
,這種情況下我們需要在這些初始化方法前加上convenience
:
class Person: 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 p: Person = "xiaoMing"
print(p.name)
// 輸出:
// xiaoMing
- 上面的
Person
的例子中,我們沒有像Int
中做的那樣,使用一個extension
的方式來擴展類使其可以用字面量賦值,這是因為在extension
中,我們是不能定義required
的初始化方法的。也就是說,我們無法為現(xiàn)有的非final
的class
添加字面量表達
總結(jié)一下,字面量表達是一個很強大的特性,使用得當(dāng)?shù)脑拰s短代碼和清晰表意都很有幫助;但是這同時又是一個比較隱蔽的特性:因為你的代碼并沒有顯式的賦值或者初始化,所以可能會給人造成迷惑:比如上面例子中為什么一個字符串能被賦值為 Person?你的同事在閱讀代碼的時候可能不得不去尋找這些負責(zé)字面量表達的代碼進行查看 (而如果代碼庫很大的話,這不是一件容易的事情,因為你沒有辦法對字面量賦值進行 Cmd + 單擊跳轉(zhuǎn))。