分享

Swift 4.2新特性

 xiaotianyueye 2018-06-15


Swift 4.2是Swift 4的第二次小更新,随之带来了很多很棒的改进-这使得今年将成为Swift的重要一年,并且更加确认这个社区驱动的Swift演变进程正在让一个伟大的语言变得更好。


这次我们获得了一些特性比如enum case 数组,warning与error编译指令,动态成员查找等等,这些都是Swift 4.1新特性(详见我的文章what's new in Swift 4.1)之外新增加的。Swift4.2现在距离主代码分支的最终合并仅相差几天,届时只有一些重要修正会加入。如果将来有任何改变,文中这些例子也会更新。


直到一个新版的Xcode与Swift4.2一起发布,你就可以下载最新版的Swift主干开发快照并在你当前Xcode版本上激活它。


试着自己做:我创建了一个Xcode playground展示Swift 4.2新内容,你可以编辑里面的实例。

 

enum案例的衍生集合


SE-0194介绍了一个新的CaseIterable协议,它会对一个enum中的所有cases自动生成一个数组(array)属性。


在Swift 4.2之前,它需要 hacks, hand-coding, 或者源代码生成才能实现,但是现在你需要的仅仅是让你的enum符合CaseIterable协议。在编译时,Swift会自动生成一个allCases属性,这个allCases是一个按照你定义的顺序排列,包含所有enum’s cases的数组。


例如,如下创建一个pasta形的enum,并要求Swift去自动为它创建一个allCases数组

 

enum Pasta: CaseIterable {
  case cannelloni, fusilli, linguine, tagliatelle
}

 

你可以更进一步,像使用一个普通数组一样用这个属性把[Pasta]放在代码前,我们就可以这样输入


for shape in Pasta.allCases {
  print('I like eating \(shape).')
}


这个allCases的自动合成仅替换并不使用关联数值的enums。加入下面这些并不会自动生效,但是如果需要你可以自己添加。


enum Car: CaseIterable {
   static var allCases: [Car] {
      return [.ford, .toyota, .jaguar, .bmw, .porsche(convertible: false),.porsche(convertible: true)]
   }
   
  case ford, toyota, jaguar, bmw
  case porsche(convertible: Bool)
}


这时如果你的任何一个enum cases被标记为unavailable,Swift就不能合成allCases属性。所以如果你需要allCases,你要像下面这样自己添加它:


enum Direction: CaseIterable {
  static var allCases: [Direction] {
      return [.north, .south, .east, .west]
   }

  case north, south, east, west

  @available(*, unavailable)
  case all
}


注意:要想allCases 数组被合成,你需要添加CaseIterable而不是一个extension到你enum的原始声明中。这意味着你不能用extension反向地让已存在的enums遵守一个协议。

 

Warning与error诊断指令


SE-0196 介绍新的编译指令来帮助我们在代码中标记issues. 这对使用过Objective-C的开发者应该很熟悉了,随着Swift 4.2的发布,我们也能在Swift中使用它了


这两个指令是#warning和#error,前者会强制Xcode在生成你的代码时发出一个警告,后者会发出一个编译错误这样你的代码就完全不能生成。下面说明了它们的用处


  • #warning主要用于提醒你或者别人一些工作还没有完成,Xcode模板常使用#warning标记一些你需要替换成自己代码的方法存根(method stubs)。

  • #error主要用于如果你发送一个库,需要其他开发者提供一些数据。比如,一个网络 API的认证密码,你需要用户输入它们自己的密码,就使用#error在继续之前强制他们更改这行代码。


它们的使用方式都一样,#warning('Some message') 和 #error('Somemessage'),例如

 

func encrypt(_ string: String, withpassword: String) -> String {
  #warning('This is terrible method of encryption')
  return password + String(string.reversed()) + password
}

struct Configuration {
  var apiKey: String {
      #error('Please enter your API key below then delete thisline.')
      return 'Enter your key here'
   }
}  


#warning和#error可以和已存的#if编译指令共同使用,并且只有在条件为true时才会激活。例如:

 

#if os(macOS)
#error('MyLibrary is not supported onmacOS.')
#endif


动态成员查找


SE-0195介绍了一个方法,让Swift更接近脚本语言比如Python,但是用一种类型安全的方法-你不会失去任何Swift的安全性,但是你确实增强了写类似PHP和Python代码的能力。


这个特性的核心在于一个新属性,称作@dynamicMemberLookup,它命令Swift使用一种下标方法去进行属性访问。这种下标方法,需要subscript(dynamicMember:):你可以通过所请求属性的字符串名得到,并且可以返回你想要的任何值。


让我们看一个小例子这样你就可以知道基本了。我们可以创建一个Person结构,并且从一个字典读取它的值,像这样


@dynamicMemberLookup
struct Person {
  subscript(dynamicMember member: String) -> String {
      let properties = ['name': 'Taylor Swift','city': 'Nashville']
      return properties[member, default: '']
   }
}


@dynamicMemberLookup 属性需要一个类型去执行subscript(dynamicMember:)方法,从而处理一件实际的动态成员查找工作。你可以看到,我写了一个代码,按字符串接收成员名字,并返回一个字符串。从内部看它只是在一个字典中查找这个成员名字并返回它的值。


这种结构允许我们这样写代码:


let person = Person()
print(person.name)
print(person.city)
print(person.favoriteIceCream)


它会干净地编译运行,即使name, city, 和favoriteIceCream并不作为Person类型中的属性存在。相反,它们都在运行时被查找。在前两次呼叫print()时,代码会打出Taylor Swift” 和 “Nashville”,而最后一次会是一个空白字符串,因为我们的字典并不存储任何favoriteIceCream


我的subscript(dynamicMember:) 方法必须返回一串字符,这体现了Swift的类型安全性。即使你在处理动态数据,Swift依旧保证你会得到你想要的。如果你想要多种不同的类型,就执行不同的subscript(dynamicMember:)方法,像这样


@dynamicMemberLookup
struct Employee {
  subscript(dynamicMember member: String) -> String {
      let properties = ['name': 'Taylor Swift','city': 'Nashville']
      return properties[member, default: '']
   }
   
  subscript(dynamicMember member: String) -> Int {
      let properties = ['age': 26, 'height': 178]
      return properties[member, default: 0]
   }
}


现在任何属性都可以通过不止一种方式被访问,Swift需要你知道应该运行哪一种。它可以是内部的,比如如果你向一个只接收字符串的函数传递返回值,或者它可以是外部的,像这样

 

let employee = Employee()
let age: Int = employee.age


无论哪种方式,Swift必须明确知道哪种下标要被呼叫


你甚至可以强行要求subscript返回一个闭包(Closure):

 

@dynamicMemberLookup
struct User {
  subscript(dynamicMember member: String) -> (_ input: String) ->Void {
      return {
          print('Hello! I live at the address \($0).')
      }
   }
}

let user = User()
user.printAddress('555 Taylor SwiftAvenue')


当它运行时,user.printAddress返回一个闭包,即输出一串字符,之后('555 Taylor Swift Avenue')部分立即通过这串字符来呼叫它。

 

如果你对包含一些普通属性和方法的类型使用动态成员下标,这些属性和方法会始终代替动态成员被使用。例如,我们可以定义一个附有动态成员下标的Singer结构,其中内置有name属性

 

struct Singer {
  public var name = 'Justin Bieber'
  subscript(dynamicMember member: String) -> String {
      return 'Taylor Swift'
   }
}

let singer = Singer()
print(singer.name)


这个代码会打出“Justin Bieber”,因为name属性会代替动态成员下标被使用


@dynamicMemberLookup完全参与到Swift的类型系统中,这意味着你可以用在protocols, structs, enums,和 classes中,即使classes被标记为@objc


实际中,这意味着两件事情。首先,你可以使用@dynamicMemberLookup建立一个类,并且任何从它继承的类都会自动为@dynamicMemberLookup属性. 所以,下例会打出“I’m a sandwich”,因为HotDog继承于Sandwich

 

@dynamicMemberLookup
class Sandwich {
  subscript(dynamicMember member: String) -> String {
      return 'I'm a sandwich!'
   }
}

class HotDog: Sandwich { }


let chiliDog = HotDog()
print(chiliDog.description)


注意:如果你不认为热狗是三明治,那就来我的推特打我呀

 

第二,你可以通过在协议中定义,来反向地让其他类型使用@dynamicMemberLookup,通过扩展协议添加一个默认执行的subscript(dynamicMember:),之后让其他类型按你想要的方式符合你的协议。


例如,下例创建了一个新的Subscripting协议,提供一个默认的subscript(dynamicMember:),它会返回一条信息,之后扩展Swift的String去使用这个协议。


@dynamicMemberLookup
protocol Subscripting { }

extension Subscripting {
  subscript(dynamicMember member: String) -> String {
      return 'This is coming from the subscript'
   }
}

extension String: Subscripting { }
let str = 'Hello, Swift'
print(str.username)


在他的Swift Evolution 提案中,Chris Lattner 也给了一个JSONenum例子,即使用动态成员查找通过JSON为引导(navigating)建立更自然的语法

 

@dynamicMemberLookup
enum JSON {
 case intValue(Int)
 case stringValue(String)
 case arrayValue(ArrayJSON>)
 case dictionaryValue(DictionaryString, JSON>)

 var stringValue: String? {
    if case .stringValue(let str) = self {
       return str
    }
    return nil
  }

 subscript(index: Int) -> JSON? {
    if case .arrayValue(let arr) = self {
       return index < arr.count="" arr[index]="" :="">
    }
    return nil
  }

  subscript(key:String) -> JSON? {
    if case .dictionaryValue(let dict) = self {
       return dict[key]
    }
    return nil
  }

 subscript(dynamicMember member: String) -> JSON? {
    if case .dictionaryValue(let dict) = self {
       return dict[member]
    }
    return nil
  }
}


没有动态成员查找你将需要为JSON enum  引导一个实例,像这样

 

let json =JSON.stringValue('Example')
json[0]?['name']?['first']?.stringValue


但是有了动态成员查找你可以用下面这个


json[0]?.name?.first?.stringValue


我认为这个例子特别重要,因为它指出了@dynamicMemberLookup的要点,它能把定制下标转化为简单的点语法

 

注意:使用动态成员查找意味着如果不具备全部的有效性,代码的完成会损失很多,因为不存在东西去完成。尽管这不算是什么惊喜,这也是Python IDEs有时不得不处理的问题。Chris Lattner (SE-0195的作者)在他的提案中讨论了未来关于代码完成的可能性,它值得一读

 

增强的条件一致性


条件一致性在Swift 4.1中引入,允许当特定条件被满足时,类型就去符合一个协议。


例如,如果我们有Purchaseable协议


protocol Purchaseable {
  func buy()
}


和一个符合这个协议的简单类型

 

struct Book: Purchaseable {
  func buy() {
      print('You bought a book')
   }
}


之后如果数组中所有元素符合Purchasable,我们就可以让数组符合Purchaseable,

 

extension Array: Purchaseable whereElement: Purchaseable {
  func buy() {
      for item in self {
          item.buy()
      }
   }
}


在编译时它运行的很好,但是有一个问题,如果你要在运行时查询一个条件一致性,你的代码会崩溃,因为这在Swift 4.1中不支持。


现在,在Swift 4.2中已经被修复,所以如果你收到一种类型的数据,想要检查它是否可以被转化为一个条件一致性协议,它可以做到了。


例如:

let items: Any = [Book(), Book(), Book()]

if let books = items as? Purchaseable {
  books.buy()
}


另外,对Hashable一致性自动合并的支持在Swift 4.2被大幅提高,来自Swift 标准库的几个内置类型,包括optionals, arrays, dictionaries, 和 ranges-现在当他们的元素符合Hashable时会自动符合Hashable协议


例如:

struct User: Hashable {
  var name: String
  var pets: [String]
}


Swift 4.2 可以为这个结构自动合并 Hashable一致性, 但Swift 4.1不能.

 

本地集合元素移除


SE-0197介绍一个新的removeAll(where:) 方法,它具有高性能,本地为集合过滤。给它一个闭合环境运行,它会除去所有符合条件的目标。


例如,如果你有一个名称的collection,并且想移除叫“Terry”的人,你可以用这个


var pythons = ['John','Michael', 'Graham', 'Terry', 'Eric','Terry']
pythons.removeAll {$0.hasPrefix('Terry') }
print(pythons)


现在,你也许会想到你可以通过filter()来实现,像这样


pythons = pythons.filter {!$0.hasPrefix('Terry') }


然而,这样并不能有效利用存储,它指定你不需要的而不是你想要的,而且更高级的本地方法会带来一系列复杂性,这些是新手很讨厌的。Ben Cohen,SE-0197的作者,在dotSwift 2018发表了一番话,发言中他更详细地讨论了这项提案的实现-如果你想学到为什么它这样高效,你可以去那里看。

 

布尔值切换


SE-0199为布尔值带来了一个新的toggle()方法,即在true和false之间翻转。这个引发了Swift社区的一片讨论,部分人认为它太细枝末节,也有部分是因为Swift论坛的讨论时不时变得失控。


执行这个提案的完整代码仅仅几行


extension Bool {
 mutating func toggle() {
    self = !self
  }
}


然而,最终它有助于让Swift代码变得更自然

 

var loggedIn = false
loggedIn.toggle()


就像这个提案中提到,它在更复杂的数据结构中特别有用:


myVar.prop1.prop2.enabled.toggle()避免了在手动操作时潜在的输入错误。


这个提案让Swift更简单安全的写入,并且完全是附加的,所以我认为大部分人会很快去使用它。

 

展望Swift 5.0

苹果形容Swift 4.2为”为了实现Swift 5中ABI稳定性的中继点”,但是我希望你能明白这是一个很保守的声明-我们正在得到一些很好的新特性,既有对过去特性的改进,也有ABI的改变。


更好的是,在最终发布之前我们也许还会看到新的特性被加入-我们可能同时看到SE-0192, SE-0193, SE-0202, 和 SE-0206的到来。


然而,我们归根结底依旧期待Swift 5.0能提供大家盼望已久的ABI稳定性。苹果谨慎小心步步为营的方法似乎会走的更好,而且它让人怀有希望,觉得Swift 5.0将会更值得等待。


相关推荐:


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多