Swift枚举

398 阅读9分钟

C语言的枚举

  • C语言的枚举写法
enum 枚举名 { 
	枚举值1,
	枚举值2,
	......
 };
  • 我们通过枚举表示一周的七天
enum Week {
	MON,TUS,WED,THU,FRI,SAT,SUN
};
  • c语言中,枚举的第一个成员默认是为0,后面的枚举值一次类推,也可以直接指定对应的值,它后面的枚举值依然是一次递增
enum Week {
	MON=3,TUS,WED,THU=9,FRI,SAT,SUN
};
// 这个枚举对应的值就是3,4,5,9,10,11,12

Swift的枚举

Swift枚举的使用

  • 同样实现这个枚举,其写法如下
enum Week {
    case MON
    case TUS
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}
// 也可以直接一个case,用逗号隔开
enum Week {
    case MON,TUS,WED,THU,FRI,SAT,SUN
}
  • 这样表达的是一个整型的枚举,和c语言的是一致的,Swift中也可以用枚举表达String
enum Week : String {
    case MON = "MON"
    case TUS = "TUS"
    case WED = "WED"
    case THU = "THU"
    case FRI = "FRI"
    case SAT = "SAT"
    case SUN = "SUN"
}
  • =右边就是Swift中的RawValue,如果我们没有写后面的字符串,会默认的隐式RawValue分配,也可以对枚举的成员单独指定RawValue
  • 查看sil文件,可以看到自动生成了一个init?(rawValue:)方法,和一个get方法
enum Week : String {
  case MON
  case TUS
  case WED
  case THU
  case FRI
  case SAT
  case SUN
  typealias RawValue = String
  init?(rawValue: String)
  var rawValue: String { get }
}
  • 我们来看下init?(rawValue:)函数是什么时候调用的,通过符号断点来观察
  • 运行发现:Week.MON.rawValue并不会触发init函数,只有调用Week(rawValue: "MON")才会进到符号断点,再通过打印下面的代码
print(Week(rawValue: "MON"))
print(Week(rawValue: "hello"))

// 打印结果  
Optional(SwiftEnumerate.Week.MON)
nil
  • 查看sil文件中init方法的实现,首先我们会将枚举值全部放到一个数组里面,然后通过_findStringSwitchCase方法匹配传入的字符串,如果匹配到了就返回一个index,没有匹配到就返回一个-1,再循环遍历所有的枚举值,匹配到了就取出对应的枚举值,然后包装成一个可选项的值返回,如果都没有匹配到返回的就是nil,所以返回值是一个可选项。
