Swift 5有什么新功能?

3,456 阅读9分钟

好消息! Swift 5最终在Xcode 10.2中可用! 此版本带来了ABI的稳定性,并通过一些期待已久的功能改进了语言。

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

在本教程中,您将了解Swift 5中最重要的更改. Swift 5需要Xcode 10.2,因此请确保在开始之前已安装好。

入门

Swift 5Swift 4.2兼容,但与早期的Swift版本不兼容。但是,由于ABI稳定性,未来版本将与Swift 5二进制兼容。

ABI稳定性支持使用不同Swift版本编译的应用程序和库之间的二进制兼容性。 Swift标准库和运行时嵌入在操作系统中,因此应用程序不会在任何平台上分发自己的库副本。这导致更好的工具解耦和OS集成。

您还需要ABI稳定性来分发跨多个Swift版本的二进制框架。这需要模块格式稳定性,这可以稳定包含编译器的框架公共接口表示的模块文件。

您将在本教程的每个部分中找到类似**[SE-0001]**的Swift Evolution建议编号。您可以浏览每个提案链接,详细了解每项新变化。

遵循本教程的最佳方法是在操场上试用新功能。

启动Xcode 10.2并选择File ▸ New ▸ Playground。将平台设置为iOS,将模板设置为Blank。命名并将其保存在您想要的任何地方开始的时候了!

注意:需要快速提醒一下Swift 4.2亮点? 查看Swift 4.2教程:Swift 4.2中有哪些新功能?

语言改进

Swift 5中有许多语言功能,例如动态可调用类型,处理未来的枚举等等。

Integer的Multiple检测

Swift 4.2中,您可以使用余数运算符确定数字是否是另一个的倍数:

let firstNumber = 4
let secondNumber = 2
if secondNumber != 0 && firstNumber % secondNumber == 0 {
  print("\(secondNumber) * \(firstNumber / secondNumber) = \(firstNumber)")
}

这段代码的工作原理:

  1. 检查secondNumber是否为0
  2. 检查将firstNumber除以secondNumber会返回0的余数。
  3. 执行除法运算。

您必须检查secondNumber是否为0,因为运算符会抛出错误。

Swift 5通过向BinaryInteger SE-0225添加isMultiple(of :)来简化这一过程:

if firstNumber.isMultiple(of: secondNumber) {
  print("\(secondNumber) * \(firstNumber / secondNumber) = \(firstNumber)")
}

即使你将参数传递给0isMultiple(of :)也可以工作,结果代码更清晰。

避免转义字符串

Swift 4.2使用转义序列来表示字符串中的反斜杠和引号:

let escape = "You use escape sequences for \"quotes\"\\\"backslashes\" in Swift 4.2."
let multiline = """
                You use escape sequences for \"\"\"quotes\"\"\"\\\"\"\"backslashes\"\"\"
                on multiple lines
                in Swift 4.2.
                """

Swift 5添加转义字符串的新方式。 您在字符串的开头和结尾添加#,这样您就可以使用反斜杠和引号而不会出现问题。SE-0200

let raw = #"You can create "raw"\"plain" strings in Swift 5."#
let multiline = #"""
                You can create """raw"""\"""plain""" strings
                on multiple lines
                in Swift 5.
                """#

在原始字符串中使用字符串插值时,必须在反斜杠后使用#号:

let track = "Nothing Else Matters"
print(#"My favorite tune\song is \#(track)."#)

在某些情况下,您需要在字符串的开头和结尾使用多个#

let hashtag = ##"You can use the Swift "hashtag" #swift in Swift 5."##

在上面的代码中,您在hashtag的开头和结尾添加##,以便您可以在字符串中表示#。 字符串开头使用的#必须与其末尾的数字相匹配。

Swift 4.2中,您可以在正则表达式中转义反斜杠,如下所示:

// 1
let versions = "3 3.1 4 4.1 4.2 5"
let range = NSRange(versions.startIndex..., in: versions)
// 2
let regex = try! NSRegularExpression(pattern: "\\d\\.\\d")
// 3
let minorVersions = regex.matches(in: versions, range: range)
// 4
minorVersions.forEach { print(versions[Range($0.range, in:  versions)!]) }

以下是此代码的工作原理:

  1. 声明版本并定义覆盖整个字符串的范围。
  2. 定义一个匹配版本中所有次要Swift版本的正则表达式。
  3. 使用matches(in: options: range:)确定次要版本范围。
  4. 使用范围从版本中获取次要版本。

Swift 5使用原始字符串简化了正则表达式:

let regex = try! NSRegularExpression(pattern: #"\d\.\d"#)

在此代码中,您使用反斜杠数量的一半来编写正则表达式,因为您不需要在原始字符串中转义反斜杠。

图片

注意:需要有关正则表达式如何在Swift中工作的更多详细信息? 查看正则表达式教程:正则表达式简介

使用Character新的属性

在处理字符时,Swift 4.2需要常见任务的变通方法:

let id = "ID10"
var digits = 0
id.forEach { digits += Int(String($0)) != nil ? 1 : 0 }
print("Id has \(digits) digits.")

在此代码中,首先将每个字符转换为String,然后再转换为Int,以确定id的位数。

但是,Swift 5Character添加了属性,使角色更易于使用SE-0221

id.forEach { digits += $0.isNumber ? 1 : 0 }

在这种情况下,您使用isNumber来检查每个字符是否都是数字。 查看您可以使用的其他属性的提案。

使用新的Unicode标量属性

Swift 4.2中,您为Unicode标量实现了文本处理算法,如下所示:

let username = "bond007"
var letters = 0
username.unicodeScalars.forEach { 
  letters += (65...90) ~= $0.value || (97...122) ~= $0.value ? 1 : 0
}
print("Username has \(letters) letters.")

在此代码中,您可以通过检查每个字符的Unicode标量是代表小写字母还是大写字母来计算用户名的字母数。

Swift 5Unicode标量添加了属性,简化了文本处理SE-0211

username.unicodeScalars.forEach { letters += $0.properties.isAlphabetic ? 1 : 0 }

在此代码中,您使用isAlphabetic检查每个字符是否为数字。 链接的提案显示您可以检查的所有属性。

删除子序列

Swift 4.2Sequence自定义点返回SubSequence,如下所示:

extension Sequence {
  func remove(_ s: String) -> SubSequence {
    guard let n = Int(s) else {
      return dropLast()
    }
    return dropLast(n)
  }
}

let sequence = [5, 2, 7, 4]
sequence.remove("2") // [5, 2]
sequence.remove("two") // [5, 2, 7]

在这种情况下,如果sInt或最后一个元素,remove(_ :)将删除序列中的最后n个元素。

Swift 5用序列中的具体类型替换SubSequence SE-0234

extension Sequence {
  func remove(_ s: String) -> [Element] {
    guard let n = Int(s) else {
      return dropLast()
    }
    return dropLast(n)
  }
}

在此代码中,remove(_ :)返回[Element],因为dropLast()dropLast(_ :)返回[Element]

字典更新

Swift 5为词典带来了期待已久的改进:

压缩字典

Swift 4.2使用mapValuesfilterreduce来过滤字典中的nil值,如下所示:

let students = ["Oana": "10", "Nori": "ten"]
let filterStudents = students.mapValues(Int.init)
  .filter { $0.value != nil }
  .mapValues { $0! }
let reduceStudents = students.reduce(into: [:]) { $0[$1.key] = Int($1.value) }

此代码使用带有filterreducemapValues来确定学生的有效成绩。 这两种方法都需要多次字典传递并使代码复杂化。

Swift 5使用compactMapValues(_ :)来获得更有效的解决方案SE-0218

let mapStudents = students.compactMapValues(Int.init)

它以更少的代码行完成同样的事情,整洁!

重命名DictionaryLiteral为KeyValuePairs

Swift 4.2使用DictionaryLiteral来声明字典,如下所示

let pets: DictionaryLiteral = ["dog": "Sclip", "cat": "Peti"]

DictionaryLiteral不是字典或文字。 这是一个键值对列表。

Swift 5DictionaryLiteral重命名为KeyValuePairs SE-0214

let pets: KeyValuePairs = ["dog": "Sclip", "cat": "Peti"]

数字协议更新

Swift 4.2为向量实现数值:

// 1
struct Vector {
  let x, y: Int
  
  init(_ x: Int, _ y: Int) {
    self.x = x
    self.y = y
  }
}

// 2
extension Vector: ExpressibleByIntegerLiteral {
  init(integerLiteral value: Int) {
    x = value
    y = value
  }
}

// 3
extension Vector: Numeric {
  var magnitude: Int {
    return Int(sqrt(Double(x * x + y * y)))
  }  

  init?<T>(exactly value: T) {
    x = value as! Int
    y = value as! Int
  }
  
  static func +(lhs: Vector, rhs: Vector) -> Vector {
    return Vector(lhs.x + rhs.x, lhs.y + rhs.y)
  }
  
  static func +=(lhs: inout Vector, rhs: Vector) {
    lhs = lhs + rhs
  }
  
  static func -(lhs: Vector, rhs: Vector) -> Vector {
    return Vector(lhs.x - rhs.x, lhs.y - rhs.y)
  }
  
  static func -=(lhs: inout Vector, rhs: Vector) {
    lhs = lhs - rhs
  }
  
  static func *(lhs: Vector, rhs: Vector) -> Vector {
    return Vector(lhs.x * rhs.y, lhs.y * rhs.x)
  }
  
  static func *=(lhs: inout Vector, rhs: Vector) {
    lhs = lhs * rhs
  }
}

// 4
extension Vector: CustomStringConvertible {
  var description: String {
    return "(\(x) \(y))"
  }
}

以下是此代码的工作原理:

  1. Vector声明xyinit(_: _ :)

  2. 实现init(integerLiteral :)以使Vector符合ExpressibleByIntegerLiteral作为数字一致性的要求。

  3. 通过定义向量的大小,声明init(exactly:)并实现+(lhs: rhs :)+=(lhs: rhs :)-(lhs: rhs :)-=(lhs: rhs: )*(lhs:rhs :)*=(lhs: rhs :)

  4. 实现描述以使Vector符合CustomStringConvertible

上面的代码使您可以轻松地使用向量:

var first = Vector(1, 2) // (1,2)
let second = Vector(3, 4) // (3,4)
let third = first + second // (4,6)
first += second // (4,6)
let fourth = first - second // (1,2)
first -= second // (1,2)

Swift 5实现了向量的AdditiveArithmetic,因为您无法定义2D向量的叉积SE-0233。 它不需要ExpressibleByIntegerLiteral一致性:

extension Vector: AdditiveArithmetic {
  static var zero: Vector {
    return Vector(0, 0)
  }
  
  static func +(lhs: Vector, rhs: Vector) -> Vector {
    return Vector(lhs.x + rhs.x, lhs.y + rhs.y)
  }
  
  static func +=(lhs: inout Vector, rhs: Vector) {
    lhs = lhs + rhs
  }
  
  static func -(lhs: Vector, rhs: Vector) -> Vector {
    return Vector(lhs.x - rhs.x, lhs.y - rhs.y)
  }
  
  static func -=(lhs: inout Vector, rhs: Vector) {
    lhs = lhs - rhs
  }
}

在这段代码中,你通过定义零并实现+(lhs: rhs :)+=(lhs: rhs :)-(lhs: rhs :)- =(lhs: rhs :)来使Vector符合AdditiveArithmetic

图片

注意:想要了解有关Swift中运算符重载的更多信息? 查看运算符重载教程:在Swift中重载自定义运算符

字符串插值更新

Swift 4.2通过插入段实现字符串插值:

let language = "Swift"
let languageSegment = String(stringInterpolationSegment: language)
let space = " "
let spaceSegment = String(stringInterpolationSegment: space)
let version = 4.2
let versionSegment = String(stringInterpolationSegment: version)
let string = String(stringInterpolation: languageSegment, spaceSegment, versionSegment)

在此代码中,编译器首先包装每个文本段,然后使用init(stringInterpolationSegment :)插入一个。 然后,它用init(stringInterpolation :)将所有段包装在一起。

Swift 5采用了完全不同的方法SE-0228:

// 1
var interpolation = DefaultStringInterpolation(
  literalCapacity: 7,
  interpolationCount: 1)
// 2
let language = "Swift"
interpolation.appendLiteral(language)
let space = " "
interpolation.appendLiteral(space)
let version = 5
interpolation.appendInterpolation(version)
// 3
let string = String(stringInterpolation: interpolation)

这是代码的作用:

  1. 使用特定容量和插值计数定义DefaultStringInterpolation实例。
  2. 调用appendLiteral(_ :)appendInterpolation(_ :)将文字和插值添加到插值中。
  3. 通过调用init(stringInterpolation :)生成最终的插值字符串。

处理未来的查点案件

Swift 4.2无法正确处理新的枚举案例,如下所示:

// 1
enum Post {
  case tutorial, article, screencast, course
}

// 2
func readPost(_ post: Post) -> String {
  switch post {
    case .tutorial:
      return "You are reading a tutorial."
    case .article:
      return "You are reading an article."
    default:
      return "You are watching a video."
  }
}

// 3
let screencast = Post.screencast
readPost(screencast) // "You are watching a video."
let course = Post.course
readPost(course) // "You are watching a video."

以下是上面代码中发生的情况:

  1. 在网站上定义所有类型的博客文章。
  2. 要使交换机无穷尽,请添加默认值。
  3. 由于截屏视频和课程是视频,因此默认处理.screencast.course

以下是处理播客在Swift 4.2中的工作原理:

enum Post {
  case tutorial, article, podcast, screencast, course
}

let podcast = Post.podcast
readPost(podcast) // "You are watching a video."

在此代码中,您使用默认处理.podcast,即使播客不是视频。 Swift 4.2不会对此发出警告,因为该开关是详尽无遗的。

Swift 5处理添加的枚举案例SE-0192

func readPost(_ post: BlogPost) -> String {
  switch post {
    case .tutorial:
      return "You are reading a tutorial."
    case .article:
      return "You are reading an article."
    @unknown default:
      return "You are reading a blog post."
  }
}

readPost(screencast) // "You are reading a blog post."
readPost(course) // "You are reading a blog post."
readPost(podcast) // "You are reading a blog post."

在此代码中,您将默认标记为**@unknown**,并且Swift警告您切换并非详尽无遗。 默认处理.screencast.course.podcast,因为截屏视频,课程和播客是博客文章。

图片

Result

Swift 5Result添加到标准库SE-0235

// 1
enum ConnectionError: Error {
  case noNetwork, noDatabase
}

// 2
let networkSuccess = Result<String, ConnectionError>.success("Network connected!")
let databaseSuccess = Result<String, ConnectionError>.success("Database connected!")
let networkFailure = Result<String, ConnectionError>.failure(.noNetwork)
let databaseFailure = Result<String, ConnectionError>.failure(.noDatabase)
let sameSuccess = networkSuccess == databaseSuccess
let sameFailure = networkFailure == databaseFailure
let success: Set = [networkSuccess, databaseSuccess]
let failure: Set = [networkFailure, databaseFailure]
let successDictionary = [
  networkSuccess: try! networkSuccess.get(),
  databaseSuccess: try! databaseSuccess.get()
]
let failureDictionary = [
  networkFailure: ConnectionError.noNetwork,
  databaseFailure: ConnectionError.noDatabase
]

以下是此代码的工作原理:

  1. 声明最常见的连接错误。
  2. 比较连接结果,将它们添加到集合中。 您使用这些集作为字典的键,因为Result实现了EquatableHashable

遵循Equatable和Hashable的Never类

Swift 5符合Never to EquatableHashable SE-0215

let alwaysSucceeds = Result<String, Never>.success("Network connected!")
let neverFails = Result<String, Never>.success("Database connected!")
let alwaysFails = Result<Never, ConnectionError>.failure(.noNetwork)
let neverSucceeds = Result<Never, ConnectionError>.failure(.noDatabase)
let sameValue = alwaysSucceeds == neverFails
let sameError = alwaysFails == neverSucceeds
let alwaysSuccess: Set = [alwaysSucceeds, neverFails]
let alwaysFailure: Set = [alwaysFails, neverSucceeds]
let alwaysSuccessDictionary = [
  alwaysSucceeds: try! alwaysSucceeds.get(),
  neverFails: try! neverFails.get()
]
let alwaysFailureDictionary = [
  alwaysFails: ConnectionError.noNetwork,
  neverSucceeds: ConnectionError.noDatabase
]

在此代码中,您定义始终返回值或错误的连接结果,比较它们,将它们添加到集合并将它们用作字典键。

动态可调用类型

Swift 5定义了可与脚本语言(如PythonRuby)互操作的动态可调用类型SE-0216

// 1
@dynamicCallable
class DynamicFeatures {
  // 2
  func dynamicallyCall(withArguments params: [Int]) -> Int? {
    guard !params.isEmpty else {
      return nil
    }
    return params.reduce(0, +)
  }
  
  func dynamicallyCall(withKeywordArguments params: KeyValuePairs<String, Int>) -> Int? {
    guard !params.isEmpty else {
      return nil
    }
    return params.reduce(0) { $1.key.isEmpty ? $0 : $0 + $1.value }
  }
}

// 3
let features = DynamicFeatures()
features() // nil
features(3, 4, 5) // 12
features(first: 3, 4, second: 5) // 8

上面的代码如下:

  1. DynamicFeatures标记为**@dynamicCallable**以使其成为动态可调用类型。

  2. 要使DynamicFeatures符合**@dynamicCallable**,请实现dynamicCallwithArguments :)和dynamicCall(withKeywordArguments :)

  3. 使用普通语法调用功能,编译器调用dynamicCall(withArguments :)dynamicCall(withKeywordArguments :)

Swift Package Manager更新

Swift 5Swift Package Manager添加了一些功能:

平台部署设置

Swift 5允许您在Package.swift SE-0236中定义所需的最低平台部署目标版本:

let package = Package(name: “Package”, platforms: [
  .macOS(.v10_14), 
  .iOS(.v12),
  .tvOS(.v12), 
  .watchOS(.v5)
])

您可以在SupportedPlatform中使用macOS()iOS()tvOS()watchOS()来设置Package所需的最低平台版本。

目标构建设置

Swift 5Package.swift中声明了特定于目标的构建设置。 它们定制包管理器在目标构建期间如何调用构建工具SE-0238

依赖镜像

Swift 5Swift Package Manager SE-0219带来了依赖镜像。

swift package config set-mirror --package-url <package> --mirror-url <mirror>

即使原始源不可用或被删除,镜像也允许您访问依赖项。

set-mirror使用镜像更新依赖项,后者替换所有其他镜像。

使用unset-mirror从依赖项中删除镜像:

swift package config unset-mirror --package-url <package>
swift package config unset-mirror —mirror-url <mirror> 
swift package config unset-mirror --all

其他的一些改进

Swift 5还增加了一些其他急需的功能和改进:

制作可编码范围

Swift 5增加了Codable对范围的一致性SE-0239

let temperature = 0...10
let encoder = JSONEncoder()
let data = try! encoder.encode(temperature)
let decoder = JSONDecoder()
let temperatureRange = try! decoder.decode(ClosedRange<Int>.self, from: data)

您使用JSONEncoder对温度进行编码并使用JSONDecoder解码数据,因为默认情况下,范围在Swift 5中实现了Codable

展平嵌套的Optionals

Swift 4.2使用**try?**创建嵌套的选项:

extension Int {
  // 1
  enum DivisionError: Error {
    case divisionByZero
  }
  
  // 2
  func divideBy(_ number: Int) throws -> Int {
    guard number != 0 else {
      throw DivisionError.divisionByZero
    }
    return self / number
  }
}

// 3
let number: Int? = 10
let division = try? number?.divideBy(2)
if let division = division, 
   let final = division {
  print(final)
}

这是代码的作用:

  1. 使用DivisionError扩展Int
  2. divideBy(_ :)如果number0则抛出.divisionByZero
  3. 解开分区两次,因为它是一个Int??

Swift 5以不同的方式处理SE-0230

if let division = division {
  print(division)
}

尝试? 在Swift 5中不会创建嵌套的选项,因此你需要解包一次,因为它是一个Int?

从Collection中删除自定义点

您可以从Swift 4.2中的Collection访问自定义点:

extension Array {
  var first: Element? {
    return !isEmpty ? self[count - 1] : nil
  }
  
  var last: Element? {
    return !isEmpty ? self[0] : nil
  }
}

let names = ["Cosmin", "Oana", "Sclip", "Nori"]
names.first // "Nori"
names.last // "Cosmin"

在此代码中,首先返回姓名的姓氏,最后返回数组的第一个元素。

两个计算属性都不能按预期工作,因此Swift 5从集合SE-0232中删除了它们的自定义点。

Identity Key的路径

Swift 4.2使用.self来访问值:

class Tutorial {
  let title: String
  let author: String
  init(title: String, author: String) {
    self.title = title
    self.author = author
  }
}

var tutorial = Tutorial(title: "What's New in Swift 5.0?", author: "Cosmin Pupaza")
tutorial.self = Tutorial(title: "What's New in Swift 5?", author: "Cosmin Pupăză")

在此代码中,您使用.self一次性更改教程的标题和作者。

Swift 5为值访问添加了identity key路径SE-0227

tutorial[keyPath: \.self] = Tutorial(
  title: "What's New in Swift 5?",
  author: "Cosmin Pupăză")

在此代码中,您使用\.self来更新教程。

通过强制来初始化

Swift 5中,如果类型符合文字协议SE-0213,则文字初始化程序将文字强制转换为其类型:

let value = UInt64(0xFFFF_FFFF_FFFF_FFFF)

Swift 4.2中,上面的代码行在编译时产生溢出错误。

Swift版本编译的更新

Swift 4.2在编译条件中使用>=

let favoriteNumber = 10
var evenNumber = true

#if !swift(>=5)
  evenNumber = favoriteNumber % 2 == 0
#else 
  evenNumber = favoriteNumber.isMultiple(of: 2)
#endif

#if !compiler(>=5)
  evenNumber = favoriteNumber % 2 == 0  
#else
  evenNumber = favoriteNumber.isMultiple(of: 2)
#endif

这些条件检查Swift版本是否大于或等于5,并在满足条件时编译这些代码。

Swift 5增加<用于更清洁的条件SE-0224

#if swift(<5)
  evenNumber = favoriteNumber % 2 == 0   
#else
  evenNumber = favoriteNumber.isMultiple(of: 2)  
#endif

#if compiler(<5)
  evenNumber = favoriteNumber % 2 == 0 
#else
  evenNumber = favoriteNumber.isMultiple(of: 2)   
#endif

使用具有关联值的枚举个案的可变参数

您可以在Swift 4.2中使用具有关联值的枚举情况的可变参数:

enum BlogPost {
  case tutorial(_: String...)
  case article(_: String...)
}

您可以使用String...获取教程和文章详细信息。 这在Swift 5中是不可能的,所以你应该使用数组:

enum BlogPost {
  case tutorial([String])
  case article([String])
}

这次使用**[String]**设置教程和文章详细信息。

弃用字符串索引编码偏移量

Swift 4.2字符串使用UTF-16编码。 因此,encodedOffset将返回UTF-16字符串的偏移量:

let swiftVersion = "Swift 4.2"
let offset = swiftVersion.endIndex.encodedOffset

在这里你可以获得swiftVersionendIndex的偏移量。 这对Swift 5中使用的UTF-8字符串编码不起作用,因此Swift 5utf16Offset(in :)替换encodedOffset来处理这两种情况SE-0241

let swiftVersion = "Swift 5"
let offset = swiftVersion.endIndex.utf16Offset(in: swiftVersion)

新的指针方法

Swift 5添加了ConttiguousStorageIfAvailable(_ :)SequencewithContiguousMutableStorageIfAvailable(_ :)MutableCollection,为协议扩展SE-0237中的withUnsafeBufferPointer(_ :)withUnsafeMutableBufferPointer(_ :)提供通用实现。

SIMD矢量更新

Swift 5将处理器的SIMD类型操作添加到标准库中。它们为SIMD向量和矩阵提供低级支持。它们还简化了<simd/simd.h> SE-0229Objective-CC和**C++**实现。

然后去哪儿?

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

Swift 5Swift 4.2增加了许多很酷的功能,使语言ABI稳定。这是语言发展过程中的一个重要里程碑,因为从现在开始,这一变化将会减少。

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

您还可以查看Swift Evolution提案,了解下一个Swift版本中会发生什么变化。在这里,您可以针对当前正在审核的提案提供反馈,甚至可以自行提交提案!

项目示例: 项目工程地址