Swift基础语法(四)枚举和可选项的认识

Swift基础语法文章汇总

本文介绍枚举类型和可选项的认识和使用

主要内容:

  1. 枚举的简单使用
  2. 枚举的内存计算
  3. 可选项的简单使用

1、枚举的简单使用

1.1 基本定义

代码:

//1、定义一个枚举类型
enum Dir1 : String{
    case north
    case south
    case east
    case west
}

//2、简写
enum Dir2{
    case north,south,east,west
}

//3、使用
let north = Dir1.north.rawValue
print("wy:\(north)")//wy:north

说明:

  • 注意书写方式,enum表示枚举
  • 每一项可以用case来表示
  • 也可以简写,只写一个case,后面的成员用,隔开

注意:

  • 可以确定类型,也可以不确定
  • 可以直接获取每个成员的值

1.2 关联值

有时将枚举的成员值跟其他类型的值关联存储在一起,需要让成员值来标识关联值是哪一个成员
代码:

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

//可以给枚举成员赋值
var date = Date.digit(year: 2022, month: 2, day: 10)
//此处date的枚举类型已经确定,所以可以直接使用.string(),而不需要加上Date进行说明
date = .string("2022-02-10")

/*
 1、此处通过传入的date得知枚举类型,所以在判断时可以直接使用.digit()/.string()
 2、在switch判断时,可以获取到其中的关联值进行使用
 */
switch date {
case .digit(let year,let month,let day):
    print(year,month,day)
case let .string(value):
    print(value)
}

说明:

  • 在定义枚举的成员中有两个成员,一个是digit,一个是string类型,两种表示日期的方式
  • 使用关联值在成员中不仅可以表示它是哪一种表示类型,还可以直接表示这个值的具体数据
  • 比如成员后面加上(),()里面加上数据的数据类型,之后在外界使用这个成员的时候就可以直接赋值了
  • 在使用时,可以直接获取到成员的具体值

注意:

  • 区别于其他语言的枚举值只能区分不同类型,这里的枚举值可以关联这个成员表示的具体数据
  • 枚举的成员值和枚举值是关联起来的,这样就可以通过成员值来查找枚举值
  • 如果一个枚举变量已经确定了枚举类型,那么就可以直接使用.digit()/.string()来赋值

1.3 原始值

枚举成员可以使用相同类型的默认值预先对应,这个默认值叫做原始值,也就是这个成员所对应的值不会被外界赋值。

代码:

//1、原始值:定义时赋值
enum Grade : String {
    case perfect = "A"
    case great = "B"
    case good = "C"
    case bad = "D"
}
print(Grade.perfect.rawValue)//A

//如果是数值类型,默认是有顺序的数值
enum Season : Int {
    case spring = 1 ,summer, autumn = 4, winter
}
print(Season.summer.rawValue)//2

说明:

  • 此处是在定义时赋一个原始值,以后在外界使用时就一直是这个原始值,而且外界无法更改
  • 在获取原始值需要使用rawValue
  • 如果是字符串,则原始值为字符串成员本身
  • 如果是数值,则按顺序递增,并且第一个成员为0

隐式原始值

//2、原始值:隐式原始值
//如果是字符串类型,则默认值是其本身
enum Dir3 : String{
    case north
    case south
    case east
    case west
}

//等价于
enum Dir4 : String{
    case north = "north"
    case south = "south"
    case east = "east"
    case west = "west"
}

说明:

  • 如果没有在定义时赋原始值,也是有默认原始值的
  • 如果值是字符串类型,这个默认原始值就是其成员的字符串本身
  • 如果值是Int类型,这个默认原始值就是数值

注意:

  • 原始值当不显式赋值时,会有其默认原始值

1.4 递归枚举

代码:

//递归枚举,声明到enum前
indirect enum ArithExpr {
    case number(Int)
    case sum(ArithExpr,ArithExpr)
    case difference(ArithExpr,ArithExpr)
}

//递归枚举,声明到case前
enum ArithExpr {
    case number(Int)
    indirect case sum(ArithExpr,ArithExpr)
    indirect case difference(ArithExpr,ArithExpr)
}

//定义枚举变量,可以直接通过number来定义,也可以通过sum/difference递归定义
let ten = ArithExpr.number(10)
let five = ArithExpr.number(5)
let sum = ArithExpr.sum(ten, five)
let difference = ArithExpr.difference(ten, five)

//取出枚举变量中的数据进行加减运算
//需要注意的是:如果传入sum/difference需要递归取出number值
func calculate(_ expr: ArithExpr) -> Int {
    switch expr {
    case let .number(value):
        return value
    case let .sum(left, right):
        return calculate(left) + calculate(right)
    case let .difference(left, right):
        return calculate(left) - calculate(right)
    }
}

calculate(ten)//10
calculate(five)//5
calculate(sum)//15
calculate(difference)//5

