Swift 枚舉(enum)詳解

Swift 枚舉(enum)詳解

[TOC]

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

1. 枚舉

1.1 C中枚舉

首先我們來(lái)看看C語(yǔ)言中枚舉的寫法。這里我們以一周7天作為示例。

普通寫法:

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

以上就是C語(yǔ)言中枚舉的常見(jiàn)寫法enum關(guān)鍵字,加上枚舉名稱,大括號(hào)里面的不同的枚舉值使用逗號(hào)分隔開(kāi)來(lái)。此時(shí)的枚舉值默認(rèn)從0開(kāi)始,依次是1,2,3……

自定義枚舉值:

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

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

此時(shí)枚舉值就會(huì)從1開(kāi)始依次向后排列,你也可以給每個(gè)枚舉都定義不同的枚舉值,如果直接給TUE定義為2,而沒(méi)給MON定義,則MON的枚舉值會(huì)是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;

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

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

1.2 Swift中枚舉

Swift中最常見(jiàn)的枚舉寫法:

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

Swift中也可以簡(jiǎn)化為如下寫法:

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

Swift中枚舉很強(qiáng)大,我們可以創(chuàng)建一個(gè)枚舉值是String類型的enum,其實(shí)也不應(yīng)該說(shuō)是枚舉值,而是枚舉的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)然,如果我們不想寫后面的字符串,也可以簡(jiǎn)寫成如下的形式:

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

定義枚舉變量:

var w: Week = .MON

枚舉變量的定義很簡(jiǎn)單,跟普通變量的定義沒(méi)什么差別。

枚舉的訪問(wèn):

我們可以訪問(wèn)枚舉的變量和枚舉的rawValue
首先我們需要注意的是如果沒(méi)有聲明枚舉的類型,是沒(méi)有rawValue屬性可以訪問(wèn)的。

image

一般情況下我們可以通過(guò)以下方式訪問(wèn)枚舉:

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

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

打印結(jié)果如下:

image

1.3 枚舉值和其RawValue的存儲(chǔ)

在上面枚舉的訪問(wèn)中我們可以看到關(guān)于MON字符串的打印,既然可以打印出來(lái),說(shuō)明是存儲(chǔ)了相關(guān)的字符串的,那么是怎么存儲(chǔ)的呢?又是怎么獲取出來(lái)的呢?下面我們通過(guò)sil代碼進(jìn)行分析(使用如下命令生成并打開(kāi)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 }
}

通過(guò)sil代碼中對(duì)枚舉的定義可以看到:

  1. Swift中一致的枚舉
  2. 取了一個(gè)別名,也就是String類型是RawValue
  3. 添加了一個(gè)可選類型的init方法
  4. 一個(gè)計(jì)算屬性rawValue,通過(guò)其get方法獲取枚舉的原始值

下面我們?cè)?code>main函數(shù)中看看:

image

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

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

下面我們看看rawValuegetter方法:

image

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

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

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

image

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

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

image

按照以上的寫法是會(huì)報(bào)編譯錯(cuò)誤的。

1.4 枚舉.init

1.4.1 觸發(fā)方式

在上面的分析時(shí)我們知道枚舉會(huì)有一個(gè)init方法,那么這個(gè)方法是什么時(shí)候調(diào)用的呢?我們添加如下符號(hào)斷點(diǎn):

image

添加如下代碼:

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

運(yùn)行后并沒(méi)有觸發(fā)該符號(hào)斷點(diǎn)。

下面我們?cè)谔砑尤缦麓a:

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

運(yùn)行后即可觸發(fā)符號(hào)斷點(diǎn):

image

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

1.4.2 init分析

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

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

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

從打印結(jié)果中可以看到,第一個(gè)輸出的是可選值SwiftEnum.Week.MON,第二個(gè)是nil,很顯然Hello不是我們的枚舉,那么這些是怎么實(shí)現(xiàn)的呢?我們?cè)俅尾榭?code>sil代碼,此時(shí)我們可以直接看Week.init方法的實(shí)現(xiàn)。

Xnip2021-03-04_15-28-16 2

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

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

關(guān)于上面提到的兩個(gè)函數(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初始化一個(gè)數(shù)組,將其以元組的形式返回?cái)?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
}

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

1.5 枚舉的遍歷

