Swift 枚舉(enum)詳解

Swift 枚舉(enum)詳解

[TOC]

本文將介紹Swift中枚舉的一些用法和其底層原理的一些探索牍帚,以及探索一下OC中的枚舉與Swift中枚舉互相調(diào)用和枚舉類型的內(nèi)存占用情況印机。

1. 枚舉

1.1 C中枚舉

首先我們來看看C語言中枚舉的寫法夯尽。這里我們以一周7天作為示例凡怎。

普通寫法:

enum Week {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

以上就是C語言中枚舉的常見寫法enum關(guān)鍵字盟榴,加上枚舉名稱郑口,大括號里面的不同的枚舉值使用逗號分隔開來竿屹。此時的枚舉值默認從0開始枫虏,依次是1妇穴,2爬虱,3……

自定義枚舉值:

如果我們不想使用默認的枚舉值,則可以這樣寫

enum Week {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

此時枚舉值就會從1開始依次向后排列腾它,你也可以給每個枚舉都定義不同的枚舉值跑筝,如果直接給TUE定義為2,而沒給MON定義瞒滴,則MON的枚舉值會是0曲梗。

枚舉變量的定義:

enum Week {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

enum Week week;

enum Week{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;

enum{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;

我們可以通過以上三種方法創(chuàng)建枚舉變量:

  1. 創(chuàng)建一個枚舉,然后聲明一個枚舉變量
  2. 創(chuàng)建一個枚舉并聲明一個枚舉變量
  3. 也可以省略枚舉名稱妓忍,直接聲明一個枚舉變量

1.2 Swift中枚舉

Swift中最常見的枚舉寫法:

enum Week{
    case MON
    case TUE
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}

Swift中也可以簡化為如下寫法:

enum Week{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

Swift中枚舉很強大虏两,我們可以創(chuàng)建一個枚舉值是String類型的enum,其實也不應(yīng)該說是枚舉值世剖,而是枚舉的RawValue

enum Week: String{
    case MON = "MON"
    case TUE = "TUE"
    case WED = "WED"
    case THU = "THU"
    case FRI = "FRI"
    case SAT = "SAT"
    case SUN = "SUN"
}

當(dāng)然定罢,如果我們不想寫后面的字符串,也可以簡寫成如下的形式:

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

定義枚舉變量:

var w: Week = .MON

枚舉變量的定義很簡單旁瘫,跟普通變量的定義沒什么差別祖凫。

枚舉的訪問:

我們可以訪問枚舉的變量和枚舉的rawValue
首先我們需要注意的是如果沒有聲明枚舉的類型,是沒有rawValue屬性可以訪問的境蜕。

image

一般情況下我們可以通過以下方式訪問枚舉:

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

print(Week.MON)
print(Week.MON.rawValue)

打印結(jié)果如下:

image

1.3 枚舉值和其RawValue的存儲

在上面枚舉的訪問中我們可以看到關(guān)于MON字符串的打印蝙场,既然可以打印出來,說明是存儲了相關(guān)的字符串的粱年,那么是怎么存儲的呢售滤?又是怎么獲取出來的呢?下面我們通過sil代碼進行分析(使用如下命令生成并打開sil代碼)

swiftc -emit-sil main.swift >> ./main.sil && open main.sil

為了方便分析我們將代碼修改為如下:

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
print(w)

執(zhí)行生成sil代碼的名后:

enum week : String {
  case MON
  case TUE
  case WED
  case SUN
  typealias RawValue = String
  init?(rawValue: String)
  var rawValue: String { get }
}

通過sil代碼中對枚舉的定義可以看到:

  1. Swift中一致的枚舉
  2. 取了一個別名台诗,也就是String類型是RawValue
  3. 添加了一個可選類型的init方法
  4. 一個計算屬性rawValue完箩,通過其get方法獲取枚舉的原始值

下面我們在main函數(shù)中看看:

image

關(guān)于w變量的初始化即分析注釋寫在了截圖中。

  1. 首先創(chuàng)建一個全局變量w拉队,并為變量w開辟內(nèi)存地址
  2. 將枚舉類型Week.MON存儲到%5
  3. 將枚舉Week的rawValue.getter函數(shù)存儲到%6
  4. 調(diào)用%6中存儲的函數(shù)弊知,%5作為參數(shù),返回值存儲到%7
  5. 將%7中獲取到額值存儲到%3粱快,至此變量w初始化完成

下面我們看看rawValuegetter方法:

image

我們可以看到在rawValuegetter方法中主要實現(xiàn)是:

