Swift概述


随着越来越多的应用在使用 Swift 语言开发iOS,学习 Swift 语言也很有必要。这个博客不是详细的 Swift 的教程,主要是为了 Swift 初学者,尤其之前使用 OC 开发 iOS 应用的人员,在转入 Swift 这门语言时,应该了解的一些基础知识,为方便进一步的学习做一些铺垫。

1 Swift语言的发展

Swift revolution

  • Swift2.2 时推出的 Playground 可以让初学者方便的练习语法,并且 Playground 的注释支持 Markdown 语法,写注释非常方便。

  • ABI 稳定是很多大厂选择使用Swift语言开发项目的主要原因。因为之前 Swift 版本不兼容,Swift3.0 编译的Framework 在 Swift4.0 中都没法使用,必须修改 Framework 的源码,重新打包,对于有很多Framework依赖的大型App来讲这就是灾难。

  • Swift5.5 支持异步语法 async/await/actor 在编写异步的代码时,会更加的优雅,避免回调地狱。

2 Swift工程化

如果是重新开发的新的 App 使用 Swift的话,没有什么技术债,工程化比较简单,甚至可以用 Swift Package Manager 官方的包管理工具。但是如果在之前用 OC 开发的应用基础上使用 Swift 的话工程化要做的东西还是挺多。

2.1 Module工程化

什么是Module化,说白了就是编译器的一种引用方式,很多语言 JavaScript python 等等都是支持 Module化的。OC 语言是继承了 C 的头文件引用方式,在引用一个 xxx.h 文件时,编译器会重新编译 xxx.h 文件的所有语法。这样方式相当于把语法的复杂性暴露给了开发者。下面列举下使用头文件引用的弊端。

  1. 编译时间会大大增加,因为每个 xxx.h 文件都要重新编译。

  2. 头文件,在编译时,可能会造成大量的符号冲突,所以不得不用 #ifDef 宏语法来避免,可读性很差。

  3. 头文件引用把各种语言的语法暴露出来,比如 C++ 引用 C 语言的时候,不得写各种兼容语法 extern C。如果能使用 Module 的方式,统一引用的接口,就可以避免这种问题。

所以苹果在2012 年的时候在LLVM编译器规范中提出了Module化的引用方式。Clang Module

OC 语言已经支持了 Clang Module 的引用方式,但是需要开发者在编译项目的时候选择支持 Module 化。(2020年的C++20版本也支持了Clang Module)

由于 Swift 工程 Framework 模块的引用强制使用 Module 化,所以所有的 OC 的 Framework 必须都要支持。

2.2 Module化的问题

苹果在 Module 化时,也提过Module的缺点。

  1. 没有很好的版本控制,很多语言基本都有版本控制工具,但是对于 C、C++ 一直没有很好的版本控制工具。Swift 版本控制一直做的不好,直到 5.1 ABI稳定后,算是解决了版本控制的问题。

  2. 没有 namespace 的概念,例如相同的 class 或者 struct,在不同的 Module下会有符号冲突。为了解决这个问题 Swift 的命名空间基于 Module 而不是显式指明 namespace,每个 Module 代表了 Swift 中的一个命名空间,也就是说,不同的 Framework 里的 struct 和 class 是可以一样的。

2.3 引用C和C++的问题

由于 Swift 语言完全独立的语法,不像 OC 是基于C语言的基础上开发的。所以 Swift 在引用 C和C++时,需要走 OC 桥接才行,类似于 Java 调用 C++ 代码需要走 JNI 一样。

3 Swift 语法

Swift 由于是开源的,每个语法的由来也都是有文档可查的。有兴趣的可以看下 Swift 所有基础类型的Meta定义 TypeMetaDataSwift 语法设计手册

下面主要介绍下 Swift 语言的一些重要新特点。以及和 OC 语法的不同之处。平时开发中可能会经常遇到。如果要进一步学习,可以看 Swift 语法官方教程

3.1 Optional 语法

包装类型,所有的类型(包含基础类型 Int、Float 等等)我们先看苹果的定义。


enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

在 OC 中我们往往认为 int float 这种基础类型不像NSString一样应该没有 nil 的概念。但是在 swift 里面所有的类型都有nil的概念。看下这个写法 var int test = 0 和 ` var int test? = nil 是完全不同的。其中有 ? 结尾的表示的变量 test`是 Optional类型,也就是可以做解包(unWrapped)的操作,如何拆包呢?如下面的写法:


var test : Int?

test = 0
if let result = test {
    print(result)
}

为了验证Optional类型的变量和non-optional的区别,我们可以这些尝试下。


var test : Int = 0

if let result = test {
    print(result)
}

