Swift 4 新特性

作者: 山天大畜 | 来源:发表于2017-06-30 09:50 被阅读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进行了大量的更新。其中最大的变化包括:

    • 从分支或提交中获取依赖资源
    • 对可接受的包版本有了更多的控制
    • 用一个更通用的解决方案取代不直观的pinning命令
    • 定义用于编译的Swift版本的能力
    • 为每个Target指定源文件的位置
      这些都是走向SPM过程的重大步骤。SPM还有一段很长的路要走,但是我们可以通过积极参与其中来帮助它。
      更多内容请查看Swift 4 Package Manager Update

    展望

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

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

    相关文章

      网友评论

        本文标题:Swift 4 新特性

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