Swift 4 新特性——What's New in Swift 4?

    本文翻译自raywenderlich.com中的文章《What’s New in Swift 4?》,由于本人水平有限,翻译中不准确或者有错误的地方,敬请谅解和指正。

    提示:本教程使用集成在 Xcode 9 beta 1 中Swift 4 版本。

    Swift 4 是苹果公司计划于2017年秋季发布的最新主要版本,它主要关注与Swift 3 的兼容性,以及继续向ABI稳定迈进。

    皮皮Swift 4,我们走

    Swift 4 包含于Xcode 9中。你可以从苹果公司的开发者页面下载最新的Xcode 9(你必须有一个开发者账号)。每个Xcode版本将会包含Swift 4当时版本的快照。

    当你阅读时,你会发现链接都是SE-xxxx形式。这些链接将会为你展示相关的Swift演化提案。如果你想深入学习某个专题,可以翻阅它们。

    我推荐在playground中尝试每个 Swift 4 的特性或更新。这将帮助强化你头脑中的知识,赋予你深入学习每个专题的能力。扩展他们、打破它们,与playground中的例子斗,其乐无穷。

    提示:这篇文章将随着每个Xcode beta版本更新。如果你使用不同的快照,不能保证代码可以运行。

    移植到 Swift 4

    Swift 3 移植到 4 要比 2.2 到 3容易一些。总的来说,大多数变化都是新增的,不需要大量的个人情怀(personal touch)。因此,Swift移植工具将为你完成大部分的修改工作量。

    Xcode 9同时支持 Swift 4 以及Swift 3 的中间版本 Swift 3.2。项目中的target既可以是 Swift 3.2 也可以是 Swift 4,让你在需要的时候一点一点移植。转换到 Swift 3.2不是完全没有工作量,你需要更新部分代码来兼容新的SDK,再者因为 Swift 还没有ABI稳定,所以你需要用Xcode 9 重新编译依赖关系。

    当你准备移植到 Swift 4,Xcode再次提供了移植工具帮助你。Xcode中,你可以使用“Edit/Convert/To Current Swift Syntax”启动转换工具。

    选择了想要转换的target之后,Xcode将提示你Objective-C推断的偏好设置,选择推荐选项以通过限制推断来减少二进制大小(更多这个专题,查看下面的限制@objc推断)

推断偏好设置

    为了更好的理解你代码中将会出现的变化,我们先来看看 Swift 4中的API 变化。

    API变化

在转入Swift 4 新增特性之前,让我们先看下已有API的变化或更新。

    字符串Strings

    Swift 4 中String类收到颇多关爱,这份提案包含很多变化,来看看最大变化【SE-0163】。

假如你感觉怀旧,strings像Swift 2.0之前那样再次成为集合。这一变化移除了String必须的character数组。现在你可以直接在String对象上迭代。


let galaxy = "Milky Way 🐮"

for char in galaxy {

print(char)

}


    你不仅可以对String做逻辑迭代,还可以得到Sequence和Collection中的额外特性。


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例子阐述了对Character的小改进。你可以直接访问Character的UnicodeScalarView属性。之前你需要生成一个新String【SE-0178】。

    另外一个新增特性是StringProtocol。它声明了大部分String中声明的功能。这样做是为改进slice如何工作。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,常见于emoji表情。下面是一些常见的例子:


"👩‍💻".count // Now: 1, Before: 2

"👍🏽".count // Now: 1, Before: 2

"👨‍❤️‍💋‍👨".count // Now: 1, Before, 4


    这只是String Manifesto中的一部分变化,你可以在String Manifesto读到所有这些的原始动机,以及未来可预见的解决提案。

    字典和集合

    集合类型不断发展,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]


    重复Key解决方案

    你现在可以在初始化字典时,使用任意方法处理重复key问题。这可以避免“无提示的改写键值对”。


// 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,你可以快速的在苹果的Swift文档中学习。

    筛选

    字典和集合(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]


    字典映射

字典获得了一个非常有用的方法,可以直接映射它的值:


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


     字典缺省值

    访问字典中的值时,常见的实践方法,是使用??操作符,当值为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"]]


这在处理特殊形式的数据时变得很便利。

    预留容量

    序列和字典都有了显式预留容量的能力。


// Improved Set/Dictionary capacity reservation

starWordsCount.capacity  // 6

starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity

starWordsCount.capacity // 24


    对序列和字典重新分配内存空间,是非常耗费资源的操作。当你知道需要多少数据时,使用reserveCapacity(_:)是提升性能的简单方法。

    这包含了很多信息,所以查看这两个类型的文档,找到优化代码的方法。

    私有访问修饰符

    有些人不喜欢 Swift 3 中新增的 fileprivate。理论上,它很赞,但实际上它的用法让人糊涂。这样做的目的是,在成员内部使用private,而在相同文件中想要在成员中分享访问权限则使用 fileprivate。

    这个问题源自于,Swift鼓励使用扩展让代码逻辑分组。扩展被看做是原始成员声明作用域的外围,使得扩展需要 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 的亮瞎双眼的新特性。这些特性不会打破已有代码,因为他们都是简单新增。

    归档和序列化

    讲到这里,序列化和归档自定义类型,你需要一些套路。对于类,你需要创建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类型可编码和解码。如果所有属性都是Codable,编译器自动生成协议的实现。

    真正对一个对象编码,你需要将它传给一个编码器。Swift 4 正在积极推进Swift编码器。根据不同的模式给对象编码。【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对象。注意检查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,编码和解码变得类型安全,不需要依赖@objc协议的限制。

    键值编码

    因为函数在Swift中是闭包,所以你可以持有函数的引用而不调用它们。但做不到的是,持有属性的引用而不实际访问属性下的数据。

    Swift 4 新增了让人兴奋的特性,可以引用类型的keypath来get/set实例的底层数据。


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)


    这里创建了一些force的实例,设置了名字、光剑和主人。要创建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参数。下面是一些例子,使用key path扩展到子对象、设置属性等。


// 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中key path的优雅之处在于它们是强类型的,不再有Objective-C的杂乱。

    多行String字面量

    很多编程语言的特性中包含创建多行字符串字面量的能力。Swift 4用三个引号"""包裹文本,实现了这个简单但有用的语法。


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信息或者创建长格式文本时,非常有用。

    单边范围

    为减少冗长、提高可读性,标准库可以使用单边范围确定开始、结束索引。这使得从集合截取变得方便。


// Collection Subscript

var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

let outsideAsteroidBelt = planets[4...] // Before: planets[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: 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"]


    本例中,你可以传入两个不同的序列类型(数组和集合),得到相关值的数组。

    杂项

    MutableCollection现在有了可变方法swapAt(_:_:),正向看起来的那样,它交换给定数组中的值。


// Very basic bubble sort with an in-place swap

func bubbleSort(_ array: [T]) -> [T] {  

    var sortedArray = array  

    for i in 0..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声明可以直接包含自身的值,而不需要其他套路。

    类和协议的存在性

    区分Objective-C和Swift的重要特性就是,Swift可以定义一个类型,既遵从一个类又遵从一些协议【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推断

    要提供Swift API给Objective-C,你要使用@objc编译器属性。很多情况下,Swift编译器可以为你推断。推断的三个主要问题在于:

    1、二进制大小大幅度增加的隐患

    2、不确定@objc何时被推断。

    3、无意间造成和Objective-C函数冲突的可能性增加。

Swift 4拿出大板斧,限制了@objc推断【SE-0160】。这意味着,当你需要Objective-C全部动态分发能力时,必须显式使用@objc。

    NSNumber桥接

   NSNumber和Swift numbers之间有很多恼人的恶臭,萦绕在语言周围太长时间了。幸运的是,Swift解决了它们【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包管理器

    过去几个月,Swift包管理器已经有一定数量的更新,其中大的变更包括:

    1、根据分支或者提交哈希进行源码以来

    2、可接受包版本的更多控制

    3、替换不直观的命令,使用一些更常用的解决模式

    4、使用编译器定义Swift版本的能力

    5、为每个target指定源文件路径

    这些大变化都是Swift包管理器需要做的,SPM还有很长的路要走,我们都可以通过提案,帮助它更好发展。

    一直在路上

    在写这篇文章时,仍有15分已接受的提案在排队。如果你想知道接下来会发生什么,查看Swift演化提案、选择“Accepted”。

    路怎么走,你们自己挑?

    Swift语言这几年不断发育成熟。提案进程和社区参与,使得大家能够跟踪语言变化,也使得我们每个人都可以直接影响语言的演化。

    上面的 Swift 4 变化,我们终于发现ABI稳定就在下一个转角。Swift升级的阵痛在变小。构建性能和工具都大幅度提升。在苹果生态外使用Swift变得越发可行了。设想一下,我们离一个直观的实现,还差一小部分的String重写。

    Swift还会迎来很多改变。跟上每个变化的节奏,查看以下资源:

1、Swift变更日志

2、Swift演化提案

3、WWDC 2017 视频

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容