Swift 5.1有什么新功能?

3,586 阅读15分钟

Swift 5.1终于发布了!本文将带您了解该语言在最新版本中必须提供的改进和更改。

注意: 当前版本为Swift 5, iOS 13, Xcode 11, 如转载本文章, 请联系作者, 并给出文章的源地址

好消息:Swift 5.1现在可以在Xcode 11 beta版中使用了!这个版本带来了模块的稳定性,并改进了具有重要特性的语言。在本教程中,您将了解Swift 5.1的新特性。你需要Xcode 11 beta版才能与Swift 5.1兼容,所以在开始之前安装它吧。

入门

Swift 5.1Swift 5兼容。由于ABI稳定性,它还与Swift 5以及未来版本的Swift二进制兼容。

Swift 5.1Swift 5中引入的ABI稳定性之上增加了模块稳定性。虽然ABI稳定性在运行时负责应用程序兼容性,但模块稳定性使编译时的库兼容性成为可能。 这意味着您可以将第三方框架与任何编译器版本一起使用,而不是仅使用它构建的版本。

每个教程部分都包含Swift Evolution建议编号,例如**[SE-0001]**。 您可以通过单击每个提案的链接标记来浏览每个更改。

我建议您通过在操场上尝试新功能来学习本教程。 启动Xcode 11并转到File ▸ New ▸ Playground。 选择iOS作为平台,选择空白作为模板。 将其命名并将其保存在您想要的位置。 开始的时候了!

注:需要重温Swift 5的亮点吗?查看Swift 5教程:Swift 5有什么新功能?

语言改进

此版本中有许多语言改进,包括不透明的结果类型,函数构建器,属性包装器等。

Opaque Result Types

您可以使用协议作为Swift 5中函数的返回类型。

打开新的Playground后,通过导航到View ▸ Navigators ▸ Show Project Navigator打开项目导航器。 右键单击Sources文件夹,选择New File并将文件命名为BlogPost。 使用名为BlogPost的新协议的定义替换新文件的内容。

public protocol BlogPost {
  var title: String { get }
  var author: String { get }
}

右键单击顶层Playground并选择New playground Page。重新命名新的Playground页面Opaque教程,并粘贴在它:

// 1
struct Tutorial: BlogPost {
  let title: String
  let author: String
}

// 2
func createBlogPost(title: String, author: String) -> BlogPost {
  guard !title.isEmpty && !author.isEmpty else {
    fatalError("No title and/or author assigned!")
  }
  return Tutorial(title: title, author: author)
}

// 3
let swift4Tutorial = createBlogPost(title: "What's new in Swift 4.2?",
                                    author: "Cosmin Pupăză")
let swift5Tutorial = createBlogPost(title: "What's new in Swift 5?", 
                                    author: "Cosmin Pupăză")

一步一步来:

  1. 为教程声明标题和作者,因为教程实现了BlogPost
  2. 检查titleauthor是否有效,如果测试成功,则从createBlogPost(title:author :)返回Tutorial
  3. 使用createBlogPost(title:author:)创建swift4Tutorialswift5Tutorial

您还可以重用createBlogPost(title:author:)的原型和逻辑来创建屏幕广播,因为屏幕广播也是隐藏在幕后的博客文章。

右键单击顶层Playground并选择New playground Page。重命名新的Playground页面Opaque的屏幕截图,并粘贴到其中:

struct Screencast: BlogPost {
  let title: String
  let author: String
}

func createBlogPost(title: String, author: String) -> BlogPost {
  guard !title.isEmpty && !author.isEmpty else {
    fatalError("No title and/or author assigned!")
  }
  return Screencast(title: title, author: author)
}

let swift4Screencast = createBlogPost(title: "What's new in Swift 4.2?", 
                                      author: "Josh Steele")           
let swift5Screencast = createBlogPost(title: "What's new in Swift 5?", 
                                      author: "Josh Steele")

Screencast实现了BlogPost,因此您可以从createBlogPost(title:author:)返回Screencast,并使用createBlogPost(title:author:)创建swift4Screencastswift5Screencast

导航到源文件夹中的BlogPost.swift,并使BlogPost符合Equatable

public protocol BlogPost: Equatable {
  var title: String { get }
  var author: String { get }
}

此时,您将得到一个错误,即BlogPost只能用作通用约束。这是因为Equatable有一个名为Self的关联类型。具有关联类型的协议不是类型,即使它们看起来像类型。相反,它们有点像类型占位符,说“这可以是任何符合该协议的具体类型”。

Swift 5.1允许您使用这些协议作为常规类型,使用不透明的结果类型SE-0244

Opaque的教程页面中,向createBlogPost的返回类型添加一些,表示它返回BlogPost的具体实现。

func createBlogPost(title: String, author: String) -> some BlogPost {

类似地,在Opaque的屏幕显示页面中,使用some来告诉编译器createBlogPost返回某种类型的BlogPost

func createBlogPost(title: String, author: String) -> some BlogPost {

您可以从createBlogPost: TutorialScreencast返回实现BlogPost的任何具体类型。

现在,您可以检查之前创建的教程和屏幕截图是否相同。在Opaque Tutorials的底部,粘贴以下代码来检查swift4Tutorialswift5Tutorial是否相同。

let sameTutorial = swift4Tutorial == swift5Tutorial

在不透明的屏幕截图的底部,粘贴以下内容,检查swift4Screencastswift5Screencast是否相同。

let sameScreencast = swift4Screencast == swift5Screencast

单表达式函数隐式返回

Swift 5的单表达式函数中使用return:

extension Sequence where Element == Int {
  func addEvenNumbers() -> Int {
    return reduce(0) { $1.isMultiple(of: 2) ? $0 + $1 : $0 }
  }

  func addOddNumbers() -> Int {
    return reduce(0) { $1.isMultiple(of: 2) ? $0 : $0 + $1 }
  }
}

let numbers = [10, 5, 2, 7, 4]
let evenSum = numbers.addEvenNumbers()
let oddSum = numbers.addOddNumbers()

addEvenNumbers()addOddNumbers()中使用reduce(_:_:)来确定偶数和奇数的和。

Swift 5.1降低了单表达式函数的返回值,因此在本例中它们的行为类似于单行闭包SE-0255:

extension Sequence where Element == Int {
  func addEvenNumbers() -> Int {
    reduce(0) { $1.isMultiple(of: 2) ? $0 + $1 : $0 }
  }

  func addOddNumbers() -> Int {
    reduce(0) { $1.isMultiple(of: 2) ? $0 : $0 + $1 }
  }
}

这一次代码更简洁,更容易理解。

注:想了解更多关于reduce(_:_:)如何在Swift中工作?查看函数式编程教程: Swift中的函数式编程介绍

函数构造器

Swift 5.1使用函数构建器实现构建器模式SE-XXXX:

@_functionBuilder
struct SumBuilder {
  static func buildBlock(_ numbers: Int...) -> Int {
    return numbers.reduce(0, +)
  }
}

使用**@_functionBuilder注释SumBuilder**,使其成为函数生成器类型。函数构造器是一种特殊类型的函数,其中每个表达式(文字、变量名、函数调用、if语句等)都是单独处理的,并用于生成单个值。例如,您可以编写一个函数,其中每个表达式都将该表达式的结果添加到数组中,从而使您自己的数组成为文字类型。

注意:在Xcode beta中,函数构建器的注释是**@_functionBuilder**,因为这个建议还没有得到批准。一旦获得批准,预期注释将成为**@functionBuilder**。

通过实现具有特定名称和类型签名的不同静态函数,可以创建函数构建器。buildBlock(_: T...)是惟一必需的。还有一些函数可以处理if语句、选项和其他可以作为表达式处理的结构。

使用函数生成器时,要用类名注释函数或闭包:

func getSum(@SumBuilder builder: () -> Int) -> Int {
  builder()
}

let gcd = getSum {
  8
  12
  5
}

传递给getSum的闭包计算每个表达式(在本例中是三个数字),并将这些表达式的结果列表传递给构建器。函数构建器以及隐式返回是SwiftUI干净语法的构建块。它们还允许您创建自己的特定于域的语言。

属性包装

当你在Swift 5中处理计算属性时,你要处理很多样板代码:

var settings = ["swift": true, "latestVersion": true]

struct Settings {
  var isSwift: Bool {
    get {
      return settings["swift"] ?? false
    }
    set {
      settings["swift"] = newValue
   }
  }

  var isLatestVersion: Bool {
    get {
      return settings["latestVersion"] ?? false
    }
    set {
      settings["latestVersion"] = newValue
    }
  }
}

var newSettings = Settings()
newSettings.isSwift
newSettings.isLatestVersion
newSettings.isSwift = false
newSettings.isLatestVersion = false

isSwiftisLatestVersion在设置中获取和设置给定键的值。Swift 5.1通过定义属性包装器SE-0258去除重复代码:

// 1
@propertyWrapper
struct SettingsWrapper {
  let key: String
  let defaultValue: Bool

  // 2
  var wrappedValue: Bool {
    get {
      settings[key] ?? defaultValue
    }
    set {
      settings[key] = newValue
    }
  }
}

// 3
struct Settings {
  @SettingsWrapper(key: "swift", defaultValue: false) var isSwift: Bool
  @SettingsWrapper(key: "latestVersion", defaultValue: false) 
    var isLatestVersion: Bool
}

以上代码的工作原理如下:

  1. 使用**@propertyWrapper注释SettingsWrapper**,使其成为属性包装器类型。
  2. 使用wrappedValue在设置中获取和设置键。
  3. 标记isSwiftisLatestVersion作为**@SettingsWrapper**来使用相应的包装器实现它们。

图片

合成结构中初始化函数的默认值

默认情况下,Swift 5不会为结构中的属性设置初始值,所以您可以为它们定义自定义初始化器:

struct Author {
  let name: String
  var tutorialCount: Int

  init(name: String, tutorialCount: Int = 0) {
    self.name = name
    self.tutorialCount = tutorialCount
  }
}

let author = Author(name: "George")

在这里,如果作者通过了测试并在网站上加入了教程团队,则将tutorialCount设置为0

Swift 5.1允许直接设置结构属性的默认值,因此不再需要自定义初始化器SE-0242:

struct Author {
  let name: String
  var tutorialCount = 0
}

这一次代码更干净、更简单。

静态成员的Self

Swift 5中,你不能使用Self来引用数据类型的静态成员,所以你必须使用类型名:

struct Editor {
  static func reviewGuidelines() {
    print("Review editing guidelines.")
  }

  func edit() {
    Editor.reviewGuidelines()
    print("Ready for editing!")
  }
}

let editor = Editor()
editor.edit()

网站上的编辑在编辑教程之前会检查编辑指南,因为它们总是在变化。

你可以用Swift 5.1 SE-0068中的Self重写整个代码:

struct Editor {
  static func reviewGuidelines() {
    print("Review editing guidelines.")
  }

  func edit() {
    Self.reviewGuidelines()
    print("Ready for editing!")
  }
}

这次使用Self调用reviewGuidelines()

创建未初始化数组

您可以在Swift 5.1 SE-0245中创建未初始化的数组:

// 1
let randomSwitches = Array<String>(unsafeUninitializedCapacity: 5) {
  buffer, count in
  // 2
  for i in 0..<5 {
    buffer[i] = Bool.random() ? "on" : "off"
  }
  // 3
  count = 5
}

逐步浏览上述守则:

  1. 使用init(unsafeUninitializedCapacity:initializingWith:)创建具有特定初始容量的随机开关。
  2. 循环通过随机开关,并使用random()设置每个开关状态。
  3. 为随机开关设置初始化元素的数量。

Diffing命令集合

Swift 5.1允许您确定有序集合之间的差异SE-0240

假设有两个数组:

let operatingSystems = ["Yosemite",
                        "El Capitan",
                        "Sierra",
                        "High Sierra",
                        "Mojave",
                        "Catalina"]
var answers = ["Mojave",
               "High Sierra",
               "Sierra",
               "El Capitan",
               "Yosemite",
               "Mavericks"]

operatingSystems包含了所有的macOS版本,从最老的版本到最新的版本。答案以相反的顺序列出它们,同时添加和删除其中一些。

区分集合要求您使用#if Swift(>=)检查最新的Swift版本,因为所有区分方法都标记为**@available for Swift 5.1**:

#if swift(>=5.1)
  let differences = operatingSystems.difference(from: answers)
  let sameAnswers = answers.applying(differences) ?? []
  // ["Yosemite", "El Capitan", "Sierra", "High Sierra", "Mojave", "Catalina"]

获取操作系统和答案之间的difference(from:),并使用apply(_:)将它们应用于答案。

或者,你也可以手动操作:

  // 1
  for change in differences.inferringMoves() {
    switch change {
      // 2
      case .insert(let offset, let element, let associatedWith):
        answers.insert(element, at: offset)
        guard let associatedWith = associatedWith else {
          print("\(element) inserted at position \(offset + 1).")
          break
        }
        print("""
              \(element) moved from position \(associatedWith + 1) to position 
              \(offset + 1).
              """)
      // 3
      case .remove(let offset, let element, let associatedWith):
        answers.remove(at: offset)
        guard let associatedWith = associatedWith else {
          print("\(element) removed from position \(offset + 1).")
          break
        }
        print("""
              \(element) removed from position \(offset + 1) because it should be 
                at position \(associatedWith + 1).
              """)
    }
  }
#endif

下面是这段代码的作用:

  1. 使用inferringMoves()确定差异中的移动,并循环遍历它们。
  2. 如果change.insert(offset:element:associatedWith:),则在偏移量处向答案添加元素;如果associatedWith不是nil,则将插入视为移动。
  3. 如果change.remove(offset:element:associatedWith:),则从答案的偏移处删除元素,如果associatedWith不是nil,则认为删除是一个移动。

图片

静态和类下标

Swift 5.1允许您在类中声明静态和类下标SE-0254:

// 1
@dynamicMemberLookup
class File {
  let name: String

  init(name: String) {
    self.name = name
  }

  // 2
  static subscript(key: String) -> String {
    switch key {
      case "path":
        return "custom path"
      default:
        return "default path"
    }
  }

  // 3
  class subscript(dynamicMember key: String) -> String {
    switch key {
      case "path":
        return "custom path"
      default:
        return "default path"
    }
  }
}

// 4
File["path"]
File["PATH"]
File.path
File.PATH

事情是这样的:

  1. 将文件标记为**@dynamicMemberLookup**,以便为自定义下标启用点语法。
  2. 创建一个静态下标,返回文件的默认或自定义路径。
  3. 使用动态成员查找定义前一个下标的类版本。
  4. 使用相应的语法调用这两个下标。

注:想了解更多关于斯威夫特下标?查看下标教程: 自定义Swift下标

动态查找成员变量的路径

Swift 5.1实现键路径的动态成员查找SE-0252:

// 1
struct Point {
  let x, y: Int
}

// 2
@dynamicMemberLookup
struct Circle<T> {
  let center: T
  let radius: Int

  // 3
  subscript<U>(dynamicMember keyPath: KeyPath<T, U>) -> U {
    center[keyPath: keyPath]
  }
}

// 4
let center = Point(x: 1, y: 2)
let circle = Circle(center: center, radius: 1)
circle.x
circle.y

一步一步来:

  1. 声明xyPoint
  2. 使用**@dynamicMemberLookup注释Circle**,以启用其下标的点语法。
  3. 创建一个通用下标,它使用键路径从Circle访问center属性。
  4. 使用动态成员查找而不是键路径在circle上调用中心属性。

注:需要更多关于如何在斯威夫特动态成员查找工作的细节?查看动态特性教程: Swift中的动态特性

Keypaths元组

你可以在Swift 5.1中使用元组的关键路径:

// 1
struct Instrument {
  let brand: String
  let year: Int
  let details: (type: String, pitch: String)
}

// 2
let instrument = Instrument(brand: "Roland",
                            year: 2019,
                            details: (type: "acoustic", pitch: "C"))
let type = instrument[keyPath: \Instrument.details.type]
let pitch = instrument[keyPath: \Instrument.details.pitch]

事情是这样的:

  1. 申报仪器的品牌、年份及详细资料。
  2. 使用键路径从乐器的细节中获取类型和音高。

weak和unknown属性的Equatable和Hashable一致性

Swift 5.1自动合成具有弱和无标识存储特性的结构的EquatableHashable一致性。

假设您有两个类:

class Key {
  let note: String

  init(note: String) {
    self.note = note
  }
}

extension Key: Hashable {
  static func == (lhs: Key, rhs: Key) -> Bool {
    lhs.note == rhs.note
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(note)
  }
}

class Chord {
  let note: String

  init(note: String) {
    self.note = note
  }
}

extension Chord: Hashable {
  static func == (lhs: Chord, rhs: Chord) -> Bool {
    lhs.note == rhs.note
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(note)
  }
}

通过实现==(lhs:rhs:)hash(into:)KeyChord都符合EquatableHashable

如果你在结构体中使用这些类,Swift 5.1将能够合成Hashable:

struct Tune: Hashable {
  unowned let key: Key
  weak var chord: Chord?
}

let key = Key(note: "C")
let chord = Chord(note: "C")
let tune = Tune(key: key, chord: chord)
let chordlessTune = Tune(key: key, chord: nil)
let sameTune = tune == chordlessTune
let tuneSet: Set = [tune, chordlessTune]
let tuneDictionary = [tune: [tune.key.note, tune.chord?.note], 
                      chordlessTune: [chordlessTune.key.note, 
                      chordlessTune.chord?.note]]

TuneEquatableHashable,因为valuechordEquatableHashable

因为它是Hashable,你可以将tunechordlessTune进行比较,将它们添加到tuneSet并将它们用作tuneDictionary的键。

可选的Enumeration Case

Swift 5.1为可选枚举情况生成警告:

// 1
enum TutorialStyle {
  case cookbook, stepByStep, none
}

// 2
let style: TutorialStyle? = .none

这是如何工作的:

  1. TutorialStyle定义不同的样式。
  2. Swift会发出警告,因为编译器不清楚.none在这种情况下的含义是什么:Optional.noneTutorialStyle.none

匹配非可选项的可选枚举

您可以使用可选模式将非选项与Swift 5中的可选枚举进行匹配:

// 1
enum TutorialStatus {
  case written, edited, published
}

// 2
let status: TutorialStatus? = .published

switch status {
  case .written?:
    print("Ready for editing!")
  case .edited?:
    print("Ready to publish!")
  case .published?:
    print("Live!")
  case .none:
    break
}

上面的代码执行以下操作:

  1. 声明TutorialStatus的所有可能状态。
  2. 使用可选模式打开状态,因为您将其定义为可选模式。

在这种情况下,Swift 5.1删除了可选的模式匹配:

switch status {
  case .written:
    print("Ready for editing!")
  case .edited:
    print("Ready to publish!")
  case .published:
    print("Live!")
  case .none:
    break
}

这段代码更清晰,更容易理解。

注意:想要了解有关Swift中模式匹配的更多信息? 查看模式匹配教程:Swift中的模式匹配

字符串的新功能

Swift 5.1为字符串添加了一些急需的功能SE-0248

UTF8.width("S")
UTF8.isASCII(83)

在这里,您确定Unicode标量值的UTF-8编码宽度,并检查给定的代码单元是否表示ASCII标量。 查看您可以使用的其他API的提案。

连续的字符串

Swift 5.1对连续字符串实现重要更改SE-0247

var string = "**Swift 5.1**"
if !string.isContiguousUTF8 {
  string.makeContiguousUTF8()
}

您检查UTF-8编码的字符串是否与isContiguousUTF8连续,并使用makeContiguousUTF8()来实现,如果不是。 看一下提案,看看你可以用连续的字符串做些什么。

其他的一些改进

您应该了解Swift 5.1中的一些其他功能:

转换元组类型

Swift 5.1改进了元组类型的转换:

let temperatures: (Int, Int) = (25, 30)
let convertedTemperatures: (Int?, Any) = temperatures

您可以为convertedTemperatures分配温度,因为在这种情况下您可以将(Int, Int)转换为(Int?, Any)

具有重复标签的元组

您可以在Swift 5中声明带有重复标签的元组:

let point = (coordinate: 1, coordinate: 2)
point.coordinate

在这种情况下,不清楚坐标是否从点返回第一个或第二个元素,因此Swift 5.1删除了元组的重复标签。

使用任何参数重载函数

Swift 5更喜欢任何参数而不是泛型参数,只有一个参数的函数重载:

func showInfo(_: Any) -> String {
  return "Any value"
}

func showInfo<T>(_: T) -> String {
  return "Generic value"
}

showInfo("Swift 5")

在这种情况下,showInfo()返回“Any value”。 Swift 5.1以相反的方式工作:

func showInfo(_: Any) -> String {
  "Any value"
}

func showInfo<T>(_: T) -> String {
  "Generic value"
}

showInfo("**Swift 5.1**")

showInfo()这次返回“Generic value”。

为自动关闭参数键入别名

你不能在Swift 5中为**@autoclosure**参数声明类型别名:

struct Closure<T> {
  func apply(closure: @autoclosure () -> T) {
    closure()
  }
}

apply(closure :)在这种情况下使用autoclosure声明闭包。 您可以在Swift 5.1中的apply(closure :)原型中使用类型别名:

struct Closure<T> {
  typealias ClosureType = () -> T

  func apply(closure:  @autoclosure ClosureType) {
    closure()
  }
}

apply(closure :)这次使用ClosureType进行闭包。

从Objective-C方法返回self

如果你的类包含一个在Swift 5中返回Self的**@objc方法,你必须从NSObject**继承:

class Clone: NSObject {
  @objc func clone() -> Self {
    return self
  }
}

因为Clone扩展了NSObject,所以clone()返回Self。 在Swift 5.1中不再是这种情况:

class Clone {
  @objc func clone() -> Self {
    self
  }
}

克隆这次不必继承任何东西。

稳定的ABI图书馆

您可以在Swift 5.1中使用**-enable-library-evolution来更改库类型而不会破坏其ABI**。 标记为**@frozen**的结构和枚举不能添加,删除或重新排序存储的属性和案例SE-0260

然后去哪儿?

您可以使用本教程顶部或底部的“下载材料”链接下载最终的Playground

Swift 5.1Swift 5中已经引入的功能添加了许多不错的功能。它还为语言带来了模块稳定性,并实现了WWDC中引入的新框架(如SwiftUICombine)所使用的复杂范例。

您可以在官方Swift CHANGELOGSwift标准库差异上阅读有关此Swift版本更改的更多信息。

您还可以查看Swift Evolution提案,了解下一版Swift的内容。 在这里,您可以为当前审核的提案提供反馈,甚至可以自行提交提案!

项目示例: 工程示例