您當前的位置:首頁 > 體育

Swift 5.5新特性(上)

作者:由 貓克杯 發表于 體育時間:2021-06-22

本文對

https://www。

whatsnewinswift。com/?

from=5。4&to=5。5

進行了節選和翻譯

喜歡文章?不如來個 ❤️➕ 三連?關注專欄,關注我

更多內容,歡迎關注公眾號 「Swift花園」

相比於 Xcode 和 SwiftUI 的新特性和改進,Swift 語言本身在 5。5 版本迎來的變化可謂巨大了。Paul Hudson 在其 “What‘s new in Swift?” 網站上已經更新了 Swift 5。4 到 Swift 5。5 的變化,文件和範例都非常詳細,同時也很瑣碎。筆者挑選了自己認為比較重要的特性,在本文中和讀者一起探索學習。

動手實踐是最好的學習方式。建議想要嘗試和學習 Swift 5。5 新特性,同時又希望省點力的讀者,可以下載 Paul 放在 Github 上的 Playground,利用裡面的程式碼快速上手實踐。本文在介紹 Swift 5。5 新特性時,也會直接使用該 Playground 裡面的程式碼範例。

與併發無關的新特性

#if

字尾成員表示式

SE-0308 使得 Swift 可以在後綴成員表示式前使用

#if

條件。程式碼範例如下:

Text

“Welcome”

#if

os

iOS

font

