swift-枚举、可选项、结构体

Posted by sunzhongliang on December 29, 2019

枚举

swift中的枚举跟C语言、OC当中的枚举有很大区别,也强大很多(C语言、OC当中只能定义为int类型,而swift当中可以定义复杂类型)
定义枚举:

enum Direction {
    case north
    case south
    case east
    case west
}

// 也可以这样写,效果一样
enum Direction {
    case north, south, east, west
}

// 使用方式
var dir = Direction.east
dir = Direction.west
dir = .north
print(dir)  // 输出north

关联值

有时会将枚举的成员值跟其他类型的关联存储在一起,会非常有用

// 定义分数枚举
enum Score {
    case points(Int) // 具体的分数值
    case grade(Character)  // 级别,A/B/C
}

// 使用
var score = Score.points(99)
print(score) // 输出99
score = Score.grade("A") 
print(score)  // 输出A

也可以表示更复杂类型:

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

var d = Date.digit(year: 2019, month: 12, day: 29)
var s = Date.string("2019-12-29")

原始值

枚举成员可以使用相同类型的默认值预先关联,这个默认值就叫:原始值

// 可以认为PokerSuit枚举值是一个原始值类型
enum PokerSuit : Character {
    case spade = "♠"
    case heart = "❤"
    case diamond = "♦"
    case club = "♣"
}
var suit = PokerSuit.spade
print(suit) // 输出spade
print(suit.rawValue) // 输出♠
print(PokerSuit.club.rawValue) // 输出♣

// Int 类型的默认原始值
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

递归枚举

// 枚举类型的成员里面也有枚举类型
indirect enum ArithExpr {
    case number(Int)
    case sum(ArithExpr, ArithExpr)
    case difference(ArithExpr, ArithExpr)
}
// 使用
let five = ArithExpr.number(5)
let four = ArithExpr.number(4)
let sum = ArithExpr.sum(five, four)
let difference = ArithExpr.difference(sum, five)

可选项

可选项,一般也叫做可选类型,它允许将值设置为nil
比如,在swift这样定义变量并修改会编译失败,默认情况下是不允许设置为nil值的,只有在声明为可选项之后才能设置为nil值

var str: String = "123"
str = nil   // 编译报错:'nil' cannot be assigned to type 'String' 

如何声明为可选项呢?在类型后面加个问号?就可以了

var str: String? = "123"
str = nil  // 此时就可以赋值为nil值了

var age: Int? = 1
age = nil  // 设置为可选项之后,Int类型的也可以赋值为nil值了

// 如果Int类型的不初始化且不设置为可选值,就会编译失败
var age: Int
print(age) // Variable 'age' used before being initialized

// Int类型的设置为可选值之后,就不会编译失败
var age1: Int?
print(age1)  // 输出nil

在实际工作当中,比如要实现一个函数,根据下标获取数组元素的具体值,但如果下标越界可以返回nil值来以防止出现非法的数据等等

var array = [1,11,22,33]

func getItemWithIndex(_ index: Int) -> Int? {
    if (index < 0 || index >= array.count) {
        return nil
    }
    return array[index]
}

print(getItemWithIndex(1))

强制解包

可选项是对其他类型的一种包装,可以将它理解为一个盒子
如果可选项的值是nil,可以理解为一个空盒子
如果不为nil,可以理解为盒子里装的是被包装类型的数据

如果要从可选项当中取出被包装的数据,需要使用感叹号! 进行强制解包

var age: Int? = 10
var newAge: Int = age!
newAge += 10

print(newAge) // 输出20

可选项绑定

可以使用可选项绑定来判断可选项是否包含nil值
如果包含就自动解包,把值赋值给一个常量(let)或者变量(var),并返回true或者false

if let number = Int("1232") { // 如果1232转换成功就会赋值给number常量并返回true
    print("字符串转换Int成功: \(number)") // number常量作用域仅仅只限于这里
}
else {
    print("字符串转换Int失败")
}

// 再来一个复杂的
// 如果4转换为Int成功
if let first = Int("4") {
    // 如果32转换为Int成功
    if let second = Int("32") {
        // 如果first<second 并且second<=100
        if first < second && second <= 100 {
            print("\(first) < \(second) < 100")
        }
    }
}

// 等价于这种写法, 如果包含可选项绑定表达式,是不能用&&来写的,只能用,
if let first = Int("4"),
    let second = Int("32"),
    first < second && second <= 100 {
    print("\(first) < \(second) < 100")
}

while 循环中使用可选项绑定

需求:遍历数组,将遇到的正数加起来,如果遇到负数或者是非数字,则停止遍历

var strs = ["1","11","a","-12","123","0"]

var index = 0 // 定义遍历的下标
var sum = 0 // 总和
// 根据下标取出strs的值转换为Int,转换成功赋值给常量num, 并且num>0
while let num = Int(strs[index]), num > 0 {
    sum += num
    index += 1
}
print(sum) // 输出12

空合并运算符(Nil-Coalescing Operator)

空合并运算符用??来表示, 比如a ?? b
含义是:如果a不为nil就返回a,如果a为nil就返回b,类似于三元运算符
a一定要是可选项
b是可选项,或者不是可选项
ab的类型一定要相同
如果b不是可选项,返回a时会自动解包

guard 语句

guard语句的条件为false时,就会执行大括号里面的代码
guard语句的条件为true时,就会跳过guard语句

grard 条件 else {
    // do something...
    退出作用域
    // 使用return、break,continue,throw error 等
}

