Swift5之我對@propertyWrapper的思考二

語言: CN / TW / HK

前言

書接上篇《Swift5之我對@propertyWrapper的思考一》尾部所拋出的一個問題:如何解決@propertyWrapper帶來的負向影響?

我目前的一個答案是:使用其他方案代替@propertyWrapper實現代碼複用以及和其類似的語法糖效果。

那麼該如何做呢?我的探索答案是:自定義數據類型+字面量協議+“重載強制類型轉換操作符”。

下面讓我結合上篇中《@propertyWrapper方案示例》,對我的探索答案一一展開闡述。

// @propertyWrapper方案示例  // ======= 實現的 propertyWrapper TwelveOrLess ========  @propertyWrapper  struct TwelveOrLess {      var wrappedValue: Int {          didSet {              wrappedValue = min(wrappedValue, 12)          }      }  ​      init(wrappedValue: Int) {          self.wrappedValue = min(wrappedValue, 12)      }  }  ​  // ======= 測試用例 ========  struct NormalRectangle {      var height: Int      var width: Int  }  ​  struct ConstrainedRectangle {      @TwelveOrLess var height: Int      @TwelveOrLess var width: Int  }  ​  func test_TwelveOrLess() {      let normalRectangle = NormalRectangle(height: 24, width: 24)      print("(type(of: normalRectangle.height))") // Prints "Int"  ​      let constrainedRectangle = ConstrainedRectangle(height: 24, width: 24)      print("(type(of: constrainedRectangle.height))") // Prints "TwelveOrLess"  }

自定義數據類型

萬變不離其宗,實現代碼複用的一種方式,就是自定義數據類型,封裝相同邏輯。而@propertyWrapper的做法也是讓開發者按照約定定義一種數據類型,來實現代碼複用。下面,就讓我們對照例子,先實現一個自定義的數據類型TwelveOrLess和對應的測試用例吧:

// ======= 實現的自定義數據類型 TwelveOrLess ========  struct TwelveOrLess {      var wrappedValue: Int {          didSet {              wrappedValue = min(wrappedValue, 12)          }      }  ​      init(wrappedValue: Int) {          self.wrappedValue = min(wrappedValue, 12)      }  }  ​  // ======= 測試用例 ========  struct ConstrainedRectangle {      var height: TwelveOrLess      var width: TwelveOrLess  }  ​  func test_TwelveOrLess() {      // 報錯:Cannot convert value of type 'Int' to expected argument type 'TwelveOrLess'      let constrainedRectangle = ConstrainedRectangle(height: 24, width: 24)      print("(type(of: constrainedRectangle.height))") // Prints "TwelveOrLess"  }

與上篇《@propertyWrapper方案示例》對比,ConstrainedRectangle中的屬性heightwidth在編譯時和運行時都是明確的數據類型:TwelveOrLess——這種顯性的、前後一致的表達和表現,能有效幫助讓開發者特別是初級開發者避免遇到上篇示例展示的那樣疑惑:屬性heightwidth在運行時類型為何產生了突變。

不過,這時候編譯時,我們會遇到報錯:Cannot convert value of type 'Int' to expected argument type 'TwelveOrLess'

因為在編譯時,屬性heightwidthTwelveOrLess類型,因此無法接受Int類型的字面量值進行實例初始化。而在上篇《@propertyWrapper方案示例》中,可以使用Int類型的字面量值進行實例初始化,為了對齊《@propertyWrapper方案》的開發體驗,我們應該怎麼做呢?那就是使用字面量協議。

字面量協議

字面量協議是Swift 提供給自定義數據類型實現和基礎數據類型一樣的通過字面量進行實例初始化的能力。此處不作過多展開,有興趣的童鞋可看我的這篇介紹文章:《Swift的字面量類型(Literal Type)和字面量協議(Literal Protocol)》

為了解決上述報錯以及對齊《@propertyWrapper方案》的開發體驗,只需要讓TwelveOrLess實現ExpressibleByIntegerLiteral字面量協議即可:

