Optionals Overview
Swift语言一个很大的特色和难点就是Optional可选值的概念,这也是Swift相对于Objective-C语言来说一个很大的改进和安全性保障。这里先通过简单的optional和non-optional类型变量定义,让读者有一个初步的认识,如下代码所示,
// non-optional类型
var helloworld1: String = "hello world 1"
// Optional类型
var helloworld2: String? = "hello world 2"
var helloworld3: String?
通常情况下,在Swift中定义属性,它们默认是non-optional(非Optional类型)。定义为non-optional类型的变量必须在定义的时候或在-init
方法中初始化,如果non-optional没有赋值初始化,则会发生编译错误,这是non-optoinal会出现的第一种编译错误①,例如下面的语句,helloworld1定义之后赋值初始化,而helloworld2定义之后没有赋值初始化,就会发生编译错误,
class Messnger {
var helloworld1: String = "Swift is awesome!" // OK
var helloworld2: String // compile-time error
}
回顾Objecive-C语言,我们可能经常定义一些属性或变量,在使用之前却忘了进行赋值,这可能会导致一些莫名其妙的bug,甚至使应用程序Crash;为了解决这样的问题,保证程序安全性,Swift要求non-optional类型必须保证完整地初始化,其实这是从语法上对开发者进行了规范。所以上述代码中的non-optional类型的helloworld2变量,应该跟helloworld1一样,赋值以明确的值。
既然必须对non-optional变量赋值,是否可以直接赋值为nil呢?答案是NO,因为non-optional不能赋值为nil,只有Optional类型的变量才能赋值为nil,例如下面的代码
// non-optional
var helloworld1 = nil // compile-time error
// Optional类型
var helloworld2: String? = nil // OK
var helloworld3: String? = nil // OK
如果将nil赋值给non-optional类型,编译器会提示说“不能为non-optional设置为nil值”,这是non-optoinal会出现的第二种编译错误②,只有Optional类型的变量才能被设置为nil,对于Objective-C来说,就不会出现以上两种编译错误,即
- 定义non-optional不进行初始化 ①
- 为non-optional变量指定为nil ②
使用?
关键字来定义Optional变量可以解决上面的问题,类似文章开头的范例,请看下面的变量定义,
class Messenger {
var helloworld1: String = "Swift is awesome" // OK
var helloworld2: String? // OK
}
此处helloworld1是默认non-optional类型,对于non-optional类型的变量必须在定义的时候或者在-init
方法中明确的指定初始值,否则编译器会提示编译错误;helloworld2是Optional,Optional类型变量helloworld2在定义的时候可以不指定初始值,而且也可以将nil赋值给Optional的变量。
Why Optionals
Swift为什么要使用Optional这样的特性呢,在学习Swift和编码的实践中,读者会意识到苹果做这样的设计更多的是为了程序安全考量,Swift一开始就被设计为一种更加安全的编程语言。读者通过上面的例子可以看出来,Swift的Optional特性提供了编译时检测(compile-time check),这样可以避免很多发生在运行时的错误(run-time error)。应用程序的生命周期就是“编码 - 调试 - Debug -运行”,以往在编写Objective-C代码时候,因为Objective-C语言并没有很严格的安全检查,所以可能会导致bug或者Crash发生在运行阶段,特别是发布到App Store的程序出现了这样的问题会很棘手,也就是说Objective-C将编码失误的隐患留到了运行时候;反观Swift,它通过比较严格的安全检查,在应用程序声明周期的编码部分就开始排查隐患,这样的好处是显而易见的。
接下通过一个范例,读者会对Swift的Optional特性的强大有更好的了解,我以一个简单的股票模型来举例说明,
先展示Objective-C代码,
- (NSString *)findStockCode:(NSString *)company {
if ([company isEqualToString:@"Apple"]) {
return @"APPL"
} else if ([company isEqualToString@"Google"]) {
return @"GOOG";
}
return nil;
}
/*
方法-findStockCode用来获取公司的股票码,
在这个范例中,该方法仅仅返回Apple和Google的股票码,
对于其他的输入,则返回nil。
*/
// Usage of -findStockCode method
NSString *stockCode = [self findStockCode:@"Facebook"];
NSString *text = @"Stock Code - ";
NSString *message = [text stringByAppendingString:stockCode]; // runtime
NSLog(@"%@", message)
当我们输入公司名为Fackbook时,-findStockMethod
在编译时候没有什么问题,但是在运行时,该方法返回nil,导致了-stringByAppendingString
方法Crash。
但是我们使用Swift Optional特性时候,相对于Objective-C,它不是在运行时(run time)才发现错误,而是在编译时(compile time)就检测出语法错误,从而排除了安全隐患,
用Swift重写-getStockCode,代码如下所示,
func findStockCode(company: String) -> String? {
if company == "Apple" {
return "APPL"
} else if company == "Google" {
return "GOOG"
}
return nil
}
// Usage of -findStockCode method
var stockCode: String? = findStockCode("Facebook")
let text = "Stock Code -"
let message = text + stockCode // compile-time error
print(message)
-findStockCode
方法返回值定义为String?,它表示既有可能返回为空,也有可能不为空,这是可选的,所以定义字符串stockCode也是String?类型,此时编译器已经检测到潜在的错误 - "value of optional type String? is not unwrapped",开发者必须解决上面的问题才能编译。
通过上面的范例我们了解到,Swift的Optional特性强制的进行nil-check(空数据检查),并且通过编译错误提示开发者代码存在潜在的问题,so,多多使用Optional可以保证更高的代码质量。
Unwrapping Optionals
上面的代码出现了编译错误,怎样更改让代码编译通过呢?按照Objective-C的编程经验,我们会判断stockCode字符串是否为空,将代码修改如下,
var stockCode: String? = findStockCode("Facebook")
let text = "Stock Code - "
if stockCode { // detect stockCode if contains value
let message = text + stockCode!
pritn(message)
}
我们对此做了改进,使用if来判断Optional类型的stockCode是nil还是包含有效值,如果stockCode不为空,则使用!
来对其进行强制解包。
使用Optional值之前需要对其解包,为什么要解包呢?
因为Optional本身也是一种枚举值,它包含两种值和Optional.None和Optional.Some,所谓的nil就是Optional.None,非nil就是Optional.Some,定义为Optional的变量或常量,首先会通过Some(T)
包裹Wrap
其原始值,也就是说它们的真实内容被Optional包裹起来,所以使用的时候需要对Optional进行解包Unwrap
。
上面我们使用stockCode!来进行强制解包可选值,并且使用被可选值包裹(Wrap)
的值。
需要强调的是若对Optional使用!
强制解包,必须保证该Optional包含一个不为空的值(a non-nil value),否则就会导致Crash。上面的stockCode!,在编译时仅仅进行了nil-check之后我们就对其进行强制解包,在运行时,这直接导致了Crash,
var stockCode: String? = findStockCode("Facebook") // return nil
let text = "Stock Code - "
let message = text + stockCode! // runtime error
/*
此处没有编译错误,编译器认为Optional值stockCode确定包含有效值,
然而在运行时Crash,Xcode会提示"fatal error: Can't unwrap Optional.Nome"
*/
Optional Binding
因为使用!进行强制解包可能会导致Crash,Apple提供了更加安全和简单的解包Optional方式Optional Binding(可选绑定)
,使用Optional Binding来检测一个Optional是否包含有效值,如果该Optional变量确实包含有效值,则将其解包,并且将解包的值赋值给一个临时的常量或变量,使用Optional Binding对代码进行优化,如下所示,
var stockCode: String? = findStockCode("Facebook")
let text = "Stock Code - "
if let tempStockCode = stockCode {
let message = text + tempStockCode
print(message)
}
除了if let
,也可以使用if var
,使用Optional Binding,翻译过来就是“如果stockCode包含有效值,则对其解包,将解包的值复制给临时常量tempStockCode,然后运行条件语句中的代码;否则直接跳过条件语句的代码块”,这样就避免了使用!
进行强制解包导致的Crash。
Optional Chaining
在讨论Optional Binding(可选绑定)之前,对上面的范例代码做稍微的改造,首先创建一个名为Stock的类,它拥有有个Optional类型的属性,名为code(股票码)和price(价格);然后修改-findStockCode方法,将返回值由String改为Stock,如下所示,
/*
输入公司名称,返回该公司股票信息Stock,
Stock包括股票码和价格
*/
class Stock {
var code: String?
var price: Double?
}
func findStockCode(company: String) -> Stock? {
if company == "Apple" {
let appl: Stock = Stock()
appl.code = "APPL"
appl.price = 90.32
return appl
} else if company == "Google" {
let goog: Stock = Stock()
goog.code = "GOOG"
goog.price = 556.36
return goog
}
return nil
}
如果想购买100股Apple的股票,可以这样编写代码,
// Usage -findStockCode with param "Apple"
if let stock = findStockCode("Apple") {
if let singlePrice = stock.price {
// 购买100股Apple股票
let totalCost = singlePrice * 100
print(totalCost)
}
}
为了计算“购买”100股Apple的股票所需Money,首先对-findStockCode
的返回值Stock?进行解包,接着对Stock?的price?可选值Optional属性进行解包,这样写没有什么问题,这样过多的嵌套if导致了代码的复杂度增加,可读性降低。
这时候Optional Chaining可以帮助简化代码,Optional Chaining可选绑定特性允许我们通过?.
操作符来连接多个Optional属性,代码稍作优化,如下所示,
if let singlePrice = findStockCode("Apple")?.price {
let totalCost = singlePrice * 100
print(totalCost)
}
Optional Chaining提供了更为灵活的方式去获取price的值,这样优化后的代码更加简洁更加清晰。
以上,是对Swift的Optional特性做的一个简单介绍和范例演示,有些概念或理论说的可能不正确,欢迎强烈吐槽。如果读者想深入了解和研究,可以在Google搜索更多资源,或者阅读苹果的官方文档Apple's Swift guide
参考链接
公众号
欢迎关注本人公众号 foolishlion,请扫描下方二维码。
foolishlion.jpg
网友评论
func findStockCode(company: String) -> Stock? {
if company == "Apple" {
let appl: Stock = Stock()
appl.code = "APPL"
appl.price = 90.32
return appl
} else if company == "Google" {
let goog: Stock = Stock()
goog.code = "GOOG"
goog.price = 556.36
return goog
}
return nil
}
if stockCode { // detect stockCode if contains value
let message = text + stockCode!
pritn(message)
}
这样不是已经判断stockCode非空了么,强制解包有什么不妥?为什么会crash?
之前写demo时候并没有把实际运行检测一下,罪过罪过。
以上只是来举例说明强制解包的使用,出现错误希望读者理解。另外,根据本人的编码经验,最好不要使用"!"来进行强制解包,因为涉及到多人合作,你可以确定你自己定义的Optional属性不会为nil,但是你不能确定别人的Optional不会为nil,所以为了安全强烈推荐通过Optional Binding来解包。
我们的Leader在review代码时候,发现有人使用"!"强制解包时候就骂,因为测试时候出现很多次强制解包导致的Crash,甚至有一次线上版本出现了Crash。