(。

largeTitle

#else

font

(。

headline

#endif

條件還可以巢狀:

#if

os

iOS

font

(。

largeTitle

#if

DEBUG

foregroundColor

(。

red

#endif

#else

font

(。

headline

#endif

注意:條件分支之後必須都是字尾表示式,不能是其他型別的表示式。

CGFloat

Double

型別可以互換使用

SE-0307 引入:Swift 現在能夠在

CGFloat

Double

之間按需自動進行隱式轉換。

如 Paul 所言,這真的是一個微小但卻提升 Swift 程式設計師生活質量的改進。

大量使用

CGFloat

的 API,現在由 Swift 默默地幫你橋接到

Double

帶關聯值的列舉型別的

Codable

自動合成

SE-0295 升級了 Swift 的

Codable

系統,現在能支援帶關聯值的列舉。例如:

enum

Weather

Codable

{

case

sun

case

wind

speed

Int

case

rain

amount

Int

chance

Int

}

上面的程式碼有一個簡單的 case,一個帶單一關聯值的 case,還有一個帶兩個關聯值的 case。我們可以在 Swift 5。5 中用

JSONEncoder

或者其他型別的編碼器對下面的列舉變數進行編碼並取得 JSON 字串。

let

forecast

Weather

=

sun

wind

speed

10

),

sun

rain

amount

5

chance

50

do

{

let

result

=

try

JSONEncoder

()。

encode

forecast

let

jsonString

=

String

decoding

result

as

UTF8

self

print

jsonString

}

catch

{

print

“Encoding error:

\(

error

localizedDescription

}

lazy

關鍵字現在也能用於區域性作用域

func

printGreeting

to

String

->

String

{

print

“In printGreeting()”

return

“Hello,

\(

to

}

func

lazyTest

()

{

print

“Before lazy”

lazy

var

greeting

=

printGreeting

to

“Paul”

print

“After lazy”

print

greeting

}

lazyTest

()

在實踐中,這個特性對於選擇性執行程式碼非常有用:你可以以懶載入的方式準備某個結果,但只有在實際用到該結果時才會執行相關的工作。

屬性包裝器現在可用於函式和閉包的引數

SE-0293 拓展了屬性包裝器,現在它們可以應用於函式和閉包的引數。

對引數應用屬性包裝器並不會改變引數傳遞的不可變屬性,並且你仍然可以透過下劃線來訪問包裝器內封裝的型別。

看下面的程式碼:

func

setScore1

to

score

Int

{

print

“Setting score to

\(

score

}

// 呼叫

setScore1

to

50

setScore1

to

-

50

setScore1

to

500

假如我們希望分數只能處於

0。。。100

的範圍,我們可以編寫一個簡單的屬性包裝器:

@

propertyWrapper

struct

Clamped

<

T

Comparable

>

{

let

wrappedValue

T

init

wrappedValue

T

range

ClosedRange

<

T

>)

{

self

wrappedValue

=

min

max

wrappedValue

range

lowerBound

),

range

upperBound

}

}

然後把上面的函式改寫成:

func

setScore2

(@

Clamped

range

0。

。。

100

to

score

Int

{

print

“Setting score to

\(

score

}

setScore2

to

50

setScore2

to

-

50

setScore2

to

500

用相同的數值呼叫

setScore2()

產生的結果和

setScore()

不同,因為數字會被 clamped 為 50,0,100。

在泛型上下文中使用靜態成員查詢

SE-0299 使得 Swift 可以在泛型函式中執行靜態成員查詢。聽起來有點晦澀,看下面的例子更容易理解。

之前我們在 SwiftUI 中可能寫過這樣的程式碼:

Toggle

“Example”

isOn

constant

true

))

toggleStyle

SwitchToggleStyle

())

現在可以改成這樣:

Toggle

“Example”

isOn

constant

true

))

toggleStyle

(。

switch

與併發相關的新特性

async

await

關鍵字

SE-0296 為 Swift 引入了非同步函式,這使得我們可以像編寫同步程式碼那樣處理非同步程式碼。簡單來說,我們需要兩個步驟:第一步是用新的

async

關鍵字標記函式為非同步,第二步是用

await

關鍵字來呼叫非同步函式。這同 C# 和 JavaScript 是類似的。

當然,有 async/await 機制的程式語言除了 C# 和 JavaScript,還有 Python, F#, Kotlin, Rust, Dart 等,它們有的是實現為關鍵字,有的則是實現為函式或者庫。Swift 的步伐雖然慢了一些,但也不算晚。

在引入

async

await

之前,假設我們要實現一個邏輯:從服務端拉取海量的資料記錄,然後進行計算,最後上傳回伺服器。那麼程式碼可能會長下面這個樣子:

func

fetchWeatherHistory

completion

@

escaping

([

Double

])

->

Void

{

// 省略複雜的網路程式碼,這裡我們直接用100000條記錄來代替

DispatchQueue

global

()。

async

{

let

results

=

1。

。。

100_000

)。

map

{

_

in

Double

random

in

-

10。

。。

30

}

completion

results

}

}

func

calculateAverageTemperature

for

records

Double

],

completion

@

escaping

Double

->

Void

{

// 對陣列求和然後求平均值

DispatchQueue

global

()。

async

{

let

total

=

records

reduce

0

+

let

average

=

total

/

Double

records

count

completion

average

}

}

func

upload

result

Double

completion

@

escaping

String

->

Void

{

// 省略網路程式碼,傳送回伺服器

DispatchQueue

global

()。

async

{

completion

“OK”

}

}

上面的網路程式碼我有意用捏造的資料來代替了,因為網路部分跟我們的主題無關。讀者只需要知道這些函式很耗時,所以我們採用了完成閉包來處理,而不是阻塞的方式。當我們要使用這幾個函式時,我們需要將它們連結起來,給每個函式呼叫都提供完成閉包。程式碼可能如下:

fetchWeatherHistory

{

records

in

calculateAverageTemperature

for

records

{

average

in

upload

result

average

{

response

in

print

“Server response:

\(

response

}

}

}

希望你能看出上面這種方式的問題:

完成閉包可能會被多處呼叫,也可能被忘記呼叫

@escaping (String) -> Void

這樣的引數語法閱讀起來相對困難

隨著每一層完成閉包的增加,呼叫方的程式碼結構會演變成所謂的“末日金字塔”(也常稱為“回撥地獄”)

在 Swift 5。0 引入

Result

之前,完成處理要回傳錯誤也是更加困難

在 Swift 5。5,我們可以透過標記這些函式為非同步,並且返回值而不是依賴完成閉包來解決上面提到的問題,程式碼如下:

func

fetchWeatherHistory

()

async

->

Double

{

1。

。。

100_000

)。

map

{

_

in

Double

random

in

-

10。

。。

30

}

}

func

calculateAverageTemperature

for

records

Double

])

async

->

Double

{

let

total

=

records

reduce

0

+

let

average

=

total

/

Double

records

count

return

average

}

func

upload

result

Double

async

->

String

{

“OK”

}

藉助非同步返回值的語法,我們已經可以移除很多程式碼,而呼叫方的程式碼則更簡潔:

func

processWeather

()

async

{

let

records

=

await

fetchWeatherHistory

()

let

average

=

await

calculateAverageTemperature

for

records

let

response

=

await

upload

result

average

print

“Server response:

\(

response

}

如你所見,所有的閉包和縮排都不見了,這使得程式碼的形式變成所謂的“直行程式碼” —— 除去

await

關鍵字,它們看起來就跟同步程式碼一樣。

對於非同步函式的工作方式,有一些很直接且特定的規則:

同步函式不能直接呼叫非同步函式 —— 這樣做不合理,因此 Swift 會丟擲錯誤

非同步函式可以呼叫其他非同步函式,但同時也可以呼叫同步函式

假設你同時有可供同步和非同步呼叫的函式,Swift 會根據當前上下文優選相應的版本 —— 如果呼叫方當前是非同步的 Swift 就會呼叫非同步的函式,否則它就會呼叫同步的函式。

上面的最後一點很重要,因為這使得庫的開發者可以同時提供同步和非同步的函式,而不必為非同步版本另行命名。

新增的

async/await

能夠完美地配合

try/catch

一起使用,這意味著非同步函式或者構造器可以按需丟擲錯誤。Swift 在這裡施加的唯一限制是關鍵字的順序,呼叫方和函式剛好是相反的。

看下面的程式碼:

enum

UserError

Error

{

case

invalidCount

dataTooLong

}

func

fetchUsers

count

Int

async

throws

->

String

{

if

count

>

3

{

throw

UserError

invalidCount

}

return

Array

([

“Antoni”

“Karamo”

“Tan”

]。

prefix

count

))

}

