版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2019.08.13 星期二 |
前言
這個專題我們就一起看一下Swfit相關的基礎知識。感興趣的可以看上面幾篇。
1. Swift基礎知識相關(一) —— 泛型(一)
2. Swift基礎知識相關(二) —— 編碼和解碼(一)
開始
首先看下主要內容
主要內容:在本Swift教程中,您將學習如何創建自定義運算符,重載現有運算符以及設置運算符優先級。
接著,看下寫作環境
Swift 5, iOS 13, Xcode 11
運算符是任何編程語言的核心構建塊。你能想象編程而不使用+
或=
嗎?
運算符非?;A,大多數語言都將它們作為編譯器(或解釋器)的一部分。另一方面,Swift編譯器并不對大多數操作符進行硬編碼,而是為庫提供了創建自己的操作符的方法。它將工作留給了Swift標準庫(Swift Standard Library)
,以提供您期望的所有常見標準庫。這種差異是微妙的,但為巨大的定制潛力打開了大門。
Swift運算符特別強大,因為您可以通過兩種方式更改它們以滿足您的需求:為現有運算符分配新功能(稱為運算符重載 operator overloading
),以及創建新的自定義運算符。
在本教程中,您將使用一個簡單的Vector
結構體并構建自己的一組運算符,以幫助組合不同的向量。
打開Xcode,然后轉到File?New?Playground
創建一個新playground
。選擇Blank
模板并命名您的playground
為CustomOperators
。刪除所有默認代碼,以便您可以從空白平板開始。
將以下代碼添加到您的playground
:
struct Vector {
let x: Int
let y: Int
let z: Int
}
extension Vector: ExpressibleByArrayLiteral {
init(arrayLiteral: Int...) {
assert(arrayLiteral.count == 3, "Must initialize vector with 3 values.")
self.x = arrayLiteral[0]
self.y = arrayLiteral[1]
self.z = arrayLiteral[2]
}
}
extension Vector: CustomStringConvertible {
var description: String {
return "(\(x), \(y), \(z))"
}
}
在這里,您可以定義一個新的Vector
類型,其中三個屬性符合兩個協議。 CustomStringConvertible
協議和description
計算屬性允許您打印Vector
的友好字符串表示。
在playground
的底部,添加以下行:
let vectorA: Vector = [1, 3, 2]
let vectorB = [-2, 5, 1] as Vector
你剛剛用簡單的數組創建了兩個向量Vectors
,沒有初始化器!那是怎么發生的?
ExpressibleByArrayLiteral
協議提供無摩擦的接口來初始化Vector
。該協議需要一個具有可變參數的不可用初始化程序:init(arrayLiteral:Int ...)
。
可變參數arrayLiteral
允許您傳入由逗號分隔的無限數量的值。例如,您可以創建Vector
,例如Vector(arrayLiteral:0)
或Vector(arrayLiteral:5,4,3)
。
該協議進一步方便,并允許您直接使用數組進行初始化,只要您明確定義類型,這是您為vectorA
和vectorB
所做的。
這種方法的唯一警告是你必須接受任何長度的數組。如果您將此代碼放入應用程序中,請記住,如果傳入長度不是三的數組,它將會崩潰。如果您嘗試初始化少于或多于三個值的Vector
,則初始化程序頂部的斷言assert
將在開發和內部測試期間在控制臺中提醒您。
單獨的矢量Vectors
很好,但如果你能用它們做事情會更好。正如你在小學時所做的那樣,你將從加法開始你的學習之旅。
Overloading the Addition Operator
運算符重載的一個簡單示例是加法運算符。 如果您將它與兩個數字一起使用,則會發生以下情況:
1 + 1 // 2
但是,如果對字符串使用相同的加法運算符,則它具有完全不同的行為:
"1" + "1" // "11"
當+
與兩個整數一起使用時,它會以算術形式添加它們。 但是當它與兩個字符串一起使用時,它會將它們連接起來。
為了使運算符重載,您必須實現一個名稱為運算符符號的函數。
注意:您可以將重載函數定義為類型的成員,這是您將在本教程中執行的操作。 這樣做時,必須將其聲明為靜態
static
,以便可以在沒有定義它的類型的實例的情況下訪問它。
在playground
的尾部添加以下代碼:
// MARK: - Operators
extension Vector {
static func + (left: Vector, right: Vector) -> Vector {
return [
left.x + right.x,
left.y + right.y,
left.z + right.z
]
}
}
此函數將兩個向量作為參數,并將它們的和作為新向量返回。 要做矢量加法,只需相加其各個組件即可。
要測試此功能,請將以下內容添加到playground
的底部:
vectorA + vectorB // (-1, 8, 3)
您可以在playground
的右側邊欄中看到合成矢量。
1. Other Types of Operators
加法運算符是所謂的中綴infix
運算符,意味著它在兩個不同的值之間使用。 還有其他類型的運算符:
-
infix:在兩個值之間使用,例如加法運算符(例如,
1 + 1
) -
prefix:在值之前添加,如負號運算符(例如
-3
)。 -
postfix:在一個值之后添加,比如
force-unwrap
運算符(例如,mayBeNil!
) - ternary:在三個值之間插入兩個符號。 在Swift中,不支持用戶定義的三元運算符,只有一個內置的三元運算符,您可以在 Apple’s documentation中閱讀。
您想要重載的下一個運算符是負號符號,它將更改Vector
的每個組件的符號。 例如,如果將它應用于vectorA
,即(1,3,2)
,則返回(-1,-3,-2)
。
在擴展名內的上一個靜態static
函數下面添加此代碼:
static prefix func - (vector: Vector) -> Vector {
return [-vector.x, -vector.y, -vector.z]
}
假設運算符是中綴infix
,因此如果您希望運算符是不同的類型,則需要在函數聲明中指定運算符類型。 負號運算符不是中綴,因此您將前綴prefix
修飾符添加到函數聲明中。
在playground
的底部,添加以下行:
-vectorA // (-1, -3, -2)
在側欄中檢查結果是否正確。
接下來是減法,留給你自己實現。 完成后,請檢查以確保您的代碼與我的代碼類似。 提示:減法與添加負號相同。
試一試,如果您需要幫助,請查看下面的解決方案!
static func - (left: Vector, right: Vector) -> Vector { return left + -right }
通過將此代碼添加到playground
的底部來測試您的新運算符:
vectorA - vectorB // (3, -2, 1)
2. Mixed Parameters? No Problem!
您還可以通過標量乘法將向量乘以數字。 要將兩個向量相乘,可以將每個分量相乘。 你接下來要實現這個。
您需要考慮的一件事是參數的順序。 當您實施加法時,順序無關緊要,因為兩個參數都是向量。
對于標量乘法,您需要考慮Int * Vector
和Vector * Int
。 如果您只實現其中一種情況,Swift編譯器將不會自動知道您希望它以其他順序工作。
要實現標量乘法,請在剛剛添加的減法函數下添加以下兩個函數:
static func * (left: Int, right: Vector) -> Vector {
return [
right.x * left,
right.y * left,
right.z * left
]
}
static func * (left: Vector, right: Int) -> Vector {
return right * left
}
為避免多次寫入相同的代碼,第二個函數只是將其參數轉發給第一個。
在數學中,向量有另一個有趣的操作,稱為cross-product
。 cross-product
的原理超出了本教程的范圍,但您可以在Cross product Wikipedia page頁面上了解有關它們的更多信息。
由于在大多數情況下不鼓勵使用自定義符號(誰想在編碼時打開表情符號菜單?),重復使用星號和cross-product
運算符會非常方便。
與標量乘法不同,Cross-products
將兩個向量作為參數并返回一個新向量。
添加以下代碼以在剛剛添加的乘法函數之后添加cross-product
實現:
static func * (left: Vector, right: Vector) -> Vector {
return [
left.y * right.z - left.z * right.y,
left.z * right.x - left.x * right.z,
left.x * right.y - left.y * right.x
]
}
現在,將以下計算添加到playground
的底部,同時利用乘法和cross-product
運算符:
vectorA * 2 * vectorB // (-14, -10, 22)
此代碼找到vectorA
和2
的標量倍數,然后找到該向量與vectorB
的交叉乘積。 請注意,星號運算符始終從左向右,因此前面的代碼與使用括號分組操作相同,如(vectorA * 2)* vectorB
。
3. Protocol Operators
一些運算符是協議的成員。 例如,符合Equatable
的類型必須實現==
運算符。 類似地,符合Comparable
的類型必須至少實現<
和==
,因為Comparable
繼承自Equatable
。 Comparable
類型也可以選擇實現>
,> =
和<=
,但這些運算符具有默認實現。
對于Vector
,Comparable
并沒有太多意義,但Equatable
卻很重要,因為如果它們的組件全部相等,則兩個向量相等。 接下來你將實現Equatable
。
要符合協議,請在playground
的末尾添加以下代碼:
extension Vector: Equatable {
static func == (left: Vector, right: Vector) -> Bool {
return left.x == right.x && left.y == right.y && left.z == right.z
}
}
將以下行添加到playground
的底部以測試它:
vectorA == vectorB // false
此行按預期返回false
,因為vectorA
具有與vectorB
不同的組件。
符合Equatable
不僅能夠檢查這些類型的相等性。您還可以獲取矢量數組的contains(_:)
方法!
Creating Custom Operators
還記得我是怎么說通常不鼓勵使用自定義符號嗎?與往常一樣,該規則也有例外。
關于自定義符號的一個好的經驗法則是,只有在滿足以下條件時才應使用它們:
- 它們的含義是眾所周知的,或者對閱讀代碼的人有意義。
- 它們很容易在鍵盤上打字。
您將實現的最后一個運算符匹配這兩個條件。矢量點積產生兩個向量并返回單個標量數。您的運算符會將向量中的每個值乘以另一個向量中的對應值,然后將所有這些乘積相加。
點積的符號為?
,您可以使用鍵盤上的Option-8
輕松鍵入。
您可能會想,“我可以在本教程中對其他所有操作符執行相同的操作,對吧?”
不幸的是,你還不能那樣做。在其他情況下,您正在重載已存在的運算符。對于新的自定義運算符,您需要首先創建運算符。
直接在Vector
實現下面,但在CustomStringConvertible
一致性擴展之上,添加以下聲明:
infix operator ?: AdditionPrecedence
這將?
定義為必須放在兩個其他值之間的運算符,并且與加法運算符+
具有相同的優先級。 暫時忽略優先級別。
既然已經注冊了此運算符,請在運算符擴展的末尾添加其實現,緊接在乘法和cross-product
運算符*
的實現之下:
static func ? (left: Vector, right: Vector) -> Int {
return left.x * right.x + left.y * right.y + left.z * right.z
}
將以下代碼添加到playground
的底部以進行測試:
vectorA ? vectorB // 15
到目前為止,一切看起來都不錯......或者是嗎? 在playground
的底部嘗試以下代碼:
vectorA ? vectorB + vectorA // Error!
Xcode對你不滿意。 但為什么?
現在,?
和+
具有相同的優先級,因此編譯器從左到右解析表達式。 編譯器將您的代碼解釋為:
(vectorA ? vectorB) + vectorA
此表達式歸結為Int + Vector
,您尚未實現并且不打算實現。 你能做些什么來解決這個問題?
Precedence Groups
Swift中的所有運算符都屬于一個優先級組(precedence group)
,它描述了運算符的計算順序。 還記得學習小學數學中的操作順序嗎? 這基本上就是你在這里所要處理的。
在Swift標準庫中,優先級順序如下:
以下是關于這些運算符的一些注釋,因為您之前可能沒有看到它們:
- 1) 按位移位運算符
<<
和>>
用于二進制計算。 - 2) 您使用轉換運算符,
is
和as
來確定或更改值的類型。 - 3)
nil
合并運算符??
有助于為可選值提供回退值。 - 4) 如果您的自定義運算符未指定優先級,則會自動分配
DefaultPrecedence
。 - 5) 三元運算符,
? :
,類似于if-else
語句。 - 6) 對于
=
的衍生,AssignmentPrecedence
在其他所有內容之后進行評估,無論如何。
編譯器解析具有左關聯性的類型,以便v1 + v2 + v3 ==(v1 + v2)+ v3
。 對于右關聯性結果也是正確的。
操作符按它們在表中出現的順序進行解析。 嘗試使用括號重寫以下代碼:
v1 + v2 * v3 / v4 * v5 == v6 - v7 / v8
當您準備好數學知識時,請查看下面的解決方案。
(v1 + (((v2 * v3) / v4) * v5)) == (v6 - (v7 / v8))
在大多數情況下,您需要添加括號以使代碼更易于閱讀。 無論哪種方式,理解編譯器評估運算符的順序都很有用。
1. Dot Product Precedence
您的新dot-product
并不適合任何這些類別。 它必須少于加法(如前所述),但它是否真的適合CastingPrecedence
或RangeFormationPrecedence
?
相反,您將為您的點積運算符創建自己的優先級組。
用以下內容替換?
運算符的原始聲明:
precedencegroup DotProductPrecedence {
lowerThan: AdditionPrecedence
associativity: left
}
infix operator ?: DotProductPrecedence
在這里,您創建一個新的優先級組并將其命名為DotProductPrecedence
。 您將它放在低于AdditionPrecedence
的位置,因為您希望加法優先。 你也可以將它設為左關聯,因為你想要從左到右進行評估,就像你在加法和乘法中一樣。 然后,將此新優先級組分配給?
運算符。
注意:除了
lowerThan
之外,您還可以在DotProductPrecedence
中指定higherThan
。 如果您在單個項目中有多個自定義優先級組,這一點就變得很重要。
您的舊代碼行現在運行并按預期返回:
vectorA ? vectorB + vectorA // 29
恭喜 - 您已經掌握了自定義操作符!
此時,您知道如何根據需要重載Swift操作符。 在本教程中,您專注于在數學上下文中使用運算符。 在實踐中,您將找到更多使用運算符的方法。
在ReactiveSwift
ReactiveSwift framework 框架中可以看到自定義操作符使用的一個很好的演示。 一個例子是<~
,這是反應式編程中的一個重要函數。 以下是此運算符的使用示例:
let (signal, _) = Signal<Int, Never>.pipe()
let property = MutableProperty(0)
property.producer.startWithValues {
print("Property received \($0)")
}
property <~ signal
Cartography 是另一個大量使用運算符重載的框架。 此AutoLayout
工具重載相等和比較運算符,以使NSLayoutConstraint
創建更簡單:
constrain(view1, view2) { view1, view2 in
view1.width == (view1.superview!.width - 50) * 0.5
view2.width == view1.width - 50
view1.height == 40
view2.height == view1.height
view1.centerX == view1.superview!.centerX
view2.centerX == view1.centerX
view1.top >= view1.superview!.top + 20
view2.top == view1.bottom + 20
}
此外,您始終可以參考Apple的官方文檔 custom operator documentation。
有了這些新的靈感來源,您可以走出世界,通過運算符重載使代碼更簡單。不過還是要小心使用自定義操作符!
下面看下相關整體代碼
struct Vector {
let x: Int
let y: Int
let z: Int
}
extension Vector: ExpressibleByArrayLiteral {
init(arrayLiteral: Int...) {
assert(arrayLiteral.count == 3, "Must initialize vector with 3 values.")
self.x = arrayLiteral[0]
self.y = arrayLiteral[1]
self.z = arrayLiteral[2]
}
}
precedencegroup DotProductPrecedence {
lowerThan: AdditionPrecedence
associativity: left
}
infix operator ?: DotProductPrecedence
extension Vector: CustomStringConvertible {
var description: String {
return "(\(x), \(y), \(z))"
}
}
let vectorA: Vector = [1, 3, 2]
let vectorB: Vector = [-2, 5, 1]
// MARK: - Operators
extension Vector {
static func + (left: Vector, right: Vector) -> Vector {
return [
left.x + right.x,
left.y + right.y,
left.z + right.z
]
}
static prefix func - (vector: Vector) -> Vector {
return [-vector.x, -vector.y, -vector.z]
}
static func - (left: Vector, right: Vector) -> Vector {
return left + -right
}
static func * (left: Int, right: Vector) -> Vector {
return [
right.x * left,
right.y * left,
right.z * left
]
}
static func * (left: Vector, right: Int) -> Vector {
return right * left
}
static func * (left: Vector, right: Vector) -> Vector {
return [
left.y * right.z - left.z * right.y,
left.z * right.x - left.x * right.z,
left.x * right.y - left.y * right.x
]
}
static func ? (left: Vector, right: Vector) -> Int {
return left.x * right.x + left.y * right.y + left.z * right.z
}
}
vectorA + vectorB // (-1, 8, 3)
-vectorA // (-1, -3, -2)
vectorA - vectorB // (3, -2, 1)
extension Vector: Equatable {
static func == (left: Vector, right: Vector) -> Bool {
return left.x == right.x && left.y == right.y && left.z == right.z
}
}
vectorA == vectorB // false
vectorA ? vectorB // 15
vectorA ? vectorB + vectorA // 29
后記
本篇主要講述了重載自定義運算符,感興趣的給個贊或者關注~~~