编译器就会报如下错误。 error: initializer for conditional binding must have Optional type, not 'Int' if let result = test {

这个语法对swift的安全性尤其重要,OC 中经常需要添加一些安全的气垫类对nil做检测,swift 只要定义一个变量为 Optional的话,你就知道这个变量是否可能为空,访问时需要如上面的写法就可以了。当然解包也有一些便捷的语法糖如下写法:


var test : Int?
test = 1
print(result ?? 0)

所以变量定义为Optional的时候,在使用时已经要解包使用,这样就避免了空指针的问题。Swift 为了兼容一些便捷的语法,定义了一种强制解包的操作 !,表示解包的变量一定不为nil,上面的例子也可以这样写:

var test : Int?
test = 0
print(test!)

! 语法尤其小心,当test=0 这行代码被注释时,就会崩溃。

3.2 struct的使用


struct Person {
    let name: String
    mutating func changeName(new: String) {
        self.name = new
    }
}

上面就是一个简单的 struct 定义,struct 所有的赋值都是copy的。

 
 let joe = Person("joe")

 let joeCopy = joe

并且一旦定义 struct 中的变量为常量,就无法改变变量的值,如果需要改变就要用下面的写法。


 struct Person {
    var name: String
    mutating func changeName(new: String) {
        self.name = new
    }
}

var joe = Person(name: "joe")
joe.changeName(new: "allen")

Swift 中常见的基本的类型 Int Float 等等都是struct的。struct设计主要是存储数据使用,数据一旦被创建之后,就很少被修改,我们只是需要使用这些对象的值就行。而 class 一般表示的在一定的生命周期内,数据状态不停的改变。这点就是选择struct和class的基本原则。

3.3 枚举的使用

Swift 枚举非常强大,功能堪比类的功能。下面介绍下枚举的写法。


enum Direct:String {
    case up = "up"
    case down = "down"
    case left = "left"
    case right = "right"
}

枚举在 Swift 中本质上是和struct、class 齐平的一种数据结构,我们也可以自定义枚举如下:


struct Person:Equatable,ExpressibleByStringLiteral {
    let title :String
    let level :Int
    
    public static func == (lhs: Person, rhs: Person) -> Bool {
    return (lhs.title == rhs.title && lhs.level == rhs.level)
    }
        
    public init(stringLiteral value: String) {
        let components = value.components(separatedBy: ",")
        if components.count == 2 {
            self.title = components[0]
            self.level = Int(components[1]) ?? 0
        } else {
            self.title = "primary"
            self.level = 0
        }
    }
    
    public init(unicodeScalarLiteral value: String) {
        self.init(stringLiteral: value)
    }
    public init(extendedGraphemeClusterLiteral value: String) {
        self.init(stringLiteral: value)
    }
    
}
enum Engineer:Person {
    case primary = "primary,0"
    case junior = "junior,1"
    case senior = "senior,2"
    case professor = "professor,3"
}

let joe = Engineer.junior

print(joe)

讲到枚举类型,就不得不说 Swift 的模式匹配这个特点,什么是模式匹配,就是使用一种通用的模式,来解构数据中相应类型的具体值。听起来有点抽象。举个例子,正则表达式匹配,就是模式匹配的一种,他是匹配定义好的字符串的模式,解构出字符串中符合模式的值,如果无法匹配就代表解构失败。根据Swift的模式匹配的文档中的讲解,模式匹配包含下面几种。

  • 通配符模式
  • 标识符模式
  • 值绑定模式
  • 元组模式
  • 枚举匹配模式
  • 可选模式
  • 类型转换模式
  • 表达式模式

这几种模式匹配中,我们这里重点拿枚举匹配模式介绍,其他的本质上原理是一样的。枚举匹配是当编译器识别 case 关键字时,会解构 case 后面的变量和表达式,来匹配变量的具体值,如果成功就返回YES,否则就返回NO。所以模式匹配可以表示如下。


func ~=<T>(pattern: T -> Bool, value: T) -> Bool {
    return pattern(value)
}

那模式匹配的目的是为了干什么的,减少程序员的代码量。在 OC 当我们遇到不知道的类型时,需要这样写。


id name = @"LiLei";

NSArray * list = @[@(1),@(2),@"name"];

(for value in list) {
    if ([value isKindOf [NSNumber class]]) {
        NSLog(@"%@",value)
    }
}

swift 有了模式匹配可以这样写。


let list: [Any?] = [1,2,"name"]

for case let result? as Int? in list {
  print(result)
}

上面的例子中 case 语句后面的表达式的意思是需要符合 Int 类型的模式,这样编译器就会自动判断从list取得的每个数据,是否是Int类型如果是就赋值给result。甚至还可以增加一个 where 的匹配模式如下


for case let result? as Int? where result > 10 in list {
  print(result)
}

所以有了匹配模式后,代码会变的非常的简洁。

3.4 Protocol的使用

Swift 有一个很重要的设计理念就是面向协议编程。之前写过一遍博客分析过面向协议编程。面向协议本质上就是让大家少用继承,多用组合的方式来实现一些复杂的功能。

如何实现上面说的理念,本质上就是把共有的方案抽离出来定义成协议,然后再通过extension的语法实现这个协议。


protocol FatherProtocol {
    func myChildName() -> String
}

extension FatherProtocol {
    func myChildName() -> String {
        return "xxx"
    }
}

3.5 错误处理

Swift 语言终于有了,do-catch语句处理错误的形式。


do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
} catch {
    statements
}