  1. 通過接收到的枚舉值去匹配一個分支
  2. 在分支中構(gòu)建對于的String
  3. 返回上一步構(gòu)建的String

那么這個字符串是從哪里來的呢秩彤?根據(jù)匹配的分支中的方法名稱我們可以知道這是獲取一個內(nèi)置的字符串的字面量。其實就是從Mach-O文件的__TEXT.cstring中事哭。下面我們通過查看Mach-O來驗證漫雷。

image

所以說rawValue的值是通過調(diào)用枚舉的rawValue。getter函數(shù)鳍咱,從Mach-O對應(yīng)的地址中取出字符串并返回降盹。

那么枚舉值呢?其實在上面關(guān)于rawValue探索的時候就可以知道了谤辜,枚舉值在sil代碼中就是:#Week.MON!enumelt蓄坏,枚舉值和rawValue本質(zhì)上是不一樣的价捧,從下面的例子可以得到結(jié)論:

image

按照以上的寫法是會報編譯錯誤的。

1.4 枚舉.init

1.4.1 觸發(fā)方式

在上面的分析時我們知道枚舉會有一個init方法涡戳,那么這個方法是什么時候調(diào)用的呢结蟋?我們添加如下符號斷點:

image

添加如下代碼:

var w: Week = .MON
print(w.rawValue)

運行后并沒有觸發(fā)該符號斷點。

下面我們在添加如下代碼:

var w = Week(rawValue: "MON")
print(w)

運行后即可觸發(fā)符號斷點:

image

所以這里init方法是為枚舉通過rawValue初始化的時候調(diào)用的妹蔽。

1.4.2 init分析

首先我們來看看如下代碼的打印結(jié)果:

print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))

<!--打印結(jié)果-->
Optional(SwiftEnum.Week.MON)
nil

從打印結(jié)果中可以看到椎眯,第一個輸出的是可選值SwiftEnum.Week.MON挠将,第二個是nil胳岂,很顯然Hello不是我們的枚舉,那么這些是怎么實現(xiàn)的呢舔稀?我們再次查看sil代碼乳丰,此時我們可以直接看Week.init方法的實現(xiàn)。

Xnip2021-03-04_15-28-16 2

方法比較長内贮,在里面添加了相關(guān)的注釋和分析产园,折疊的代碼基本上是與其上面的代碼一致。現(xiàn)在總結(jié)如下:

  1. 首先開辟一塊內(nèi)存用于后續(xù)存儲構(gòu)建出來的枚舉
  2. 通過_allocateUninitializedArray函數(shù)創(chuàng)建一個元組夜郁,元組中包含
    1. 與枚舉個數(shù)大小一樣的數(shù)組什燕,用于存儲枚舉中的rawValue在本示例中是staticString
    2. 數(shù)組的首地址
  3. 開始一個一個的構(gòu)建枚舉rawValue存儲到數(shù)組中
  4. 通過_findStringSwitchCase函數(shù)查找處要構(gòu)建的枚舉在數(shù)組中的位置index
  5. 從0到count-1依次與index作比較
    1. 如果相等則構(gòu)建對于的枚舉
    2. 如果不相等則構(gòu)建一個Optional.none!enumelt的枚舉
  6. 將構(gòu)建的枚舉存儲到開辟的地址
  7. 最后返回構(gòu)建的枚舉

關(guān)于上面提到的兩個函數(shù)源碼可以Swift源碼中找到

_allocateUninitializedArray源碼:

@inlinable @inline(__always) @_semantics("array.uninitialized_intrinsic") public func _allocateUninitializedArray<Element>(_ builtinCount: Builtin.Word) -> (Swift.Array<Element>, Builtin.RawPointer) {
  let count = Int(builtinCount)
  if count > 0 {
    // Doing the actual buffer allocation outside of the array.uninitialized
    // semantics function enables stack propagation of the buffer.
    let bufferObject = Builtin.allocWithTailElems_1(
      _ContiguousArrayStorage<Element>.self, builtinCount, Element.self)

    let (array, ptr) = Array<Element>._adoptStorage(bufferObject, count: count)
    return (array, ptr._rawValue)
  }
  // For an empty array no buffer allocation is needed.
  let (array, ptr) = Array<Element>._allocateUninitialized(count)
  return (array, ptr._rawValue)
}

可以看到此處就是根據(jù)傳入的countBuiltin.Word初始化一個數(shù)組竞端,將其以元組的形式返回數(shù)組和數(shù)組首地址屎即。

_findStringSwitchCase源碼:

/// The compiler intrinsic which is called to lookup a string in a table
/// of static string case values.
@_semantics("findStringSwitchCase")
public // COMPILER_INTRINSIC
func _findStringSwitchCase(
  cases: [StaticString],
  string: String) -> Int {

  for (idx, s) in cases.enumerated() {
    if String(_builtinStringLiteral: s.utf8Start._rawValue,
              utf8CodeUnitCount: s._utf8CodeUnitCount,
              isASCII: s.isASCII._value) == string {
      return idx
    }
  }
  return -1
}

我們可以看到這里接收一個數(shù)組和要匹配的字符串,然后通過一個for循環(huán)匹配字符串事富,如果匹配到了則返回數(shù)組中對應(yīng)的index技俐,否則返回-1。

1.5 枚舉的遍歷

一般我們很少會對枚舉進行遍歷操作统台,在Swift中可以通過遵守CaseIterable協(xié)議來實現(xiàn)對枚舉的遍歷雕擂。

enum Week: String, CaseIterable{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 使用for循環(huán)遍歷
var allCase = Week.allCases
for c in allCase{
    print(c)
}

// 函數(shù)是編程遍歷
let allCase = Week.allCases.map({"\($0)"}).joined(separator: ", ")
print(allCase)

1.6 關(guān)聯(lián)值

Swift中如果想要表示復(fù)雜的含義,可以在枚舉中關(guān)聯(lián)更多的信息贱勃。下面我們舉個例子井赌,如果需要有一個形狀的枚舉,里面有圓形和矩形贵扰。圓形有半徑仇穗,矩形有長寬,那么這個枚舉就可以寫成如下代碼:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}
  • 其中括號中的radius以及widthheight就是關(guān)聯(lián)值
  • 如果沒枚舉中使用關(guān)聯(lián)值則枚舉就沒有rawValue屬性了拔鹰,因為關(guān)聯(lián)值是一組值仪缸,而rawValue是單個值,可以通過sil代碼驗證
image

sil代碼中我們并沒有發(fā)現(xiàn)init方法RawValue別名以及rawValueget方法列肢。

在這個枚舉中radius恰画、width宾茂、height這些都是自定義的標(biāo)簽,也可以不寫拴还,如下所示跨晴,但并不推薦這種方式,因為可讀性非常差

enum Shape{
    case circle(Double)
    case rectangle(Int, Int)
}

那么有關(guān)聯(lián)值的枚舉該如何初始化呢片林?其實也很簡單端盆,下面我們就來創(chuàng)建一下

var shape = Shape.circle(radius: 10.0)
shape = Shape.circle(radius: 15)
shape = Shape.rectangle(width: 10, height: 10)

2. 其他用法

2.1 模式匹配

2.1.1 簡單的模式匹配

顧明思議,模式匹配就是匹配每一個枚舉值费封,通常我們可以使用switch語句來進行模式匹配焕妙。如果使用switch進行模式匹配:

