Swift基礎(chǔ)知識相關(guān)(三) —— 重載自定義運算符(一)

版本記錄

版本號 時間
V1.0 2019.08.13 星期二

前言

這個專題我們就一起看一下Swfit相關(guān)的基礎(chǔ)知識。感興趣的可以看上面幾篇。
1. Swift基礎(chǔ)知識相關(guān)(一) —— 泛型(一)
2. Swift基礎(chǔ)知識相關(guān)(二) —— 編碼和解碼(一)

開始

首先看下主要內(nèi)容

主要內(nèi)容:在本Swift教程中,您將學(xué)習(xí)如何創(chuàng)建自定義運算符,重載現(xiàn)有運算符以及設(shè)置運算符優(yōu)先級。

接著,看下寫作環(huán)境

Swift 5, iOS 13, Xcode 11

運算符是任何編程語言的核心構(gòu)建塊。你能想象編程而不使用+=嗎?

運算符非?;A(chǔ),大多數(shù)語言都將它們作為編譯器(或解釋器)的一部分。另一方面,Swift編譯器并不對大多數(shù)操作符進(jìn)行硬編碼,而是為庫提供了創(chuàng)建自己的操作符的方法。它將工作留給了Swift標(biāo)準(zhǔn)庫(Swift Standard Library),以提供您期望的所有常見標(biāo)準(zhǔn)庫。這種差異是微妙的,但為巨大的定制潛力打開了大門。

Swift運算符特別強大,因為您可以通過兩種方式更改它們以滿足您的需求:為現(xiàn)有運算符分配新功能(稱為運算符重載 operator overloading),以及創(chuàng)建新的自定義運算符。

在本教程中,您將使用一個簡單的Vector結(jié)構(gòu)體并構(gòu)建自己的一組運算符,以幫助組合不同的向量。

打開Xcode,然后轉(zhuǎn)到File?New?Playground創(chuàng)建一個新playground。選擇Blank模板并命名您的playgroundCustomOperators。刪除所有默認(rèn)代碼,以便您可以從空白平板開始。

將以下代碼添加到您的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類型,其中三個屬性符合兩個協(xié)議。 CustomStringConvertible協(xié)議和description計算屬性允許您打印Vector的友好字符串表示。

playground的底部,添加以下行:

let vectorA: Vector = [1, 3, 2]
let vectorB = [-2, 5, 1] as Vector

你剛剛用簡單的數(shù)組創(chuàng)建了兩個向量Vectors,沒有初始化器!那是怎么發(fā)生的?

ExpressibleByArrayLiteral協(xié)議提供無摩擦的接口來初始化Vector。該協(xié)議需要一個具有可變參數(shù)的不可用初始化程序:init(arrayLiteral:Int ...)

可變參數(shù)arrayLiteral允許您傳入由逗號分隔的無限數(shù)量的值。例如,您可以創(chuàng)建Vector,例如Vector(arrayLiteral:0)Vector(arrayLiteral:5,4,3)。

該協(xié)議進(jìn)一步方便,并允許您直接使用數(shù)組進(jìn)行初始化,只要您明確定義類型,這是您為vectorAvectorB所做的。

這種方法的唯一警告是你必須接受任何長度的數(shù)組。如果您將此代碼放入應(yīng)用程序中,請記住,如果傳入長度不是三的數(shù)組,它將會崩潰。如果您嘗試初始化少于或多于三個值的Vector,則初始化程序頂部的斷言assert將在開發(fā)和內(nèi)部測試期間在控制臺中提醒您。

單獨的矢量Vectors很好,但如果你能用它們做事情會更好。正如你在小學(xué)時所做的那樣,你將從加法開始你的學(xué)習(xí)之旅。


Overloading the Addition Operator

運算符重載的一個簡單示例是加法運算符。 如果您將它與兩個數(shù)字一起使用,則會發(fā)生以下情況:

1 + 1 // 2

但是,如果對字符串使用相同的加法運算符,則它具有完全不同的行為:

"1" + "1" // "11"

當(dāng)+與兩個整數(shù)一起使用時,它會以算術(shù)形式添加它們。 但是當(dāng)它與兩個字符串一起使用時,它會將它們連接起來。

為了使運算符重載,您必須實現(xiàn)一個名稱為運算符符號的函數(shù)。

注意:您可以將重載函數(shù)定義為類型的成員,這是您將在本教程中執(zhí)行的操作。 這樣做時,必須將其聲明為靜態(tài)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
    ]
  }
}

此函數(shù)將兩個向量作為參數(shù),并將它們的和作為新向量返回。 要做矢量加法,只需相加其各個組件即可。

要測試此功能,請將以下內(nèi)容添加到playground的底部:

vectorA + vectorB // (-1, 8, 3)

您可以在playground的右側(cè)邊欄中看到合成矢量。

1. Other Types of Operators

加法運算符是所謂的中綴infix運算符,意味著它在兩個不同的值之間使用。 還有其他類型的運算符:

  • infix:在兩個值之間使用,例如加法運算符(例如,1 + 1
  • prefix:在值之前添加,如負(fù)號運算符(例如 -3)。
  • postfix:在一個值之后添加,比如force-unwrap運算符(例如,mayBeNil!
  • ternary:在三個值之間插入兩個符號。 在Swift中,不支持用戶定義的三元運算符,只有一個內(nèi)置的三元運算符,您可以在 Apple’s documentation中閱讀。

您想要重載的下一個運算符是負(fù)號符號,它將更改Vector的每個組件的符號。 例如,如果將它應(yīng)用于vectorA,即(1,3,2),則返回(-1,-3,-2)。

在擴展名內(nèi)的上一個靜態(tài)static函數(shù)下面添加此代碼:

static prefix func - (vector: Vector) -> Vector {
  return [-vector.x, -vector.y, -vector.z]
}

假設(shè)運算符是中綴infix,因此如果您希望運算符是不同的類型,則需要在函數(shù)聲明中指定運算符類型。 負(fù)號運算符不是中綴,因此您將前綴prefix修飾符添加到函數(shù)聲明中。

playground的底部,添加以下行:

-vectorA // (-1, -3, -2)

在側(cè)欄中檢查結(jié)果是否正確。

接下來是減法,留給你自己實現(xiàn)。 完成后,請檢查以確保您的代碼與我的代碼類似。 提示:減法與添加負(fù)號相同。

試一試,如果您需要幫助,請查看下面的解決方案!

static func - (left: Vector, right: Vector) -> Vector {
  return left + -right
}

通過將此代碼添加到playground的底部來測試您的新運算符:

vectorA - vectorB // (3, -2, 1)

2. Mixed Parameters? No Problem!

您還可以通過標(biāo)量乘法將向量乘以數(shù)字。 要將兩個向量相乘,可以將每個分量相乘。 你接下來要實現(xiàn)這個。

您需要考慮的一件事是參數(shù)的順序。 當(dāng)您實施加法時,順序無關(guān)緊要,因為兩個參數(shù)都是向量。

對于標(biāo)量乘法,您需要考慮Int * VectorVector * Int。 如果您只實現(xiàn)其中一種情況,Swift編譯器將不會自動知道您希望它以其他順序工作。

要實現(xiàn)標(biāo)量乘法,請在剛剛添加的減法函數(shù)下添加以下兩個函數(shù):

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
}

為避免多次寫入相同的代碼,第二個函數(shù)只是將其參數(shù)轉(zhuǎn)發(fā)給第一個。

在數(shù)學(xué)中,向量有另一個有趣的操作,稱為cross-product。 cross-product的原理超出了本教程的范圍,但您可以在Cross product Wikipedia page頁面上了解有關(guān)它們的更多信息。

由于在大多數(shù)情況下不鼓勵使用自定義符號(誰想在編碼時打開表情符號菜單?),重復(fù)使用星號和cross-product運算符會非常方便。

與標(biāo)量乘法不同,Cross-products將兩個向量作為參數(shù)并返回一個新向量。

添加以下代碼以在剛剛添加的乘法函數(shù)之后添加cross-product實現(xiàn):

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
  ]
}