// Week.init(rawValue:)
sil hidden @$s4main4WeekO8rawValueACSgSS_tcfC : $@convention(method) (@owned String, @thin Week.Type) -> Optional<Week> {
// %0 "rawValue"                                  // users: %164, %158, %79, %3
// %1 "$metatype"
bb0(%0 : $String, %1 : $@thin Week.Type):
  %2 = alloc_stack $Week, var, name "self"        // users: %162, %154, %143, %132, %121, %110, %99, %88, %165, %159
  debug_value %0 : $String, let, name "rawValue", argno 1 // id: %3
  %4 = integer_literal $Builtin.Word, 7           // user: %6
  // function_ref _allocateUninitializedArray<A>(_:) 创建一个元组,里面有枚举值数组,和指针
  %5 = function_ref @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %6
  %6 = apply %5<StaticString>(%4) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %8, %7
  %7 = tuple_extract %6 : $(Array<StaticString>, Builtin.RawPointer), 0 // users: %80, %79 
  %8 = tuple_extract %6 : $(Array<StaticString>, Builtin.RawPointer), 1 // user: %9
  %9 = pointer_to_address %8 : $Builtin.RawPointer to [strict] $*StaticString // users: %17, %69, %59, %49, %39, %29, %19
  %10 = string_literal utf8 "MON"                 // user: %12 创建一个字符串
  %11 = integer_literal $Builtin.Word, 3          // user: %16 它的长度是3,我们在内存中分配的大小
  %12 = builtin "ptrtoint_Word"(%10 : $Builtin.RawPointer) : $Builtin.Word // user: %16
  br bb1                                          // id: %13

bb1:                                              // Preds: bb0
  %14 = integer_literal $Builtin.Int8, 2          // user: %16
  br bb2                                          // id: %15

bb2:                                              // Preds: bb1
  %16 = struct $StaticString (%12 : $Builtin.Word, %11 : $Builtin.Word, %14 : $Builtin.Int8) // user: %17
  store %16 to %9 : $*StaticString                // id: %17
  %18 = integer_literal $Builtin.Word, 1          // user: %19
  %19 = index_addr %9 : $*StaticString, %18 : $Builtin.Word // user: %27 获取当前数组中的值的地址,返回相对于首地址的第(%18)个元素的地址
  %20 = string_literal utf8 "TUS"                 // user: %22
  %21 = integer_literal $Builtin.Word, 3          // user: %26
  %22 = builtin "ptrtoint_Word"(%20 : $Builtin.RawPointer) : $Builtin.Word // user: %26
  br bb3                                          // id: %23
  ......
bb14:                                             // Preds: bb13
  %76 = struct $StaticString (%72 : $Builtin.Word, %71 : $Builtin.Word, %74 : $Builtin.Int8) // user: %77
  store %76 to %69 : $*StaticString               // id: %77
  // function_ref _findStringSwitchCase(cases:string:)
  %78 = function_ref @$ss21_findStringSwitchCase5cases6stringSiSays06StaticB0VG_SStF : $@convention(thin) (@guaranteed Array<StaticString>, @guaranteed String) -> Int // user: %79 遍历之前创建的数组匹配就返回对应的index
  %79 = apply %78(%7, %0) : $@convention(thin) (@guaranteed Array<StaticString>, @guaranteed String) -> Int // users: %149, %138, %127, %116, %105, %94, %83, %147, %136, %125, %114, %103, %92, %81
  release_value %7 : $Array<StaticString>         // id: %80
  debug_value %79 : $Int, let, name "$match"      // id: %81
  %82 = integer_literal $Builtin.Int64, 0         // user: %84
  %83 = struct_extract %79 : $Int, #Int._value    // user: %84
  %84 = builtin "cmp_eq_Int64"(%82 : $Builtin.Int64, %83 : $Builtin.Int64) : $Builtin.Int1 // user: %85  循环比较当前的返回值和index,根据结果往下跳转不同的分支
  cond_br %84, bb15, bb16                         // id: %85
  
bb15:                                             // Preds: bb14
  %86 = metatype $@thin Week.Type
  %87 = enum $Week, #Week.MON!enumelt             // user: %89
  %88 = begin_access [modify] [static] %2 : $*Week // users: %89, %90
  store %87 to %88 : $*Week                       // id: %89
  end_access %88 : $*Week                         // id: %90
  br bb29                                         // id: %91
......
// 遍历完全部枚举值都没有找到,就会返回一个none
bb28:                                             // Preds: bb26
  release_value %0 : $String                      // id: %158
  dealloc_stack %2 : $*Week                       // id: %159
  %160 = enum $Optional<Week>, #Optional.none!enumelt // user: %161
  br bb30(%160 : $Optional<Week>)                 // id: %161
// 匹配成功就创建一个可选项返回
bb29:                                             // Preds: bb27 bb25 bb23 bb21 bb19 bb17 bb15
  %162 = load %2 : $*Week                         // user: %163
  %163 = enum $Optional<Week>, #Optional.some!enumelt, %162 : $Week // user: %166
  release_value %0 : $String                      // id: %164
  dealloc_stack %2 : $*Week                       // id: %165
  br bb30(%163 : $Optional<Week>)                 // id: %166

// %167                                           // user: %168
bb30(%167 : $Optional<Week>):                     // Preds: bb29 bb28
  return %167 : $Optional<Week>                   // id: %168
} // end sil function '$s4main4WeekO8rawValueACSgSS_tcfC'
  • Swift源码中_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
}
  • 再看下它是如何获取rawValue
// main 这里调用Week.rawValue.getter,返回一个字符串
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main1aSSvp                     // id: %2
  %3 = global_addr @$s4main1aSSvp : $*String      // user: %8
  %4 = metatype $@thin Week.Type
  %5 = enum $Week, #Week.MON!enumelt              // user: %7 创建一个枚举值 MON
  // function_ref Week.rawValue.getter
  %6 = function_ref @$s4main4WeekO8rawValueSSvg : $@convention(method) (Week) -> @owned String // user: %7
  %7 = apply %6(%5) : $@convention(method) (Week) -> @owned String // user: %8
  store %7 to %3 : $*String                       // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} // end sil function 'main'