一般我們很少會(huì)對(duì)枚舉進(jìn)行遍歷操作,在Swift中可以通過(guò)遵守CaseIterable協(xié)議來(lái)實(shí)現(xiàn)對(duì)枚舉的遍歷。

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)更多的信息。下面我們舉個(gè)例子,如果需要有一個(gè)形狀的枚舉,里面有圓形和矩形。圓形有半徑,矩形有長(zhǎng)寬,那么這個(gè)枚舉就可以寫成如下代碼:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}
  • 其中括號(hào)中的radius以及widthheight就是關(guān)聯(lián)值
  • 如果沒(méi)枚舉中使用關(guān)聯(lián)值則枚舉就沒(méi)有rawValue屬性了,因?yàn)殛P(guān)聯(lián)值是一組值,而rawValue是單個(gè)值,可以通過(guò)sil代碼驗(yàn)證
image

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

在這個(gè)枚舉中radius、width、height這些都是自定義的標(biāo)簽,也可以不寫,如下所示,但并不推薦這種方式,因?yàn)榭勺x性非常差

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

那么有關(guān)聯(lián)值的枚舉該如何初始化呢?其實(shí)也很簡(jiǎn)單,下面我們就來(lái)創(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 簡(jiǎn)單的模式匹配

顧明思議,模式匹配就是匹配每一個(gè)枚舉值,通常我們可以使用switch語(yǔ)句來(lái)進(jìn)行模式匹配。如果使用switch進(jìn)行模式匹配:

  1. 必須列舉當(dāng)前所有可能的情況,否則就會(huì)報(bào)編譯錯(cuò)誤
  2. 如果不想匹配這么多case則可以使用defalut
  3. 在同一個(gè)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")
}

其實(shí)這個(gè)匹配也很簡(jiǎn)單,我們通過(guò)查看sil代碼就可以知道:

image

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

如果我們不關(guān)心關(guān)聯(lián)值,關(guān)聯(lián)值枚舉的寫法與普通枚舉沒(méi)有什么區(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)值枚舉,肯定是會(huì)關(guān)心關(guān)聯(lián)值的,當(dāng)關(guān)心關(guān)聯(lián)值時(shí)其寫法如下:

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),這里的每個(gè)case中都使用了let或者var,這里因?yàn)橐褂藐P(guān)聯(lián)值,所以需要使用let聲明一下。或者放在最前面,或者對(duì)每個(gè)需要使用的變量前都添加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 其他匹配

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

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

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

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
}

此時(shí)的打印是20,對(duì)于上面的例子,必須caserectanglesquare,而且rectangle必須是10,square后面的width是10。

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

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

注意: 以上命名必須一致,比如都使用x,如果一個(gè)x一個(gè)y就不行了。

2.2 枚舉的嵌套

2.2.1 枚舉嵌套枚舉

顧名思義,枚舉嵌套枚舉就是在枚舉中還有枚舉,比如我們玩游戲時(shí)會(huì)有上下左右四個(gè)方向鍵,有時(shí)候也需要兩兩組合去使用,所以我們通過(guò)這個(gè)例子可以編寫如下枚舉:

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)
}

使用起來(lái)也很簡(jiǎn)單:

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")
        }
    }
}

使用起來(lái)也很簡(jiǎn)單:

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

2.3 枚舉中包含屬性

Swift中允許在枚舉中包含計(jì)算屬性和類型屬性,但不能包含存儲(chǔ)屬性。

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中的枚舉也可以包含方法,可以是實(shí)例方法也可以是類方法

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")
    }
}

使用起來(lái)依舊很簡(jiǎn)單:

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表達(dá)一個(gè)復(fù)雜的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)的時(shí)候,我們可以通過(guò)使用indrect關(guān)鍵字來(lái)讓enum更簡(jiǎn)潔。

比如我們想要通過(guò)枚舉來(lái)表達(dá)一個(gè)鏈表的結(jié)構(gòu),鏈表需要存儲(chǔ)數(shù)據(jù)以及指向它的下一個(gè)節(jié)點(diǎn)的指針,如果不使用indiret修飾則會(huì)報(bào)編譯錯(cuò)誤:

image

此時(shí)我們可以寫成如下兩種方式就不會(huì)報(bào)錯(cuò)了:

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)鍵字呢?

因?yàn)?code>enum是值類型,它的大小在編譯期就需要確定,如果按照開(kāi)始的寫法是不能夠確定當(dāng)前enum的大小的,所以從系統(tǒng)的角度來(lái)說(shuō),在不知道給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.

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

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

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

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

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

