What's New in Swift 4 ? Swift 4 更新了什么?

1. One-sided ranges 单边range

可以省略范围range的上限或下限

let esports = ["Hearthstone", "CS:GO", "League of Legends",
               "Super Smash Bros", "Overwatch", "Gigantic"]
esports[3...]
// returns ["Super Smash Bros", "Overwatch", "Gigantic"]
// In Swift 3, you had to write
esports[3..<esports.endIndex]
esports[...2]
// returns ["Hearthstone", "CS:GO", "League of Legends"]
esports[..<2]
// returns ["Hearthstone", "CS:GO"]
  • Infinite sequences 无限序列

一个单边range可以用来创建一个无限序列,只定义序列的开始即可

let uppercase = ["A", "B", "C", "D"]
let asciiCodes = zip(65..., uppercase)
print(Array(asciiCodes))
// prints [(65, "A"), (66, "B"), (67, "C"), (68, "D")]
  • Pattern matching 条件匹配

一个单边range可以大大简化switch语句的匹配条件

func gameRank(_ index: Int) -> String {
  switch index {
  case ...1:
    return "Oldie but goodie"
  case 3...:
    return "Meh"
  default:
    return "Awesome-sauce!"
  }
}
gameRank(2)
// prints "Awesome-sauce!"

switch表达式不必为Int类型