現(xiàn)在,將以下計算添加到playground的底部,同時利用乘法和cross-product運算符:

vectorA * 2 * vectorB // (-14, -10, 22)

此代碼找到vectorA2的標(biāo)量倍數(shù),然后找到該向量與vectorB的交叉乘積。 請注意,星號運算符始終從左向右,因此前面的代碼與使用括號分組操作相同,如(vectorA * 2)* vectorB。

3. Protocol Operators

一些運算符是協(xié)議的成員。 例如,符合Equatable的類型必須實現(xiàn)==運算符。 類似地,符合Comparable的類型必須至少實現(xiàn)<==,因為Comparable繼承自Equatable。 Comparable類型也可以選擇實現(xiàn)>> =<=,但這些運算符具有默認(rèn)實現(xiàn)。

對于Vector,Comparable并沒有太多意義,但Equatable卻很重要,因為如果它們的組件全部相等,則兩個向量相等。 接下來你將實現(xiàn)Equatable。

要符合協(xié)議,請在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

此行按預(yù)期返回false,因為vectorA具有與vectorB不同的組件。

符合Equatable不僅能夠檢查這些類型的相等性。您還可以獲取矢量數(shù)組的contains(_:)方法!


Creating Custom Operators

還記得我是怎么說通常不鼓勵使用自定義符號嗎?與往常一樣,該規(guī)則也有例外。

關(guān)于自定義符號的一個好的經(jīng)驗法則是,只有在滿足以下條件時才應(yīng)使用它們:

  • 它們的含義是眾所周知的,或者對閱讀代碼的人有意義。
  • 它們很容易在鍵盤上打字。

您將實現(xiàn)的最后一個運算符匹配這兩個條件。矢量點積產(chǎn)生兩個向量并返回單個標(biāo)量數(shù)。您的運算符會將向量中的每個值乘以另一個向量中的對應(yīng)值,然后將所有這些乘積相加。

點積的符號為?,您可以使用鍵盤上的Option-8輕松鍵入。

您可能會想,“我可以在本教程中對其他所有操作符執(zhí)行相同的操作,對吧?”

不幸的是,你還不能那樣做。在其他情況下,您正在重載已存在的運算符。對于新的自定義運算符,您需要首先創(chuàng)建運算符。

直接在Vector實現(xiàn)下面,但在CustomStringConvertible一致性擴展之上,添加以下聲明:

infix operator ?: AdditionPrecedence

這將?定義為必須放在兩個其他值之間的運算符,并且與加法運算符+具有相同的優(yōu)先級。 暫時忽略優(yōu)先級別。

既然已經(jīng)注冊了此運算符,請在運算符擴展的末尾添加其實現(xiàn),緊接在乘法和cross-product運算符*的實現(xiàn)之下:

static func ? (left: Vector, right: Vector) -> Int {
  return left.x * right.x + left.y * right.y + left.z * right.z
}

將以下代碼添加到playground的底部以進(jìn)行測試:

vectorA ? vectorB // 15

到目前為止,一切看起來都不錯......或者是嗎? 在playground的底部嘗試以下代碼:

vectorA ? vectorB + vectorA // Error!

Xcode對你不滿意。 但為什么?

現(xiàn)在,?+具有相同的優(yōu)先級,因此編譯器從左到右解析表達(dá)式。 編譯器將您的代碼解釋為:

(vectorA ? vectorB) + vectorA

此表達(dá)式歸結(jié)為Int + Vector,您尚未實現(xiàn)并且不打算實現(xiàn)。 你能做些什么來解決這個問題?


Precedence Groups

