【譯】使用Swift自定義運算符重載

原文連接: www.raywenderlich.com/4018226-ove…html

swift重載自定義運算符

在本Swift教程中,您將學習如何建立自定義運算符,重載現有運算符以及設置運算符優先級。git

運算符是任何編程語言的核心構建模塊。 你能想象編程而不使用+或=嗎? 運算符很是基礎,大多數語言都將它們做爲編譯器(或解釋器)的一部分進行處理。 可是Swift編譯器並不對大多數操做符進行硬編碼,而是爲庫提供了建立的操做符的方法。 它將工做留給了Swift標準庫,用來提供您指望的全部常見操做符。 這種微妙的差別爲巨大的定製潛力打開了大門。github

Swift運算符特別強大,由於您能夠經過兩種方式更改它們以知足您的需求:爲現有運算符分配新功能(稱爲運算符重載),以及建立新的自定義運算符。 在本教程中,您將使用一個簡單的Vector結構並構建本身的一組運算符,以組合不一樣的向量。編程

入門

打開Xcode,而後經過File▶New▶Playground建立一個Playground。 選擇空白模板並命名您的PlaygroundCustomOperators。 刪除全部默認代碼,以便您能夠從空白平板開始。swift

將如下代碼添加到您的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
複製代碼

在沒有初始化器的狀況下,你剛剛用了兩個Array來建立兩個Vector!那是如何發生的呢? ExpressibleByArrayLiteral協議提供了一個平滑的接口來初始化Vector。該協議須要一個具備可變參數的不可用初始化程序:init(arrayLiteral:Int ...)ide

可變參數arrayLiteral容許您傳入由逗號分隔的無限數量的值。例如,您能夠建立Vector,像這樣Vector(arrayLiteral:0)Vector(arrayLiteral:5,4,3)函數

該協議進一步方便,並容許您直接使用數組進行初始化,只要您明肯定義類型,這是您爲vectorAvectorB所作的。

這種方法的惟一警告是你必須接受任何長度的數組。若是您將此代碼放入應用程序中,請記住,若是傳入長度不是三的數組,它將會崩潰。若是您嘗試初始化少於或多於三個值的Vector,則初始化程序頂部的斷言將在開發和內部測試期間在控制檯中提醒您。

單獨的矢量很好,但若是你能用它們作事情會更好。正如你在學校時所作的那樣,你將開始你學習 加法 之旅。

重載加法運算符

運算符重載的一個簡單示例是加法運算符。 若是您將它與兩個數字一塊兒使用,則會發生如下狀況:

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的右側邊欄中看到向量相加的結果.

其餘類型的運算符

加法運算符是所謂的infx運算符,意味着它在兩個不一樣的值之間使用。 還有其餘類型的運算符:

  • infix:在兩個值之間使用,例如加法運算符(例如,1 + 1)
  • prefix:在值以前添加,如負號運算符(例如-3)。
  • postfix:在值以後添加,好比強制解包運算符(例如,mayBeNil!)
  • ternary:在三個值之間插入兩個符號。 在Swift中,不支持用戶自定義的三元運算符,只有一個內置的三元運算符,您能夠在Apple的文檔中閱讀。 您要重載的下一個運算符是負號,它將更改Vector的每一個變量的符號。 例如,若是將它應用於vectorA,即(1,3,2),則返回(-1,-3,-2)

在這個extension中的前面一個static函數下面添加以下代碼:

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

運算符默認是infix類型,若是您但願運算符是其餘類型,則須要在函數聲明中指定運算符類型。 負號運算符不是infix類型,所以您將prefix修飾符添加到函數聲明中。

playground的底部,添加如下代碼:

-vectorA // (-1, -3, -2)
複製代碼

在側欄中檢查結果是否正確。

接下來是減法,我將留給你實現本身。 完成後,請檢查以確保您的代碼與個人代碼相似。 提示:減法與加上一個負數相同。

試一試,若是您須要幫助,請查看下面的解決方案!

static func - (left: Vector, right: Vector) -> Vector {
  return left + -right
}
複製代碼

經過添加已下代碼到playground底部,測試你的新操做符的輸出:

vectorA - vectorB // (3, -2, 1)
複製代碼

不一樣類型的參數? 沒問題!

您還能夠經過標量乘法將向量乘以數字。 要將向量乘以2,能夠將每一個份量乘以2。 你接下來就要實現這個.

您須要考慮的一件事是參數的順序。 當你實現加法的時候,順序可有可無,由於兩個參數都是向量。

對於標量乘法,您須要考慮Int * VectorVector * 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 Wikipedia頁面上了解有關它們的更多信息。

因爲在大多數狀況下不鼓勵使用自定義符號(誰想在編碼時打開表情符號菜單?),重複使用星號進行叉乘操做會很是方便。

與標量乘法不一樣,叉乘將兩個向量做爲參數並返回一個新向量。

添加如下代碼以在剛剛添加的乘法函數以後實現叉乘:

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的底部,同時利用乘法和叉乘運算符:

vectorA * 2 * vectorB // (-14, -10, 22)
複製代碼