这个异常处理和其他语言没有太大的区别,从语法的定义上,可以看出来错误处理,也是有模式匹配的特点的。举个网络处理的简单例子如下:


enum NetError : Error {
    case NetBroken
    case ServerError
    case ClientError
}

func request(url : String?) throws -> String {
    var sucess = ...
    if sucess {
        return content
    } else {
        throw NetError.NetBroken
    }
}

do {
    try request(:"xxxx")
} catch er {
    print(er)
}

// 这个是便捷的写法
try? request(:"xxxx")

这里介绍下错误处理中,常用的defer关键字,可以在当前范围退出时,延迟执行指定的清理操作。往往是在错误处理分支比较多时,使用这个能力,可以节省很多代码,例如:


func processFile(filename: String) throws {
    var result :String?
    defer {
        clean...
    }
    do {
        result = try? request(:"xxxx")
    } catch er {
        print(er)
        return
    }
    parse(result)
}


3.6 高阶函数

Swift 是非常重视函数式编程的,所以高阶函数在 Swift 经常使用,例如 map flatMap 这种数组操作的函数,是官方推荐的替代 for 循环方式。平时我们高阶函数应用最多的应该就是 闭包(Closure)了。如果了解 OC 的 block,相信很容易懂。下面就是一个简单的 Swift Closure 定义:


var block : (String) -> Int?
block = (name:String) -> Int {
    if name == "0" {
        return 0
    }
    return 1
}

这里是 Closure 介绍的官方文档。关于 Closure 使用的一些高级用法,可以参看 Swift 文档中 Expressions

文档上 Closure 的尾随闭包,参数省略等等各种用法都很详细,这里不再介绍。想重点说下 Swift 高阶函数中常用的捕获列表。因为 Swift 的内存管理依旧是用引用计数,不像 java 的标记分代清理的方法。所以 Swift 依旧有大量的循环引用的问题。举个例子:


class Human {
    var lanuage = "Objc"

    var block: (() -> Void)?

    deinit {
        print("deinit")
    }

    func recycle() -> () -> () {
        let code = {
            print(self.lanuage)
        }
        block = code
        return code
    }
}

var human: Human! = Human()
human.lanuage = "Swift"
human.recycle()
human = nil

然而你会发现控制台里面并没有输出 deinit,human 这个实例被循环引用了。Swift 在类里面默认会把self变量放到捕获列表里面,上面的闭包的写法和下面本质上是等同的。


let code = { [self] in
    print(self.lanuage)
}

所以要在捕获列表中,对 self 变量做弱引用声明,因为是弱引用,闭包在使用这个变量时就要定义为 optional,因为有可能这个变量为空,所以我们更改上面的代码如下:

let code = { [weak self] in
    print(self?.lanuage)
}

再运行就可以看到控制台输出 deinit,另外在控制 self 引用的时候还有个关键字 unowned,这个关键字和 weak 的区别是:unowned 同样不会强引用变量 self,但是它不会改变变量的类型为 optional,也就是说如果变量self被释放后,闭包中仍然使用的话会崩溃。所以在用 unowned 关键字的时候,要保证闭包在调用的时候,self不会被释放。

3.7 模板编程

模板编程也是 Swift 一大特点,对于代码重用来讲非常有用。

Swift 的模板编程借鉴了 C++ 很多特性,在学习 Swift 模板中,可以了解 Swift 模板编程中类型推断的方式,在写出来一些模块,类型推断出现 Confuse 报错的时候,可以更好的修改。如果感兴趣可以看下 Swift 类型推断的算法 Hindley-Milner

模板编程在做一些基础库时,非常又用。目前我们使用 Swift 主要是为了做上层的业务,还不需要很深入的了解模板编程。这里就不详细介绍,感兴趣可以看下官方教程 Swift 模板编程

3.8 并发编程

  1. async/await用法

和很多语言的async/await的语法,几乎一样。可以解决回调地狱地狱问题,使代码的可读性更强。这里引用下官方文档的例子如下:


listPhotos(inGallery: "Summer Vacation") { photoNames in
    let sortedNames = photoNames.sorted()
    let name = sortedNames[0]
    downloadPhoto(named: name) { photo in
        show(photo)
    }
}

