美文网首页
Swift 4 新特性——What's New in S

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

作者: 周杰木有伦 | 来源:发表于2017-06-15 17:30 被阅读291次

        本文翻译自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 视频

    相关文章

      网友评论

          本文标题:Swift 4 新特性——What's New in S

          本文链接:https://www.haomeiwen.com/subject/xkosqxtx.html