guard语句特别适合用来提前退出
当使用guard语句进行可选项绑定时,绑定的常量(let),变量(var)也能在外层作用域中使用
比如实现一个login登录方法,传入一个字典,如果username和password都传入则登录成功,用guard来实现则非常精简

func login(_ info: [String : String]) {
    guard let username = info["username"] else {
        print("请输入用户名")
        return
    }
    
    guard let password = info["password"] else {
        print("请输入密码")
        return
    }
    
    print("验证成功:\(username), \(password)")
}

login(["username" : "jack"])

隐式解包(Impliciyly Unwrapped Optional)

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

// 隐式解包的可选项
let num1: Int! = 10
let num2: Int = num1
print(num2)

结构体

Swift标准库中,绝大多数的公开类型都是结构体类型,而枚举和类只占一小部分,比如Bool, Int, Double, String, Array, Dictionary等类型。
结构体定义语法:

struct Date {
    var year: Int
    var month: Int
    var day: Int
}
var date = Date(year: 2019, month: 1, day: 1)

所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)

结构器的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值, 如:

struct Point {
    var x: Int
    var y: Int
}
var p1 = Point(x:10, y:20)
var p1 = Point(y:20) // 编译报错:Missing argument for parameter 'x' in call
var p1 = Point(x:10) // 编译报错:Missing argument for parameter 'y' in call
var p1 = Point()     // 编译报错:Missing argument for parameter 'x' in call

// 这种情况初始化器只要可以保证结构体有初始值就不会报错
struct Point {
    var x: Int = 0
    var y: Int
}
var p1 = Point(x:10, y:20)
var p1 = Point(y:20)
var p1 = Point(x:10) // 编译报错:Missing argument for parameter 'y' in call
var p1 = Point()     // 编译报错:Missing argument for parameter 'x' in call

// 这种情况,可选项都有个默认值nil, 因此不会报错
struct Point {
    var x: Int?
    var y: Int?
}
var p1 = Point(x:10, y:20)
var p1 = Point(y:20)
var p1 = Point(x:10)
var p1 = Point()

自定义初始化器
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器,如:

struct Point {
    var x: Int = 0
    var y: Int = 0
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}
var p1 = Point(x:10, y:20)
var p1 = Point(y:20) // 编译报错:Missing argument for parameter 'x' in call
var p1 = Point(x:10) // 编译报错:Missing argument for parameter 'y' in call
var p1 = Point()     // 编译报错:Missing argument for parameter 'x' in call

本来有默认值,但因为自己实现了初始化器,编译器不会自动帮我们生成默认值了

在swift里面,初始化器的格式非常简单,方法名叫:init 而且前面不用写func 后面也不用写返回值

窥探初始化器的本质

以下两段代码完全等效

struct Point {
    var x: Int
    var y: Int
    init() {
        self.x = 0
        self.y = 0
    }
}

struct Point {
    var x: Int = 0
    var y: Int = 0
}

的定义和结构体相似,但编译器没有为类自动生成可以传入成员值的初始化器

class Point {
    var x: Int = 0
    var y: Int = 0
}
let p1 = Point()
let p2 = Point(x: 10, y: 20) // Argument passed to call that takes no arguments
let p3 = Point(x: 10)   // Argument passed to call that takes no arguments
let p4 = Point(y: 20)   // Argument passed to call that takes no arguments

如果类成员没有默认值,编译就会报错

class Point { // Class 'Point' has no initializers
    var x: Int
    var y: Int
}
let p1 = Point() // 'Point' cannot be constructed because it has no accessible initializers

类的初始化器

如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器, 成员的初始化就是在这个初始化器中完成的,以下两段代码完全等效

// 代码1
class Point {
    var x: Int = 10
    var y: Int = 20
}
let p1 = Point()

// 代码2
class Point {
    var x: Int
    var y: Int
    init() {
        x = 10
        y = 20
    }
}
let p2 = Point()

结构体和类的本质区别

  1. 结构体是值类型(枚举也是值类型), 是引用类型(指针类型)
    • 值类型被赋予给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝操作
    • 引用类型在被赋予给一个变量、常量或者被传递给一个函数的时候,其值不会被拷贝,引用的是已存在的实例本身而不是拷贝操作。
  2. 所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始化可以通过属性的名称传递到成员逐一构造器之中;类实例没有默认的成员逐一构造器。

共同点:

  • 都可以定义属性用于存储值
  • 都可以定义方法用于提供功能
  • 定义下标操作使得可以通过下标语法来访问实例所包含的值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 实现协议以提供某种标准功能

如果值类型是在函数里面创建的,那么内存就是在栈空间里,

class Size {
    var width = 1
    var height = 2
}

struct Point {
    var x = 3
    var y = 4
}

// 在函数里面调用结构体和类
func test() {
    var size = Size(); // 调用类
    var point = Point(); // 调用结构体
}

值类型、引用类型的let的区别
如图,同样用let声明的结构体和类
共同点:不能改声明的结构体对象和类对象本身 不同点:结构体不可以修改声明的结构体对象的成员,但类可以修改声明的类对象的成员

MemberLayout

可以使用MemberLayout获取数据类型占用的内存大小

var age = 10

// MemoryLayout支持泛型
MemoryLayout<Int>.stride  // 分配占用的空间大小;8个字节
MemoryLayout<Int>.size // 实际用到的空间大小;8个字节
MemoryLayout<Int>.alignment // 对齐参数;8个字节
// 也可以打印具体的变量
MemoryLayout.size(ofValue: age) // 8个字节

本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.