Swift 5 新特性

概览

Swift 5 发布了,这是一个重要里程碑。

此版本终于迎来了 ABI 稳定,因此 Swift 运行时现在可以引入到 Apple 平台各类操作系统的不同版本中,包括 macOS、iOS、tvOS 与 watchOS。Swift 5 还引入了构建块的新功能,包括重新实现 String、在运行时对执行内存的独占访问与新数据类型,以及对动态可调用类型的支持。

Swift 5 兼容 Swift 4、Swift 4.1 和 Swift 4.2,Xcode 10.2 中包含了一个代码迁移器,可以自动处理许多迁移需要用到的源码更改。

最重要的更新:ABI稳定

  1. 什么是ABI。
    在运行时,Swift程序二进制文件通过ABI与其他库和组件交互,ABI定义了很多底层细节:如何调用函数,如何在内存中表示数据,甚至是元数据的位置以及如何访问它。
  2. 什么是ABI稳定
    此前发布的Swift版本中ABI还没稳定,Swift并没包含在操作系统中,所以每一个应用内部都包含其Swift版本所对应的动态链接库
    Swift 5将为Swift的标准库提供稳定的ABI,Swift将包含在操作系统中,新版本的编译器会根据稳定的ABI来编译生成的应用程序二进制文件。也就是说,以后新发布的编译器也可以编译旧版Swift 5代码,源代码实现兼容。
  3. ABI稳定带来的便利
    • 因为源代码兼容,开发者能够跨多个Swift版本维护单个代码库;
    • Swift应用程序包会变得更小;
    • Swift语言变化以及变化频率都会有所下降;
  4. ABI稳定下阶段的目标
    关于Swift ABI 稳定规划,Swift 5完成的是第一阶段的源兼容(Source compatibility),下半部分是二进制framework和运行时兼容(Binary framework & runtime compatibility),详细描述可见宣言Swift ABI Stability Manifesto。

新特性

Raw Strings (原始字符串)

SE-0200,增加了创建原始字符串的功能,使得写带有特殊符号的字符串更加简单。

  • 要使用原始字符串, 可使用#将字符串包裹起来;
let str = #"This is also a Swift string literal"#
  • 原始字符串中反斜杠和双引号会被视为字符串中的文字字符;
// before
let rain = "The \"rain\" in \"Spain\" falls \\mainly on the Spaniards." 
// after
let rain = #"The "rain" in "Spain" falls \mainly on the Spaniards."#  
  • 由于反斜杠作为原始字符串中的字符,所以在插入值的时候需要在后面再加个 #;
let answer = 42
// before
let dontpanic = "The answer to life, the universe, and everything is \(answer)"
// after
let dontpanic = #"The answer to life, the universe, and everything is \#(answer)"#
  • 如果需要在原始字符串包含 # 时, 前后应用 ## 包裹字符串