说明:

  • 在ArithExpr枚举中的sum和difference中关联值也是ArithExpr枚举,这就是递归枚举
  • 在枚举前加上indirect就表示该枚举可以被成员递归使用
  • 也可以简写成第二种方式,只在需要使用递归枚举的成员前加上indirect
  • 在下面的使用中就可以看到使用sum时直接传入了five和four两个枚举
  • 之后在使用时,可以获取到number中的具体值来计算。
  • 在calculate中也需要递归取出number值才能计算。

2、枚举内存大小

原始值和枚举值计算大小的方式是不一样的。

关于内存对齐可以看我的另一篇博客苹果的内存对齐原理

2.1 原始值内存计算

代码:

enum Dir1 : String{
    case north
    case south
    case east
    case west
}

MemoryLayout.stride//分配空间大小
MemoryLayout.size//实际占用空间大小

运行结果:

原始值内存.png

说明:

  • 枚举中所有原始值占用的内存大小是1
  • 这是因为原始值都是通过序号来存储的,并没有具体值存储在枚举中,枚举中仅存储有序号

2.2 关联值内存计算

代码:

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

//可以给枚举成员赋值
var date = Date.digit(year: 2022, month: 2, day: 10)
//此处date的枚举类型已经确定,所以可以直接使用.string(),而不需要加上Date进行说明
date = .string("2022-02-10")

MemoryLayout.stride(ofValue: date)//分配空间大小
MemoryLayout.size(ofValue: date)//实际占用空间大小

运行结果:

关联值内存大小.png

说明:

  • 枚举中关联值占用的内存大小是数据类型的大小加上一个字节
  • 关联值是直接存储在枚举中的,因此需要加上成员的数据类型的大小
  • 并且还有序号来关联这个关联值。因此还必须加上一个字节
  • 此处digit占有24个字节,string占有8个字节,但因为他们是互斥关系,所以选用最大的24个字节。
  • 枚举必须有一个字节用来存储成员的序号,因此是24+1= 25个字节
  • 而通过内存对齐,需要分配32个字节

2.3 枚举内存计算

代码:

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

MemoryLayout.stride//分配空间大小
MemoryLayout.size//实际占用空间大小
MemoryLayout.alignment//对齐参数

运行结果:

运行结果.png

说明:

  • 枚举Password共有两个成员number和other
    • number是关联值,关联值直接存储在枚举中,因此number占用大小为32个字节
    • other是原始值,原始值在枚举中只有序号,不占用实际空间
  • 可以直接计算类型的内存空间,也可以计算变量的数据类型的内存空间
    • 可以通过MemroyLayout方式来获取数据类型的占用内存空间大小
    • MemroyLayout.size(ofValue:pwd)这种方式可以获取pwd变量的数据类型的占用内存空间大小
  • 有两种空间计算
    • 第一种是stride,这种计算得到的是系统分配的空间大小
    • 第二种是size,这种计算得到的是实际占用的空间大小

注意:

  • 着重注意原始值和关联值的大小计算是不一样的,关联值存储在枚举中,原始值存储并非存储在枚举中,而是通过序号来索引的
  • 所有原始值占用的大小合起来是1,关联值的大小是数据类型的大小

2.4 总结

  • 关联值存储在枚举中,会占用枚举变量的内存,原始值不占用枚举变量的内存
  • 枚举必须有一个字节用来存储枚举成员的序号
  • 枚举的关联值占用内存大小选用最大成员的内存大小
  • 分配空间大小是8字节对齐

3、可选项

可选项,也叫可选类型,可选项的目的就是允许将值设置为nil,因此如果想要获取或判断的值可能包含nil,那么它就应该用可选项来处理。
在类型的后面加上?就可以设置为可选类型。

3.1 基本使用

代码:

//1、可选项的认识
var name: String? = "wy"
//可选项可以设置为nil
name = nil

//2、可选项如果没有赋初始值,则说明是nil
var age: Int?
age = 10
age = nil

说明:

  • 可选类型的数据可以设置为nil
  • 正常情况下数据类型是没有默认值的,在使用前必须要赋值
  • 而如果设置为可选项,就有默认值,默认值就是nil

理解:

  • 可选项可以理解为是一个盒子,如果赋值,里面就装有一个数据,强制解包就是获取盒子内的数据。当然此时盒子内仍然有数据
  • 如果不为nil,那么盒子里装的是:被包装类型的数据
  • 如果为nil,那么它是个空盒子

3.2 强制解包

强制解包就是将可选项的变量所包含的数据取出来

代码:

//3、强制解包取出盒子中的数据(通过!来强制解包)
var age: Int? = 10
var ageInt: Int = age!
ageInt += 10

说明:

  • 强制解包就是将可选项的变量所包含的数据取出来
  • 通过在可选项变量后加上!就可以将其解包出来

注意:

  • 如果对值为nil的可选项(空盒子)进行解包,会报错,因为Swift正常类型中是没有nil这个概念的
  • 此时可选项的变量仍然包含有该数据,并未删除

