Swift5 基础(二)可选项、结构体、类、闭包

Swift5 基础教程与进阶合集

一、 可选项(Optional)

定义

  • 可选项,一般也叫可选类型,它允许将值设置为nil
  • 在类型名称后面加个问好?来定义一个可选项

强制解包(Forced Unwrapping)

  • 如果要从可选项中取出被包装的数据,需要使用感叹号!进行强制解包
  • 如果对值为nil的可选项(空盒子)进行强制解包,将会产生运行时错误
var a : String?
a!//error: Fatal error: Unexpectedly found nil while unwrapping an Optional value

可选值绑定(Optional Binding)

  • 可以使用可选项绑定类判断可选项是否包含之,如果包含就自动解包,把值给一个临时的常量(let)或者变量(var),并返回true,否则返回false
  • 可选绑定可以在if、while、switch-case、guard中
if let number = Int("123"){
    print("字符串转换整形成功:\(number)") //字符串转换整形成功:123
} else {
    print("字符串转化整形失败")
}

enum Season : Int{
    case Spring = 1, summer,autumn,winter
}
var season : Season? = .summer
/*
 这里可以用var来接收,表示可修改
 可以使用同名变量接收
 */
if var season = season {
    season = .winter
    print(season.rawValue)//4
}

多个可选绑定

  • 多个可选绑定可写在一排,用逗号间隔开,并且还可以加上逻辑表达式,它们的关系是一个逻辑与(&&)的关系
if let first = Int("4"),let second = Int("5") , first < second,second < 10{
    print("成功了,first:\(first),second:\(second)")//成功了,first:4,second:5
}

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

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T

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

guard语句

guard 条件 else {
    //do something
    退出当前作用域
    //return、break、continue、throw error
}
  • 当guard语句的条件为false时,就会执行大括号里面的代码
  • 当guard语句的条件为true时,就会跳过guard语句执行后面的语句
  • guard特别适合用来提前退出
  • 当使用guard语句进行可选绑定时,绑定的常量(let)、变量(var)如果绑定成功,可以在guard后面的语句中使用

隐式解包(Implicitly Unwrapped Optional)

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

可选项字符串插值

  • 可选项在字符串插值或者直接打印时,编译器会发出警告
var age : Int? = 18
print("My age is \(age)")//String interpolation produces a debug description for an optional value; did you mean to make this explicit?
  • 以下三种方法可以消除警告
print("My age is \(age!)")//My age is 18
print("My age is \(String(describing: age))")//My age is Optional(18)
print("My age is \(age ?? 0)")//My age is 18

可选项的本质是枚举

let a: Optional<Int> = 1

如上代码所示,我们的点进源码的定义处,可以看到:

@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {

/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none

/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
}

所以,可选项的本质是枚举,它有两个case,分别是none和some,如果可选值为nil,它是none,如果有值,它是some。

可选嵌套

有如下代码

let a: Int? = 1
let b: Int?? = a
let c: Int??? = b

我们使用fr v -R查看其结构

(lldb) fr v -R a
(Swift.Optional<Swift.Int>) a = some {
  some = {
    _value = 1
  }
}
(lldb) fr v -R b
(Swift.Optional<Swift.Optional<Swift.Int>>) b = some {
  some = some {
    some = {
      _value = 1
    }
  }
}
(lldb) fr v -R c
(Swift.Optional<Swift.Optional<Swift.Int?>>) c = some {
  some = some {
    some = some {
      some = {
        _value = 1
      }
    }
  }
}

可以看出,它是一层层的可选值封装,就像下面的二叉树结构:


从上图中我们可以看到,none可以出现在任意一层,那么在每一层的效果一样吗?
我们看如下代码:

let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = nil

同样查看内存结构