func

save

users

String

])

async

throws

->

String

{

let

savedUsers

=

users

joined

separator

“,”

if

savedUsers

count

>

32

{

throw

UserError

dataTooLong

}

else

{

return

“Saved

\(

savedUsers

!”

}

}

func

updateUsers

()

async

{

do

{

let

users

=

try

await

fetchUsers

count

3

let

result

=

try

await

save

users

users

print

result

}

catch

{

print

“Oops!”

}

}

我們可以看到,定義可丟擲錯誤的非同步函式的關鍵字順序是

async throws

,而呼叫方則需要寫作

try await

async/await:非同步序列

SE-0298 引入了一個新的協議:

AsyncSequence

,用於遍歷非同步的序列。

AsyncSequence

的使用方式跟

Sequence

幾乎一致,除了你需要讓實現的型別遵守

AsyncSequence

AsyncIterator

,並且

next

方法必須以

async

標記。當迭代推進到序列的末尾時,

next()

要返回

nil

,這和

Sequence

是一樣的。

舉個例子,我們實現一個

DoubleGenerator

,從 1 開始,每次被呼叫時返回前一個數值的兩倍:

struct

DoubleGenerator

AsyncSequence

{

typealias

Element

=

Int

struct

AsyncIterator

AsyncIteratorProtocol

{

var

current

=

1

mutating

func

next

()

async

->

Int

{

defer

{

current

&*=

2

}

if

current

<

0

{

return

nil

}

else

{

return

current

}

}

}

func

makeAsyncIterator

()

->

AsyncIterator

{

AsyncIterator

()

}

}

提示:

如果你把上面程式碼中的

async

都移除,你相當於擁有了一個完成相同事情的

Sequence

—— 所以說兩種序列很相似。當然,相應的協議約束也要改變

一旦我們有了非同步序列,我們就可以用

for await

語句在非同步上下文中遍歷它,像下面這樣:

func

printAllDoubles

()

async

{

for

await

number

in

DoubleGenerator

()

{

print

number

}

}

AsyncSequence

協議還提供了許多常見方法的預設實現,包括

map()

compactMap()

allSatisfy()

等。 例如,我們可以用

contains

來檢查生成器是否包含特定數字:

func

containsExactNumber

()

async

{

let

doubles

=

DoubleGenerator

()

let

match

=

await

doubles

contains

16_777_216

print

match

}

當然,這些方法都需要在非同步上下文中使用。

更高效的只讀屬性

SE-0310 升級了 Swift 的只讀屬性,以支援

async

throws

關鍵字(可以單獨或者同時使用)。

舉個例子,我們建立一個

BundleFile

struct 來載入一個檔案的內容,可能遇到檔案不存在、檔案內容無法讀取、或者內容太大讀取時間很長等等情況。我們可以像下面這樣標記

contents

屬性為

async throws

enum

FileError

Error

{

case

missing

unreadable

}

struct

BundleFile

{

let

filename

String

var

contents

String

{

get

async

throws

{

guard

let

url

=

Bundle

main

url

forResource

filename

withExtension

nil

else

{

throw

FileError

missing

}

do

{

return

try

String

contentsOf

url

}

catch

{

throw

FileError

unreadable

}

}

}

}

因為

contents

同時是非同步和可丟擲錯誤的,我們必須使用

try await

來讀取:

func

printHighScores

()

async

throws

{

let

file

=

BundleFile

filename

“highscores”

try

await

print

file

contents

}

結構化併發

SE-0304 引入了一整套執行、取消以及監控併發的操作,這些是基於

async/await

關鍵字和非同步序列。

出於演示的目的,我們引入下面這兩個函式 —— 一個模擬從特定地點拉取天氣指數的非同步函式,一個獲取斐波那契數列指定位置上的數字的同步函式。

enum

LocationError

Error

{

case

unknown

}

func

getWeatherReadings

for

location

String

async

throws

->

Double

{

switch

location

{

case

“London”

return

1。

。。

100

)。

map

{

_

in

Double

random

in

6。

。。

26

}

case

“Rome”

return

1。

。。

100

)。

map

{

_

in

Double

random

in

10。

。。

32

}

case

“San Francisco”

return

1。

。。

100

)。

map

{

_

in

Double

random

in

12。

。。

20

}