Swift中的所有運算符都屬于一個優(yōu)先級組(precedence group),它描述了運算符的計算順序。 還記得學(xué)習(xí)小學(xué)數(shù)學(xué)中的操作順序嗎? 這基本上就是你在這里所要處理的。

在Swift標(biāo)準(zhǔn)庫中,優(yōu)先級順序如下:

以下是關(guān)于這些運算符的一些注釋,因為您之前可能沒有看到它們:

  • 1) 按位移位運算符<<>>用于二進(jìn)制計算。
  • 2) 您使用轉(zhuǎn)換運算符,isas來確定或更改值的類型。
  • 3) nil合并運算符??有助于為可選值提供回退值。
  • 4) 如果您的自定義運算符未指定優(yōu)先級,則會自動分配DefaultPrecedence。
  • 5) 三元運算符,? :,類似于if-else語句。
  • 6) 對于=的衍生,AssignmentPrecedence在其他所有內(nèi)容之后進(jìn)行評估,無論如何。

編譯器解析具有左關(guān)聯(lián)性的類型,以便v1 + v2 + v3 ==(v1 + v2)+ v3。 對于右關(guān)聯(lián)性結(jié)果也是正確的。

操作符按它們在表中出現(xiàn)的順序進(jìn)行解析。 嘗試使用括號重寫以下代碼:

v1 + v2 * v3 / v4 * v5 == v6 - v7 / v8

當(dāng)您準(zhǔn)備好數(shù)學(xué)知識時,請查看下面的解決方案。

(v1 + (((v2 * v3) / v4) * v5)) == (v6 - (v7 / v8))

在大多數(shù)情況下,您需要添加括號以使代碼更易于閱讀。 無論哪種方式,理解編譯器評估運算符的順序都很有用。

1. Dot Product Precedence

您的新dot-product并不適合任何這些類別。 它必須少于加法(如前所述),但它是否真的適合CastingPrecedenceRangeFormationPrecedence?

相反,您將為您的點積運算符創(chuàng)建自己的優(yōu)先級組。

用以下內(nèi)容替換?運算符的原始聲明:

precedencegroup DotProductPrecedence {
  lowerThan: AdditionPrecedence
  associativity: left
}

infix operator ?: DotProductPrecedence

在這里,您創(chuàng)建一個新的優(yōu)先級組并將其命名為DotProductPrecedence。 您將它放在低于AdditionPrecedence的位置,因為您希望加法優(yōu)先。 你也可以將它設(shè)為左關(guān)聯(lián),因為你想要從左到右進(jìn)行評估,就像你在加法和乘法中一樣。 然后,將此新優(yōu)先級組分配給?運算符。

注意:除了lowerThan之外,您還可以在DotProductPrecedence中指定higherThan。 如果您在單個項目中有多個自定義優(yōu)先級組,這一點就變得很重要。

您的舊代碼行現(xiàn)在運行并按預(yù)期返回:

vectorA ? vectorB + vectorA // 29

恭喜 - 您已經(jīng)掌握了自定義操作符!

此時,您知道如何根據(jù)需要重載Swift操作符。 在本教程中,您專注于在數(shù)學(xué)上下文中使用運算符。 在實踐中,您將找到更多使用運算符的方法。

ReactiveSwift ReactiveSwift framework 框架中可以看到自定義操作符使用的一個很好的演示。 一個例子是<~,這是反應(yīng)式編程中的一個重要函數(shù)。 以下是此運算符的使用示例:

let (signal, _) = Signal<Int, Never>.pipe()
let property = MutableProperty(0)
property.producer.startWithValues {
  print("Property received \($0)")
}

property <~ signal

Cartography 是另一個大量使用運算符重載的框架。 此AutoLayout工具重載相等和比較運算符,以使NSLayoutConstraint創(chuàng)建更簡單:

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。

有了這些新的靈感來源,您可以走出世界,通過運算符重載使代碼更簡單。不過還是要小心使用自定義操作符!

下面看下相關(guān)整體代碼

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

后記

本篇主要講述了重載自定義運算符,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容