为什么Swift说自己是安全的?

4,150 阅读5分钟

了解Swift的同学,都知道Apple在宣传Swift时,安全是重头戏。但是Swift的安全体现在哪些方面呢?以下为个人临时想到的一家之言,欢迎指教补充。

类型安全

Swift的初学者或者刚转Swift的开发,对Swift有一些槽点,可选类型的'?',Int和Double不能隐式转换,没有自带的运行时等等,但是这些恰恰是Swift设计的优点,换句话说是故意这样设计的。

  • Objective-C的nil,默认情况下调用任何方法都是不会报错的,写在数组和字典的字面量里面的nil,会直接Crash。
  • 如果是一些简单的场景,或许不够明显,反而会认为很方便。
  • 但是如果是复杂的项目,几十几百个人维护一个大App,各个组件之间提供出来的接口被其他组件使用,每个组件为保证自己的方法健全,往往要对传参做类型验证,空验证,如果忘了考虑或者不去管,有时就会导致一些问题出现,如类型不对,显示异常等。
  • 在Swift里面一个Option类型就把空这个难题风险最小化了,任何时候你都能确定你做用的参数或属性是否可能为空,有时候习惯了以后写起来真的挺爽的,逻辑完备。
  • 光是一个Option只能处理nil的情况,与此类似的还有Swift静态编译的特性,异常俘获的特性,这些特性共同保证了Swift的代码在写下那一刻就是确定的。Swift中写一个方法,不需要考虑太多,只需保证把定义的方法 input进来的参数处理并output。
  • 不用担心参数空不空、不用担心定义了Array类型会不会传进来一个String类型、不用担心莫名其妙的Crash。而这些在Objective-C基本是个奢望。
  • 为啥说Swift开发效率比OC高,除了语法简洁、面向协议、高度抽象的泛型、另一方面原因就是因为这个。

不安全的情况

  • 当然并不是完全不用考虑异常情况,想要写出安全的Swift代码,还是有一些注意事项的
  • 比如数组越界,在Swift里面也是需要注意的。
  • 一些和OC或者C++交流的代码也需要注意是否会出现实际调用空异常。
  • 使用Swift提供的那些Unsafe底层指针操作API,也是需要小心再小心。
  • Swift直接把能直接操作内存的这些API直接名字就定义成Unsafe,当然是希望我们尽量不要使用,这就极大概率避免了野指针的情况
  • 而OC光是定义个属性,一不小心写错了都能导致野指针出现。

内存安全

Swift相对于OC加了个值类型,Struct、Enum都是值类型,定义属性可以定义可变不可变

  • 很多人都知道let是线程安全的,为啥安全呢,因为它在初始化后就不再变化,只会发生读操作不会发生写操作,当然是线程安全的,那么它在初始化时是不是线程安全的呢?答案肯定是安全的,这要分几种情况
  1. 如果是定义在方法内的常量,不用说肯定不会发生两个线程同时初始化同一个常量的情况
  2. 如果是一个类的常量属性,它在类定义的时候就初始化了,而对象初始化也不会出现两个线程同时初始化同一个对象的情况
  3. 另一种情况就是全局常量和静态常量,他们是在第一次访问的时候初始化的,而为了防止两个线程同时第一次访问一个常量,他们在初始化时会被swift_once包起来,这一点通过断点可以看到。至于swift_once的作用,OC开发一看名字就知道是啥了。
  • Swift的标准库使用了大量的值类型,同时也建议我们使用尽量使用值类型,而值类型的明面上的定义就是每次改变都会copy出来一份新的,不会影响原来的值,这就为线程安全提供了很大空间
  • 不过要注意的是这个copy过程并不是线程安全的,实际的copy大部分情况下也会按照COW(写时拷贝)尽可能少的发生实际的copy
  • 所以如果多线程操作一个值类型变量时,最好不要让这个copy过程发生在多线程中,举个例子
var value: CGPoint = .zero
for _ in 0..<2000 {
    DispatchQueue.global().async {
        let old = value
        value.x += 1
        value.y += 1
        print(old, value)
        if old != CGPoint(x: value.x - 1, y: value.y - 1) {
            assertionFailure("")
        }
    }
}

(1.0, 1.0) (6.0, 6.0)
Demo/AppDelegate.swift:45: Fatal error
(17.0, 17.0) (18.0, 18.0)
(5.0, 5.0) (6.0, 6.0)
Demo/AppDelegate.swift:45: Fatal error
(2.0, 2.0) (6.0, 6.0)
Demo/AppDelegate.swift:45: Fatal error
(19.0, 19.0) (20.0, 20.0)
Demo/AppDelegate.swift:45: Fatal error
(4.0, 4.0) (5.0, 5.0)
Demo/AppDelegate.swift:45: Fatal error

这种情况,肯定是线程不安全的,有一种避免Crash的写法是

let value: CGPoint = .zero
for _ in 0..<2000 {
    DispatchQueue.global().async { [value] in
        var value = value
        let old = value
        value.x += 1
        value.y += 1
        print(old, value)
        if old != CGPoint(x: value.x - 1, y: value.y - 1) {
            assertionFailure("")
        }
    }
}

区别在于value被其他线程引用时添加了[value]显式捕获,这时相当于给闭包加了一个let value = value,此时闭包外的变量再怎么变化,闭包内用到的value的值永远是闭包创建时的值

  • 当然这个例子不具备实际意义,这种情况因为多线程中value用到的值不是最新的,实际开发还是按照实际情况处理。