此代碼找到vectorA和2的標量乘法,而後找到該向量與vectorB的交叉乘積。 請注意,星號運算符始終從左向右,所以前面的代碼與使用括號分組操做相同,如(vectorA * 2)* vectorB

運算符協議

某些協議須要實現一些運算符符。 例如,符合Equatable的類型必須實現==運算符。 相似的,符合Comparable的類型必須至少實現<==,由於Comparable繼承自EquatableComparable類型也能夠選擇實現>,>=和<=,但這些運算符具備默認實現。

對於VectorComparable並無太多意義,但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
  }
}
複製代碼

添加如下代碼在palyground的底部,而且測試他的輸出

vectorA == vectorB // false
複製代碼

上述代碼返回false由於vectorAvectorB有不一樣的組件. 實現Equatable協議,不只可以檢查這些類型的相等性。 您還得到了自由訪問contains(_:)的矢量數組!

建立自定義運算符

請記住咱們一般不鼓勵使用自定義運算符. 固然該規則也有例外。

關於自定義符號的一個好的經驗法則是,只有在知足如下條件時才應使用它們:

  • 它們的含義是衆所周知的,或者對閱讀代碼的人有意義。
  • 它們很容易在鍵盤上打出來. 最後一個你要實現的運算符符合這兩個條件。 矢量點積運算兩個向量並返回單個標量數。 您的運算符會將向量中的每一個值乘以另外一個向量中的對應值,而後將全部這些乘積相加。 點積的符號爲,您可使用鍵盤上的Option-8輕鬆鍵入。 您可能會想,「我能夠在本教程中對其餘全部操做符執行相同的操做,對吧?」

不幸的是,你還不能那樣作。 在其餘狀況下,您重載已存在的運算符。 對於新的自定義運算符,您須要首先建立運算符。

直接在Vector實現最下面,但在CustomStringConvertible擴展之上,添加如下聲明:

infix operator •: AdditionPrecedence
複製代碼

定義爲放在兩個其餘值之間的運算符,而且與加法運算符+具備相同的優先級。 暫時忽略優先級,由於你會在學到它。

既然已經註冊了此運算符,請在運算符擴展的末尾添加其實現,緊接在運算符*的實現的下面:

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!
複製代碼

如今,+具備相同的優先級,所以編譯器從左到右解析表達式。 編譯器將您的代碼解釋爲:

(vectorA • vectorB) + vectorA
複製代碼

此表達式歸結爲Int + Vector,您還沒有實現而且不打算實現。 你要如何來解決這個問題?

優先級組(Precedence Groups)

Swift中的全部運算符都屬於一個優先級組,它描述了運算符的運算順序。 還記得學習小學數學中的操做順序嗎? 這基本上就是你在這裏所要處理的。 在Swift標準庫中,優先順序以下:

)

如下是關於這些運算符的一些註釋,您以前可能歷來沒有看到過它們:

  1. 位移運算符<<>>用於二進制計算。
  2. 您使用轉換運算符isas來肯定或更改值的類型。
  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))
複製代碼

在大多數狀況下,您須要添加括號以使代碼更易於閱讀。 不管哪一種方式,理解編譯器評估運算符的順序都頗有用。

Dot運算符優先級

你新定義的dot-product不適合任何這些類別。 它的優先級必須小於加號運算符(如前所述),但它是否真的適合CastingPrecedenceRangeFormationPrecedence

取而代之,您將爲您的點積運算符建立本身的優先級組。

用如下內容替換運算符的原始聲明:

precedencegroup DotProductPrecedence {
  lowerThan: AdditionPrecedence
  associativity: left
}

infix operator •: DotProductPrecedence
複製代碼

在這裏,您建立一個新的優先級組並將其命名爲DotProductPrecedence。 您將它放在低於AdditionPrecedence的位置,由於您但願加號運算符優先。 你也能夠將它設爲左關聯,由於你想要從左到右進行評估,就像加法和乘法中同樣。 而後,將此新優先級組分配給運算符。

注意:除了lowerThan以外,您還能夠在DotProductPrecedence中指定higherThan。 若是您在單個項目中有多個自定義優先級組,這一點就變得很重要。 你以前寫的代碼返回了你但願的結果:

vectorA • vectorB + vectorA // 29
複製代碼

恭喜💐--你掌握了自定義運算符

接下來你能夠作些什麼

你能夠閱讀本教程完整的代碼,代碼已經在最後面給出. 此時,您知道如何根據須要定義Swift操做符。 在本教程中,您專一於在數學領域中使用運算符。 在實踐中,您將找到更多使用運算符的方法。

ReactiveSwift框架中能夠看到自定義運算符使用的一個很好的演示。 一個例子是<~用來綁定,這是響應式編程中的一個重要功能。 如下是此運算符的使用示例:

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
}
複製代碼

此外,您始終能夠參考官方文檔 custom operator documentation

有了這些新的靈感來源,您能夠走出世界,經過運算符重載使代碼更簡單。 當心不要對自定義運算符太迷戀!:]

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

複製代碼
相關文章
相關標籤/搜索