3.3 可选项绑定

正常使用方式:

/*
 正常判断方式
 1、number为可选项,因此可以判断是否为nil
 2、在获取数据时需要强制解包
 */
let number = Int("123")
if number != nil {
    print("wy:字符串转换整数成功:\(number!)")
} else {
    print("wy:字符串转换中整数失败")
}

说明:

  • 这里可以看到因为Int("123")强行将字符串转换为Int类型,这里有可能转换失败,所以这个操作就有可能返回nil,因此这里返回的应当是可选类型
  • number得到的就是一个可选类型,因此这里可以用nil判断,如果不为nil,就可以解包获取包含的数据

注意:

  • 除了我们认为的设置为可选项,还有很多情况是系统自动设置为可选项的
  • 比如在这里强转类型后有可能转换失败,返回nil,所以获取到的数据应当是可选项。
  • 后续我们在返回一个数据时也应当这样来做,无法判断是否成功,就可以返回为可选项
  • 可选项在判断语句中使用逗号隔开,不能用&&

可选项绑定方式:

//可选项绑定方式
/*
 1、可以直接在判断条件中获取可选项
 2、特别要注意的是,此时会自动解包,因此直接使用number,而无需强制解包
 */
if let number = Int("123") {
    print("wy:字符串转换整数成功:\(number)")
} else {
    print("wy:字符串转换中整数失败")
}

说明:

  • 也可以直接写到if语句中
  • 使用可选项绑定来判断可选项是否包含值
  • 如果包含就会自动解包,把解包后的值赋给一个变量或常量,并返回True,因此这里的Number就是解包后的数据
  • 而且这个number只能作用在true的作用域,如果是不包含值根本不会自动解包,所以number并不会作用在false中

判断条件中:

if let first = Int("4"),let second = Int("42"), first < second && second < 100 {
    print("\(first) < \(second) < 100")
}

//等价于:
if let first = Int("4"){
    if let second = Int("42") {
        if first < second && second < 100 {
            print("\(first) < \(second) < 100")
        }
    }
}

3.4 空合并运算符

合并方式:
a ?? b

  • a 是可选项
  • b 是可选项 或者 不是可选项
  • b 跟 a 的存储类型必须相同
  • 如果 a 不为nil,就返回 a
  • 如果 a 为nil,就返回 b
  • 如果 b 不是可选项,返回 a 时会自动解包

代码:

//5、空合并运算符
//a和b都是可选项
let a: Int? = 1
let b: Int? = 2
let c = a ?? b
print(c)//Optional(1)

//a和b都是可选项,a是nil
let a: Int? = nil
let b: Int? = 2
let c = a ?? b
print(c)//Optional(2)

//a和b都是可选项,a和b都是nil
let a: Int? = nil
let b: Int? = nil
let c = a ?? b
print(c)//nil

//a是可选项,b不是可选项
let a: Int? = 1
let b: Int = 2
let c = a ?? b
print(c)//1

//a是可选项,b不是可选项,且a是nil
let a: Int? = nil
let b: Int = 2
let c = a ?? b
print(c)//2

说明:

  • 只有当b不是可选项的是,返回a时才会自动解包
  • 如果b是可选项,返回a或b的时候并不会自动解包
  • a和b的存储类型一定要一样

注意:

3.5 隐式解包

可以直接使用!实现隐式解包

代码:

//6、隐式解包,num1既可以当做直接数,也可以当做可选项判断是否为nil
let num1: Int! = 10
let num2: Int = num1
if num1 != nil {
    print(num1+6)
}

说明:

  • 在某些情况下,可选项一旦被设定值之后,就会一直拥有值
  • 在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为它能确定每次访问的时候都有值
  • 可以在类型后面加个感叹号 ! 定义一个可以隐式解包的可选项

3.6 多重可选项

可选项可以嵌套使用

代码:

//7、多重可选项
var num1: Int? = 10
var num2: Int?? = num1
var num3: Int?? = 10

说明:

  • 可选项可以包装数据类型,也可以包装可选项
  • 并且这里num3使用??也是多重可选项,因此先对10进行包装,再将其包装到num3上

注意:

  • 如果是nil,则不管几层包装都是空盒子

3.7 字符串插值

可选项在字符串插值或直接打印时,编译期会发出警告

警告:

警告.png

代码:

//8、字符串插值
var wyAge: Int? = 10
print("My age is \(wyAge!)")
print("My age is \(String(describing: wyAge))")
print("My age is \(wyAge ?? 0)")

说明:

  • 可选项只是一个盒子,我们所需要的是盒子里的数据,所以编译期在发现直接打印盒子时就会报错
  • 我们可以取出盒子中的数据再进行打印

注意:

  • 如果是nil,则不管几层包装都是空盒子

你可能感兴趣的:(Swift基础语法(四)枚举和可选项的认识)