default

throw

LocationError

unknown

}

}

func

fibonacci

of

number

Int

->

Int

{

var

first

=

0

var

second

=

1

for

_

in

0。

。<

number

{

let

previous

=

first

first

=

second

second

=

previous

+

first

}

return

first

}

結構化併發主要的變化是引入了

Task

TaskGroup

這兩個新的型別,它們可以讓我們以獨立或者協同的方式執行併發操作。

最簡單的使用形式是建立一個

Task

物件,然後把希望執行的非同步操作傳給它。這會立即啟動一個非同步執行緒來執行,而我們可以用

await

來等待結果完成。

比如,我們可以在後臺執行緒多次呼叫

fibonacci(of:)

,以取代序列中的前50個數字:

func

printFibonacciSequence

()

async

{

let

task1

=

Task

{

()

->

Int

in

var

numbers

=

Int

]()

for

i

in

0。

。<

50

{

let

result

=

fibonacci

of

i

numbers

append

result

}

return

numbers

}

let

result1

=

await

task1

value

print

“斐波那契數列中的前50個數字:

\(

result1

}

如你所見,我顯式編寫了

Task { () -> [Int] in }

以便 Swift 知道任務會返回。我們也可以利用型別推斷和

map

函式,寫出下面這樣更極簡的程式碼:

let

task1

=

Task

{

0。

。<

50

)。

map

fibonacci

}

再次強調,任務一經建立就會開始執行。

printFibonacciSequence()

會在其所處的執行緒上繼續往下執行,同時斐波那契數列的數字也被計算。

提示:

我們的任務操作是一個非逃逸閉包,因為任務是即時執行。因此當你在一個類或者結構體中使用

Task

時,你並不需要使用

self

來訪問屬性或者方法。

當我們要讀取完成的數字時,

await task1。value

能夠確保

printFibonacciSequence()

暫定住,直到任務完成輸出就緒。假設你並不需要關心任務的返回結果 —— 只需要任務啟動,任其自行結束 —— 那麼你並不需要儲存任務。

對於會丟擲未捕獲錯誤的任務操作,讀取任務的

value

屬性也會自動丟擲這些錯誤。因此,我們可以程式碼中同時編寫多個任務,等待它們全部完成:

func

runMultipleCalculations

()

async

throws

{

let

task1

=

Task

{

0。

。<

50

)。

map

fibonacci

}

let

task2

=

Task

{

try

await

getWeatherReadings

for

“Rome”

}

let

result1

=

await

task1

value

let

result2

=

try

await

task2

value

print

“斐波那契數列中的前50個數字:

\(

result1

print

“羅馬的天氣指數:

\(

result2

}

Swift 為我們提供了

high

default

low

以及

background

幾種內建的任務優先順序,可以透過在建立任務時由構造器

Task(priority: 。high)

來定製。如果僅針對蘋果的平臺,還可以使用我們更為熟悉的

userInitiated

代替

hight

,使用

utility

代替

low

,但

userInteractive

是保留給主執行緒使用的。

除了執行操作,

Task

還為我們提供了一些靜態方法以便控制程式碼的執行:

呼叫

Task。sleep()

會導致當前任務休眠指定納秒的時間,所以,要指定 1秒,引數要提供 1_000_000_000

呼叫

Task。checkCancellation()

會檢查是否有人透過

cancel()

方法取消了任務,如果有則會丟擲一個

CancellationError

呼叫

Task。yield()

會掛起當前任務一段時間,以便讓出時間片給其他正在等待的任務。這個 API 是很重要,尤其是在你在一個迴圈中執行開銷非常昂貴的工作時。

我們可以用下面的程式碼來理解上面幾種操作:

func

cancelSleepingTask

()

async

{

let

task

=

Task

{

()

->

String

in

print

“Starting”

await

Task

sleep

1_000_000_000

try

Task

checkCancellation

()

return

“Done”

}

// 任務已經開始,但我們趁它處於休眠時把它取消掉

task

cancel

()

do

{

let

result

=

try

await

task

value

print

“結果:

\(

result

}

catch

{

print

“任務已經被取消”

}

}

在上面的程式碼中,

Task。checkCancellation()

會發現任務已經被取消,於是立即丟擲

CancellationError

,但這個錯誤並不會馬上來到我們面前,知道我們嘗試讀取

task。value

提示:

我們可以使用

task。result

來獲取一個

Result

的值,它包含了任務成功或者失敗的值。比如,上面的程式碼我們會獲得

Result

。這就不要求

try

語句了,因為我們需要自行處理成功和失敗的情況。

為了避免篇幅過長,Swift 5。5 的新特性介紹將拆分為兩篇文章來完成。

我的公眾號 這裡有Swift及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~

Swift 5.5新特性(上)

標簽: Swift  非同步  async  程式碼  函式