let str = ##"My dog said "woof"#gooddog"##
  • 多行原始字符串用 #""" 开头 """#结尾
  let multiline = #"""
  The answer to life,
  the universe,
  and everything is \#(answer).
  """#
  • 由于不用反斜杠转义,使得正则表达式更加简洁明了
// before
let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"
// after
let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#

标准类型Result

SE-0235,增加Result类型到Swift标准库,用于统一在异步完成处理程序中看到的笨拙的不同参数。

public enum Result {
    case success(Success), failure(Failure)
}

例如,在URLSession完成处理包含三个参数,处理起来就不怎么优雅:

URLSession.shared.dataTask(with: url) { (data, response, error) in
    guard error != nil else { self.handleError(error!) }
    
    guard let data = data, let response = response else { return /* Impossible? */ }
    
    handleResponse(response, data: data)
}

这几行代码暴露了Swift缺乏对错误自动处理的缺点,在这里不仅是因为需要对error做处理,而且缺少比如data和response都为空等特殊情况的处理,使用Result则会非常的优雅:

URLSession.shared.dataTask(with: url) { (result: Result<(response: URLResponse, data: Data), Error>) in 
    switch result {
    case let .success(success):
        handleResponse(success.response, data: success.data)
    case let .error(error):
        handleError(error)
    }
}

自定义字符串插值

SE-0228大大改进了Swift的字符串插值系统,使其更高效,更灵活。

主要用法就是在String.StringInterpolation的拓展里添加自定义的插值方法,比如加一个格式化字符串的方法:

extension String.StringInterpolation {
    public mutating func appendInterpolation(_ number: Double?, format formatString: String) {
        if let number = number{
            return appendLiteral(String(format: formatString, number))
        } else {
            return appendLiteral("nil")
        }
    }
}

然后就可以在字符串中使用自定义的插值方法

let number = 123.666
print("Hello, number is \(number")
print("Hello, number is \(number, format:"%.2f")")
print("Hello, number is \(nil, format:"%.2f")")
============================================================================
Hello, number is 123.666
Hello, number is 123.67
Hello, number is nil

所有字符串的处理、统计、添加html属性等操作都可以直接通过新的插值方式实现。

Dynamically callable types(动态可调用类型)

SE-0216@dynamicCallable为Swift 添加了一个新属性,它带来了将类型标记为可直接调用的功能,支持应用于结构,枚举,类和协议,但扩展不可以。

如果需要添加@dynamicCallable属性, 就必须要实现以下方法中的一个或者两个:

// 不需要指定参数名
func dynamicallyCall(withArguments args: [Int]) -> Double
// 指定参数名的方法
func dynamicallyCall(withKeywordArguments args: KeyValuePairs) -> Double

对比Swift 5之前的定义和调用方式:

// 定义方式
struct RandomNumberGenerator {
    func generate(numberOfZeroes: Int) -> Double {
        let maximum = pow(10, Double(numberOfZeroes))
        return Double.random(in: 0...maximum)
    }
}

// 调用方式
let random = RandomNumberGenerator()
let num = random.generate(numberOfZeroes: 2)

使用Swift 5 的@dynamicCallable属性

// 定义方式
@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let numberOfZeroes = Double(args.first ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

// 调用方式
let random = RandomNumberGenerator()
let num = random(2) // random(2)等同于random.dynamicallyCall(withArguments: [2])

处理未来添加的枚举类型

SE-0192 在枚举新增加一个 @unknown 属性,用于区分两种稍有不同的情况:1. default 的 case 里面处理所有其他的 case;2. 需要单独处理所有 case,只有真正不符合所有 case,才会进入 default提示。比如:

enum PasswordError: Error {
    case short
    case obvious
    case simple
}

func showOld(error: PasswordError){
    switch error {
        case .short:
        print("Your password was too short.")
        case .obvious:
        print("Your password was too obvious.")
        default:
        print("Your password was too simple.")
    }
}

上面代码假如我们再加个 case old,执行代码时它会自动进入到default分支,这是显然是,因为这个密码是一个旧密码而不是密码太简单,这时候可以用 @unknown,如下

func showNew(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    @unknown default:
        print("Your password wasn't suitable.")
    }
}

这时Xcode会产生一个警告⚠️Switch must be exhaustive. Do you want to add missing cases?,因为新增了case old ,switch没有明确地处理每一个分支。

可变参数

Swift 5之前,可以编写一个带有可变参数的枚举, 如下:

enum X {
    case foo(bar: Int...) 
}
func baz() -> X {
    return .foo(bar: 0, 1, 2, 3) 
} 

Swift 5之后, 上述定义改成数组参数, 而不是可变参数, 如下:

enum X {
    case foo(bar: [Int]) 
} 

func baz() -> X {
    return .foo(bar: [0, 1, 2, 3]) 
} 

try?可选值嵌套

SE-0230 让try?返回的多层 optional value嵌套值变成只有一层,无论有多少可嵌套的可选值,返回值永远只是一个可选值。先看一下这个例子

struct User {
    var id: Int

    init?(id: Int) {
        if id < 1 {
            return nil
        }

        self.id = id
    }

    func getMessages() throws -> String {
        // complicated code here
        return "No messages"
    }
}

let user = User(id: 1)
let messages = try? user?.getMessages()

Swift4.2及其之前的版本中,上面返回的是一个String??,2层嵌套的可选值,如果有多层嵌套处理起来也是相当更麻烦。在Swift 5中就完美的解决了这个问题,如果当前值是可选的,那么try?将不会将值包装在可选值中,因此最终结果只是一个String?

整型倍数判断

SE-0225为整数类型添加了一个方法isMultiple(of:),可以检查一个整数是否为另一个整数的倍数。

let rowNumber = 4

if rowNumber.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}

count函数

SE-0220,在Swift之前的版本中,有一个函数filter可以过滤出数组中符合条件的的元素,组成一个新的数组,在Swift 5中新增了一个函数count(where:), 可以获取数组中符合条件的元素的个数。

let arr = [1, 2, 34, 5, 6, 7, 8, 12, 45, 6, 9]

let filter = arr.filter({ $0 > 10 })
print(filter)  // [34, 12, 45]

let count = arr.count(where: { $0 > 10 })
print(count)   // 3

compactMapValues过滤字典元素

SE-0218compactMapValues()为字典添加了一种新方法,将Swift4.x的版本有两个函数compactMapmapValue结合在一起。

  • compactMap: 返回一个操作后得到的新的数组, 类似flatMap
  • mapValues: 字典中的函数, 对字典的value值执行操作, 返回改变value后的新的字典
let times = [
    "first": 2,
    "second": 43,
    "three": 12,
    "four": 3
]

let compact = times.compactMap({ $0.value > 10 }) 
// [true, false, true, false]

let mapValues = times.mapValues({ $0 + 2 }) 
// ["second": 45, "first": 4, "three": 14, "four": 5]

compactMapValues是将上述两个方法的功能合并在一起, 返回一个对value操作后的新字典, 并且自动过滤不符合条件的键值对,下面的例子展示把非整形类型的键值对过滤掉:

let times = [
    "Hudson": "38",
    "Clarke": "42",
    "Robinson": "35",
    "Hartis": "DNF"
]
let finishers1 = times.compactMapValues { Int($0) }
// ["Clarke": 42, "Robinson": 35, "Hudson": 38]

参看文献:

  1. swift-evolution
  2. Swift5.0新特性

你可能感兴趣的:(Swift 5 新特性)