iOS-Swift-可选项

一. 可选项的基本使用

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

可选项

二. 强制解包(Forced Unwrapping)

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

盒子

如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用感叹号 ! 进行强制解包

强制解包

上面,强制解包以后Int?类型变成了Int类型。

如果对值为nil的可选项(空盒子)进行强制解包,将会产生运行时错误

强制解包错误

三. 可选项绑定(Optional Binding)

  • 一般的判断方法

判断可选项是否包含值

判断可选项是否包含值
  • 可选项绑定

但是每次都这样判断太麻烦,我们可以使用可选项绑定来判断可选项是否包含值,如果包含就自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回true,否则返回false。

可选项绑定
  • 下面两种写法等价
等价写法

如果条件判断中有可选项绑定就不能用&&,只能多个条件之间用逗号隔开。

  • while循环中使用可选项绑定
while循环中使用可选项绑定

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

① 单个??

a ?? b

  1. a 是可选项,b 是可选项 或者 不是可选项
  2. b 跟 a 的存储类型必须相同 (比如a是Int?,b就必须是Int?或者Int)
  3. 如果 a 不为nil,就返回 a,如果 b 不是可选项,返回 a 时会自动解包
  4. 如果 a 为nil,就返回 b

根据上面的几条规则,可以发现:优先返回a,返回值类型⼀定和b类型保持⼀致

返回值类型⼀定和b类型保持⼀致

想要知道为什么返回值类型⼀定和b类型保持⼀致,我们可以查看Swift对 ?? 的定义:

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

可以发现:
① 第一个参数必须是可选项,第二个参数可以是可选项也可以不是可选项
② 但是返回值类型总是和第二个参数的类型保持一致。

注意:当可选项只有一个?的时候上面的结论没有任何问题,当有不止一个?的时候就不太一样了,这时候可以参考??的源码实现,如:iOS-Swift-标准库源码分析+项目实战

② 多个??一起使用

多个??

③ ??跟if let配合使用

??跟if let配合使用

五. guard语句

  • 使用if语句进行登录判断

如果我们用if语句实现登陆功能,代码如下:

if语句实现登陆

运行代码:

运行代码
  • guard语句
guard语句

当guard语句的条件为true时,就会跳过guard语句,往后执行
当guard语句的条件为false时,就会执行大括号里面的代码
guard语句特别适合用来“提前退出”。

可以发现:guard语句只有条件成立的时候就继续往下走,条件不成立就执行else{}里面的内容,{}里面一般就是return

  • 使用guard语句进行登录判断

所以上面的if登录判断我们可以修改为使用guard语句进行登录判断,这样就简单明了多了。

当使用guard语句进行可选项绑定时,绑定的常量(let)、变量(var)也能在外层作用域中使用,如下:

条件检验,提前退出

六. 隐式解包(Implicitly Unwrapped Optional)

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

首先要明白,无论是Int?还是Int!都是可选类型,所以可以使用!进行强制解包,也可以通过打印验证,如下:

let num1 : Int! = 10
let num2 : Int = num1!

print(num1,num2)
//Optional(10) 10

上面代码,由于定义num1的时候定义的是可选项,所以在使用num1的时候可以用!解包,然后把它的值赋值给num2。又因为定义num1的时候就使用了隐式解包可选项,它会自动解包,所以在使用num1的时候其实可以省略!,所以上面的代码也可以下面这样写:

let num1 : Int! = 10
let num2 : Int = num1

print(num1,num2)
//Optional(10) 10

这样代码更简洁,以后使用num1的时候就不用加!了,如下:

不用加!

由于隐式解包会偷偷的解包,所以如果刚开始的值为nil,那么偷偷解包就会报错,如下:

let num1 : Int! = nil
let num2 : Int = num1 //这里会偷偷的解包
然后报错:Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

可能你会想,如果能保证一直有值,那么使用 var num1 : Int = 10 不是更好嘛!(其实的确也是的)

其实开发中尽量不要使用Int!,那你可能会说Int!不是没啥用了吗?
其实还是有点用的,就是你希望别人给你的是有值的,但是别人还是有可能给你nil,这种情况就可以使用Int!。

就比如你开发了一个第三方框架,使用Int!就是希望别人给你的就是有值的,如果别人传nil,你就直接崩掉,如果有值,你就可以直接拿到这个值进行操作,也不用解包。当然第三方框架一直崩来崩去也不好,所以还是不要使用Int!。

七. 字符串插值

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

警告

至少有3种方式消除警告:

消除警告

一般使用最后一种方式。

八. 多重可选项

1. 有值

var num1: Int? = 10    //包装Int类型的可选类型
var num2: Int?? = num1 //包装Int?可选类型的可选类型
var num3: Int?? = 10   //可能你会想,右边不应该传Int?类型的吗,其实传10编译器会自动帮我们包装的

他们内部结构如下:

有值,内部结构

可以发现,num2和num3是完全一样的,也可以通过打印来验证:

print(num2 == num3) //true
可以这样理解:num2和num3都有值10,并且类型都是Int??,所以当然相等啦!

2. 没有值

如果没有值,那么就不一样了,如下:

var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil

他们内部结构如下:

没有值,内部结构

打印发现num2和num3不一样,如下:

print(num2 == num3) // false
可以这样理解:虽然他们类型相等,但是num2里面有个可选类型,num3里面为空,所以肯定不相等啦!

下面代码值是多少?

(num2 ?? 1) ?? 2  // 2 
首先num2不为nil,所以返回num2,又因为1是Int类型,所以会对num2解包,解包之后发现为nil,nil ?? 2,所以最后结果为2。

(num3 ?? 1) ?? 2  // 1
num3为nil,所以返回1,1 ?? 2,所以最后结果为1。

3. 使用lldb指令 frame variable –R 或者 fr v –R 查看内存结构

运行如下代码,打断点:

var num1: Int? = 10
var num2: Int?? = num1
var num3: Int?? = 10

print("Hello, World!")  //打断点

执行help frame指令:

(lldb) help frame
     Commands for selecting and examing the current thread's stack frames.

Syntax: frame <subcommand> [<subcommand-options>]

The following subcommands are supported:

      info       -- List information about the current stack frame in the
                    current thread.
      recognizer -- Commands for editing and viewing frame recognizers.
      select     -- Select the current stack frame by index from within the
                    current thread (see 'thread backtrace'.)
      variable   -- Show variables for the current stack frame. Defaults to all
                    arguments and local variables in scope. Names of argument,
                    local, file static and file global variables can be
                    specified. Children of aggregate variables can be specified
                    such as 'var->child.x'.  The -> and [] operators in 'frame
                    variable' do not invoke operator overloads if they exist,
                    but directly access the specified element.  If you want to
                    trigger operator overloads use the expression command to
                    print the variable instead.
                    It is worth noting that except for overloaded operators,
                    when printing local variables 'expr local_var' and 'frame
                    var local_var' produce the same results.  However, 'frame
                    variable' is more efficient, since it uses debug
                    information and memory reads directly, rather than parsing
                    and evaluating an expression, which may even involve JITingand running code in the target program.

可以发现,后面跟variable可以查看内存结构,

执行help frame variable指令:

(lldb) help frame variable
......
Command Options Usage:
  frame variable [-AFLORTacglrst] [-y <name>] [-z <name>] [-f <format>] [-G <gdb-format>] [-d <none>] [-S <boolean>] [-D <count>] [-P <count>] [-Y[<count>]] [-V <boolean>] [-Z <count>] [<variable-name> [<variable-name> [...]]]
......
       -R ( --raw-output )
            Don't use formatting options.
......

可以发现使用-R就可以不使用格式化选项,就是内存结构原封不动的打印出来。

执行简化的指令:fr v -R num1、fr v -R num2、fr v -R num3

(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = some {
  some = {
    _value = 10
  }
}

(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
  some = some {
    some = {
      _value = 10
    }
  }
}

(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = some {
  some = some {
    some = {
      _value = 10
    }
  }
}

可以发现,num1、num2、num3都是some,说明都不为空,打印出来的内存结构也和我们上面的结构图一模一样的。

如果是nil呢?

var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil

print("Hello, World!") //打断点

执行指令后打印:

(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = none {
  some = {
    _value = 0
  }
}

(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
  some = none {
    some = {
      _value = 0
    }
  }
}

(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = none {
  some = some {
    some = {
      _value = 0
    }
  }
}

可以发现,num1、num3为none,所以none后面的东西都没有意义了,忽略不考虑。num2为some,它里面包装的是:

none {
    some = {
        _value = 0
    }
}

其实就是值为nil的空盒子,num2的内存结构也和我们上面的结构图一模一样的。

九. 可选项的本质

可选项的本质是enum类型,只有none、some两个case。

Swift源码中可选项的定义如下:

public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped) //关联值,关联什么值和上面的泛型有关
    public init(_ some: Wrapped)
}

既然知道可选项本质了,所以可选项可以这么写:

var age: Int? = 10 //本质如下
var age0: Optional<Int> = Optional<Int>.some(10) //最具体
var age1: Optional = .some(10) //省略泛型类型
var age2 = Optional.some(10) //省略类型
var age3 = Optional(10) //使用初始化器,接收一个包装的值

age = nil //本质如下
age = .none
var age: Int? = nil  //本质如下 
var age = Optional<Int>.none
var age: Optional<Int> = .none

或者混着使用:

var age: Int? = .none
age = 10
age = .some(20)
age = nil

可选项也是支持绑定的

switch age {
case let v?:  //如果age有值,就将age解包赋值给v
    print("some", v) //这时候v就是Int类型,而不是Int?类型
case nil:  //如果age为nil,就会来到这里
    print("none")
}

//同理,也可以使用枚举,这种方式更容易理解
switch age {
case let .some(v):
    print("some", v)
case .none:
    print("none")
}

如果可选项有两个?,那么它本质就是:

var age_: Int? = 10
var age: Int?? = age_ //本质如下
var age = Optional.some(Optional.some(10))
var age: Optional<Optional> = .some(.some(10))

age = nil //本质如下
age = .none

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

推荐阅读更多精彩内容