/// Produce an emoji based on a numeric value
/// - parameter rating: a value between 0 and 1
func sentiment(_ rating: Double) -> String {
  switch rating {
  case ..<0.33:
     return "😞"
  case ..<0.66:
     return "😐"
  default:
     return "😁"
sentiment(0.5)
// returns "😐"

2. Strings 字符串

String再次成为了集合类型

let text = "Hakuna Matata"
let unicodeText = "👇🏿👏🏻🤝🇦🇹🇧🇿🇧🇹🇫🇯🇧🇷"

text.count      // 13
text.isEmpty    // false
"".isEmpty      // true

// `reversed()` returns a `ReversedCollection<String>` so
// it must be converted back to `String`
String(text.reversed()) 
// "atataM anukaH"

很容易遍历字符串中的每个字符,同样适用于Unicode字符

for c in unicodeText {
  print(c) // prints each of 8 characters, one to a line
}

字符串下标不能为原始类型如Int,相反,它们必须是一个String.Index或Range<String.Index>

var index = text.index(text.startIndex, offsetBy: 7)
text[index]
// "M"

// You can use prefix(upTo:) and suffix(from:) 
// but why not use one-sided ranges instead

let lastWord = text[index...]
// lastWord is "Matata"

index = text.index(text.endIndex, offsetBy: -7)
let firstWord = text[..<index]
// firstWord is "Hakuna"
  • Introducing Substring 引入Substring类型

Swift试图高效率管理与String相关的buffer,因此,String对象的下标操作返回的是原始字符串buffer的一部分,增加的是buffer的引用计数。这会导致String未被使用到的那一大部分还驻留在内存中。虽然在技术上这不是内存泄漏,但看起来就像是内存泄漏。
Swift标准库通过String的下标操作,返回了一个新类型: Substring,从而解决了这个问题。大部分API还将继续使用String类型作为参数,你得把Substring类型显式转换成String类型使用才行。这让那个较大的原始字符串很自然的出了作用域,从而得到释放。
标准库还引入了一个新的协议:StringProtocol。大部分String的API都迁移到了StringProtocol。String和Substring都遵从了这个新协议。

type(of: lastWord)
// Substring.Type

lastWord.uppercased()
// "MATATA"

let lastWordAsString = String(lastWord)
type(of: lastWordAsString)
// String.Type

注意:在Xcode中选择点击一个变量名查看它的类型,将会看到返回的是String.Subsequence。这只是Substring类型的简单关联。type(of: lastWord) 就是如此,实际上返回的是Substring.Type。

  • Unicode魔法

在Swift3中,如果你想访问Character中单独的Unicode值,首先你得把它转成String。而现在,Character有了个unicodeScalars属性。

let c: Character = "🇨🇭"
Array(c.unicodeScalars)
// [127464, 127469]

Swift3不喜欢下列任何一个表达式,在每种情况下都给出了错误的答案。而Swift4可以正确的处理它们,给出你所看到的预期的长度,而不是编码所需的长度。

"🇧🇿🇫🇯🇧🇷".count    // 3
"👇🏿".count        // 1
"👏🏻".count        // 1
"🇦🇹".count        // 1

如前所述,遍历字符串会正确地显示每个Unicode字符。

  • Range<String.Index> 和 NSRange 转换

Swift字符串的范围用Range<String.Index>来表示。而许多Foundation的API (例如 NSRegularExpression, NSAttributedString, NSLinguisticTagger) 则需要用到NSRange
Foundation现在包含了新的构造方法,使NSRangeRange<String.Index>可以互相转换。再也不需要用UTF-16的offset和view把事情弄的一团糟了。

let population = "1️⃣👩🏽‍🌾2️⃣🐓3️⃣👨‍👩‍👧‍👦"
population.count
// 6

var nsRange = NSRange(population.startIndex..., 
                      in: population)
// population.startIndex... is a Range<String.Index>
// (0, 29)

population.utf16.count
// 29

let display = NSMutableAttributedString(
        string: population, 
        attributes: [.font: UIFont.systemFont(ofSize: 20)])

正如预期那样,NSRange的长度匹配UTF-16视图的长度,display会这样显示:

let oneIndex = population.index(of: "1️⃣")!
let twoIndex = population.index(of: "2️⃣")!
let threeIndex = population.index(of: "3️⃣")!
var range = oneIndex..<twoIndex
nsRange = NSRange(range, in: population)
display.addAttribute(.font, 
                     value: UIFont.systemFont(ofSize: 40), 
                     range: nsRange)

上面代码查找每个数字图标的索引,为字符串的第一部分创建range(Range<String.Index>),再转成NSRange,然后采用了新的字体属性,现在display看起来是这样的:

range = twoIndex..<threeIndex
nsRange = NSRange(range, in: population)
display.addAttribute(.font, 
                     value: UIFont.systemFont(ofSize: 30), 
                     range: nsRange)

接下来,代码生成了字符串中间那部分的range,转换成NSRange并采用了另一种新的字体属性,display现在看起来是这个样子:

把NSRange转成Range<String.Index>也很容易。
注意,这是个可以失败的构造方法,所以返回结果是可选的。

let textInput = "You have traveled 483.2 miles."
let pattern = "[0-9]+(\\.([0-9])?)?"
let regex = try! NSRegularExpression(pattern: pattern, 
                                     options: [])
nsRange = NSRange(textInput.startIndex..., in: textInput)
let mileage = regex.rangeOfFirstMatch(in: textInput, 
                                      range: nsRange)
range = Range(mileage, in: textInput)!
textInput[range]
// "483.2"

注意,这些新的构造方法也使得像UITextFieldDelegate的textField(_:shouldChangeCharactersIn:replacementString:)这样的东西容易写了,没有转成NSString那么丑了,这在iOS11代码中是如此常见。

  • 多行的字符串字面量

现在你可以创建多行的字符串文本了,很容易生成精确格式化的输出,或者直接在你的代码里粘贴漂亮的输入(例如JSON,HTML)。字符串插值依然可以,甚至你可以摆脱换行符\n,使它们不出现在字面量里。
多行的字面量是以"""和结束界定符之间的部分来确定每行的开头有多少空白被去掉了。你可以在字面量里面使用括号。

let firstVerse = """
  Half a league, half a league,
    Half a league onward,
  All in the valley of Death
    Rode the six hundred.
  "Forward, the Light Brigade!
  "Charge for the guns!" he said:
  Into the valley of Death
    Rode the six hundred.
  """

print(firstVerse)的结果是这样的:

let details = """
  Note that the indentation of the
    closing delimiter determines
    the amount of whitespace removed.
  You can insert \(firstWord) and \
  \(lastWord) and escape newlines too!
  """

print(details)会是这样:

3. Dictionary 增强

[SE-0165]
作为一个Swift开发者,你知道Dictionary在你的日常生活中有多重要。Swift 4带来了一系列的改善,使它更强大,更有用,更可用。

  • Sequence-based initializer 基于序列的构造器

现在你可以通过键值对的顺序排列来创建字典。例如,你可以创建个项目的编号列表:

let groceries = Dictionary(uniqueKeysWithValues: zip(
  1...,
  ["Prosciutto", "Heavy Cream", "Butter", "Parmesan", 
   "Small shells"])
)
// [5: "Small shells", 2: "Heavy Cream", 3: "Butter", 
//  1: "Prosciutto", 4: "Parmesan"]

或者,你已经有了一波元组:

let housePointTotals = [("Slytherin", 472), 
                        ("Ravenclaw", 426), 
                        ("Hufflepuff", 352), 
                        ("Gryffindor", 312)]
let banquetBegins = Dictionary(
      uniqueKeysWithValues: housePointTotals)
// ["Ravenclaw": 426, "Hufflepuff": 352, "Gryffindor": 312, 
//  "Slytherin": 472]
  • Merging 合并

现在Dictionary有了个可以让你合并两个字典为一个字典的构造器。如果你想合并一个字典到另一个里面,Swift也提供了一个合并的方法。它们都允许你指定一个闭包来解决因重复的Key导致的合并冲突。

let duplicates = [("a", 1), ("b", 2), ("a", 3), ("b", 4)]
let oldest = Dictionary(duplicates) { (current, _) in
  current
}
// ["b": 2, "a": 1]

你可以用闭包来转换输入。这儿有个更屌的example,它拿到一个键值对数组,将它转换成一个包含数组的字典:

let sortingHat = [
  ("Gryffindor", "Harry Potter"), ("Slytherin", "Draco Malfoy"),
  ("Gryffindor", "Ron Weasley"), 
  ("Slytherin", "Pansy Parkinson"),
  ("Gryffindor", "Hermione Granger"), 
  ("Hufflepuff", "Hannah Abbott"),
  ("Ravenclaw", "Terry Boot"), ("Hufflepuff", "Susan Bones"),
  ("Ravenclaw", "Lisa Turpin"), 
  ("Gryffindor", "Neville Longbottom")
]
let houses = Dictionary(
  sortingHat.map { ($0.0, [$0.1]) },
  uniquingKeysWith: { (current, new) in
    return current + new
})
// ["Ravenclaw": ["Terry Boot", "Lisa Turpin"],
// "Hufflepuff": ["Hannah Abbott", "Susan Bones"],
// "Slytherin": ["Draco Malfoy", "Pansy Parkinson"],
// "Gryffindor": ["Harry Potter", "Ron Weasley",
// "Hermione Granger", "Neville Longbottom"]]

如你所见,你可以做的不仅是创建个字典。假设你想知道每一个字母出现在一个字符串中的出现频率是多少?那么字典合并能够帮你!

let spell = "I solemnly swear I am up to no good"
var frequencies: [Character: Int] = [:]
let baseCounts = zip(
  spell.filter { $0 != " " }.map { $0 },
  repeatElement(1, count: Int.max))
frequencies = Dictionary(baseCounts, uniquingKeysWith: +)
// ["w": 1, "p": 1, "n": 2, "o": 5, "I": 2, "u": 1, "t": 1,
//  "d": 1, "a": 2, "r": 1, "m": 2, "s": 2, "e": 2, "l": 2,
//  "g": 1, "y": 1]

如果您有一组默认值,并希望将它们与用户的设置合并,对于merge方法来说是个绝佳的使用。

let defaultStyling: [String: UIColor] = [
  "body": .black, "title": .blue, "byline": .green
]
var userStyling: [String: UIColor] = [
  "body": .purple, "title": .blue
]
userStyling.merge(defaultStyling) { (user, _) -> UIColor in
  user
}
// ["body": .purple, "title": .blue, "byline": .green]
  • Default value for subscript 下标默认值

字典的值是作为可选类型返回的。虽然这是必要的,但它要求你采取尽可能简单的代码,并使用可选绑定、强制解包或可选链来解包。在过去,解决这个问题的一种常见方法是使用nil合并运算符(??)来提供默认值,使结果是非可选的。
这是个非常常见的做法,而Swift 4增加了在下标中指定默认值的能力。

let swift3 = banquetBegins["House elves"] ?? 0
let swift4 = banquetBegins["House elves", default: 0]
// both are 0; both are Int and not Int?
let housePoints = banquetBegins["Hufflepuff", default: 0]
// value is 352 with or without the default. 
// Without, type is Int?; with, Int.

默认下标提供了另一种方法来实现您先前看到的频率计数器。

frequencies.removeAll()
spell.filter { $0 != " " }.map { 
  frequencies[$0, default: 0] += 1
}
// produces the same results as before
  • Filtering and mapping 过滤和映射

在Swift 4中,过滤字典的返回结果会保持它的结构和类型。

let oddGroceries = groceries.filter { $0.key % 2 == 1 }
// [5: "Small shells", 3: "Butter", 1: "Prosciutto"]

这也适用于集合:

let set: Set = ["a", "b", "c", "d", "e"]
let filteredSet = set.filter { $0.hashValue % 2 == 0 }
// ["b", "d"]

map函数总是返回一个数组。通常在使用字典的时候你不希望这样! Swift 4添加了mapValue,以允许你保留字典的结构和类型。

let mirroredGroceries = oddGroceries.mapValues { 
  String($0.reversed())
}
// [5: "sllehs llamS", 3: "rettuB", 1: "ottuicsorP"]
  • Grouping 分组

Dictionary最强大的新特性之一是,它可以基于任意谓词对数据进行分区,创建存储类似数据的组。最简单的例子是分组名单的第一个字母。

let names = ["Harry", "ron", "Hermione", "Hannah",
             "neville", "pansy", "Padma"].map { $0.capitalized }
let nameList = Dictionary(grouping: names) { $0.prefix(1) }
// ["H": ["Harry", "Hermione", "Hannah"], "R": ["Ron"],
// "N": ["Neville"], "P": ["Pansy", "Padma"]]

你可以指定一个任意的谓词,这样你就能更有创造力。

enum Multiples {
  case threes, notThrees
}
let numbers = 1...18
let predicate: (Int) -> Multiples =
  { $0 % 3 == 0 ? .threes : .notThrees }
let multiplesOfThree = Dictionary(grouping: numbers,
                                  by: predicate)
// [.threes: [3, 6, 9, 12, 15, 18],
// [.notThrees: [1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17]]
type(of: multiplesOfThree)
// [Multiples: [Int]]

在一个更实际的例子中,您可能希望根据一个结构中隐藏的值来组成您的组。将分组与Swift 4的新keyPath特性组合起来,使这成为一种轻而易举的事情。

// 1
struct Student {
  let firstName: String
  let lastName: String
}
// 2
let classRoll = sortingHat.map { $0.1.split(separator: " ") }
  .map { Student(firstName: String($0[0]), 
                 lastName: String($0[1])) }
// 3
let lastNameKeypath = \Student.lastName
// 4
let contactList = Dictionary(grouping: classRoll) {
  $0[keyPath: lastNameKeypath].prefix(1)
}

上面的代码做了如下事情:

  1. 定义一个Student结构体
  2. 使用之前的student列表创建Student值的数组
  3. 使用新的keyPath语法,引用StudentlastName字段
  4. lastName的第一个字母,给Student分组
  • Generic subscripts and associated type constraints

[SE-0142, SE-0148]
在Swift 3中使用混合数据类型字典是相当痛苦的,因为在使用它之前需要对每个值进行类型转换。Swift 4允许下标返回泛型类型。

struct Grade {
  private var data: [String: Any]
  
  init(data: [String: Any]) {
    self.data = data
  }
  
  subscript<T>(key: String) -> T? {
    return data[key] as? T
  }
}

let gradebook = Grade(data: ["name": "Neil Armstrong",
                             "exam": "LEM Landing",
                             "grade": 97])
let who: String? = gradebook["name"]
let grade: Int?  = gradebook["grade"]
// No need to coerce the type with "as?"

这绝不是实现结构体的最佳方法。然而,如果你正在使用新的Decodable,将会简化你自定义的init(from:)方法。

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

推荐阅读更多精彩内容