Swift 4.0 與 Swift 3.0 - 差異和新功能

已發表: 2021-10-05

蘋果發布著名的 Swift 語言新版本的那一天終於到來了。 我們應該從中得到什麼? 在充滿期待的同時,我們決定對Swift 4版本中將出現的新更新進行簡要概述。

3 Swift 的基石。

作為一種非常棒的編寫代碼的語言,Swift 有其自身的優勢,並且據稱“比 Objective-C 語言更長壽”。

歡迎您閱讀 Swift 和 Objective-C 之間的主要區別

Swift速度快類型安全並且非常具有表現力。 它可用於在手機和平板電腦、台式機和服務器上編寫軟件——顯然,在所有運行代碼的地方。 它歡迎您使用它 - 使用 Apple 的學習如何編碼 Swift Playgrounds 應用程序或在 Xcode 中使用 Playgrounds,您可以立即看到您的工作結果,無需專心開發和運行應用程序第一名。 隨著每個新的附加版本,它變得更好更快,Swift 4 版本就是這種情況。
準備好了?

Xcode 9 為 Swift 4 提供的另一個很棒的功能 - 您不必太擔心即將到來的遷移,並且在閱讀本文時您會弄清楚原因。

說到這裡,讓我們簡要地探討一下今年秋季為我們帶來的糖果和 Swift 4 新功能。

入門

如果沒有方便的 IDE,即  開發人員世界中的 Xcode,語言本身並不是很有用。 您可以從 Mac App Store 或 Apple Developer 網站的下載頁面下載最新版本的 Xcode 9,但首先要確保您擁有一個有效的開發者帳戶。 它非常穩定,因此您可以在日常編碼例程中用它替換 Xcode 8。

您還可以使用 xcversion 安裝多個版本的 Xcode

如果您正在開始一個新項目 - 您很高興。 但是如果你已經有一個用 Swift 3.x 編寫的項目 - 你必須經歷一個遷移過程。

我們建議首先在 Playground 上試用 - 習慣使用新功能。

在閱讀本文時,您會注意到“SE-____”格式的 Swift Evolution 提案鏈接。

遷移到 Swift 4

從 Swift 的一個主要版本到下一個版本的遷移一直非常激烈,尤其是從 Swift 2.x 到 3.0。 通常每個項目大約需要 1-2 天,但遷移到 Swift 4 更容易一些,並且可以更快地通過。

遷移前準備

Xcode 9 不僅支持 Swift 4,還支持過渡版本 3.2,因此您的項目編譯應該沒有任何困難。 這是可能的,因為 Swift 4 編譯器和遷移工具支持這兩種語言版本。 您可以為每個目標指定不同的 Swift 版本,如果某些第三方庫尚未更新或者您的項目中有多個目標,這將非常有用。 然而,不僅僅是語言,SDK 也發生了一些變化,因此隨著 Apple 繼續完善 SDK API,很可能必須對您的代碼應用一些更新......

Swift 遷移工具

與往常一樣,Apple 提供了捆綁在 Xcode 中的 Swift 遷移工具,可以幫助從以前的 Swift 版本遷移。 您可以在 Xcode 中啟動它,方法是轉到Edit -> Convert -> To Current Swift Syntax...並選擇要轉換的目標。

然後會詢問您要應用哪種 Objective-C 推理偏好:

由於附加更改在 Swift 4 中占主導地位,因此 Swift 遷移工具將為您管理大部分更改。

可可豆

大多數  開發人員在他們的項目中使用 CocoaPods 依賴項管理器,因為 Swift Package Manager 雖然改進得非常快,但還沒有達到應有的成熟度。 如上所述,並非所有第三方庫都已更新到 Swift 4,因此您在編譯其中一些庫時可能會看到錯誤。 一個可能的解決方案來解決這個問題,指定版本,雨燕3.2的哪些不是通過添加還沒有被更新的豆莢post_install腳本你Podfile

 old_swift_3_pods = [ 'PodName1', 'PodName2', ] post_install do |installer| installer.pods_project.targets.each do |target| if old_swift_3_pods.include? target.name target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '3.2' end end end end