使用async/await语法后,代码会更简洁。


func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

这里是官方文档有更详细的介绍。

  1. actor用法

actor 本质上为了解决多线程同步的问题,定义为 actor 的类,所有的属性和操作本质上都是线程安全的,默认都是加锁的。actor 并发模型,是很多函数式编程语言,多线程处理的方式,可以在编程语言的层面上避免了多线程锁使用的问题。actor模式的由来可以看actor模式


actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int

    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
}

由于一个 actor 本质上就是一个状态机,对 actor 的操作,就相当于消息事件的处理,所以我们在使用 actor 对象时,需要配合 aync/await 语法使用,通过await来等待消息处理的结果。


let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"

3.9 swift lint

Swift 语法检测可以用Realm语法规范

4 Swift不同语言的桥接

Swift 语言桥接C和C++必须通过OC来做,Swift 定义的函数如果需要 OC 引用的话,必须加上 @obj 的语法标注,Swift所有@开头的代表的是语法标注,有点像 java 中的语法标注,是在编译期间,可以改变AST语法一些结构。@obj就代表,在编译的 AST 中,插入 OC 的函数定义,便于 OC 的定义。其实标记语法在 SwiftUI 中大量使用,具体可以了解 @State语法的使用。

这里再说下 Swift 引用 OC 变量时,需要注意 nonnull 这两种形式在Swift中会被识别为非Optional类型,如果强制解包就会崩溃。

@property (nonatomic, strong,) NSString *name;
@property (nonatomic, strong, nonnull) NSString *name;

@property (nonatomic, strong, nullable) NSString *name;

并且 Swift 中的 struct 和 enum 类型,OC是无法引用的。所以Swift的库,如果想暴露出OC的接口,兼容性还是很差的。

4.1 Swift的patch能力

目前Swift patch能力还是比较弱的,一种方式通过把Swift函数,通过桥接成obj的方法,然后再通过objc的runtime方式进行patch,这个本质上就丧失了swift的便捷性。

另外一种方式就像hook C 的方式。通过动态库加载过程中,函数符号绑定时,找出来swift方法的具体内存地址,然后进行替换。这个可以参考fish hook。这种方法必须要把需要hook的代码,都做成dylib动态库进行加载,才能完成。并且寻找Swift的函数符号地址可能也不是一个容易的事情。

5 Swift语言的学习计划

下面是苹果官方关于 Swift 语言学习和发展的计划。

Swift开发计划 Swift开源项目 Swift官网学习资料 如果对Swift语法设计感兴趣的,可以看下苹果开源的文档Swift语法设计

苹果每次更新语法时,都会给出示例。例如 Swift 5.6示例

下面是一些比较有用的swift学习的论坛

https://www.objc.io/

swift官方文档的中文翻译

swiftUI的书籍

6 总结

Swift 语言由于是出自开源的 LLVM 编译器作者之手(LLVM是 Python,Rust,C++等一系列语言的编译器),所以里面吸收了很多语言的优秀特性,Swift 说到底是一门静态语言,学习的成本比 JS,Python,Ruby,Dark 这种弱类型语言,成本还是略高的,但是总体来讲比 C++ 这种语言学习成本要低很多。并且 Swift 同时具备了弱类型语言的语法便捷性,也具备强类型语言的安全和性能,所以还是很推荐大家学习的。

Swift 语言目前主要还是应用在苹果生态下的桌面和移动应用开发。但是由于 Swift 语言安全性和性能方面有很多优秀的特性,Swift 语言也产出了很多服务器开发的框架,例如 SwiftNIO 苹果开源的非阻塞IO网络框架。并且也有一些公司在 Swift 服务器开发上持续投入。例如 vapor 框架是一个开源的 HTTP 服务器,这个就是基于 SwiftNIO 框架实现的。不过由于苹果一向任性的做法,Swift 的语言版本兼容性做的一直很差(Go语言到现在还没有2.0,Swift 马上都6.0了),造成很多服务器开发者不敢轻易的采用,到了 Swift5.1 之后ABI稳定了,会不会有更多 Swift server 的项目,我们拭目以待。

这里再次强调下,这篇博客不是 Swift 的学习教程。只是挑选出来 Swift 语言的一些重要新特性做了介绍,对于将来使用 Swift 的时候可以更好理解其中的语法。当然文章中如果有一些错误之处,还望指出和勘正。希望和大家一同学习,共同提高,能够更好的运用 Swift 语言到实际的编程中。

参考文献

  1. https://docs.swift.org/swift-book/

  2. https://github.com/realm/SwiftLint

  3. https://github.com/apple/swift-evolution

  4. https://github.com/twostraws/whats-new-in-swift-5-6

  5. https://swiftgg.gitbook.io/swift/

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我