Swift系列四 – 枚舉

適度給類型起別名能夠讓程式碼更加易懂,開發效率更高,可維護性更好。

一、typealias(別名)

typealias用來給類型起別名。

typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64

typealias Date = (year: Int, month: Int, day: Int)
func test(_ date: Date) {
    print(date.year)
}
test((2019, 6, 25))
// 輸出:2019

typealias IntFn = (Int, Int) -> Int
func diff(v1: Int, v2: Int) -> Int {
    v1 - v2
}
let fn: IntFn = diff
fn(10, 5)
// 輸出:5

Void的本質就是空元祖的別名:public typealias Void = ()

二、枚舉

Swift枚舉和C/OC語言不一樣,以前寫OC的時候枚舉本質是int類型,但Swift中枚舉可以是多類型的。

官方建議:枚舉名稱使用大寫,成員使用小寫。

2.1. 基本用法

// 定義枚舉
enum YBColor {
    case white
    case black
    case gray
}

// 等價於上面的程式碼
//enum YBColor {
//    case white, black, gray
//}

var color = YBColor.white
color = YBColor.black
color = .gray // 簡寫(因為此時已經確定變數color是YBColor類型)
print(color) // 輸出:gray

// 循環控制
switch color {
case .white:
    print("white")
case .black:
    print("black")
case .gray:
    print("gray")
}

2.2. 關聯值

有時將枚舉的成員值其他類型的值關聯存儲在一起,會非常有用.

案例:

enum Score {
    case points(Int)
    case grade(Character)
}

// 數值表達
var score = Score.points(96)
// 等級/字元表達
score = .grade("A")


enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}

var date = Date.digit(year: 2019, month: 06, day: 25)
date = .string("2019-06-25")
switch date {
case .digit(let year, let month, let day):
    print(year, month, day, separator:"/")
case let .string(value):
    print(value)
}
/*
 輸出:
 2019-06-25
*/

let寫在枚舉成員前面意味著枚舉成員形參只能是常量,放在形參裡面可以自定義選擇是var還是let

2.2. 原始值

枚舉成員可以使用相同類型的默認值預先關聯,這個默認值叫做:原始值。

enum Direction : Character {
    case up = "w"
    case down = "s"
    case left = "a"
    case right = "d"
}
var direction = Direction.up
print(direction) // 輸出:up
print(direction.rawValue) // 輸出:w
print(Direction.down.rawValue) // 輸出:s

如果枚舉的原始值類型是IntString,Swift會自動分配原始值:

enum Direction : String {
    case up = "up"
    case down = "down"
    case left = "left"
    case right = "right"
}
var direction = Direction.up
print(direction) // 輸出:up
print(direction.rawValue) // 輸出:up
print(Direction.down.rawValue) // 輸出:down

// 等價
enum Direction : String {
    case up, down, left, right
}
var direction = Direction.up
print(direction) // 輸出:up
print(direction.rawValue) // 輸出:up
print(Direction.down.rawValue) // 輸出:down

Int類型,成員值自增(類似C/OC枚舉):

enum Season : Int {
    case spring, summer, autumn, winter
}
print(Season.spring.rawValue) // 輸出:0
print(Season.summer.rawValue) // 輸出:1
print(Season.autumn.rawValue) // 輸出:2
print(Season.winter.rawValue) // 輸出:3

enum Season : Int {
    case spring = 1, summer, autumn = 4, winter
}
print(Season.spring.rawValue)
print(Season.summer.rawValue)
print(Season.autumn.rawValue)
print(Season.winter.rawValue)

2.3. 遞歸枚舉

  • 關鍵字:indirect
  • 可以把需要遞歸枚舉的成員前面加indirect,也可以為了方便直接加到枚舉定義前面。
indirect enum ArithExpr {
    case number(Int)
    case sum(ArithExpr, ArithExpr)
    case diff(ArithExpr, ArithExpr)
}

//enum ArithExpr {
//    case number(Int)
//    indirect case sum(ArithExpr, ArithExpr)
//    indirect case diff(ArithExpr, ArithExpr)
//}

let five = ArithExpr.number(5)
let four = ArithExpr.number(4)
let two = ArithExpr.number(2)
let sum = ArithExpr.sum(five, four)
let diff = ArithExpr.diff(sum, two)