然後運行

pod install

現在您可以毫無錯誤地編譯 pod。

讓我們來看看 Swift 4 API 的更改和添加。

API 更改和添加

字符串

由於 SE-0163 提案, String現在符合Collection協議。 還記得 Swift 1.x 嗎?

現在不需要characters數組屬性,因為您可以直接遍歷String

 let string = "Hello, Mind Studios!" for character in string { print(character) }

這也意味著您可以在String上使用任何Collection方法和屬性,例如countisEmptymap()filter()index(of:)等等:

 string.count // No more `string.characters.count` string.isEmpty // false let index = string.index(of: " ") // 6 let reversedCollection = "abc".reversed() let reversedString = String(reversedCollection) // "cba" // String filtering let string = "ni123n456iniASijasod! 78a9-kasd aosd0" let numbersString = string.filter { Int(String($0)) != nil } // "1234567890"

新的Substring類型

Swift 4 帶來了新的Substring類型,它表示String Substring序列(如上面提到的 SE-0163 中所述)。

 // Split string into substrings let string = "Hello, Mind Studios!" let parts = string.split(separator: " ") // ["Hello,", "Mind", "Studios!"] type(of: parts.first!) // Substring.Type

StringSubstring現在都支持新的StringProtocol ,這使它們幾乎相同且可互操作:

 var hello = parts.first! // Concatenate a String onto a Substring hello += " !" // "Hello, !" // Create a String from a Substring let helloDog = String(hello) // "Hello, !"

重要的提示

SE-0163 有一個非常重要的注意事項:

 Long-term storage of `Substring` instances is discouraged. A substring holds a reference to the entire storage of a larger string, not just to the portion it presents, even after the original string's lifetime ends. Long-term storage of a substring may therefore prolong the lifetime of elements that are no longer otherwise accessible, which can appear to be memory leakage.

這意味著Substring旨在用作String序列的臨時存儲。 如果要將其傳遞給某些方法或其他類 - 首先將其轉換為String

 let substring: Substring = ... // Substring let string = String(substring) // String someMethod(string)

無論如何,Swift 的類型系統將幫助您不要將Substring傳遞到需要String地方(假設您沒有使用 new StringProtocol作為參數類型)。

多行字符串文字

SE-0168 使用三個雙引號"""為多行字符串文字引入了一種簡單的語法,這意味著可以粘貼大多數文本格式(例如 JSON 或 HTML)或一些長文本而無需任何轉義:

 let multilineString = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mattis lorem et leo laoreet fermentum. Mauris pretium enim ac mi tempor viverra et fermentum nisl. Sed diam nibh, posuere non lectus at, ornare bibendum erat. Fusce mattis sem ac feugiat vulputate. Morbi at nunc maximus, vestibulum orci et, dictum neque. Vestibulum vulputate augue ac libero vulputate vestibulum. Nullam blandit et sapien non fermentum. Proin mollis nisl at vulputate euismod. """

在字符串文字中轉義換行符

SE-0182 添加了在多行字符串文字中使用反斜杠轉義換行符的功能。

 let escapedNewline = """ Line 1, Line 2 \ next part of line 2, Line 3 """ print(escapedNewline)
 Line 1, Line 2 next part of line 2, Line 3

改進的 Unicode 支持

Swift 4 支持 Unicode 9,這意味著 unicode 字符計數問題現已消失:

 "".count // 1, in Swift 3: 2 "".count // 1, in Swift 3: 2 "".count // 1, in Swift 3: 2 - person + skin tone "".count // 1, in Swift 3: 4 "".count // 3, in Swift 3: 1

上面強調的所有更改和已實施的提案(以及更多其他提案)都源於一組廣泛描述的功能,稱為字符串宣言。

