iOS杂文iOS-SwiftiOS 进阶知识集

Swift 4 新特性

2017-06-30  本文已影响668人  山天大畜

Swift 4是苹果最新推出的一次语言升级,计划在2017年秋发布测试版。它的主要目标是提供与Swift 3的源代码兼容性,以及ABI的稳定性。
本文重点介绍了Swift此次的变化,它将对你的代码产生重大影响。然后,让我们开始吧!

开始

Swift 4要求安装Xcode 9,你可以从苹果的开发者网站下载Xcode 9的最新版本(你必须有一个开发者帐户)。
阅读此文时,你会注意到有[SE-xxxx]格式的链接。这些链接将带您进入相关的Swift进化建议。如果你想了解更多,一定要进去看看。
我建议在playground里去尝试每一个Swift 4的功能,这将有助于巩固你头脑中的知识,使你有能力深入每一个话题。试着扩展思考这些例子,祝你玩得开心!

升级到Swift 4

从Swift 3到4的迁移要比从2.2到3轻松得多。大多数变化都是附加的,不需要我们太多的介入。正因为如此,快速迁移工具将为您处理大部分更改。
Xcode 9同时支持Swift 4以及3,你的项目中的Target可以是Swift 3.2或Swift 4,如果需要,可以逐步进行迁移。
当你准备迁移到Swift 4,Xcode提供了迁移工具来帮助你。在Xcode中,您可以通过Edit/Convert/To Current Swift Syntax…来打开转换工具。
在选择好需要转换的Target之后,Xcode会提示你选择在Objective-C中的偏好。选择推荐的选项可以减少你的二进制文件的大小(更多关于这个话题,看看限制@ objc Inference

为了更好地理解你的代码中会有哪些变化,我们将首先介绍Swift 4中API的更改。

API的变化

Strings

在Swift 4中,String当之无愧获得了非常多的关注,它包含了很多变化。 SE-0163
如果你是个怀旧的人,String又变得像以前的Swift 2一样了,此更改去掉了String中的characters数组,你可以直接以数组的方式遍历String对象:

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

不仅是遍历,Sequence和Collection的一些特性也应用到了String上:

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 "

另外,新增了StringProtocol接口,它声明了在String上的大部分功能。这个变化的原因是为了增大slices的应用范围。Swift 4添加了Substring类型来引用String的子序列。
String和Substring都实现了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, 4

这只是String声明中提到的一个更改子集,您可以去阅读更多的动机和建议的解决方案。

Dictionary & Set

对于集合类型,Set和Dictionary一直都不是很直观。这一次,Swift给了他们一些关爱 SE-0165

序列的初始化

首先是增加了通过键值对序列(元组)创建字典的能力:

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]
重复主键的解决方案

现在可以用任意方式处理主键重复的字典初始化过程:

// 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和+来处理,表示当主键有重复时把其内容相加。

过滤

Dictionary和Set都有能力来过滤结果输出到新的变量:

// 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 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的值的通常做法是用nil来赋默认值。在Swift 4中,这种语法变得更加简洁:

// 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]

这在以前,代码需要用if-let包裹起来,而Swift 4中只需要一行代码即可完成!

字典分组
// 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"]]
这个用在为数据按照特定模式分组时很方便
储备容量

Sequence和Dictionary现在都具备了储备容量的能力:

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

重新分配容量是很消耗的操作,用reserveCapacity(_:)能轻松提高代码性能,前提是你知道你大概有多少数据量。

Private访问修饰符

Swift 3的fileprivate有一些让人不是很喜欢的地方。从理论上讲,它的诞生是伟大的,但在实践中,关于如何使用它常常令人困惑。private的用途是保证在成员本身私有使用,fileprivate则是当你想在同一个文件内共享访问成员时使用。
问题是Swift鼓励我们使用扩展将代码分解为不同的逻辑组。扩展被认为是原始成员声明的范围之外,从而导致fileprivate被广泛的需要,但这并不符合fileprivate被设计的初衷。
Swift 4认识到了这种扩展之间需要共享相同访问控制范围的需求。 SE-0169

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!"