// Week.rawValue.getter
sil hidden @$s4main4WeekO8rawValueSSvg : $@convention(method) (Week) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Week):
  debug_value %0 : $Week, let, name "self", argno 1 // id: %1
  switch_enum %0 : $Week, case #Week.MON!enumelt: bb1, case #Week.TUS!enumelt: bb2, case #Week.WED!enumelt: bb3, case #Week.THU!enumelt: bb4, case #Week.FRI!enumelt: bb5, case #Week.SAT!enumelt: bb6, case #Week.SUN!enumelt: bb7 // id: %2
// 匹配枚举值,构建字符串返回
bb1:                                              // Preds: bb0
  %3 = string_literal utf8 "MON"                  // user: %8
  %4 = integer_literal $Builtin.Word, 3           // user: %8
  %5 = integer_literal $Builtin.Int1, -1          // user: %8
  %6 = metatype $@thin String.Type                // user: %8
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %7 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
  %8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %9
  br bb8(%8 : $String)                            // id: %9
  ...... //省略掉其他case的判断,只判断为MON的时候
// %52                                            // user: %53
bb8(%52 : $String):                               // Preds: bb7 bb6 bb5 bb4 bb3 bb2 bb1
  return %52 : $String                            // id: %53
} // end sil function '$s4main4WeekO8rawValueSSvg'
  • 这种情况下枚举的值都是字符串常量,存储的位置应该是在mach-o文件的__TEXT,__cstring,可以看到,当前我们指定enumRawValueString之后,系统自动创建了一块连续的内存空间来默认存放当前case对应的字符串。

这里需要注意的一点就是枚举值和RawValue是两个不同的东⻄,我们没有办法把一个枚举值分配一个String的变量,即使这个enumString类型的

关联值

  • Swift中,我们可以通过枚举值表达一个复杂的信息,就是关联值,通过枚举来表达一个形状,有圆形,⻓方形,圆形有半径,⻓方形有宽,高,这个时候关联值就显得非常有用了
enum Shape {
    case circle(radious:Double)
    case rectangle(width:Double,height:Double)
}
  • 我们使用了关联值之后,就没有RawValue,因为我们每个case都有一组值来表达,所以不需要使用RawValue,这里我们使用的radiouswidthheight是我们自己取的标签,可以省略的
  • 创建一个关联值的枚举值
enum Shape {
    case circle(radious:Double)
    case rectangle(width:Double,height:Double)
    
}

var circle = Shape.circle(radious: 10.0)
var rectangle = Shape.rectangle(width: 15.0, height: 18.0)

枚举的其他用法

模式匹配

  • 使用switch匹配枚举值时,需要遍历所有可能的情况,不然编译器会报错,如果不想匹配所有的case,可以使用defalut关键字代表默认的情况。
  • 关联值的匹配
enum Shape {
    case circle(radious:Double)
    case rectangle(width:Double,height:Double)
    
}

var circle = Shape.circle(radious: 10.0)
var rectangle = Shape.rectangle(width: 15.0, height: 18.0)

switch circle {
    case let .circle(radious): print("circle radious\(radious)")
    case let .rectangle(width, height) : print("rectangle width:\(width),height:\(height)")
}
switch rectangle {
    case .circle(let radious): print("circle radious\(radious)")
    case .rectangle(let width,let height) : print("rectangle width:\(width),height:\(height)")
}
  • 匹配单个关联值
if case let Shape.circle(radious) = circle {
    print(radious)
}
  • 不同的case的相同的关联值
enum Shape{ 
	case circle(radious: Double, diameter: Double) 
	case rectangle(width: Double, height: Double) 
	case square(width: Double, width: Double) 
}
let shape = Shape.circle(radious: 10.0, diameter: 20.0) 
switch shape {
	case let .circle(x, 20.0), let .square(x, 20.0): 
		print(x) 
    default: 
		break 
} 
  • 使用通配符
switch shape {
	case let .circle(_, x), let .square(x, _):
		print(x) 
    default:
		break
}

switch shape {
	case let .circle(x, y), let .rectangle(y, x):
		print("x=\(x),y=\(y)") 
    default:
		break
}

枚举的嵌套

  • 枚举中嵌套枚举
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)
}

结构体中的嵌套

  • 结构体中嵌套枚举
struct Skill {
    enum KeyType {
        case up
        case down
        case left
        case right
    }
    
    let key : KeyType
}

枚举中包含属性和方法

  • 枚举中能够包含计算属性,类型属性,不能包含存储属性
  • 枚举中定义实例方法、static方法
enum Week : Int {
    case MON
    case TUS
    case WED
    case THU
    case FRI
    case SAT
    case SUN
    
    mutating func nextDay() {
        if self == .SUN {
            self = Week(rawValue: 1)!
        }else{
            self = Week(rawValue: 1+self.rawValue)!
        }
    }
    static func doSomething() {
        print("111\(self.init(rawValue: 1))")
    }
    
}

枚举的大小

  • 一个case时候的大小
enum noMean {
    case a
}

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

// 打印结果为:0 1
  • 普通的case没有关联值的时候,枚举的大小为一个字节,一个字节所能表示的最大值为255,如果我们的case大于255的时候大小就会由Int8->Int16,如果不够就会继续扩大2倍
enum noMean {
    case a
    case b
}

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

// 打印结果为:1 1

// 修改rawValue的类型 打印结果不变
enum noMean : String {
    case a
    case b
}
// 打印结果为:1 1
  • 有关联值时候的枚举的大小取决于最大的case内存大小,但是大家看每次应该是16, 24, 32 这样的偶数,但是这里每次都是多加1,这里是不是就是我们当前case的值啊,占用 1 字节,只有一个case的时候,默认size为0。
enum Shape{
    case circle(radious: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
// 打印结果为 8,8

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
// 打印结果为17,24
  • 打印内存结构
需要注意的一点是,由于字节对齐的原因,有时候`case`的1字节会被编译器优化加到前面的位置上,它的大小不会再+1
  • 通过一段代码来验证下刚刚的结论
  • 修改circleradious的类型为Int
  • 总结
    • 1,RawValue的枚举大小默认是一字节(UInt8),也就意味着当前最大能存储的大小是 255。如果超过这个大小,当前枚举的大小就会从UInt8->UInt16依次类推
    • 2,关联值的枚举大小与最大的case的内存大小相关。

indirect关键字

  • 我们想通过枚举实现一个链表,就需要使用indirect关键字
  • 通过编译器的修正后的写法如下
indirect enum List<T> {
    case end
    case node(T, next : List<T>)
}
或
enum List<T> {
    case end
    indirect case node(T, next : List<T>)
}
  • 枚举是值类型,也就意味着它的大小在编译期就确定了,那么这个过程中对于我们当前这个List的大小就不能确定,从系统的⻆度,我不知道当前要给这个枚举分配多大的内存空间。所以怎么办?官方文档上有如下解释

You indicate that an enumeration case is recursive by writing indi rect before it, which tells the compiler to insert the necessary l ayer of indirection.

  • 打印下它的大小
  • 打印下实例对象的内存结构
  • 查看sil文件,alloc_box的本质就是调用swift_allocObjet,所以indirect关键字其实就是通知编译器,我当前的枚举是递归的,自然而然大小也就不确定,所以赶紧给我分配一块堆区的内存空间存放。

Swift枚举和OC混编

oc使用Swift枚举

  • 在oc中枚举仅仅是一个整数值,如果想要将Swift的枚举暴露给oc,需要添加@objc关键字,并且当前枚举应该是Int类型的
@objc enum Week : Int{
    case MON
    case TUE
}
  • xxx-Swift.h头文件中的定义,导入头文件就可以在oc中使用了
typedef SWIFT_ENUM(NSInteger, Week, closed) {
  WeekMON = 0,
  WeekTUE = 1,
};

Swift使用OC枚举

  • 通过oc定义下面3种枚举
NS_ENUM(NSInteger, OCENUM){
    Value1,
    Value2
};

typedef enum{
    Num1,
    Num2
} OCNum;

typedef NS_ENUM(NSInteger, CEnum) {
    CEnumInvalid = 0,
    CEnumA = 1,
    CEnumB,
    CEnumC
};
  • 查看对应的Swift头文件
public var OCENUM: OCENUM
public enum OCENUM : Int {
    case Value1 = 0
    case Value2 = 1
}

public struct OCNum : Equatable, RawRepresentable {
    public init(_ rawValue: UInt32)
    public init(rawValue: UInt32)
    public var rawValue: UInt32
}

public enum CEnum : Int {
    case invalid = 0
    case A = 1
    case B = 2
    case C = 3
}