美文网首页SwiftiOSSwift 学习
Swift的安全保障 - Optional特性

Swift的安全保障 - Optional特性

作者: flionel | 来源:发表于2016-07-23 17:13 被阅读462次
    Optional.png

    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

    相关文章

      网友评论

      • 荔枝lizhi_iOS程序猿:楼主将的很清晰 ,
      • LoveY34:有一个问题:下面的函数明明返回的是一个optional变量,可以代码中返回的是一个普通变量,这两个不冲突嘛?我试了函数中返回的的确是一个普通变量,但是调用方法的地方获取的的确是一个optional变量,有点乱
        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?
        flionel:@温华 确实,你说的很对,是我理解错误了,谢谢指出错误。
        之前写demo时候并没有把实际运行检测一下,罪过罪过。

        以上只是来举例说明强制解包的使用,出现错误希望读者理解。另外,根据本人的编码经验,最好不要使用"!"来进行强制解包,因为涉及到多人合作,你可以确定你自己定义的Optional属性不会为nil,但是你不能确定别人的Optional不会为nil,所以为了安全强烈推荐通过Optional Binding来解包。
        我们的Leader在review代码时候,发现有人使用"!"强制解包时候就骂,因为测试时候出现很多次强制解包导致的Crash,甚至有一次线上版本出现了Crash。

      本文标题:Swift的安全保障 - Optional特性

      本文链接:https://www.haomeiwen.com/subject/yddwjttx.html