现在将允许你把fileprivate用于初衷的目的。

新增API

现在让我们来看看Swift 4新增的功能,这些功能将不会影响你现有的代码。

Archival & Serialization

以前的Swift,如果你想要序列化、归档你的自定义类型,你需要做很多事情。例如对于class,你需要继承自NSObject类和NSCoding接口。
对于struct和enum,你需要黑科技,例如创造一个子对象继承自NSObject和NSCoding。
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])

在这个例子中,你能看到我们只需要继承自Codable接口,就可以让Swift类型EncodableDecodable。如果所有的属性都是Codable,这个接口将自动被编译器实现。
为了编码一个对象,你需要把它传入编码器。Swift 4中实现了很多编码器,他们能对你的对象进行不同模式的编码 SE-0167

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对象,同样我们可以在解码变回原对象:

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]

Key-Value Coding

在以前,你可以可以在不调用函数的情况下引用函数,因为这些函数都是闭包。但是对于属性,你只能通过实际访问他的数据而保存对属性的引用。
令人兴奋的是,Swift 4的key path具备这个能力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)

上面创建了一些对象和实例,你只需要简单的用一个\标记在属性前,来创建一个key path:

// 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属性的一个key path,你可以以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的这种优美的写法是强类型的,不像Objective-C那样是用字符串来表示!

Multi-line String Literals

很多语言都有的一种常用特性就是多行字符串,Swift 4中增加了这种功能,通过三个引号来使用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中,这将非常有用。

One-Sided Ranges

为了减少冗长的代码,提高可读性,标准库现在可以通过one-sided ranges来推断开始和结束的指标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]

如你所见,通过one-sided ranges,你不需要再指定起始和结束的位置。

Infinite Sequence

当你的序列起始值是可数类型时,你可以定义一个无穷序列:

// 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

one-sided ranges的另一个用法是模式匹配:

// 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

Generic Subscripts

下标是一种访问数据成员的重要方式,为了提高使用范围,下标现在可以支持泛型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"]

这个例子中,你可以看到传入两个不同序列类型作为下标(Array和Set),会得到他们各自的值。

Miscellaneous

这是Swift 4变化最大的部分,我们快速浏览一些片段。

MutableCollection.swapAt(::)

MutableCollection拥有了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]

Associated Type Constraints

现在可以使用Where子句约束关联类型SE-0142

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

Class and Protocol Existential

标识一个同时继承了class和protocols的属性有了一种新的写法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()

Limiting @objc Inference

我们用@objc来标记Objective-C调用Swift的API方法。很多时候Swift编译器会为你推断出来,但推断却会来带下面三个问题:

  1. 潜在可能会显著增大你的二进制文件
  2. 不是很明显能确定什么时候@objc会被推断
  3. 无意间创建一个Objective-C的方法,会增大冲突的概率

Swift 4通过限制@objc SE-0160的推断来试图解决这些问题。这意味着,当你需要Objective-C的动态调度能力时,你需要显示使用@objc。

NSNumber Bridging

NSNumber和Swift的一些数值已经困扰了大家很长时间,幸运的是,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进行了大量的更新。其中最大的变化包括:

展望

Swift语言这些年已经真正地成长起来并发展成熟。参与社区与提交建议使我们能非常容易的跟踪它的发展变化,这也使我们任何人都能直接影响并进化这门语言。
Swift 4有了这些变化,我们终于即将到达,ABI稳定性就在眼前。升级Swift版本的痛苦将会越来越小,构建性能的工具将越来越先进,在苹果生态系统以外使用Swift将变得越来越可行。
Swift未来还会有很多东西,想要关注最新的相关信息,请查看以下资源:

本文翻译自What’s New in Swift 4?

上一篇下一篇

猜你喜欢

热点阅读