訪問控制

Swift 3 為訪問控制帶來了一個非常矛盾的元素fileprivate訪問修飾符,這可能真的很令人困惑。
以前, private訪問級別修飾符用於對其他類型隱藏類型成員,私有成員只能通過類型定義中定義的方法和屬性訪問,而將相同的類型擴展放在一邊,因為它們無法訪問這些成員。
fileprivate可用於在同一文件中共享類型成員的訪問權限,例如屬性和方法。
事實上,當某些類型的擴展無法訪問該類型的成員時,使用private導致問題,因此在這種情況下使用fileprivate是一個非常常見的解決方案,這導致了另一個問題:相同的文件也可以訪問這些成員。

Swift 4 通過允許類型的擴展訪問同一文件中的該類型的private成員來整理事情,如 SE-0169 中所述:

 struct User { private let firstName: String private let lastName: String } extension User: CustomStringConvertible { var description: String { return "User: \(firstName) \(lastName)" } }

字典和集合

Sequence初始化Dictionary

Dictionary現在可以用Sequence初始化,但並不是所有的序列都可以在這個初始化器中傳遞,只有那些包含元組(Key, Value) ,其中Key是 Dictionary 的鍵類型, Value表示字典值類型:

 let stocksIdentifiers = ["AAPL", "GOOGL", "NKE"] let stocksValues = [158.28, 940.13, 53.73] let pairs = zip(stocksIdentifiers, stocksValues) let stocksValuesDict = Dictionary(uniqueKeysWithValues: pairs) // ["GOOGL": 940.13, "NKE": 53.73, "AAPL": 158.28]

這裡zip函數從 2 個序列創建一對( Tuple s),您可以在 Swift 標準庫文檔中閱讀有關此函數的更多信息。

合併字典

通過將閉包傳遞給uniquingKeysWith參數,您可以指定在從Tuple序列創建字典時應如何處理重複鍵,該參數用於組合來自 2 個相同鍵的值。

示例 1:

 let duplicates = [("a", 1), ("b", 5), ("a", 3), ("b", 3)] let dictionary = Dictionary(duplicates, uniquingKeysWith: { (first, _) in return first }) // ["b": 5, "a": 1]

這裡我們保留第一個值,忽略所有具有相同鍵的下一個值。

示例 2:

計算每個字符在字符串中出現的次數。

 let string = "Hello!" let pairs = Array(zip(string, repeatElement(1, count: string.count))) let counts = Dictionary(pairs, uniquingKeysWith: +) // ["H": 1, "e": 1, "o": 1, "l": 2, "!": 1]

示例 3:

使用merge方法:

 let values = ["a": 1, "b": 5] var additionalValues = ["b": 3, "c": 2, "a": 3] additionalValues.merge(values, uniquingKeysWith: +) // ["b": 8, "c": 2, "a": 4]

帶默認值的下標

以前,一種常見的做法是使用 nil 合併運算符在值為 nil 時給出默認值。

斯威夫特 3:

 let dict = ["a": 1, "b": 5] dict["c"] ?? 0 // 0

Swift 4 在下標上引入了新的default值(SE-0165 的一部分):

 let dict = ["a": 1, "b": 5] dict["c", default: 0] // 0, equals to `dict["c"] ?? 0` in Swift 3