(lldb) fr v -R a
(Swift.Optional<Swift.Int>) a = none {
  some = {
    _value = 0
  }
}
(lldb) fr v -R b
(Swift.Optional<Swift.Optional<Swift.Int>>) b = some {
  some = none {
    some = {
      _value = 0
    }
  }
}
(lldb) fr v -R c
(Swift.Optional<Swift.Optional<Swift.Int?>>) c = some {
  some = some {
    some = none {
      some = {
        _value = 0
      }
    }
  }
}
(lldb) fr v -R d
(Swift.Optional<Swift.Optional<Swift.Int?>>) d = none {
  some = some {
    some = some {
      some = {
        _value = 0
      }
    }
  }
}

我们看到,b和c都是some,而d是none,拿c来说,c是一个Optional.some(Optional.some(Optional.none)),而d因为是直接赋值为nil,所以它是一个Optional.none.

假如这个时候我们进行可选绑定

let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = nil

if let _ = a{
    print("a不为空")
}
if let _ = b{
    print("b不为空")
}
if let _ = c{
    print("c不为空")
}
if let _ = d{
    print("a不为空")
}
//打印结果
b不为空
c不为空

我们再看这个例子

let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = nil

if a == b{
    print("a和b相等")
}
if b == c{
    print("b和c相等")
}
if a == c{
    print("a和c相等")
}
if c == d {
    print("c和d相等")
}
if a == d{
    print("a和d相等")
}
//打印结果
a和b相等
b和c相等
a和c相等

为什么结果是这样呢,我们查看关于可选值的==的定义

extension Optional : Equatable where Wrapped : Equatable {

    /// Returns a Boolean value indicating whether two optional instances are
    /// equal.
    ///
    /// Use this equal-to operator (`==`) to compare any two optional instances of
    /// a type that conforms to the `Equatable` protocol. The comparison returns
    /// `true` if both arguments are `nil` or if the two arguments wrap values
    /// that are equal. Conversely, the comparison returns `false` if only one of
    /// the arguments is `nil` or if the two arguments wrap values that are not
    /// equal.
    ///
    ///     let group1 = [1, 2, 3, 4, 5]
    ///     let group2 = [1, 3, 5, 7, 9]
    ///     if group1.first == group2.first {
    ///         print("The two groups start the same.")
    ///     }
    ///     // Prints "The two groups start the same."
    ///
    /// You can also use this operator to compare a non-optional value to an
    /// optional that wraps the same type. The non-optional value is wrapped as an
    /// optional before the comparison is made. In the following example, the
    /// `numberToMatch` constant is wrapped as an optional before comparing to the
    /// optional `numberFromString`:
    ///
    ///     let numberToFind: Int = 23
    ///     let numberFromString: Int? = Int("23")      // Optional(23)
    ///     if numberToFind == numberFromString {
    ///         print("It's a match!")
    ///     }
    ///     // Prints "It's a match!"
    ///
    /// An instance that is expressed as a literal can also be used with this
    /// operator. In the next example, an integer literal is compared with the
    /// optional integer `numberFromString`. The literal `23` is inferred as an
    /// `Int` instance and then wrapped as an optional before the comparison is
    /// performed.
    ///
    ///     if 23 == numberFromString {
    ///         print("It's a match!")
    ///     }
    ///     // Prints "It's a match!"
    ///
    /// - Parameters:
    ///   - lhs: An optional value to compare.
    ///   - rhs: Another optional value to compare.
    @inlinable public static func == (lhs: Wrapped?, rhs: Wrapped?) -> Bool
}

再看一段代码

let a: Int? = 1
let b: Int?? = a
let c: Int??? = b
let d: Int??? = 1

if a == b{
    print("a和b相等")
}
if b == c{
    print("b和c相等")
}
if a == c{
    print("a和c相等")
}
if c == d {
    print("c和d相等")
}
if a == d{
    print("a和d相等")
}
//打印结果
a和b相等
b和c相等
a和c相等
c和d相等
a和d相等

==的注释中有这样一段话:

The comparison returns true if both arguments are nil or if the two arguments wrap values that are equal.

翻译过来就是,要返回true,要么是两个都是nil,要么是warp值相等。
按照我们上面的案例,a=1,d=1的时候,它们warp值都是1,所以相等。a=nil,d=nil的时候,a、b、c都是some,所以我们比较它里面的warp值,都是nil,所以相等,而d是none,所以d和所有其他的都不相等。
我们可以测试一下:

let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = Optional<Int>.none

if a == b{
    print("a和b相等")
}
if b == c{
    print("b和c相等")
}
if a == c{
    print("a和c相等")
}
if c == d {
    print("c和d相等")
}
if a == d{
    print("a和d相等")
}
打印结果
a和b相等
b和c相等
a和c相等
c和d相等
a和d相等

结果也和我们的注释一样。

结构体

介绍

  • Swift标准库中,绝大多数的公开类型都是结构体,枚举和类只占很小一部分
  • Bool、Int、Double、String、Array、Dictionary等常见类型都是结构体
struct Date{
    var year: Int = 2020
    var month: Int
    var day: Int
}
var date = Date(year: 2020, month: 1, day: 20)
date = Date(month: 2, day: 22)
  • 所有的结构体都会有编译器自动生成的初始化器(initializer),这个初始化器叫做逐一初始化器,它保证每个成员都会被初始化,若之前已经初始化的,逐一初始化器中可以不传

自定义初始化器

  • 一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其它初始化器了
struct Date{
    var year: Int = 2020
    var month: Int
    var day: Int
    init(year: Int,month: Int,day: Int) {
        self.year = year
        self.month = month
        self.day = day
    }
}
var date = Date(year: 2020, month: 1, day: 20)
date = Date(month: 2, day: 22)//报错 Missing argument for parameter 'year' in call

定义

  • 类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
class Date{
    var year: Int = 2020
    var month: Int
    var day: Int
    init(year: Int,month: Int,day: Int) {
        self.year = year
        self.month = month
        self.day = day
    }
}

类的初始化器

  • 如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器
  • 成员的初始化是在这个初始化器中完成的
class Point {
    var x: Int = 10
    var y: Int = 20
}
let p1 = Point()

class Point{
    var x: Int
    var y: Int
    init() {
        x = 10
        y = 20
    }
}
let p2 = Point()
//上面两段代码是完全等效的

结构体与类的本质区别

  • 结构体是值类型(枚举也是值类型),类是引用类型(指针类型)

值类型

  • 值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份
  • 类似于对文件进行copy、paste操作,产生了全新的文件副本。属于深拷贝(deep copy)
  • 在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术,仅有写操作时,才会真正执行拷贝操作

引用类型

  • 引用类型赋值给var、let或者给函数传参,是将内存地址拷贝一份,指向的是同一个文件,属于浅拷贝(shallow copy)

嵌套类型

struct Poker{
    enum Suit : String {
        case spades,hearts,diamonds,clubs
    }
    enum Rank : Int {
        case tow = 2,three,four,five,six,seven,eight,nine,ten
        case jack,queen,king,ace
    }
}
print(Poker.Suit.hearts.rawValue)//hearts

var rank = Poker.Rank.five
rank = .king
print(rank.rawValue)//13

枚举、结构体、类都可以定义方法

  • 一般把定义在枚举、结构体、类内部的函数,叫做方法

方法占用对象的内存吗?
不占用
方法的本质就是函数
方法、函数都存放在代码段

闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。
相比OC的block,Swift的闭包有很多优化的地方:

  1. 可以根据上下文推断参数和返回值类型
  2. 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
  3. 可以使用简化参数名,如$0,$1(从0开始,表示第i个参数)
  4. 提供了尾随闭包语法(Trailing closure syntax)

闭包表达式(Closure Expression)

  • 在Swift中,可以通过func定义一个函数,也可以闭包表达式定义一个函数,因为函数就是一个特殊的闭包
func sum(_ v1: Int,_ v2: Int) -> Int { v1 + v2 }

var fn = { (v1: Int,v2: Int) -> Int in
    v1 + v2
}
fn(10,20)//30

{
    (参数列表) -> 返回值类型 in
    函数体代码
}

闭包表达式的简写

func exec(v1: Int,v2: Int,fn: (Int,Int) -> Int){
    print(fn(v1,v2))
}
exec(v1: 10, v2: 20,fn: { (v1, v2) -> Int in
    v1 + v2
})
exec(v1: 10, v2: 20,fn : { v1, v2 in
    v1 + v2
})
exec(v1: 10, v2: 20,fn : {$0 + $1})
exec(v1: 10, v2: 20,fn : +)

尾随闭包

  • 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
  • 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
func exec(v1: Int,v2: Int,fn: (Int,Int) -> Int){
    print(fn(v1,v2))
}
exec(v1: 10, v2: 20) {
    $0 + $1
}
  • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要再函数名后面写圆括号
func exec(fn: (Int,Int) -> Int) {
    print(fn(1,2))
}
exec(fn: { $0 + $1 })
exec(){ $0 + $1 }
exec{ $0 + $1 }
  • 忽略参数
func exec(fn: (Int,Int) -> Int) {
    print(fn(1,2))
}
exec{ _,_ in 10 }//10
  • 尾随闭包示例:数组的排序

函数原型
@inlinable public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows

/*
 返回true:i1排在i2前面
 返回false:i1排在i2后面
 */
func compare(i1: Int,i2: Int) -> Bool{
    //大的排在前面
    return i1 > i2
}

array.sort(by: compare)
print(array) //[9, 8, 6, 4, 3, 1]

array.sort { (i1, i2) -> Bool in
    i1 < i2
}
print(array)//[1, 3, 4, 6, 8, 9]

//下方是逐步的简写
//因为已知返回为Bool,可以省略
array.sort { (i1, i2) in i1 < i2 }
//已知参数类型,用$0,$1分别指代第一个第二个参数
array.sort { $0 < $1 }
//其它已知,直接给一个判断条件
array.sort(by: <)

闭包的定义

一个函数和它所捕获的变量/常量环境组合起来,成为闭包

  • 一般指定义在函数内部的函数
  • 一般它捕获的是外层函数的局部变量/常量
  • 闭包的内存在堆空间
  • 闭包是引用类型(指针类型)

注意
如果返回值是函数类型,那么参数的修饰要保持统一

func add(_ num: Int) -> (inout Int) -> Void{
    func plus(v: inout Int){
        v += num
    }
    return plus
}

捕获值

闭包可以在其定义的上下文中捕获常量或变量
即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内应用和修改这些值

//案例一:
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementor
}

let incrementByTen = makeIncrementor(forIncrement: 10)
// 返回的值为10
print(incrementByTen())
// 返回的值为20
print(incrementByTen())
// 返回的值为30
print(incrementByTen())

//案例二:
var num = 10
let ss = { () -> Int in
    num += 10
    return num
}
print(ss())//20
print(num)//20
print(ss())//30
print(num)//30
num = 100
print(ss())//110
print(num)//110
print(ss())//120
print(num)//120

逃逸闭包(@escaping)

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。

举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:

//因为回调在函数返回后才返回,所以必须加上@escaping标记
func download(_ completionHandler : @escaping ()->Void){
    DispatchQueue.global().async {
        Thread.sleep(forTimeInterval: 2)
        DispatchQueue.main.async {
            completionHandler()
        }
    }
}

自动闭包(@autoclosure)

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当 condition 参数为 false 时被计算求值。

自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。

func returnSelfOrOther(_ num: Int,_ other: @autoclosure () -> Int) -> Int{
    num >= 0 ? num : other()
}

let result = returnSelfOrOther(-1) { 20 }
print(result) // 20
  • 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行
  • @autoclosure会自动将20封装成闭包{20}
  • @autoclosure只支持()->T格式的参数
  • @autoclosure并非只支持最后一个参数
  • 空合运算符??使用了@autoclosure技术
  • 有@autoclosure,无@autoclosure,构成了函数重载

注意
过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260