func cal(_ expr: ArithExpr) -> Int {
    switch expr {
    case let .number(value):
        return value
    case let .sum(left, right):
        return cal(left) + cal(right)
    case let .diff(left, right):
        return cal(left) - cal(right)
    }
}
cal(diff) // 輸出:7

三、枚舉的記憶體布局

在Swift中查看記憶體佔用大小及對齊方式使用枚舉:MemoryLayout

  • size:實際用到的空間大小
  • stride:分配佔用的空間大小
  • alignment:記憶體對齊方式

下面的意思是,Int在記憶體中佔用8個位元組,記憶體對齊數是8:

MemoryLayout<int>.size // 輸出:8
MemoryLayout<int>.stride // 輸出:8
MemoryLayout<int>.alignment // 輸出:8

查看枚舉佔用記憶體:

enum Password {
    case number(Int, Int, Int, Int)
    case other
}

var pwd = Password.number(1, 2, 2, 3)

MemoryLayout.size(ofValue: pwd) // 輸出:33
MemoryLayout.stride(ofValue: pwd) // 輸出:40
MemoryLayout.alignment(ofValue: pwd) // 輸出:8

為什麼是佔用記憶體大小是33,而分配了40?

  • number(Int, Int, Int, Int)佔用32個位元組,other佔用1個位元組,所以一共只需要佔用33個位元組就夠用了
  • 因為記憶體對齊數是8,所以分配記憶體的時候只能是8的倍數,而33個位元組不夠8的倍數,所以往高位補齊後就是40了

為什麼other佔用1個位元組呢?

enum Season {
    case spring, summer, autumn, winter
}
MemoryLayout<season>.size // 輸出:1
MemoryLayout<season>.stride // 輸出:1
MemoryLayout<season>.alignment // 輸出:1

// 限定類型
enum Season: String {
    case spring, summer, autumn, winter
}
MemoryLayout<season>.size // 輸出:1
MemoryLayout<season>.stride // 輸出:1
MemoryLayout<season>.alignment // 輸出:1
  • 上面程式碼可以看出不管類型是什麼佔用的記憶體大小都是1個位元組;
  • 其實本質上是關聯值和原始值的區別。

結論一: 把傳進去的關聯值直接存儲到枚舉變數記憶體裡面的,所以枚舉變數是關聯值的話,記憶體是一定和將要存儲的關聯值大小有關。

為了證實結論一,比較下面的兩個不同類型的關聯值:

enum Password {
    case number(Int, Int, Int, Int)
    case other
}

MemoryLayout<password>.size // 輸出:33
MemoryLayout<password>.stride // 輸出:40
MemoryLayout<password>.alignment // 輸出:8

enum Password {
    case number(String, String, String, String)
    case other
}

MemoryLayout<password>.size // 輸出:65
MemoryLayout<password>.stride // 輸出:72
MemoryLayout<password>.alignment // 輸出:8

結論二: 原始值固定後是不能修改的,記憶體中只會把對應的成員值(序號)存下來,這時候1個位元組足夠用了,和枚舉類型無關(不管是Int還是String枚舉都是佔用一個位元組)。

分析下面程式碼:

enum Season: Int {
    // 序號0            序號1       序號2        序號3
    case spring = 1, summer = 2, autumn = 3, winter = 4
}

var season1 = Season.spring
var season2 = Season.spring
var season3 = Season.spring
MemoryLayout<season>.size // 輸出:1
MemoryLayout<season>.stride // 輸出:1
MemoryLayout<season>.alignment // 輸出:1

疑問:成員值在記憶體中只佔用1個位元組,Int或String的原始值是怎麼存下的?rawValue其實是另外一塊地址。

  • 關聯值才會存儲到枚舉變數中,原始值不會佔用枚舉變數記憶體
  • 我們可以通過記憶體地址看到前面的位元組被關聯值佔用,關聯值後面有一個位元組是保存成員值
    • 1個位元組存儲成員值(如果只有一個枚舉成員則不佔用記憶體)
    • N個位元組存儲關聯值(N取佔用記憶體最大的關聯值),任何一個case的關聯值都共用這N個位元組(共用體)
    • 剩餘位元組按照對齊數補齊

Switchcase其實是比較枚舉的成員值的

Tags: