翻译Raywenderlich 最新文章What’s New in Swift 4?

96
Guards翻译组
2017.06.14 14:33* 字数 3511

Swift学习有问必答群 : 313838956 ( mac版QQ有权限要求, 入群只能通过手机版 QQ申请). 本群由Guards翻译组创建并维护
入群须知:

0.0 重申: mac版QQ有权限要求, 入群只能通过手机版 QQ申请.
0.1 群主晚上每天20点--21点定时回答Swift相关的问题.
0.2 群主会在看到问题后, 第一时间回复
0.3 拒绝长时间潜水, 拒绝讨论和Swift , iOS 无关的问题



该文章转载翻译自: Raywenderlich
原文链接: What’s New in Swift 4?

译者心声

我们会不定期的更新翻译文章, 需要可以关注我们的简书

我们是一群热爱翻译并且热爱 Swift 的人, 希望通过自己的努力让不熟悉英语的程序员也能学习到国外的高质量的文章. 如发现文章中翻译错误之处, 烦请跟我们联系, 我们第一时间更正.

想成为我们中一员? 是的, 我们正在招募对翻译感兴趣的小伙伴, 如果你想提高阅读英语文档的水平, 如果你不甘寂寞, 如果你想为广大开发者做点事. QQ: 869293399

Swift 4是苹果计划在2017年秋季推出的最新版本,值得关注的是其提供了与Swift 3代码很好的兼容性,并最大限度的保持了ABI稳定性.

本文着重介绍Swift中可能会明显影响你的代码的变化。ok,我们开始吧!

导读

Xcode9中已经集成了Swift4, 你可以从developer portal下载最新版本的Xcode9 (需要开发者账号), 每个Xcode beta 都会在发布时绑定最新的Swift 4快照版本。

在阅读时,你会注意到[SE-xxxx]格式的链接。这些链将带你进入相关的Swift Evolution提案。如果你想了解更多话题,请务必查看。

我建议你在 Playground上尝试每个 Swift 4的功能或更新。这将有助于巩固你的知识,并使你能够深入并了解每个主题, 试着扩展这些例子!

注意:本文将针对每个Xcode测试版进行更新。如果你使用不同的Swift快照版本,这里的代码不能保证都有效。

迁移到Swift 4

Swift 3迁移到4从2.2到3少了很多麻烦。一般来说,多数变化都是附加的,不需要个人大量去修改。正因为如此,Swift迁移工具可以处理你代码的大部分变化。

Xcode 9同时支持Swift 4和Swift 3.2的中间版本。项目中的每个target可以是Swift3.2或Swift 4,如果需要,可以只迁移部分。然而, 迁移到Swift3.2并非完全不受限制, 你可能需要更新部分代码才能与新SDK兼容,且由于Swift ABI尚未稳定,因此你需要使用Xcode 9重新编译依赖项。

迁移到Swift 4Xcode还提供了一个迁移工具来帮助你。在Xcode中,你可以点击导航栏的 Edit/Convert/To Current Swift Syntax… 来启动转换工具。

在选择要转换的target后,Xcode会提示你优先考虑Objective - C推断的方式。 选择推荐的选项,通过限制推断来减少二进制大小(关于这个主题的更多信息,参阅Limiting @objc Inference

1.png

API 变化

为了更好地理解你代码中的变化,我们首先介绍Swift 4中变更的API

Strings

String在Swift 4中获得了很多人的喜爱。 [SE-0163] 这个提案包含很多变化,我们提取出最大的变化的.

字符串已经像之前的2.0版一样, 改为了Collection类型 。此变化消除了字符串对字符数组的依赖。现在可以直接遍历字符串对象:

  let galaxy = "Milky Way 🐮"
      for char in galaxy {
          print(char)
      }

你不仅可以通过字符串进行逻辑迭代,还可以从SequenceCollection中获取所有的bells and whistles :

  galaxy.count       // 11
  galaxy.isEmpty     // false
  galaxy.dropFirst() // "ilky Way 🐮"
  String(galaxy.reversed()) // "🐮 yaW ykliM"
  
  // Filter out any none ASCII characters
  galaxy.filter { char in
      let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
      return isASCII
  } // "Milky Way "

上面的ASCII示例演示了对字符的一小部分改进。你现在可以直接从字符访问UnicodeScalarView。在这之前,你需要实例化一个新字符串[SE - 0178]

另外还有一个StringProtocol。它声明了在String上声明的大部分方法。为了改进slice字符串的效率, Swift 4添加了Substring类型,用于引用String的子序列。

StringSubstring都实现了StringProtocol,两者几乎具有相同的功能:

   // Grab a subsequence of String
   let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
   var milkSubstring = galaxy[galaxy.startIndex...endIndex]   // "Milk"
   type(of: milkSubstring)   // Substring.Type

   // Concatenate a String onto a Substring
   milkSubstring += ""     // "Milk"

   // Create a String from a Substring
   let milkString = String(milkSubstring) // "Milk"

另一个很大的改进是String对图形集合的解析。这个处理方案来自于 Unicode 9的改编。以前,由多个代码点组成的unicode字符会导致计数大于1。
常见的情况是,这是一个带有选择的肤色的表情符号。以下是一个例子:

    "👩‍💻".count // Now: 1, Before: 2
    "👍🏽".count // Now: 1, Before: 2
    "👨‍❤️‍💋‍👨".count // Now: 1, Before, 3

这只是 String Manifesto 中提到的一个子集, 至于为何这么修改, 可以阅读你感兴趣的修改原始动机和方案。

Dictionary and Set

至于Collection类型,SetDictionary并不那么最直观的。幸运的是,Swift 团队对两者做了很好的改进 [SE-0165]

Sequence Based Initialization

首先列表可以是从一系列键值对(元组)创建一个字典:

 let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
 let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]
 
 // Dictionary from sequence of keys-values
 let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances)) 
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]

Duplicate Key Resolution

在初始化Dictionary时, 你现在可以使用你喜欢的方式来处理重复的键,同时避免覆盖键值对,且不会出现任何问题:

   // Random vote of people's favorite stars
  let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]

  // Merging keys with closure for conflicts
 let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]

上面的代码使用了 zip+ 来快捷地处理重复的 key 和冲突的值。

注意:如果你不熟悉 zip,你可以在Apple的Swift文档中快速了解它 [Swift Documentation]
Filtering

Dictionary 和 Set现在都可以将结果 通过filter函数 过滤到原始类型的新对象中:

    // Filtering results into dictionary rather than array of tuples
 let closeStars = starDistanceDict.filter { $0.value < 5.0 }

 closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]
Dictionary Mapping

Dictionary为直接映射其值提供了一种非常有用的方法::

  // Mapping values directly resulting in a dictionary
  let mappedCloseStars = closeStars.mapValues { "\($0)" }

  mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]
Dictionary Default Values

在Dictionary上访问某个值时,常见的做法是使用nil-coalescing operator给出默认值(译者注: 不了解nil-coalescing operator 的伙伴 可参见Nil-Coalescing OperatorDictionary Default Values可以更简洁:

   // Subscript with a default value
  let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"

   // Subscript with a default value used for mutating
   var starWordsCount: [String: Int] = [:]
   for starName in nearestStarNames {
        let numWords = starName.split(separator: " ").count
        starWordsCount[starName, default: 0] += numWords // Amazing 
    }
  starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]

swift4之前, 处理这种情况需要包装在臃肿的if - let语句中。现在简短的代码即可.

Dictionary Grouping

另一个令人惊讶称赞的是,我们可以从Sequence"中初始化Dictionary,并将其分组为bucket::

 // Grouping sequences by computed key
 let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }

 // ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]

当通过特定模式对数据进行分组时,这相当方便。

预留空间

SequenceDictionary现在都具有明确保留容量的能力。

    // Improved Set/Dictionary capacity reservation
    starWordsCount.capacity  // 6
    starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity
    starWordsCount.capacity // 24

在这些类型上,Reallocation可能是一项代价高昂的任务。如果你知道需要存储的数据量时, 使用reserveCapacity(_:)可以提高性能且便捷

私有访问修饰符

Swift3中,有些人不太喜欢使用fileprivate。理论上它很强大,但在实践中,它的用法常令人困惑。其目的是在成员内部实现私有,不同一文件中,访问公有成员变量的时候很少使用fileprivate。

问题是Swift鼓励使用extension将代码分解成逻辑组。extension 代码在原始作用域之外,所以fileprivate也被广泛使用。

Swift 4通过在类型和类型的extension间, 实现共享相同的访问范围。但这只适用于相同的源文件

    struct SpaceCraft {
        private let warpCode: String

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

    extension SpaceCraft {
    func goToWarpSpeed(warpCode: String) {
        if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate
                print("Do it Scotty!")
            }
        }
    }

    let enterprise = SpaceCraft(warpCode: "KirkIsCool")
    //enterprise.warpCode  // error: 'warpCode' is inaccessible due to 'private' protection level
    enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"
                                                       

新增API

现在让我们来看看Swift 4的新特性。这些更改不应该破坏现有的代码,因为它们只是附加的。

存档和序列化

目前为止,要序列化和归档你的自定义类型,你必须跳过一些坑。对于类类型,你需要将NSObject子类化并实现NSCoding协议。像structenum这样的值类型需要一些技巧,例如创建一个可以扩展NSObjectNSCoding的子对象。
Swift 4解决了这一问题,三种类型均可以序列化 [SE-0166]:

struct CuriosityLog: Codable {
  enum Discovery: String, Codable {
    case rock, water, martian
  }

  var sol: Int
  var discoveries: [Discovery]
}

// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])

在这个示例中,你可以看到,要使swift类型 EncodableDecodable,惟一的要求是实现Codable的协议。如果所有的属性都是Codable的,编译器自动生成该协议的相应实现

要真正对对象进行编码,需要将其传递给Encoder。 每个编码器根据不同的方案对你的对象进行编码 [SE-0167](Note: 部分提议仍在完善中):

let jsonEncoder = JSONEncoder() // One currently available encoder

 // Encode the data
 let jsonData = try jsonEncoder.encode(logSol42)
 // Create a String from the data
 let jsonString = String(data: jsonData, encoding: .utf8) // " {"sol":42,"discoveries":["rock","rock","rock","rock"]}"

它将一个对象自动编码为JSON对象。一定要检查JSONEncoder公开的属性,以定制它的输出。
该过程的最后一部分是将数据解码为具体对象:

let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder

// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol         // 42
decodedLog.discoveries // [rock, rock, rock, rock]

使用Swift 4的 encoding/decoding,你可以在不依赖@ objc协议的开销和限制的情况下获得Swift的类型安全性。

键值编码

到目前为止,你可以在不调用函数的情况下对函数进行引用,因为在Swift中函数实质是一个闭包。对于Swift 4, 可以引用类型的keyPath来获取或设置实例的底层值 [SE-0161]:

struct Lightsaber {
  enum Color {
    case blue, green, red
  }
  let color: Color
}

class ForceUser {
  var name: String
  var lightsaber: Lightsaber
  var master: ForceUser?

  init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
    self.name = name
    self.lightsaber = lightsaber
    self.master = master
  }
}

let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)

在这里,你创建了一些ForceUser实例,通过设置他们的name、lightsaber和master。要创建一个关键路径,你只需使用一个反斜杠\,后面跟上你感兴趣的属性:

// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name

// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath]  // "Obi-Wan Kenobi"

在这个实例中,你给ForceUser的name属性创建了一个keyPath。然后,你可以传递给新的subscript keyPath来使用这个keyPath。现在,默认情况下,此下标现在可用于每种类型
下面是一些使用keyPath来深入到子对象、设置属性和构建keyPath引用的例子:

// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color]  // blue

// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name  // "Obi-Wan Kenobi"

// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidious
anakin.master?.name // Darth Sidious

// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"

Swift的keyPath之美是强类型的! 没有更多的objective - c字符串混乱风格!

多行String

多行String是很多编程语言非常常见的特性。Swift4添加了这个非常简单和有用的语法是通过三个引号来实现的 [SE-0168]:

let star = "⭐️"
let introString = """
  A long time ago in a galaxy far,
  far away....

  You could write multi-lined strings
  without "escaping" single quotes.

  The indentation of the closing quotes
       below deside where the text line
  begins.

  You can even dynamically add values
  from properties: \(star)
  """
print(introString) // prints the string exactly as written above with the value of star

当创建XML/JSON消息的时候或者创建UI界面上非常长的格式化字符串的时候这是非常有用的。

半开区间

为了减少冗长和提高可读性,标准库现在可以推导出开始和结束使用半开区间。 [SE-0172]

这为确定集合的范围的起始索引或者结束索引提供了非常便利的方法。

// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..<planets.endIndex]
let firstThree = planets[..<4]          // Before: planets[planets.startIndex..<4]

正如你所看到的,半开区间减少了明确的开始或者结束索引。

无穷大

他们也允许在开始索引确定的情况下定义无穷的的结束索引。


// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]

planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, “Pluto")]

模式匹配

另一个非常大的作用是将半开区间用于模式匹配:


// Pattern matching