您還可以在使用默認值下標的同時改變字典:

 let string = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mattis lorem et leo laoreet fermentum. Mauris pretium enim ac mi tempor viverra et fermentum nisl. Sed diam nibh, posuere non lectus at, ornare bibendum erat. Fusce mattis sem ac feugiat vulputate. Morbi at nunc maximus, vestibulum orci et, dictum neque. Vestibulum vulputate augue ac libero vulputate vestibulum. Nullam blandit et sapien non fermentum. Proin mollis nisl at vulputate euismod. """ var wordsCountByLine = [Int: Int]() let lines = string.split(separator: "\n") for (index, line) in lines.enumerated() { let lineWordsCount = line.split(separator: " ").count wordsCountByLine[index, default: 0] += lineWordsCount } print(wordsCountByLine) // [2: 10, 4: 15, 5: 7, 6: 6, 7: 6, 0: 8, 1: 7, 3: 10]

特定於字典的映射和過濾器

分組序列元素

協議中的受限關聯類型

SE-0142 提案在關聯類型聲明中引入了條件子句。

 extension Sequence where Element: Numeric { var sum: Element { var result: Element = 0 for element in self { result += element } return result } } [1,2,3,4].sum

存檔和序列化(編碼/解碼)

以前,為了序列化一些自定義類型,您必須使用舊的和眾所周知的NSCoding協議。 問題在於,諸如structenum類的非類類型無法符合此協議,因此開發人員只需使用一些技巧,例如通過創建可以符合NSCoding的嵌套類來提供額外的兼容性層。

由於 SE-0166 - Codable協議的引入,Swift 4 為這個問題提供了一個非常方便的解決方案:

 struct Employee: Codable { let name: String let age: Int let role: Role enum Role: String, Codable { case manager case developer case admin } } struct Company { let name: String let officeLocation: Location? let employees: [Employee] } struct Location : Codable { let latitude: Double let longitude: Double }

在像這樣的簡單情況下,您只需要為所有自定義類型添加Codable協議一致性,編譯器將為您完成所有魔術。 就是這樣!

Codabletypealias對的組成DecodableEncodable協議,因此你可以聲明,例如,只Decodable協議一致性,如果你想你的類型實例從JSON數據進行解碼。

編碼

如果要序列化或反序列化Codable值 - 您必須使用和編碼器或解碼器對象。 Swift 4 已經帶有一組用於 JSON 和屬性列表的編碼器/解碼器,以及用於在編碼/解碼過程中可能拋出的不同類型錯誤的新CocoaError s。 NSKeyedArchiverNSKeyedUnarchiver也支持Codable類型。

 let employee = Employee(name: "Peter", age: 27, role: .manager) let company = Company(name: "Awesome Company", officeLocation: nil, employees: [employee]) let encoder = JSONEncoder() let companyData = try encoder.encode(company) let string = String(data: companyData, encoding: .utf8)! print(string) >>> { "name" : "Awesome Company", "employees" : [ { "name" : "Peter", "age" : 27, "role" : "manager" } ] }

小菜一碟,不是嗎?

解碼

解碼器用於從Data反序列化自定義Codable類型。 它不知道從數據本身解碼哪種類型,因此您應該指定要解碼的類型,例如, Employee[Employee]

 let decoder = JSONDecoder() let jsonData = """ [ { "name" : "Peter", "age" : 27, "role" : "manager" }, { "name" : "Alex", "age" : 26, "role" : "developer" }, { "name" : "Eugene", "age" : 30, "role" : "admin" } ] """.data(using: .utf8)! let employees = try decoder.decode([Employee].self, from: jsonData)
 If one of `Codable` type instances fails to decode, then whole collection will fail to decode.

自定義鍵名

在大多數情況下,我們在自定義 Swift 類型中使用的名稱與表示此類型的 JSON 數據中的鍵不匹配。 要在自定義類型屬性名稱和 JSON 鍵之間創建映射,您可以創建一個名為CodingKeys的嵌套枚舉,該枚舉應符合CodingKey協議:

 struct Country: Decodable { let id: String let name: String let phoneCode: String private enum CodingKeys: String, CodingKey { case id = "alpha3" case name case phoneCode = "phone_code" } }

自定義解碼

如果您有一個複雜的案例,您可以從Decodable協議實現您的自定義初始值Decodable

 struct Transaction { let id: Int let action: String let source: String let amount: Int let state: TransactionState let createdAt: Date let authorName: String enum TransactionState: String, Decodable { case done case canceled case processed } } extension Transaction: Decodable { private enum CodingKeys: String, CodingKey { case id case action = "action_name" case source = "source_name" case amount case state case createdAt = "created_at" case author } private enum AuthorKeys: String, CodingKey { case fullName = "full_name" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) actionName = try container.decode(String.self, forKey: .action) sourceName = try container.decode(String.self, forKey: .source) let createdAtValue = try container.decode(Double.self, forKey: .createdAt) createdAt = Date(timeIntervalSince1970: createdAtValue) state = try container.decode(TransactionState.self, forKey: .state) amount = try container.decodeIfPresent(Int.self, forKey: .amount) ?? 0 do { let authorContainer = try container.nestedContainer(keyedBy: AuthorKeys.self, forKey: .author) authorName = try authorContainer.decode(String.self, forKey: .fullName) } catch { authorName = "" } } }

鍵值編碼

Swift 4 帶來的便利功能之一是 SE-0161 中描述的 Smart KeyPath。 與 Swift 3 #keyPath() ,後者不是強類型並且僅適用於 Objective-C 成員,Swift 4 KeyPath 是一個泛型類,這意味著鍵路徑現在是強類型的。 讓我們深入研究一些例子:

 struct User { var username: String }

鍵路徑\<Type>.<path>的一般形式,其中<Type>是類型名稱,而<path>是一個或多個屬性的鏈,例如, \User.username

 let user = User(username: "max") let username = user[keyPath: \User.username] // "max"

如果它是可變的,您還可以通過此鍵路徑寫入新值:

 var user = User(username: "max") user[keyPath: \User.username] = "alex" // "alex"

關鍵路徑不限於一級層次結構:

 struct Comment { let content: String var author: User } let max = User(username: "max") let comment = Comment(content: "Nice post!", author: max) let authorUsername = comment[keyPath: \Comment.author.username] // "max"

關鍵路徑可以存儲在一個變量中:

 let authorKeyPath = \Comment.author let usernameKeyPath = authorKeyPath.appending(path: \.username) let authorUsername = comment[keyPath: usernameKeyPath] // "max"

您還可以將鍵路徑用於可选和計算屬性:

 struct Post { let title: String var comments: [Comment] var topComment: Comment? { return comments.first } } let max = User(username: "max") let alex = User(username: "alex") var post = Post(title: "What's new in Swift 4", comments: []) let topCommentAuthorUsernameKeyPath = \Post.topComment?.author.username post[keyPath: topCommentAuthorUsernameKeyPath] // nil let comment = Comment(content: "", author: alex) let anotherComment = Comment(content: "Nice post!", author: max) post.comments = [comment, anotherComment] post[keyPath: topCommentAuthorUsernameKeyPath] // "alex"

儘管 SE-0161 突出顯示了關鍵路徑中的支持下標,但它們尚未實現:

 post.comments[keyPath: \.[0].content] // error: key path support for subscript components is not implemented let firstCommentAuthorKeyPath = \Post.comments[0].author // error: key path support for subscript components is not implemented

KVO

除了新的鍵路徑之外,Swift 4 中也更新了鍵值觀察 API。

 New KVO APIs depend on Objective-C runtime and works for `NSObject` subclasses only, so it can't be used for Swift structs and classes which don't inherit `NSObject`. In order to observe property it should be marked as `@objc dynamic var`.
 class User: NSObject { @objc dynamic var name: String var username: String init(name: String, username: String) { self.name = name self.userName = userName super.init() } } let user = User(name: "Max", username: "max") let nameObservation = user.observe(\.name, options: [.new, .old]) { user, change in // NSKeyValueObservation if let oldValue = change.oldValue, let newValue = change.newValue { print("fullName has changed from \(oldValue) to \(newValue)") } else { print("fullName is now \(user.name)") } } user.name = "Alex" // name has changed from Max to Alex

如果要停止觀察,請調用invalidate()方法

nameObservation.invalidate() user.name = "Elina" // observer isn't get called

它在 deinited 時也會停止,所以如果你想保存它,請確保將它存儲在財產或其他地方。

單邊範圍

SE-0172 引入了通過現有範圍運算符的前綴/後綴版本創建的“單邊”範圍,以及新的RangeExpression協議,以簡化採用不同類型範圍的方法的創建。

無限序列

您可以使用單邊範圍來構造無限序列:

 let letters = ["a", "b", "c", "d"] let numberedLetters = Array(zip(1..., letters)) // [(1, "a"), (2, "b"), (3, "c"), (4, "d")]
 let string = "Hello, Mind Studios!" let index = string.index(of: ",")! string[..<index] // "Hello" string[...index] // "Hello,"

在模式匹配中使用單邊範圍

let value = 5 switch value { case 1...: print("greater than zero") case 0: print("zero") case ..<0: print("less than zero") default: break }

通用下標

SE-0148 下標現在可以具有通用參數和返回類型

struct JSON { let data: [String: Any] subscript<T>(key: String) -> T? { return data[key] as? T } } let jsonDictionary: [String: Any] = [ "name": "Ukraine", "flag": "", "population": 42_500_000 ] let json = JSON(data: jsonDictionary) let population: Int? = json["population"] // 42500600
 extension Dictionary where Value == String { subscript<T: RawRepresentable>(key: Key) -> T? where T.RawValue == Value { guard let string = self[key] else { return nil } return T(rawValue: string) } } enum Color: String { case red case green case blue } let dictionary = [1: "red"] let color: Color? = dictionary[1] // red

限制 Objective-C 推理

Swift 4 通過將 @objc 推理限制為僅適用於聲明必須可用於 Objective-C (SE-0160) 的情況來最小化 @objc 推理。
如果您不使用它,這會通過不編譯多餘的 Objective-C 代碼來減少您的應用程序的二進制大小,並為何時推斷 @objc 提供更多控制。 NSObject 派生類不再推斷 @objc。

但是在某些情況下,Swift 代碼會繼續進行隱式推理:

  • 具有@objc 屬性的聲明

  • 滿足@objc 協議要求的聲明

  • 具有@IBAction、@IBInspectable、@IBOutlet、@NSManaged、@GKInspectable 屬性的聲明

要為整個類啟用 @objc 推理,您可以使用新的 @objcmembers 屬性。
要禁用特定擴展或函數的 @objc 推理 - 添加新的 @nonobjc 屬性。

組合類和協議

在 Swift 4 中,我們現在可以將協議與其他 Swift 類型組合在一起:

 User & Codable & CustomStringConvertible typealias MyType = User & Codable & CustomStringConvertible

Swift 4 的好處

Swift 4 的優勢真的很大,因為它經常發生在 Apple 發布新語言版本時。 除了語言性能的提升之外,它還極大地穩定了遷移過程。 讓我們回想起將 Swift 2.2 遷移到 3.0 的過程,我們回想起轉移所有依賴項的複雜過程。 Swift 4.0 的變化讓我們無需真正“重新定位”第三方庫——你只需要更新 Swift 本身。

另外關於 Swift 4.0 與 3.0 的改進,編譯後的二進製文件大小發生了變化,導致應用程序的大小減小; 例如,過去的移動應用程序重 20 MB,而在最新的 Swift 版本中,它大約需要 17 MB。 Swift 4 和 Swift 3 之間有一個基本的區別——錯誤修復已經發生,語言變得更加快速。

Swift 已經使用多年了,而且它會隨著每個即將到來的更新而不斷發展。 每一種新語言都會更新新的開發視角,之前未知的,我們期待探索新的 iOS 視野。

不要錯過我們關於 iOS 開發的 MVP vs MVC vs MVVM vs VIPER 的文章。

由 Max Mashkov 和 Elina Bessarabova 撰寫