  1. 必須列舉當(dāng)前所有可能的情況,否則就會報編譯錯誤
  2. 如果不想匹配這么多case則可以使用defalut
  3. 在同一個case中可以列舉多種情況
enum Week{
    case MON
    case TUE
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}

var week = Week.MON

switch week {
    case .MON:
        print("周一")
    case .TUE:
        print("周二")
    case .WED:
        print("周三")
    case .SAT, .SUN:
        print("happy day")
    default : print("unknow day")
}

其實這個匹配也很簡單弓摘,我們通過查看sil代碼就可以知道:

image

2.1.2 關(guān)聯(lián)值枚舉的模式匹配

如果我們不關(guān)心關(guān)聯(lián)值焚鹊,關(guān)聯(lián)值枚舉的寫法與普通枚舉沒有什么區(qū)別:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}

let shape = Shape.circle(radius: 10.0)

switch shape {
    case .circle:
        print("the shape is circle")
    case .rectangle:
        print("the shape is rectangle")
}

但是我們使用關(guān)聯(lián)值枚舉,肯定是會關(guān)心關(guān)聯(lián)值的韧献,當(dāng)關(guān)心關(guān)聯(lián)值時其寫法如下:

switch shape{
    case let .circle(radius):
        print("circle radius: \(radius)")
    case .rectangle(let width, var height):
        height += 1
        print("rectangle width: \(width) height: \(height)")
}

可以發(fā)現(xiàn)末患,這里的每個case中都使用了let或者var,這里因為要使用關(guān)聯(lián)值锤窑,所以需要使用let聲明一下璧针。或者放在最前面渊啰,或者對每個需要使用的變量前都添加let探橱。如果使用var則可在當(dāng)前case中修改其修飾的關(guān)聯(lián)值。當(dāng)然你也可以不使用枚舉定義中的關(guān)聯(lián)值的名字虽抄,可以自定義走搁。

關(guān)于關(guān)聯(lián)值枚舉的模式匹配我們也可以看看sil代碼:

image

2.1.3 其他匹配

有時候在業(yè)務(wù)邏輯處理中,我們只是想匹配單個case迈窟,我們可以這樣寫:

if case let Shape.circle(radius) = shape {
    print("circle radius: \(radius)")
}

當(dāng)然如果我們只關(guān)心不同case的相同關(guān)聯(lián)值時就可以這樣寫:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case square(width: Double, width: Double)
}

let shape = Shape.rectangle(width: 20, height: 10)

switch shape {
    case let .rectangle(x, 10), let .square(x, 10):
        print(x)
    default: break
}

此時的打印是20私植,對于上面的例子,必須caserectanglesquare车酣,而且rectangle必須是10曲稼,square后面的width是10。

如果對于10的匹配不那么嚴(yán)格我們則可以使用通配符_

switch shape {
    case let .rectangle(x, _), let .square(x, _):
        print(x)
    default: break
}

注意: 以上命名必須一致湖员,比如都使用x贫悄,如果一個x一個y就不行了。

2.2 枚舉的嵌套

2.2.1 枚舉嵌套枚舉

顧名思義娘摔,枚舉嵌套枚舉就是在枚舉中還有枚舉窄坦,比如我們玩游戲時會有上下左右四個方向鍵,有時候也需要兩兩組合去使用,所以我們通過這個例子可以編寫如下枚舉:

enum CombineDirect{
    enum BaseDirect{
        case up
        case down
        case left
        case right
    }

    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

使用起來也很簡單:

let leftup = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)

2.2.1 結(jié)構(gòu)體嵌套枚舉

Swift允許在結(jié)構(gòu)體中嵌套枚舉鸭津,具體使用如下:

struct Skill{
   enum KeyType{
          case up
          case down
          case left
          case right
   }
   