func temperature(planetNumber: Int) {
  switch planetNumber {
  case ...2: // anything less than or equal to 2
    print("Too hot")
  case 4...: // anything greater than or equal to 4
    print("Too cold")
  default:
    print("Justtttt right")
  }
}

temperature(planetNumber: 3) // Earth

泛型下标

下标是高效访问数据类型的方式, 为了提高通用性 添加了泛型。[SE-0148]

struct GenericDictionary<Key: Hashable, Value> {
  private var data: [Key: Value]

  init(data: [Key: Value]) {
    self.data = data
  }

  subscript<T>(key: Key) -> T? {
    return data[key] as? T
  }
}

在上面这个例子中,返回值类型是泛型。你可以使用泛型下标就像这样:

// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])

// Automatically infers return type without "as? String"
let name: String? = earthData["name"]

// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]

不仅仅只是返回值类型可以是泛型,实际下标类型也可以是泛型:

extension GenericDictionary {
  subscript<Keys: Sequence>(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
    var values: [Value] = []
    for key in keys {
      if let value = data[key] {
        values.append(value)
      }
    }
    return values
  }
}

// Array subscript value
let nameAndMoons = earthData[["moons", "name"]]        // [1, "Earth"]
// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])]  // [1, "Earth"]

在这个例子中,你可以看到,传递两个不同的Sequence类型(ArraySet)会得到各自的数组值。

其他

以上列举的囊括了swift4中最大变化的部分, 现在我们快速看看其他方面的小改动

MutableCollection.swapAt(::)

MutableCollection现在具有mutate方法swapAt(_:_ :); 交换指定的索引值[SE-0173]:

// Very basic bubble sort with an in-place swap
func bubbleSort<T: Comparable>(_ array: [T]) -> [T] {
  var sortedArray = array
  for i in 0..<sortedArray.count - 1 {
    for j in 1..<sortedArray.count {
      if sortedArray[j-1] > sortedArray[j] {
        sortedArray.swapAt(j-1, j) // New MutableCollection method
      }
    }
  }
  return sortedArray
}

bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]
关联类型约束

现在可以使用where语句来约束关联类型[SE-0142]:

protocol MyProtocol {
  associatedtype Element
  associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}

通过协议,在associatedtype的声明时就可以直接约束它的值。

类和协议的存在方式

在定义某个类型时(译者注: 指变量或常量), 这特性可以让其必须符合某种类型以及遵守一组协议[SE-0156]:

protocol MyProtocol { }
class View { }
class ViewSubclass: View, MyProtocol { }

class MyClass {
  var delegate: (View & MyProtocol)?
}

let myClass = MyClass()
//myClass.delegate = View() // error: cannot assign value of type 'View' to type '(View & MyProtocol)?'
myClass.delegate = ViewSubclass()
限制@objc 推断

要向objective - c公开或使用Swift API,可以使用@ objc编译器特性。在很多情况下,编译器可以为快速推断。然而, 这种大量的推理会导致三个问题是:

  1. 有可能显著地增加二进制文件的大小;
  2. 有时候@objc会推断模糊;
  3. 增加创建Objective-C Selector冲突的可能性。

Swift 4通过限制@objc的推断来解决这个问题[SE-0160] ,也就是说在Objective-C处于完全动态的情况下,您需要使用@objc
您需要修改的几个示例程序包括私有方法,动态声明和NSObject子类的任何方法。

NSNumber Bridging

NSNumberSwift的数字之间有许多有趣的行为,这些行为长久以来一直困扰着这个语言。幸运的是,Swift 4将这些可以避免这些错误[SE-0170]
这里有一个例子

let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231

在Swift 3中会出现的怪异现象,如果数字溢出,它会从0开始。在这个例子中"999% 2 ^ 8 = 231。
只有在包含的类型中可以安全地表示这个数字时,Swift 4才通过强制转换返回一个值

Swift Package Manager

在最近几个月中,Swift Package Manager有许多更新,几个比较大的更新如下:
· 源代码的依赖来自于分支或者提交的分支
· 更容易控制的package版本
· 用更为常见的解决方案代替非直观的命令
· 能够自定义编译的swift版本
· 指定每个target的源文件的位置

这些都是SPM所做的重大改变。 SPM还有很长的路要走,但是我们可以通过保持积极的建议来帮助它更完善。
有关最近解决的提案的全面了解,请查看Swift 4 Package Manager Update

Swift 4官方文档中文版
Web note ad 1