image

可以看到node像一個(gè)對(duì)象的結(jié)構(gòu),所以說(shuō)這里面存儲(chǔ)的是一個(gè)指針,當(dāng)不確定枚舉類型大小的時(shí)候,將分配一個(gè)8字節(jié)大小的指針,指向一塊堆空間用于存儲(chǔ)這不確定大小的枚舉。

如果是end,此時(shí)存儲(chǔ)的就是case

image

那么這些是如何實(shí)現(xiàn)的呢?我們通過(guò)sil代碼來(lái)看一下:

image

這里我們可以看到使用了alloc_box,我們打開(kāi)SIL參考文檔,并找到alloc-box

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

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

image

3. 枚舉的大小

3.1 普通枚舉大小分析

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

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

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

如果我們?cè)谠黾右粋€(gè)case呢?

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

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

如果在多增加幾個(gè)呢?

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é)存儲(chǔ)在內(nèi)存中的,下面我們來(lái)分析一下:

首先我們添加如下代碼:

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

lldb調(diào)試:

image

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

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

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

此時(shí)打印枚舉的大小和步長(zhǎng)還是1,這里面的類型指的是rawValue,并不是case的值。

  • 所以枚舉中默認(rèn)是以UInt8存儲(chǔ)的,最大可以存儲(chǔ)0~255,如果不夠則會(huì)自動(dòng)轉(zhuǎn)換為UInt16,以此類推。
  • 當(dāng)只有一個(gè)case的時(shí)候,size是0,表示這個(gè)枚舉是沒(méi)有意義的
  • 枚舉中后面聲明的類型只的是rawValue的類型,不會(huì)影響枚舉的大小
  • 這些rawValue的值會(huì)存儲(chǔ)在Mach-O文件中,在使用的時(shí)候取查找,這個(gè)在上面提到過(guò),與枚舉大小沒(méi)有關(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)值的大小,此時(shí)circle中的關(guān)聯(lián)值Int占用內(nèi)存大小是8,而rectangle中兩個(gè)Int加起來(lái)是16,那么打印的這個(gè)17是怎么來(lái)的呢?其實(shí)還有存儲(chǔ)枚舉值,所以枚舉的大小此處枚舉的size = 8+8+1 = 17,由于內(nèi)存對(duì)齊,所以要分配8的整數(shù)倍,所以stride就是24。這是該枚舉中最大需要的內(nèi)存,這個(gè)內(nèi)存足夠容納circle需要的9字節(jié)的大小。

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

image

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

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

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é)果我們可以知道嵌套的枚舉,其實(shí)也就是枚舉中關(guān)聯(lián)了枚舉,它的大小同樣取決于關(guān)聯(lián)值的大小,因?yàn)?code>BaseDirect是基本的枚舉,其內(nèi)存占用為1,那么按照關(guān)聯(lián)值枚舉中的內(nèi)存占用應(yīng)該是1+1+1 = 3,那么為什么是2呢?

下面我們通過(guò)創(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

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

下面我們測(cè)試一下,多寫幾個(gè):

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

此時(shí)我們發(fā)現(xiàn),最后那個(gè)關(guān)聯(lián)值的高四位分別存儲(chǔ)了0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f。

如果我們隨便注釋幾個(gè):

image

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

如果只剩一個(gè),但不是第一個(gè):

image

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

下面我們開(kāi)始注釋枚舉中的:

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

  • 如果是兩個(gè)枚舉就是0,8
  • 如果是3或4的時(shí)候是按照0,4,8,c
  • 大約4小于等于16的時(shí)候是0,1,2,3,4......f
  • 如果大于16就不能看出上面的規(guī)律了,所以從二進(jìn)制位看

對(duì)于枚舉中嵌套枚舉,使用關(guān)聯(lián)值,又或者說(shuō)不嵌套,具有關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值是枚舉類型的時(shí)候,會(huì)優(yōu)先借用最后關(guān)聯(lián)的那個(gè)枚舉的二進(jìn)制位存儲(chǔ)具有關(guān)聯(lián)值枚舉的值,借用的位數(shù)為關(guān)聯(lián)值枚舉的個(gè)數(shù)小于等于2的冪最小值,也就是2的幾次冪才能大于等于關(guān)聯(lián)枚舉的個(gè)數(shù)。

這里我有進(jìn)一步測(cè)試,如果普通枚舉的個(gè)數(shù)不足以使用低四位表示,比如低四位最少表示16個(gè),如果多了的話,就會(huì)借用關(guān)聯(lián)值中倒數(shù)第二個(gè),也就上面例子中的第一個(gè)關(guān)聯(lián)值的高位進(jìn)行借位存儲(chǔ)。按照這個(gè)邏輯,大膽猜想,如果普通枚舉的個(gè)數(shù)為256個(gè),也就是不能借任何一個(gè)位,這種具有關(guān)聯(lián)值枚舉是不是會(huì)另外開(kāi)辟內(nèi)存存關(guān)聯(lián)值枚舉的值?其實(shí)不需要是256個(gè),只要不夠借的時(shí)候就會(huì)開(kāi)辟內(nèi)存去存儲(chǔ)關(guān)聯(lián)值枚舉的值。

舉個(gè)例子:

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

這個(gè)例子并沒(méi)有嵌套,其實(shí)與嵌套沒(méi)有任何關(guān)系,嵌套的枚舉也是單獨(dú)存儲(chǔ)的,只不過(guò)嵌套的枚舉作用域只在嵌套的大括號(hào)內(nèi)。

另外,如果枚舉值過(guò)多的時(shí)候,我們看sil代碼:

image

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

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

我猜想,對(duì)于過(guò)多case的枚舉,swift為了更好更快的匹配,使用了蘋果慣用的哈希。我嘗試在源碼中搜索了一下derived_enum_equals并沒(méi)有找到相關(guān)方法,貌似是過(guò)期被移除了,后面使用==代替。

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)系,這里就不一一驗(yàn)證了。

總的來(lái)說(shuō),結(jié)構(gòu)體中嵌套枚舉與枚舉嵌套枚舉是一樣的,他們都不存儲(chǔ)枚舉,只是作用域在其中而已。

4. 與OC混編

綜上,我們可以看到Swift中的枚舉非常強(qiáng)大,而在OC中枚舉僅僅只是一個(gè)整數(shù)值,那么在與OC混編的時(shí)候,該如何在OC中使用Swift的枚舉呢?下面我們就來(lái)探索一下:

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

如果想要在OC中使用Swift枚舉要求會(huì)很嚴(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
}

此時(shí)編譯后就可以正在project-Swift.h中看到轉(zhuǎn)換后的對(duì)應(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)建的枚舉會(huì)自動(dò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這種形式的枚舉,會(huì)轉(zhuǎn)換成結(jié)構(gòu)體,同樣可以在ocfileName.h中查看轉(zhuǎn)換后的結(jié)果,轉(zhuǎn)換后的代碼如下:

image

可以看到里面有一個(gè)rawValue屬性,以及init方法。還遵守了Equatable, RawRepresentable兩個(gè)協(xié)議。

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

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

print(num)

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

這里我們只能通過(guò)init方法去初始化,不能訪問(wèn)枚舉中的變量。

4.2.3 使用typedef NS_ENUM

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

使用typedef NS_ENUM也會(huì)自動(dòng)轉(zhuǎn)換為Swift的枚舉,轉(zhuǎn)換后的代碼如下:

image

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

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

4.3 混編時(shí)需要使用String類型的枚舉

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

如果直接聲明為String類型編譯時(shí)不會(huì)通過(guò)的,這里只能弄個(gè)假的。Swift中的枚舉還是聲明為Int,可以在枚舉中聲明一個(gè)變量或者方法,用于返回想要的字符串。

@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é)

其實(shí)感覺(jué)該篇不太適合總結(jié),因?yàn)橹苯咏o結(jié)論會(huì)使人不是很好理解,但是還是記錄一下吧。

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

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

    1. OC中只能使用SwiftInt類型的枚舉
    2. 需要使用@objc關(guān)鍵字進(jìn)行修飾
    3. 還要import Foundation
    4. swift中使用OC中的NS_ENUM的枚舉就跟普通枚舉一致
    5. 如果使用OCtypedef enum枚舉則需要通過(guò)init方法進(jìn)行初始化
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,241評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,939評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,697評(píng)論 6 409
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,182評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,406評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,772評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,973評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,638評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,866評(píng)論 1 285
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,644評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,953評(píng)論 2 373