    let key: KeyType
    
    func launchSkill(){
        switch key {
        case .left,.right:
            print("left, right")
        case .down,.up:
            print("up, down")
        }
    }
}

使用起來也很簡單:

let s = Skill(key: .up)
s.launchSkill()

2.3 枚舉中包含屬性

Swift中允許在枚舉中包含計算屬性和類型屬性彤侍,但不能包含存儲屬性。

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    
//    var radius: Double // Enums must not contain stored properties
    
    var width: Double{
        get {
            return 10.0
        }
    }
    static let height = 20.0
}

2.3 枚舉中包含方法

Swift中的枚舉也可以包含方法逆趋,可以是實例方法也可以是類方法

enum Week: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
    
    mutating func nextDay(){
        if self == .SUN{
            self = Week.MON
        }else{
            self = Week(rawValue: self.rawValue+1)!
        }
    }
    
    static func test() {
        print("test")
    }
}

使用起來依舊很簡單:

var w = Week.SUN
w.nextDay()
print(w) 

Week.test()

此處的方法都是靜態(tài)調(diào)用:

image

image
image
image

2.4 indiret在枚舉中的應(yīng)用

2.4.1 indiret

如果我們想要使用enum表達一個復(fù)雜的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)的時候盏阶,我們可以通過使用indrect關(guān)鍵字來讓enum更簡潔。

比如我們想要通過枚舉來表達一個鏈表的結(jié)構(gòu)闻书,鏈表需要存儲數(shù)據(jù)以及指向它的下一個節(jié)點的指針名斟,如果不使用indiret修飾則會報編譯錯誤:

image

此時我們可以寫成如下兩種方式就不會報錯了:

enum List<T> {
    case end
    indirect case node(T, next: List<T>)
}

indirect enum List<T> {
    case end
    case node(T, next: List<T>)
}

那么為什么要添加indirect關(guān)鍵字呢?

因為enum是值類型魄眉,它的大小在編譯期就需要確定砰盐,如果按照開始的寫法是不能夠確定當(dāng)前enum的大小的,所以從系統(tǒng)的角度來說杆融,在不知道給enum分配多大的空間楞卡,所以就需要使用indirect關(guān)鍵字霜运,官方文檔是這樣解釋的:

You indicate that an enumeration case is recursive by writing indirect before it, which tells the compiler to insert the necessary layer of indirection.

譯:您可以通過在枚舉案例之前寫indirect來表明枚舉案例是遞歸的脾歇,這告訴編譯器插入必要的間接層。

2.4.2 內(nèi)存占用

我們打印一下使用indirect修飾的枚舉內(nèi)存占用是多少呢淘捡?

enum List<T> {
    case end
    indirect case node(T, next: List<T>)
}

print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)

<!--打印結(jié)果-->
8
8

如果我們的泛型使用的是String呢?

print(MemoryLayout<List<String>>.size)
print(MemoryLayout<List<String>>.stride)

<!--打印結(jié)果-->
8
8

此時我們發(fā)現(xiàn)泛型的更換內(nèi)存占用保持不變,此時我們創(chuàng)建一個使用indirect修飾的枚舉類型的變量:

var node = List<Int>.node(10, next: List<Int>.end)

通過lldb查看node的內(nèi)存:

image

可以看到node像一個對象的結(jié)構(gòu)橙数,所以說這里面存儲的是一個指針英古,當(dāng)不確定枚舉類型大小的時候,將分配一個8字節(jié)大小的指針膘魄,指向一塊堆空間用于存儲這不確定大小的枚舉乌逐。

如果是end,此時存儲的就是case

image

那么這些是如何實現(xiàn)的呢创葡?我們通過sil代碼來看一下:

image

這里我們可以看到使用了alloc_box浙踢,我們打開SIL參考文檔,并找到alloc-box

我們可以看到alloc_box就是在堆上分配一個引用計數(shù)@box灿渴,該值足夠大洛波,可以容納T類型的值,以及一個retain count和運行時所需的任何其他元數(shù)據(jù)骚露。

其本質(zhì)是調(diào)用了swift_allocObject蹬挤,這點可以通過匯編代碼驗證:

image

3. 枚舉的大小

3.1 普通枚舉大小分析

首先看看下面這段代碼的打印結(jié)果:

enum NoMean{
    case a
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)

<!--打印結(jié)果-->
0
1

如果我們在增加一個case呢?

enum NoMean{
    case a
    case b
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)

<!--打印結(jié)果-->
1
1

如果在多增加幾個呢棘幸?

enum NoMean{
    case a
    case b
    case c
    case d
    case e
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)

<!--打印結(jié)果-->
1
1

可以看到焰扳,打印結(jié)果還是1,所以普通枚舉應(yīng)該就是以1字節(jié)存儲在內(nèi)存中的,下面我們來分析一下:

首先我們添加如下代碼:

var a = NoMean.a
var b = NoMean.b
var c = NoMean.c
var d = NoMean.d

lldb調(diào)試:

image

所以這里當(dāng)前枚舉的步長是1字節(jié)吨悍,也就意味著如果內(nèi)存中連續(xù)存儲NoMean光绕,需要跨越一個字節(jié)的長度。一個字節(jié)也就是8位畜份,最大可以表達255個數(shù)字诞帐。由于太長就不測試了,如果真的需要寫255及以上爆雹,還是建議以別的方式優(yōu)化一下停蕉。

如果枚舉后面寫了類型,比如:

enum NoMean: Int{
    case a
    case b
    case c
    case d
}

此時打印枚舉的大小和步長還是1钙态,這里面的類型指的是rawValue慧起,并不是case的值。

  • 所以枚舉中默認是以UInt8存儲的册倒,最大可以存儲0~255蚓挤,如果不夠則會自動轉(zhuǎn)換為UInt16,以此類推驻子。
  • 當(dāng)只有一個case的時候灿意,size是0,表示這個枚舉是沒有意義的
  • 枚舉中后面聲明的類型只的是rawValue的類型崇呵,不會影響枚舉的大小
  • 這些rawValue的值會存儲在Mach-O文件中缤剧,在使用的時候取查找,這個在上面提到過域慷,與枚舉大小沒有關(guān)系

3.2 關(guān)聯(lián)值枚舉的大小

如果枚舉中有關(guān)聯(lián)值荒辕,那么它的大小是多少呢?

enum Shape{
    case circle(radius: Int)
    case rectangle(width: Int, height: Int)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)

<!--打印結(jié)果-->
17
24

從打印結(jié)果我們可以知道犹褒,具有關(guān)聯(lián)值的枚舉的大小取決于關(guān)聯(lián)值的大小抵窒,此時circle中的關(guān)聯(lián)值Int占用內(nèi)存大小是8,而rectangle中兩個Int加起來是16叠骑,那么打印的這個17是怎么來的呢李皇?其實還有存儲枚舉值,所以枚舉的大小此處枚舉的size = 8+8+1 = 17座云,由于內(nèi)存對齊疙赠,所以要分配8的整數(shù)倍,所以stride就是24朦拖。這是該枚舉中最大需要的內(nèi)存圃阳,這個內(nèi)存足夠容納circle需要的9字節(jié)的大小。

下面我們璧帝,修改一下代碼順序捍岳,創(chuàng)建一下具有關(guān)聯(lián)值的枚舉,看看其內(nèi)存分布:

image

我們可以看到circle是分配了24字節(jié)的內(nèi)存空間的,內(nèi)存分布首先是存儲關(guān)聯(lián)自锣夹,然后在存儲枚舉值页徐,circle的枚舉值是存儲在第三個8字節(jié)上的,也就是存儲在最后银萍。

  • 具有關(guān)聯(lián)值的枚舉的大小取決于關(guān)聯(lián)值的大小
  • 具有關(guān)聯(lián)值的枚舉的大小是枚舉中最大的那個關(guān)聯(lián)值枚舉的大小 + 1(case 需要占用1字節(jié))变勇,
  • 如果大于255可能需要占用2字節(jié),這里沒有進行測試

3.3 枚舉嵌套枚舉的大小分析

下面我們看看枚舉嵌套枚舉中內(nèi)存占用的大刑健:

enum CombineDirect{
    enum BaseDirect{
        case up
        case down
        case left
        case right
    }

    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)

<!--打印結(jié)果-->
2
2

根據(jù)打印結(jié)果我們可以知道嵌套的枚舉搀绣,其實也就是枚舉中關(guān)聯(lián)了枚舉,它的大小同樣取決于關(guān)聯(lián)值的大小戳气,因為BaseDirect是基本的枚舉链患,其內(nèi)存占用為1,那么按照關(guān)聯(lián)值枚舉中的內(nèi)存占用應(yīng)該是1+1+1 = 3瓶您,那么為什么是2呢麻捻?

下面我們通過創(chuàng)建枚舉變量,看看其內(nèi)存分布是什么樣的呀袱,首先添加如下代碼:

// 2 0  3 0   2 1  3 1
var a = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var b = CombineDirect.rightUp(combineElement1: .right, combineElement2: .up)
var c = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var d = CombineDirect.rightDown(combineElement1: .right, combineElement2: .down)

lldb查看內(nèi)存

image

通過lldb調(diào)試的結(jié)果我們可以看到贸毕,在每個字節(jié)的低4位上存儲著關(guān)聯(lián)值的值,而在最后那個關(guān)聯(lián)值的高四位分別存儲了0压鉴,4崖咨,8,12(c)油吭,所以對于枚舉中嵌套枚舉應(yīng)該是做了相應(yīng)的優(yōu)化,借用未使用的高位存儲關(guān)聯(lián)值枚舉的枚舉值署拟。

下面我們測試一下婉宰,多寫幾個:

enum CombineDirect{
    enum BaseDirect{
        case up
        case down
        case left
        case right
    }
    
    case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)

var a = CombineDirect.upup(combineElement1: .up, combineElement2: .up)
var b = CombineDirect.updown(combineElement1: .up, combineElement2: .down)
var c = CombineDirect.upleft(combineElement1: .up, combineElement2: .left)
var d = CombineDirect.upright(combineElement1: .up, combineElement2: .right)
var e = CombineDirect.downup(combineElement1: .down, combineElement2: .up)
var f = CombineDirect.downdown(combineElement1: .down, combineElement2: .down)
var g = CombineDirect.downleft(combineElement1: .down, combineElement2: .left)
var h = CombineDirect.downright(combineElement1: .down, combineElement2: .right)
var i = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var j = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var k = CombineDirect.leftleft(combineElement1: .left, combineElement2: .left)
var l = CombineDirect.leftright(combineElement1: .left, combineElement2: .right)
var m = CombineDirect.rightup(combineElement1: .right, combineElement2: .up)
var n = CombineDirect.rightdown(combineElement1: .right, combineElement2: .down)
var o = CombineDirect.rightleft(combineElement1: .right, combineElement2: .left)
var p = CombineDirect.rightright(combineElement1: .right, combineElement2: .right)

lldb調(diào)試結(jié)果:

image

此時我們發(fā)現(xiàn),最后那個關(guān)聯(lián)值的高四位分別存儲了0推穷,1心包,2,3馒铃,4蟹腾,5,6区宇,7娃殖,8,9议谷,a炉爆,b,c,d芬首,e赴捞,f。

如果我們隨便注釋幾個:

image

我們發(fā)現(xiàn)郁稍,對應(yīng)的結(jié)果與枚舉中的順序是一致的赦政。

如果只剩一個,但不是第一個:

image

此時我們發(fā)現(xiàn)是7耀怜,與枚舉中的屬性還是一致的昼钻。

下面我們開始注釋枚舉中的:

最后發(fā)現(xiàn):

  • 如果是兩個枚舉就是0,8
  • 如果是3或4的時候是按照0封寞,4然评,8,c
  • 大約4小于等于16的時候是0狈究,1碗淌,2,3抖锥,4......f
  • 如果大于16就不能看出上面的規(guī)律了亿眠,所以從二進制位看

對于枚舉中嵌套枚舉,使用關(guān)聯(lián)值磅废,又或者說不嵌套纳像,具有關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值是枚舉類型的時候,會優(yōu)先借用最后關(guān)聯(lián)的那個枚舉的二進制位存儲具有關(guān)聯(lián)值枚舉的值拯勉,借用的位數(shù)為關(guān)聯(lián)值枚舉的個數(shù)小于等于2的冪最小值竟趾,也就是2的幾次冪才能大于等于關(guān)聯(lián)枚舉的個數(shù)。

這里我有進一步測試宫峦,如果普通枚舉的個數(shù)不足以使用低四位表示岔帽,比如低四位最少表示16個,如果多了的話导绷,就會借用關(guān)聯(lián)值中倒數(shù)第二個犀勒,也就上面例子中的第一個關(guān)聯(lián)值的高位進行借位存儲妥曲。按照這個邏輯檐盟,大膽猜想褂萧,如果普通枚舉的個數(shù)為256個遵堵,也就是不能借任何一個位怨规,這種具有關(guān)聯(lián)值枚舉是不是會另外開辟內(nèi)存存關(guān)聯(lián)值枚舉的值?其實不需要是256個锡足,只要不夠借的時候就會開辟內(nèi)存去存儲關(guān)聯(lián)值枚舉的值舶得。

舉個例子:

enum BaseDirect{
    case up
    case down
    case left
    case right
    case a
    case b
    case c
    case d
    case e
    case f
    case g
    case h
    case i
    case j
    case k
    case l,m,n,o,p,q,r,s,t,u,v,w,x,y,z
    case l1,m1,n1,o1,p1,q1,r1,s1,t1,u1,v1,w1,x1,y1,z1
    case l2,m2,n2,o2,p2,q2,r2,s2,t2,u2,v2,w2,x2,y2,z2
    case l3,m3,n3,o3,p3,q3,r3,s3,t3,u3,v3,w3,x3,y3,z3
}

enum CombineDirect{
    case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case aa(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)

<!--打印結(jié)果-->
3
3

這個例子并沒有嵌套纫骑,其實與嵌套沒有任何關(guān)系先馆,嵌套的枚舉也是單獨存儲的煤墙,只不過嵌套的枚舉作用域只在嵌套的大括號內(nèi)仿野。

另外脚作,如果枚舉值過多的時候球涛,我們看sil代碼:

image

此時我們可以發(fā)現(xiàn):

  • 多了一個hashValue的計算屬性
  • 一個遵守了Equatable協(xié)議的__derived_enum_equalsimp
  • 以及一個hash函數(shù)

我猜想宾符,對于過多case的枚舉,swift為了更好更快的匹配肝箱,使用了蘋果慣用的哈希煌张。我嘗試在源碼中搜索了一下derived_enum_equals并沒有找到相關(guān)方法骏融,貌似是過期被移除了档玻,后面使用==代替误趴。

3.4 結(jié)構(gòu)體嵌套枚舉的大小分析

首先還是看一下打印結(jié)果:

struct Skill{

   enum KeyType{
          case up
          case down
          case left
          case right
   }


    let key: KeyType

    func launchSkill(){
        switch key {
        case .left,.right:
            print("left, right")
        case .down,.up:
            print("up, down")
        }
    }
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)

<!--打印結(jié)果-->
1
1

如果只是嵌套了枚舉呢枣申?

struct Skill{
   enum KeyType{
          case up
          case down
          case left
          case right
   }
}

print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)

<!--打印結(jié)果-->
0
1

如果添加了其他屬性忠藤,則打印結(jié)果與添加的屬性類型有關(guān)系模孩,這里就不一一驗證了瓜贾。

總的來說祭芦,結(jié)構(gòu)體中嵌套枚舉與枚舉嵌套枚舉是一樣的龟劲,他們都不存儲枚舉昌跌,只是作用域在其中而已蚕愤。

4. 與OC混編

綜上萍诱,我們可以看到Swift中的枚舉非常強大裕坊,而在OC中枚舉僅僅只是一個整數(shù)值籍凝,那么在與OC混編的時候声诸,該如何在OC中使用Swift的枚舉呢双絮?下面我們就來探索一下:

4.1 OC調(diào)用Swift中的枚舉

如果想要在OC中使用Swift枚舉要求會很嚴(yán)格:

  1. 使用@objc標(biāo)記
  2. 類型必須是Int囤攀,也就是SwiftrawValue
  3. 必須導(dǎo)入import Foundation

也就是這樣:

import Foundation

@objc enum Week: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

此時編譯后就可以正在project-Swift.h中看到轉(zhuǎn)換后的對應(yīng)的OC枚舉:

image
image

調(diào)用的話就是:

4.2 Swift調(diào)用OC中的枚舉

4.2.1 NS_ENUM

NS_ENUM(NSInteger, OCENUM){
    Value1,
    Value2
};

如果使用NS_ENUM創(chuàng)建的枚舉會自動轉(zhuǎn)換成swift中的enum

可以在ocfileName.h中查看轉(zhuǎn)換后的枚舉:

image
image

使用的話需要在橋接文件中導(dǎo)入OC頭文件漓骚,然后在swift中使用:

let value = OCENUM.Value1

4.2.2 使用typedef enum

typedef enum {
    Enum1,
    Enum2,
    Enum3
}OCENum;

如果使用typedef enum這種形式的枚舉蝌蹂,會轉(zhuǎn)換成結(jié)構(gòu)體剃允,同樣可以在ocfileName.h中查看轉(zhuǎn)換后的結(jié)果齐鲤,轉(zhuǎn)換后的代碼如下:

image

可以看到里面有一個rawValue屬性给郊,以及init方法淆九。還遵守了Equatable, RawRepresentable兩個協(xié)議炭庙。

使用的話也是需要導(dǎo)入頭文件:

let num = OCEnum(0)
let num1 = OCEnum.init(0)
let num2 = OCEnum.init(rawValue: 3)

print(num)

<!--打印結(jié)果-->
OCNum(rawValue: 0)

這里我們只能通過init方法去初始化煤搜,不能訪問枚舉中的變量嘲驾。

4.2.3 使用typedef NS_ENUM

typedef NS_ENUM(NSInteger, OCENUM){
    OCEnumInvalid = 0,
    OCEnumA = 1,
    OCEnumB,
    OCEnumC
};

使用typedef NS_ENUM也會自動轉(zhuǎn)換為Swift的枚舉辽故,轉(zhuǎn)換后的代碼如下:

image

使用也是需要導(dǎo)入頭文件:

let ocenum = OCENUM.OCEnumInvalid
let ocenumRawValue = OCENUM.OCEnumA.rawValue

4.3 混編時需要使用String類型的枚舉

這里的意思是,Swift中需要使用String類型的枚舉喂走,但是又要與OC混編芋肠,暴露給OC使用帖池。

如果直接聲明為String類型編譯時不會通過的睡汹,這里只能弄個假的囚巴。Swift中的枚舉還是聲明為Int文兢,可以在枚舉中聲明一個變量或者方法姆坚,用于返回想要的字符串。

@objc enum Week: Int{
    case MON, TUE, WED
    
    var value: String?{
            switch self {
            case .MON:
                return "MON"
            case .TUE:
                return "TUE"
            case .WED:
                return "WED"
            default:
                return nil
            }
        }
    
    func weekName() -> String? {
        switch self {
        case .MON: return "MON"
        case .TUE: return "TUE"
        case .WED: return "WED"
        default:
            return nil
       }
    }
}

用法:

<!--OC用法-->
Week mon = WeekMON;