// ======= 實現的自定義數據類型 TwelveOrLess ========  struct TwelveOrLess {      var wrappedValue: Int {          didSet {              wrappedValue = min(wrappedValue, 12)          }      }  ​      init(wrappedValue: Int) {          self.wrappedValue = min(wrappedValue, 12)      }  }  ​  // 實現ExpressibleByIntegerLiteral字面量協議  extension TwelveOrLess: ExpressibleByIntegerLiteral {      typealias IntegerLiteralType = Int  ​      public init(integerLiteral value: IntegerLiteralType) {          self.init(wrappedValue: value)      }  }  ​  // ======= 測試用例 ========  struct ConstrainedRectangle {      var height: TwelveOrLess      var width: TwelveOrLess  }  ​  func test_TwelveOrLess() {      let constrainedRectangle = ConstrainedRectangle(height: 24, width: 24)      print("(type(of: constrainedRectangle.height))") // Prints "TwelveOrLess"  }

目前看起來開發體驗和《@propertyWrapper方案》差不多了,但是還是有短板,那就是賦值操作時,會報錯:Cannot convert value of type 'TwelveOrLess' to specified type 'Int'

func test_TwelveOrLess() {      // 報錯:Cannot convert value of type 'TwelveOrLess' to specified type 'Int'      var temp_h: Int = constrainedRectangle.height      print("temp_h: (temp_h)")  }

這時候該怎麼解決呢?那就是“重載強制類型轉換操作符”,在其賦值給其他類型時可進行強制轉換。

“重載強制類型轉換操作符”

然而,很不幸地,目前Swift不支持重載強制類型轉換操作符!!!

對此,這個探索方案卡在此處。但是,這裏將會通過C++展示這個探索方案,幫助讀者一窺全貌:

#include <iostream>  ​  class TwelveOrLess {    public:    int wrappedValue;    TwelveOrLess() {   }        // 重載默認賦值運算符 =(相當於Swift的實現“字面量協議”)    TwelveOrLess& operator = (const int& right) {      wrappedValue = std::min(12,right);      return *this;   }      // 重載強制類型轉換操作符(type conversion operator)    operator int() {      return wrappedValue;   }  };  ​  int main() {    // 測試字面量初始化實例    TwelveOrLess t;    t = 24;    std::cout << "t: " << t.wrappedValue << std::endl; // Prints "12"    // 測試轉換為基礎類型:重載強制類型轉換操作符後,賦值給基礎類型時,會進行自動強轉。    int i = 24;    i = t; //等價於 i = t. operator int()    std::cout << "i: " << i << std::endl; // Prints "12"  }

參照C++的做法,假若Swift支持“重載強制類型轉換操作符”,那麼,就可以實現TwelveOrLess賦值給Int類型變量的開發體驗。然而目前,Swift卻不支持。那還有沒其他解決方案呢?一種解決方案就是實現一個和賦值運算符(=)相似自定義操作符。

實現自定義操作符

Swift目前是支持開發者實現自定義操作符,因此接着上述思路,我們可給TwelveOrLess實現一個和賦值運算符(=)相似自定義操作符<<,然後通過這個操作符完成賦值操作:

// ======= 實現的自定義數據類型 TwelveOrLess ========  struct TwelveOrLess {      var wrappedValue: Int {          didSet {              wrappedValue = min(wrappedValue, 12)          }      }  ​      init(wrappedValue: Int) {          self.wrappedValue = min(wrappedValue, 12)      }  }  ​  // 實現ExpressibleByIntegerLiteral字面量協議  extension TwelveOrLess: ExpressibleByIntegerLiteral {      typealias IntegerLiteralType = Int  ​      public init(integerLiteral value: IntegerLiteralType) {          self.init(wrappedValue: value)      }  }  ​  // 實現自定義操作符 <<  infix operator <<  extension TwelveOrLess {      static func <<(left: inout Int, right: TwelveOrLess) {          left = right.wrappedValue      }  }  ​  // ======= 測試用例 ========  struct ConstrainedRectangle {      var height: TwelveOrLess      var width: TwelveOrLess  }  ​  func test_TwelveOrLess() {      let constrainedRectangle = ConstrainedRectangle(height: 24, width: 24)      print("(type(of: constrainedRectangle.height))") // Prints "TwelveOrLess"          var temp_h: Int = 21      temp_h << constrainedRectangle.height      print("temp_h: (temp_h)") // Prints "12"  }

至此,在賦值操作方面算是接近了《@propertyWrapper方案》的開發體驗。

總結

目前探索出來的方案在代碼複用、字面量初始化方面都是對齊《@propertyWrapper方案》的開發體驗,並且保障了開發者的代碼感知在編譯時和運行時是一致的。但是,由於Swift暫不支持重載強制類型轉換操作符這點,導致在賦值操作方面稍差人意。若讀者有其他好的想法,歡迎評論交流。