理解 Swift 中的元型別:.Type 與 .self

沒故事的卓同學發表於2019-03-04

元型別

元型別就是型別的型別。
比如我們說 5 是 Int 型別,此時 5 是 Int 型別的一個值。但是如果我問 Int 型別佔用多少記憶體空間,這個時候與具體某個值無關,而和型別的資訊相關。如果要寫一個函式,返回一個型別的例項記憶體空間大小。那麼這個時候的引數是一個型別資料,這個型別資料可以是直接說明的比如是 Int 型別,也可以從一個值身上取,比如 5 這個值的型別。這裡的型別資料,就是一個型別的型別,術語表述為元型別:metaType。

.Type 與 .self

Swift 中的元型別用 .Type 表示。比如 Int.Type 就是 Int 的元型別。
型別與值有著不同的形式,就像 Int 與 5 的關係。元型別也是類似,.Type 是型別,型別的 .self 是元型別的值。

let intMetatype: Int.Type = Int.self
複製程式碼

可能大家平時對元型別使用的比較少,加上這兩個形式有一些接近,一個元型別只有一個對應的值,所以使用的時候常常寫錯:

 types.append(Int.Type)
 types.append(Int.self)
複製程式碼

如果分清了 Int.Type 是型別的名稱,不是值就不會再弄錯了。因為你肯定不會這麼寫:

 numbers.append(Int)
複製程式碼

AnyClass

獲得元型別後可以訪問靜態變數和靜態方法。其實我們經常使用元型別,只是有時 Xcode 幫我們隱藏了這些細節。比如我們經常用的 tableView 的一個方法:

func register(AnyClass?, forCellReuseIdentifier: String)

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
複製程式碼

這裡的 AnyClass 其實就是一個元型別:

typealias AnyClass = AnyObject.Type
複製程式碼

通過上面的定義我們可以知道,AnyClass 就是一個任意型別元型別的別名。
當我們訪問靜態變數的時候其實也是通過元型別的訪問的,只是 Xcode 幫我們省略了 .self。下面兩個寫法是等價的。如果可以不引起歧義,我想沒人會願意多寫一個 self。

Int.max
Int.self.max
複製程式碼

type(of:) vs .self

前面提到通過 type(of:).self都可以獲得元型別的值。那麼這兩種方式的區別是什麼呢?

let instanceMetaType: String.Type = type(of: "string")
let staicMetaType: String.Type = String.self
複製程式碼

.self 取到的是靜態的元型別,宣告的時候是什麼型別就是什麼型別。type(of:) 取的是執行時候的元型別,也就是這個例項 的型別。

let myNum: Any = 1 
type(of: myNum) // Int.type
複製程式碼

Protocol

很多人對 Protocol 的元型別容易理解錯。Protocol 自身不是一個型別,只有當一個物件實現了 protocol 後才有了型別物件。所以 Protocol.self 不等於 Protocol.Type。如果你寫下面的程式碼會得到一個錯誤:

protocol MyProtocol { }
let metatype: MyProtocol.Type = MyProtocol.self
複製程式碼

正確的理解是 MyProtocol.Type 也是一個有效的元型別,那麼就需要是一個可承載的型別的元型別。所以改成這樣就可以了:

struct MyType: MyProtocol { }
let metatype: MyProtocol.Type = MyType.self 
複製程式碼

那麼 Protocol.self 是什麼型別呢?為了滿足你的好奇心蘋果為你造了一個型別:

let protMetatype: MyProtocol.Protocol = MyProtocol.self
複製程式碼

一個實戰

為了讓大家能夠熟悉元型別的使用我舉一個例子。
假設我們有兩個 Cell 類,想要一個工廠方法可以根據型別初始化物件。下面是兩個 Cell 類:

protocol ContentCell { }

class IntCell: UIView, ContentCell {
    required init(value: Int) {
        super.init(frame: CGRect.zero)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class StringCell: UIView, ContentCell {
    required init(value: String) {
        super.init(frame: CGRect.zero)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
複製程式碼

工廠方法的實現是這樣的:

func createCell(type: ContentCell.Type) -> ContentCell? {
    if let intCell = type as? IntCell.Type {
        return intCell.init(value: 5)
    } else if let stringCell = type as? StringCell.Type {
        return stringCell.init(value: "xx")
    }
    return nil
}

let intCell = createCell(type: IntCell.self)
複製程式碼

當然我們也可以使用型別推斷,再結合泛型來使用:

func createCell<T: ContentCell>() -> T? {
    if let intCell = T.self as? IntCell.Type {
        return intCell.init(value: 5) as? T
    } else if let stringCell = T.self as? StringCell.Type {
        return stringCell.init(value: "xx") as? T
    }
    return nil
}

// 現在就根據返回型別推斷需要使用的元型別
let stringCell: StringCell? = createCell()
複製程式碼

Reusable 中的 tableView 的 dequeue 採用了類似的實現:

func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath, cellType: T.Type = T.self) -> T
    where T: Reusable {
      guard let cell = self.dequeueReusableCell(withIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
        fatalError("Failed to dequeue a cell")
      }
      return cell
  }
複製程式碼

dequeue 的時候就可以根據目標型別推斷,不需要再額外宣告元型別:

 class MyCustomCell: UITableViewCell, Reusable 
tableView.register(cellType: MyCustomCell.self)

let cell: MyCustomCell = tableView.dequeueReusableCell(for: indexPath)
複製程式碼

Reference


相關文章