<!--Swift用法-->
let value = Week.MON.value
let value1 = Week.MON.weekName()

5. 總結(jié)

其實感覺該篇不太適合總結(jié)击喂,因為直接給結(jié)論會使人不是很好理解懂昂,但是還是記錄一下吧凌彬。

  1. Swift中的枚舉很強大
  2. enum中的rawValue是其中的計算屬性
  3. 如果聲明的時候不指定枚舉類型就沒有rawValue屬性(包括關(guān)聯(lián)值)
  4. rawValue中的值存儲在Mach-O中褐澎,不占用枚舉的存儲空間
  5. 枚舉值與rawValue不是同一個東西
  6. rawValue可以不寫工三,如果是Int默認0俭正,1段审,2...String等于枚舉名稱的字符串
  7. 如果枚舉中存在rawValue同時也會存在init(rawValue:)方法,用于通過rawValue值初始化枚舉
  8. 如果枚舉遵守了CaseIterable協(xié)議姥闪,且不是關(guān)聯(lián)值的枚舉筐喳,我們可以通過enum.allCases獲取到所有的枚舉避归,然后通過for循環(huán)遍歷
  9. 我們可以使用switch對枚舉進行模式匹配梳毙,如果只關(guān)系一個枚舉還可以使用if case
  10. 關(guān)聯(lián)值枚舉可以表示復(fù)雜的枚舉結(jié)構(gòu)
  11. 關(guān)聯(lián)值的枚舉沒有init方法,沒有RawValue別名奸柬,沒有rawValue計算屬性
  12. enum可以嵌套enum廓奕,被嵌套的作用域只在嵌套內(nèi)部
  13. 結(jié)構(gòu)體也可以嵌套enum懂从,此時enum的作用域也只在結(jié)構(gòu)體內(nèi)
  14. enum中可以包含計算屬性類型屬性但不能包含存儲屬性
  15. enum中可以定義實例方法和使用static修飾的方法缘薛,不能定義class修飾的方法
  16. 如果想使用復(fù)雜結(jié)構(gòu)的枚舉宴胧,或者說是具有遞歸結(jié)構(gòu)的枚舉可以使用indirect關(guān)鍵字
  • 關(guān)于枚舉的大小

    1. 默認情況下枚舉占用1字節(jié)也就是UInt8恕齐,如果不夠用也就是超過256個的時候會使用UInt16,UInt32... (太多沒有去驗證士骤,如果真的有這么多枚舉拷肌,建議通過其他方式去優(yōu)化)
    2. 如果枚舉個數(shù)過多會使用哈希來進行優(yōu)化巨缘,以便快速匹配
    3. 使用關(guān)聯(lián)值的枚舉大小取決于關(guān)聯(lián)值的大小,還要考慮內(nèi)存對齊
    4. 關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值如果是普通枚舉類型拴清,系統(tǒng)會通過借位優(yōu)化的方式節(jié)省內(nèi)存的占用
    5. 如果借位不夠了口予,會單獨開辟內(nèi)存存儲關(guān)聯(lián)值枚舉的枚舉值
    6. 在嵌套的時候,無論結(jié)構(gòu)體還是枚舉中都是不占用內(nèi)存的木张,被嵌套的枚舉是單獨存儲的舷礼,只是作用域在其內(nèi)部而已
  • 關(guān)于和OC混編

    1. OC中只能使用SwiftInt類型的枚舉
    2. 需要使用@objc關(guān)鍵字進行修飾
    3. 還要import Foundation
    4. swift中使用OC中的NS_ENUM的枚舉就跟普通枚舉一致
    5. 如果使用OCtypedef enum枚舉則需要通過init方法進行初始化
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛛株,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子笋粟,更是在濱河造成了極大的恐慌害捕,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件东涡,死亡現(xiàn)場離奇詭異疮跑,居然都是意外死亡,警方通過查閱死者的電腦和手機渐苏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞠眉,“玉大人械蹋,你說我怎么就攤上這事郊艘∠局伲” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長勉盅。 經(jīng)常有香客問我,道長宰闰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任葡盗,我火速辦了婚禮,結(jié)果婚禮上喘先,老公的妹妹穿的比我還像新娘苹祟。我一直安慰自己直焙,他們只是感情好奔誓,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蜕煌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盒刚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天涡上,我揣著相機與錄音,去河邊找鬼耻警。 笑死,一個胖子當(dāng)著我的面吹牛甸怕,可吹牛的內(nèi)容都是我干的甘穿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼梢杭,長吁一口氣:“原來是場噩夢啊……” “哼温兼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起募判,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤荡含,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后届垫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體释液,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年装处,在試婚紗的時候發(fā)現(xiàn)自己被綠了误债。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡妄迁,死狀恐怖寝蹈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情登淘,我是刑警寧澤箫老,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站黔州,受9級特大地震影響槽惫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辩撑,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仿耽。 院中可真熱鬧合冀,春花似錦、人聲如沸项贺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽开缎。三九已至棕叫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奕删,已是汗流浹背俺泣。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留完残,地道東北人伏钠。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像谨设,于是被迫代替她去往敵國和親熟掂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345