Swift程式設計[第3單元] 網路程式基礎

#21 TextField 語法問題

上一節我們將範例程式3-5c導入 Xcode 中,只需刪除第一行與最後一行並修改 ContentView 的主體(body),就能順利執行。不過仔細檢查的話,會發現有一個警告,如下圖,提示我們有個物件TextField,我們用的語法已經過時(deprecated, 棄用):

原來的程式碼如下:
TextField("Search for artworks", text: $inputText) {
搜尋字串 = inputText
作品列表 = nil
作品圖集 = []
}

Xcode 只有英文版,而且介面相當複雜,因此一旦有問題,並不容易修正,不過幸好 Xcode 也提供自動修正的建議,我們按下 "Fix" 會自動幫我們修正為建議語法。

程式碼會自動修正為:
TextField("Search for artworks", text: $inputText, onCommit:  {
搜尋字串 = inputText
作品列表 = nil
作品圖集 = []
})

注意函式的括號()包含到最後,匿名函式 { } 變成是在 () 裡面,這其實就是將匿名函式當作 TextField() 第三個參數 -- onCommit 的參數值!

沒錯,匿名函式也可以當作參數,我們用過的 URLSession.shared.dataTask() 或是 .onTapGesture 後面接一個匿名函式,其實都是參數,只是 Swift 語法很有彈性,當作參數的匿名函式可以寫在 () 裡面,也可以放在外面。

第1單元第3課介紹過函式,Swift 在呼叫函式時有幾個規則,歸納如下:

1. 參數名稱與參數值原則上都必須寫出來,中間以冒號隔開
2. 函式宣告時,若參數名稱前面加上底線(_)空格,則該名稱可省略
3. 函式最多只能有一個參數名稱可省略
4. 函式宣告時,若參數值有預設值,則呼叫函式時該參數可省略
5. 可省略的參數(已有預設值者)可多可少,數量不限

因此,在上面修正後的函式呼叫 TextField(),用了三個參數:
  • 第一個參數名稱可省略,參數值為我們給的字串 "Search for artworks"

  • 第二個參數名稱為 text,參數值為 $inputText,變數名稱前面加上錢號 $,稱為 Call-by-reference,這種參數是要從函式帶值出來,而不是一般參數(稱為 Call-by-value)是帶值進去給函式用

  • 第三個參數名稱為 onCommit,參數值為一個匿名函式

如果函式宣告時,只有一個參數是匿名函式,這時匿名函式可寫在 () 外面,並省略參數名稱。不過因為 TextField() 有兩個參數是匿名函式,一個名稱為 onEditingChanged,另一個為 onCommit,為了避免混淆,就不能寫在 () 外面。

不過,即使修正後的語法,也即將要過時失效,未來會改用 .onSubmit 修飾語來取代 onCommit 參數,較符合 SwiftUI 儘量用視圖修飾語(View Modifier)的程式風格。

等新版 Swift Playgrounds 4.0 (最晚年底前出來),能夠支援 .onSubmit 語法,我們再來修正。

標示為過時(Deprecated)的語法,通常會有一段緩衝期,在緩衝期內的程式還是可以執行,並不會馬上失效。


註解
  1. 嚴格說起來,TextField() 是物件初始化,而不是第1單元第3課的函式呼叫,不過事實上,物件初始化就是呼叫物件裡面的 init() 初始化函式,所以「物件初始化」與「函式呼叫」實質意義是相同的。
  2. Swift 同一個物件大多會有若干種參數組合,也就是不同的用法,有些複雜物件甚至多達十幾種用法,不過通常筆者只會挑一兩種最常用的當作範例,因為先熟悉一種用法,再去學其他會比較容易觸類旁通。
  3. 作為參數的匿名函式,通常放在末尾當作最後一個參數,所以官方術語稱為 trailing closure。trailing 是尾隨的、尾跡的意思。
  4. Swift Playgrounds 4.0 新語法 TextField().onSubmit 用法如下:
    TextField("Search for artworks", text: $inputText)
    .onSubmit {
    搜尋字串 = inputText
    作品列表 = nil
    作品圖集 = []
    }
#22 第6課 AsyncImage (Swift Playgrounds 4.0)

苦等半年的 Swift Playgrounds 4.0 終於發佈,新版更新了非常多功能,特別是在非同步(async)的相關指令,非同步模式是網路程式、GUI互動、並行運算的基礎,而並行運算又是大數據、人工智慧、3D繪圖等高速運算所必須,因此顯得特別重要,而且新語法能簡化程式碼的撰寫,本單元後半部分(第6~10課)會盡量採用新的指令語法,須配合使用 Swift Playgrounds 4.0。

下載 Swift Playgrounds 4.0 之前,記得要將作業系統升級到 iPadOS 15.2 或 MacOS 12.1,Swift Playgrounds下載入口: https://www.apple.com/tw/swift/playgrounds/


第2課我們學習從網路抓圖,範例3-2a是一個非常基本的網路抓圖程式:
// 3-2a 讀取網路圖片(URLSession)
// Created by Heman, 2021/09/02
import PlaygroundSupport
import SwiftUI

let 網址 = "https://picsum.photos/720/1280"

struct 抓圖: View {
@State var 下載圖片: UIImage?
var body: some View {
if 下載圖片 == nil {
ProgressView()
.onAppear {
guard let myURL = URL(string: 網址) else { return }
URLSession.shared.dataTask(with: myURL) { 回傳資料, 回應碼, 錯誤碼 in
if let 圖檔 = UIImage(data: 回傳資料!) {
print(回應碼 ?? "No response")
下載圖片 = 圖檔
} else {
print(錯誤碼 ?? "No error")
}
}.resume()
}
} else {
Image(uiImage: 下載圖片!)
.resizable()
.scaledToFit()
}
}
}

PlaygroundPage.current.setLiveView(抓圖())

原來的抓圖程式,主要是用 URLSession.shared.dataTask() 這個通用物件,大部分網路程式都可以用它來寫,是本單元前半部(第1課到第5課)的核心物件。

Swift Playgrounds 4.0 增加了一個 AsyncImage 物件,讓我們寫程式下載網路圖片變得更容易,將上面範例3-2a改用 AsyncImage 後,程式簡化如下,減少了將近一半的程式碼,不可思議吧:
// 3-6a 讀取網路圖片(AsyncImage)
// Modified (based on 3-2a) by Heman, 2021/12/16
import PlaygroundSupport
import SwiftUI

let 網址 = "https://picsum.photos/720/1280"

struct 抓圖: View {
var body: some View {
AsyncImage(url: URL(string: 網址)) { 下載圖片 in
下載圖片
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
}
}

PlaygroundPage.current.setLiveView(抓圖())

AsyncImage 也是一個 View 物件類型,跟其他 SwiftUI 的 View 物件語法類似,所以用起來很方便。物件的初始化需要一個 URL 物件實例當作參數,下載取得的圖片會傳入後面接的匿名函式,在取得下載圖片之前,可指定另一個 View 當作緩衝(在此為 ProgressView()),這個緩衝的參數稱為 placeholder,就是臨時用來佔位置的東西(圖片或視圖)。

因為 AsyncImage 是非同步的運作模式,所以運作過程跟原來範例 3-2a 完全相同,會先顯示 ProgressView() 視圖,當下載完成(取得 Response)後,才會進入後接的匿名函式,顯示已下載的圖片。

注意原來3-2a裡用來更新畫面的狀態變數(@State var)已不需要,表示 AsyncImage 能自己掌握狀態更新畫面。

執行結果如下:
#23 阿貓阿狗(AsyncImage phase)

本單元第4課曾經提過網路程式的並行(Concurrent)特性,當使用 dataTask() 連續發出多個網路請求(Request)時,各個連線作業並非依序執行(一個連線作業完成再執行下一個),而是多個作業同時並行,先下載完的就先顯示。

改用 AsyncImage 之後,同樣會有並行特性,以下範例我們連續用8次AsyncImage抓8張網路貓狗圖片,觀察它們顯示的順序,就可看到程式背後的並行特性。
// 3-6b AsyncImage phase
// Created by Heman, 2021/12/18
import PlaygroundSupport
import SwiftUI

struct 抓圖: View {
var 網址: String
var body: some View {
AsyncImage(url: URL(string: 網址)) { 狀態 in
if let 下載圖片 = 狀態.image {
下載圖片
.resizable()
.scaledToFit()
} else if 狀態.error != nil {
Image(systemName: "xmark.icloud.fill")
.scaleEffect(2)
.foregroundColor(.red)
} else {
ProgressView()
}
}
.frame(width: 200, height: 200)
}
}

let 阿貓 = "https://thecatapi.com/api/images/get?format=src&type=jpg"
let 阿狗 = "https://thedogapi.com/api/images/get?format=src&type=jpg"

struct 網路上的阿貓阿狗: View {
var body: some View {
HStack {
VStack {
抓圖(網址: 阿貓)
抓圖(網址: 阿貓)
抓圖(網址: "https://thecatapi.com/")
抓圖(網址: 阿貓)
}
VStack {
抓圖(網址: 阿狗)
抓圖(網址: "https://thedogapi.com/")
抓圖(網址: 阿狗)
抓圖(網址: 阿狗)
}
}
}
}

PlaygroundPage.current.setLiveView(網路上的阿貓阿狗())

此範例顯示視圖「網路上的阿貓阿狗」,連續使用8次「抓圖」,分兩列4 x 2顯示,
第一列是連到提供貓貓圖片的API網站,另一列是狗狗網站。其中兩張圖片網址故意寫錯,會顯示紅色的系統圖示。



程式中「抓圖」的功能與上一節相同,但是寫法有兩個差異:一是增加一個物件屬性「網址」,讓我們可以傳不同網址進去;二是使用 AsyncImage 另外一種語法,詳述如下。

上一課註解曾提過,Swift 同一個物件通常允許多種用法,用法之間以參數的差異來做區別。

本節 AsyncImage 用法與上一節不同的地方,在於少掉 placeholder 參數,這時候帶入匿名函式的參數類型,會與上一節有所不同。

範例3-6a
AsyncImage(url: URL(string: 網址)) { 下載圖片 in
下載圖片
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}

在上一節的 AsyncImage 有 placeholder 參數,這時候傳入匿名函式的參數是Image類型的「下載圖片」,在取得圖片之前,則顯示 placeholder 裡面的 ProgressView()。

3-6a 這段程式碼有個缺失,就是當「網址」錯誤,或是網路斷線等原因,無法正確取得圖片時,畫面會一直顯示ProgressView(),也就是不停地轉圈,讓使用者空等。

過去我們用 URLSession.shared.dataTask() 抓圖時,對錯誤狀況必須在程式中一一判別加以應對,對程式設計師來說,「例外處理」是一件重要但很費心的工作。

所幸 AsyncImage 另一種用法可以協助處理例外情況(注意沒有 placeholder參數):

範例3-6b
AsyncImage(url: URL(string: 網址)) { 狀態 in
if let 下載圖片 = 狀態.image {
下載圖片
.resizable()
.scaledToFit()
} else if 狀態.error != nil {
Image(systemName: "xmark.icloud.fill")
.scaleEffect(2)
.foregroundColor(.red)
} else {
ProgressView()
}
}

這時候,傳入匿名函式的參數不是Image類型,而是Image類型之外再包一層屬性,官方稱為AsyncImagePhase類型,所以文件中,傳入匿名函式的參數名稱取為 phase (階段),但在本課取名為「狀態」。

參數「狀態(或 phase)」有三種列舉(enum)值:
1. empty: 尚未取得 Response 時
2. success: 成功取得圖片(包含Image物件)
3. failure: 連線失敗或未取得圖片(包含Error物件)

所以當「狀態 == .success」,則「狀態.image」就是獲取的圖片,如果「狀態 == .error」則「狀態.error」就包含錯誤訊息(Error 物件)。

因此,上述3-6b程式碼直接測試「狀態.image」是否有值,如果有,就指定給「下載圖片」並顯示出來,如果沒有值(狀態.image == nil),就再看看「狀態.error」有沒有值,若有就代表連線出錯,我們就顯示一個紅色的系統圖示"xmark.icloud.fill",若沒有,就表示「狀態 == .empty」,還未收到 Response,就顯示 ProgressView()。

當「狀態」的值有變化時,例如從 .empty 變成 .success,或從 .empty 變成 .failure,AsyncImage 會重新計算,畫面就會從 ProgressView() 變成 Image()。

所以這樣的語法一次解決了連線過程的三種狀況,顯然比上一節的語法更好用。

為了測試「狀態 == .error」的情況,我們在「網路上的阿貓阿狗」視圖中,故意傳入兩個錯誤的網址,注意這兩個錯誤網址的位置仍然會先顯示 ProgressView(),等到回應內容無法轉成圖片時,才會顯示紅色的系統圖示,請仔細觀察下面執行過程的影片。

執行過程影片如下:


註解
  1. 注意 AsyncImage 是 View 類型,但不是 Image 類型,在第二單元曾經提過,View 是大類別,下面包含了 Text, Image, VStack...等小類別,故 AsyncImage 和 Image 是不同的物件類型。
  2. 所以 AsyncImage 物件可以用 View 修飾語,例如 .frame(),但不能直接用 Image 修飾語,例如 .resizable(), .scaleToFit()。
  3. 當下載圖片成功之後,包在 AsyncImage 裡面的才是 Image 類型;Image 與 UIImage 兩者類型也不相同,之前用 URLSession.shared.dataTask() 下載的圖片資料可轉換為 UIImage 類型,屬於 UIKit 物件庫,而 Image 與 AsyncImage 都屬於 SwiftUI 物件庫。
  4. 事實上,Image 類型是在 UIImage 外再包一層屬性,而 UIImage 則是在更基礎的 CGImage 外再包一層。
  5. 本節範例程式 AsyncImage 段落修改自官網文件,是 AsyncImage 物件的標準句型,值得背下來。
#24 第7課 async/await

在上一課提過Swift Playgrounds 4.0 對非同步的程式設計增加了更多支援,其中最重要的是增加一組新指令 async/await,本課就開始來學習這兩個新指令。

還記得本單元第1課介紹過 URLSession 底下共有5種工作:

1. dataTask() -- 一般較短暫的資料下載
2. downloadTask() -- 較長時間的檔案下載
3. uploadTask() -- 上傳資料
4. streamTask() -- 音樂、語音或影片的串流資料下載
5. webSocketTask() -- 雙向(上傳、下載)資料交換

以上都具備非同步的特性,其實 async/await 就是將網路程式的非同步特性一般化,讓一般(非網路程式)也能使用非同步的運作模式,這有什麼好處?簡單地說,就是讓任何運算工作(需要大量計算或是等待I/O者)能夠並行作業,以善用多核心CPU的優勢,加快執行速度。

對於 URLSession 底下的這些連線工作,也新增改用 async/await 的函式,包括以下4種:

1. data() -- 對應 dataTask()
2. download() -- 對應 downloadTask()
3. upload() -- 對應 uploadTask()
4. bytes() -- 指定資料範圍(如用來續傳)

以我們最常用的 URLSession.shared.dataTask() 為例,最早在本單元第1課範例3-1c出現過,原來的用法如下:

範例3-1c
URLSession.shared.dataTask(with: myURL) { data, response, error in
print("回傳資料:", data ?? "No data")
print("回應代碼:", response ?? "No response")
print("錯誤代碼:", error ?? "No error")
回傳資料 = data
}.resume()

當收到網路回應後,dataTask() 會帶入3個參數(data, response, error)到後面的匿名函式內執行。

相對應的新用法如下:

範例3-7a
Task {
let (data, response) = try await URLSession.shared.data(from: myURL)
print("回傳資料:", data ?? "No data")
print("回應代碼:", response ?? "No response")
回傳資料 = data
}

主要的差異,是URLSession.shared.data() 的語法比較接近一般函式(不再用匿名函式),會回傳兩個值,分別指定給 data 跟 response,不過須等收到網路回應(Response),回傳值才有內容,因此前面必須加上 await 指令,表示與一般函式呼叫不同,須等待非同步事件(即獲得網路回應)的發生。

取得回傳值後,再繼續往下執行原來在匿名函式內的程式碼,這幾行程式放在 Task { } 大括號中,形成一個非同步的工作單元。作業系統會適度安排非同步的工作單元,例如移到其他CPU核心,等待網路回應(或 await 所等待的非同步事件)。

所以新的用法相當於將 dataTask() 拆成兩部分,data() 是非同步傳輸資料,匿名函式則移到上一層Task { } 中管理。Task 物件類型主要用來溝通作業系統的工作排程。

以下就是用 URLSession.shared.data() 改寫3-1c的完整範例:
// 3-7a URLSession.shared.data()
// Modified (based on 3-1c) by Heman, 2021/12/16
import Foundation

let 網址 = "https://drive.google.com/file/d/13g2sCz-zXK4uCesWeY4pPflowb06qdus/view?usp=sharing"
var 回傳資料: Data?
let 計時開始 = Date()
let myURL = URL(string: 網址)!

var 時間差 = Date().timeIntervalSince(計時開始)
Task {
let (data, response) = try await URLSession.shared.data(from: myURL)
print("回傳資料:", data ?? "No data")
print("回應代碼:", response ?? "No response")
回傳資料 = data
}
while 回傳資料 == nil && 時間差 < 5.0 {
時間差 = Date().timeIntervalSince(計時開始)
}
print("花費時間(秒):", 時間差)


這是一個文字模式的程式,所以執行時要開啟主控台,以顯示輸出的文字內容,執行結果如下圖,連線時間約0.9秒,與原來3-1c用 dataTask()差不多。


註解

1. 嚴格說起來 async/await 是程式語言 Swift 5.5 的新指令,而 Swift Playgrounds 從4.0 版開始支援 Swift 5.5。
#25 非同步質因數分解

上一節提到 async/await 是一般化的非同步運算指令,除了可用於網路程式之外,也適合一般計算量大或是需要I/O(Input/Output, 如讀取檔案)的情境。

在過去第1, 2單元課程中,計算量最大的應該是「質因數分解」,在第1單元第5課的習題要求寫一個質因數分解的程式,正好可用來練習 async/await,並可比較非同步與一般模式有何差異。

我們先看程式碼以及執行的結果,再來解釋 async/await 的神奇用法:
// 3-7b 非同步質因數分解(async/await)
// Revised (based on 1-5e) by Heman, 2021/12/23
import Foundation

func 是質數嗎(_ n: Int) -> Bool {
if n < 2 { // 只判別大於1的正整數
return false
} else if n == 2 {
return true
}
for i in 2...(n-1) {
if (n % i) == 0 {
return false
}
}
return true
}

func 因數分解(_ n: Int) -> [Int] {
var 因數: [Int] = [1] // 1與n是基本的因數
if n <= 0 { // 排除零與負數
return []
} else if n == 1 {
return [1]
}
for i in 2...(n-1) {
if n % i == 0 {
因數 = 因數 + [i]
}
}
因數 = 因數 + [n]
return 因數
}

func 質因數分解(_ n: Int) -> [Int] {
let 因數 = 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}

func 非同步質因數分解(_ n: Int) async -> [Int] {
let 因數 = 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}

let 某數 = 20211223
let 數量 = 4
var 已完成 = 0
var 時間差: Double = 0.0

let 非同步計時開始 = Date()
print("[非同步]質因數分解 -- 開始時間:\(非同步計時開始)")
for i in 某數 ..< (某數 + 數量) {
Task {
let 質因數 = await 非同步質因數分解(i)
時間差 = Date().timeIntervalSince(非同步計時開始)
print("非同步 \(i): \(質因數) 時間差 \(時間差)")
已完成 = 已完成 + 1
if 已完成 == 數量 {
print("[非同步質因數分解]共花費時間(秒):\(時間差)")
}
}
}

let 計時開始 = Date()
print("[同步]質因數分解 -- 開始時間:\(計時開始)")
for i in 某數 ..< (某數 + 數量) {
let 質因數 = 質因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
print("同步 \(i): \(質因數) 時間差 \(時間差)")
}
時間差 = Date().timeIntervalSince(計時開始)
print("[同步質因數分解]共花費時間(秒):\(時間差)")

print("程式結束:\(Date())")
// Last modified by Heman, 2021/12/24
// end of program.

在這個範例程式,我們想求連續4個整數20211223, 20211224, 20211225, 20211226的質因數分解。程式中包含4個函式:

1. 是質數嗎() -- 參考第1單元第5課
2. 因數分解() -- 小於n且能整除n的,就是因數
3. 質因數分解() -- 一般版本
4. 非同步質因數分解() -- 非同步版本

質因數分解的邏輯很簡單,以8位整數 20211223 為例,先對20211223做因數分解,再來判斷每個因數是否為質數,如下圖:

程式最後,我們先做非同步的版本,再執行一般同步的版本。執行結果輸出如下:
[非同步]質因數分解 -- 開始時間:2021-12-24 10:24:42 +0000
[同步]質因數分解 -- 開始時間:2021-12-24 10:24:42 +0000
同步 20211223: [613, 32971] 時間差 32.73160099983215
非同步 20211224: [2, 11, 241, 953] 時間差 42.57900905609131
非同步 20211225: [3, 5, 31, 8693] 時間差 42.66217398643494
非同步 20211223: [613, 32971] 時間差 42.67644906044006
非同步 20211226: [2, 7, 206237] 時間差 42.81832206249237
[非同步質因數分解]共花費時間(秒):42.81832206249237
同步 20211224: [2, 11, 241, 953] 時間差 52.82666492462158
同步 20211225: [3, 5, 31, 8693] 時間差 67.27944195270538
同步 20211226: [2, 7, 206237] 時間差 81.8584920167923
[同步質因數分解]共花費時間(秒):81.85870492458344
程式結束:2021-12-24 10:26:04 +0000


從結果可以看出,同樣對4個8位數整數做質因數分解,非同步只要42秒,一般的同步模式則需要81秒!

而且還可觀察到,非同步模式會利用多個CPU核心,所以幾乎在同一秒內運算出4組質因數,運算結果順序不定,20211224最先算完。

同步模式則是依序進行,先算出20211223,再算下一組,所以只會用到一個CPU核心。

以上就可以明顯觀察到同步與非同步的運作差異,那麼程式碼又有何不同呢?

一般的質因數分解程式碼如下:
func 質因數分解(_ n: Int) -> [Int] {
let 因數 = 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}

因為質因數分解會花比較長CPU時間,因此我們可將此函式改成非同步版本,其實只要在函式宣告多加一個async指令,其他程式碼都不變,簡單吧:
func 非同步質因數分解(_ n: Int) async -> [Int] {
let 因數 = 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}

注意async要加在函式名稱後面(參數之後、返回值類型之前),語法類似英文的副詞(放在動詞之後)。

在範例程式中,我們對連續4個整數做質因數分解,因為這4個整數的質因數分解工作彼此並不相關,所以可利用非同步運算的特性,將這4個工作分散到不同的CPU核心並行運算,程式碼如下,用到上一節學過的 Task物件及await 指令:
for i in 某數 ..< (某數 + 計數) {
Task {
let 質因數 = await 非同步質因數分解(i)
....
}
}

注意程式執行順序,要先做非同步運算,再做同步運算,順序不能調換,因為非同步運算時,會將運算工作(Task)移到背景,由別的CPU核心執行,目前的CPU核心則繼續往下執行。

這一點從輸出結果也可看到,非同步開始時間與同步開始時間幾乎同時。

如果非同步運算放在最後,會遇到跟第1課範例3-1b同樣的問題,還沒完全算出結果,程式就提早結束了。

註解
  1. 如本節範例程式,async/await 經常搭配使用,async 用來定義非同步運算的函式,await 用在呼叫非同步函式。
  2. 從語法來看,async 類似英文的副詞,放在動詞(函式)之後,而 await 類似助動詞,放在動詞(函式)之前。
感謝大大分享,好文先推再說!!
#26 Task 物件:工作優先權(priority)

在上一節的程式執行結果,有細心的讀者就會發現,為什麼有一個同步工作會最先跑出結果?
[非同步]質因數分解 -- 開始時間:2021-12-24 10:24:42 +0000
[同步]質因數分解 -- 開始時間:2021-12-24 10:24:42 +0000
同步 20211223: [613, 32971] 時間差 32.73160099983215
非同步 20211224: [2, 11, 241, 953] 時間差 42.57900905609131
非同步 20211225: [3, 5, 31, 8693] 時間差 42.66217398643494
非同步 20211223: [613, 32971] 時間差 42.67644906044006
非同步 20211226: [2, 7, 206237] 時間差 42.81832206249237
[非同步質因數分解]共花費時間(秒):42.81832206249237
同步 20211224: [2, 11, 241, 953] 時間差 52.82666492462158
同步 20211225: [3, 5, 31, 8693] 時間差 67.27944195270538
同步 20211226: [2, 7, 206237] 時間差 81.8584920167923
[同步質因數分解]共花費時間(秒):81.85870492458344
程式結束:2021-12-24 10:26:04 +0000

再仔細看,對同一個整數做質因數分解,如20211225,非同步要花42秒,同步只要花15秒,非同步反而慢很多,為什麼會這樣?

分析這個執行結果,會有助於理解非同步的運作原理,因此值得我們進一步討論。這個執行結果,依照時間差排列,大致如下圖所示:

之前提過,非同步的工作會移到其他CPU核心運算,精確講起來,其實是移到其他「支線」(專業術語稱為「執行緒 "thread"」)執行,在作業系統中,可能會有多個支線(在背景執行),但只有一個是主線(在前景執行),主線與支線有不同的優先權(priority queue),通常主線優先權高於支線,所以同樣工作,放在主線執行,會比支線快。

所謂「在前景執行」,就是正在與使用者互動的App工作,與使用者互動的工作會直接影響用戶體驗,所以都安排在主線,優先權較高。

上圖顯示範例3-7b執行時,一開始有5個工作並行作業,其中4個非同步工作被排到支線(挪至背景),以較低的優先權運行;而第一個同步工作「質因數分解(20211223)」,則放在主線工作,優先權較高,所以最先計算出結果。

Task 物件可以透過參數調整支線(背景)工作的優先權,參數名稱為 priority,包括以下列舉值(由高而低):
.high
.userInitiated
.medium
.utility
.background
.low

主線的優先權會由作業系統動態調整,大致在 .medium 附近,而支線預設權限為較低的 .background。我們若想要調整支線的優先權,只要改一行程式:

Task(priority: .medium) {
...
}

將範例程式3-7b調整 priority 後再執行一次,輸出結果如下:
[非同步]質因數分解 -- 開始時間:2021-12-27 00:41:41 +0000
[同步]質因數分解 -- 開始時間:2021-12-27 00:41:41 +0000
非同步 20211224: [2, 11, 241, 953] 時間差 31.481786966323853
非同步 20211223: [613, 32971] 時間差 31.798568964004517
非同步 20211226: [2, 7, 206237] 時間差 31.895814895629883
同步 20211223: [613, 32971] 時間差 36.57404804229736
非同步 20211225: [3, 5, 31, 8693] 時間差 40.05804097652435
[非同步質因數分解]共花費時間(秒):40.05804097652435
同步 20211224: [2, 11, 241, 953] 時間差 51.304108023643494
同步 20211225: [3, 5, 31, 8693] 時間差 65.69555997848511
同步 20211226: [2, 7, 206237] 時間差 80.55143296718597
[同步質因數分解]共花費時間(秒):80.55162405967712
程式結束:2021-12-27 00:43:01 +0000

可以看出優先權調整後,輸出結果的順序有所改變,主線的第一個同步工作不再優先,而是3個非同步工作最先算出結果,但有得有失,最終全部4個非同步工作共花40秒,與上一節相較並未明顯變快。


註解
  1. 在Swift async/await 的設計中,一條支線(thread)對應一個CPU核心,每條支線可以承載多個非同步工作。參考原廠WWDC影片Swift concurrency: Behind the scenes
  2. 使用非同步運作,雖然可以善用多核CPU的優勢,加快運算速度,但並非沒有代價,主要的代價是作業系統必須頻繁在多個工作排程(主線、支線)中切換,會需要額外的CPU與記憶體資源。
  3. 觀察主線的「同步工作3」與「同步工作4」,因為作業系統已不再需要切換到支線,因此單一工作變得較快(非同步42秒,同步15秒),這顯示切換非同步工作的額外負擔相當巨大。
  4. App程式設定的工作優先權是相對式的,高優先權只是「權重」較高、佔用CPU的時間比例較多而已,不會獨佔CPU,低優先權的工作仍享有一定比例的CPU時間。
  5. 並非所有工作都可以套用非同步模式,通常是彼此之間沒有因果關聯的工作,才能用非同步模式並行作業。運算量不大的工作也不適合用非同步模式,因為作業系統切換工作的代價,可能就高於工作本身。
  6. 上述輸出結果均在macOS (Mac mini 2014)測試,iPadOS 的優先權與工作排程,似乎與 macOS 稍有不同,在筆者的 iPad 所執行結果,即使調整Task(priority: .medium),同步工作仍快於非同步,因此程式會過早結束,顯然iPad前景工作的優先權高於 .medium。
    [非同步]質因數分解 -- 開始時間:2021-12-27 00:05:57 +0000
    [同步]質因數分解 -- 開始時間:2021-12-27 00:05:57 +0000
    同步 20211223: [613, 32971] 時間差 16.51647698879242
    同步 20211224: [2, 11, 241, 953] 時間差 33.01124703884125
    非同步 20211224: [2, 11, 241, 953] 時間差 35.009182929992676
    非同步 20211223: [613, 32971] 時間差 35.022364020347595
    同步 20211225: [3, 5, 31, 8693] 時間差 49.51678705215454
    同步 20211226: [2, 7, 206237] 時間差 66.21503496170044
    [同步質因數分解]共花費時間(秒):66.21520507335663
    程式結束:2021-12-27 00:07:03 +0000
    iPad執行結果,所有同步工作均已完成,但非同步尚未全部完成,程式過早結束。

  7. 所以千萬不要誤解,以為「非同步工作一定比同步工作快」,如果不清楚非同步並行作業的基本觀念,很容易誤用 async/await,導致非同步化的並行作業反而更慢。
第8課 錯誤處理(Error Handling)

「錯誤處理」是程式設計必備的技能之一,我們最早曾經在第2單元第7課「傑森解碼器」中用過,就是 do-catch, try 等指令。由於非同步程式常用到外部資源,難免遇到意外狀況,必然需要錯誤處理,所以若想要進一步善用 async/await,就必須先熟悉 Swift 錯誤處理的語法。

錯誤處理和除錯(debug)意義不同,除錯(debug)是要排除程式的語法錯誤或邏輯錯誤,例如標點符號用錯、條件句的true/false子句用顛倒、邊界條件沒有檢查...等等,修正程式無法執行或執行結果與預期不符的問題。

錯誤處理則是對所有已知的「例外狀況」進行處置,以本單元網路程式為例,必須連到外部網站,才能抓取圖片或JSON資料,「正常狀況」下都能順利執行,但如果是網路斷線,或甚至過若干時日,外部網站已關閉,程式要如何正常執行?

舉例而言,如果將本機的WiFi關閉,再執行範例程式3-6a非同步抓圖,會顯示 ProgressView()不停轉圈,而沒有任何提示;範例程式3-2a更慘,會直接被作業系統打斷(閃退)。通常我們稱這樣的程式很脆弱(fragile),一有意外就死掉,或是不夠強健(robust),經不起風吹雨打。

好的App一定是強健的程式,能應付各種狀況,要寫出強健的程式,就必須做好錯誤處理。

我們用上一課的「質因數分解」來寫個範例。根據數學上的定義,「質數」必然是大於1的正整數,「因數」則正負整數均可,但不可為零(任何數都不可除以零),零也不能因數分解。

為了簡單起見,以下範例程式只做「正整數」的因數分解,所以如果遇到零或負整數,則當作已知的錯誤來處理。
// 3-8a 錯誤處理(Error Handling) -- 正整數因數分解
// Revised (based on 3-7b) by Heman, 2022/01/09
import Foundation

enum 因數分解錯誤: Error {
case 負整數
case 等於零
}

func 因數分解(_ n: Int) throws -> [Int] {
if n < 0 { // 排除零與負數
throw 因數分解錯誤.負整數
} else if n == 0 {
throw 因數分解錯誤.等於零
} else if n == 1 {
return [1]
}
var 因數: [Int] = [1] // 1與n是基本的因數
for i in 2...(n-1) {
if n % i == 0 {
因數 = 因數 + [i]
}
}
因數 = 因數 + [n]
return 因數
}

let 某數 = [-20220109, 0, 20220109, 20220911]
var 時間差: Double = 0.0

let 計時開始 = Date()
print("因數分解 -- 開始時間:\(計時開始)")
for i in 某數 {
do {
let 因數 = try 因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
print("\(i): \(因數) 時間差 \(時間差)")
} catch 因數分解錯誤.負整數 {
print("錯誤:暫不支援負整數的因數分解(\(i))")
} catch 因數分解錯誤.等於零 {
print("錯誤:零無法因數分解(\(i))")
} catch {
print("錯誤:其他原因(\(i))")
}
}
時間差 = Date().timeIntervalSince(計時開始)
print("[因數分解]共花費時間(秒):\(時間差)")
print("程式結束:\(Date())")

從以上實際的程式碼可以看出來,錯誤處理就是對可能發生的例外狀況,加以偵測判斷並撰寫應對的程式碼。

在語法上,「錯誤處理」分為「偵測」與「處理」兩個部分,「偵測」通常以函式為主體,由函式偵測並回報已知的錯誤或例外狀況,「處理」則是根據回報的狀況加以應對。

負責偵測錯誤的函式,在宣告時要在參數後面加上 throws 指令(注意加上 's',是第三人稱單數用的動詞),然後在偵測到的錯誤狀況下,用 throw (祈使句,不加 's')丟出錯誤狀況的代碼:
func 因數分解(_ n: Int) throws -> [Int] {
if n < 0 { // 排除零與負數
throw 因數分解錯誤.負整數
} else if n == 0 {
throw 因數分解錯誤.等於零
} else if n == 1 {
return [1]
}
...
}

錯誤狀況的代碼可以自行定義,只要符合 Error 規範的類型即可,這個 Error 規範是個空規範(empty protocol),沒有任何強制要求,用法和 View 規範類似,用 struct 或 enum 來定義都可以。
enum 因數分解錯誤: Error {
case 負整數
case 等於零
}

我們在第2單元提過,規範(protocol, 又稱協議、協定)在 Swift 語言裡面,是在一般資料類型更上一層的分類,所以上面我們定義了一個符合 Error 規範的列舉(enum)類型,稱為「因數分解錯誤」,只有兩個值,代表兩種例外狀況。前面提過,要取用列舉值時,只要用句號 . 即可,如「因數分解錯誤.等於零」。

然後在函式「因數分解」的宣告中,加入 throws 指令,代表這個函式有可能發生例外狀況,函式裡面分別偵測各種已知的例外狀況,再用 throw 丟出相對應的錯誤代碼。

當程式偵測到例外狀況,執行到 throw 時,函式會直接返回,不會在函式內繼續往下執行。

錯誤狀況的應對處理,是呼叫函式者(caller)的責任,語法上通常用 do-try-catch 的句型:
do {
let 因數 = try 因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
print("\(i): \(因數) 時間差 \(時間差)")
} catch 因數分解錯誤.負整數 {
print("錯誤:暫不支援負整數的因數分解(\(i))")
} catch 因數分解錯誤.等於零 {
print("錯誤:零無法因數分解(\(i))")
} catch {
print("錯誤:其他原因(\(i))")
}

try 其實是與 throw 成對的,就像 async/await 成對一樣,凡是宣告時加上 throws 的函式,就必須在呼叫該函式前加上 try:
func 因數分解(_ n: Int) throws -> [Int] { ... }
let 因數 = try 因數分解(i)

所以我們前面用過的 JSONDecoder() 或是 URLSession.shared.data(),現在終於知道為什麼要加上 try 了,顯然他們都是會丟出(throw)錯誤狀況的。另一方面,也知道了他們並不保證能成功取得返回值。

當函式沒有正常的返回值,而是回報錯誤時,就必須根據錯誤代碼加以處置,這就是 do-catch 的用法,若捕獲(catch)到某個或某些狀況,則如何如何...(應對內容沒有特別限制,程式設計師可自由發揮),程式碼就寫在catch後續的 { } 段落中。
do {
let 因數 = try 因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
print("\(i): \(因數) 時間差 \(時間差)")
} catch 因數分解錯誤.負整數 { ... }

若上面 try 因數分解(i) 真的回報錯誤,則下面緊接的兩行程式碼(時間差=、print())就不再繼續執行,而是跳到相對應的 catch 子句,然後結束 do-catch 句型。所以 do-catch 和 switch-case 類似,程式碼會選擇性的執行。

注意最後一個 catch 沒有接任何錯誤代碼,表示所有其他錯誤狀況都在這裡處置。當我們寫:
do { 段落A } catch { 段落B }

就表示任何錯誤狀況均由段落B處理。

以下是執行結果,注意前兩個錯誤狀況並不會列印出時間差:
因數分解 -- 開始時間:2022-01-09 06:56:06 +0000
錯誤:暫不支援負整數的因數分解(-20220109)
錯誤:零無法因數分解(0)
20220109: [1, 7, 13, 91, 222199, 1555393, 2888587, 20220109] 時間差 14.630668997764587
20220911: [1, 97, 208463, 20220911] 時間差 29.198646068572998
[因數分解]共花費時間(秒):29.198896050453186
程式結束:2022-01-09 06:56:35 +0000


註解
  1. 所謂「錯誤處理(error handling)」,並不是要「解決問題」或「修正錯誤」,而是根據App的需要,對每個設想的意外狀況加以應對。例如,遇到外部網站當掉或檔案被移除的情況,程式不可能解決這類問題,但可以提示使用者可能原因,及時提示可增進使用者體驗。
  2. 這點與除錯(debug)不同,除錯是必須找出問題(bug)的根源(root cause),並加以解決或設法避開(workaround)。
  3. 所以對軟體而言,error 與 bug 的意義不同,雖然中文都可能翻譯為「錯誤」。
#28 非同步 + 錯誤處理

上一節我們學到兩組新語法來處理錯誤或例外狀況,包括 throw/try 與 do-catch,再加上一般化非同步指令 async/await,經常會一起出現,這三組指令基本句型整理如下表:

表3-8 非同步+錯誤處理指令
指令 函式宣告(declaration) 呼叫函式(caller)
async/await func 名稱(參數) async -> 回傳類型 { ... } let x = await 名稱(參數)
throw/try func 名稱(參數) throws -> 回傳類型 { ... } let x = try 名稱(參數)
do-catch - do {
let x = try 名稱(參數)
...
} catch a, b, c {
...
} catch {
...
}
async/await + throw/try func 名稱(參數) async throws -> 回傳類型 { ... } let x = try await 名稱(參數)
Task-do-try-await-catch - Task {
do {
let x = try await 名稱(參數)
...
} catch a, b, c {
...
} catch {
...
}
}

在非同步程式中,通常也須處理可能發生的錯誤狀況,這時就會全部用到上面7個關鍵字: Task, async/await, throw/try, do-catch,看起來好像很複雜,但實際寫起來,其實邏輯非常直觀。

下面範例程式,我們將上一節的「因數分解」函式改為非同步模式,只需增加三個關鍵字:async, await 以及 Task,比起其他程式語言,Swift 語法顯得特別乾淨俐落,很接近自然語言,這也是Swift易學易用的原因之一。
// 3-8b 錯誤處理(Error Handling) -- 非同步因數分解
// Revised by Heman, 2022/01/11
import Foundation

enum 因數分解錯誤: Error {
case 負整數
case 等於零
}

func 因數分解(_ n: Int) async throws -> [Int] {
if n < 0 { // 排除零與負數
throw 因數分解錯誤.負整數
} else if n == 0 {
throw 因數分解錯誤.等於零
} else if n == 1 {
return [1]
}
var 因數: [Int] = [1] // 1與n是基本的因數
for i in 2...(n-1) {
if n % i == 0 {
因數 = 因數 + [i]
}
}
因數 = 因數 + [n]
return 因數
}

let 某數 = [-20220109, 0, 20220109, 20220911]
var 時間差: Double = 0.0

let 計時開始 = Date()
print("因數分解 -- 開始時間:\(計時開始)")
for i in 某數 {
Task {
do {
let 因數 = try await 因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
print("\(i): \(因數) 時間差 \(時間差)")
} catch 因數分解錯誤.負整數, 因數分解錯誤.等於零 {
print("錯誤:僅限正整數的因數分解(\(i))")
} catch {
print("錯誤:其他原因(\(i))")
}
}
}
時間差 = Date().timeIntervalSince(計時開始)
print("[因數分解]共花費時間(秒):\(時間差)")
print("程式結束:\(Date())")

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

注意程式最後兩行,用來控制 Swift Playgrounds 主控台不要自動結束,改為手動停止,這樣才能顯示非同步的運算結果,而不必像之前,故意在後面加額外的運算拖延時間。如下圖所示。

執行結果如下,兩個8位正整數的因數分解都在16.8秒左右結束,比上一節29.2秒才全部結束,快了將近一倍:
因數分解 -- 開始時間:2022-01-11 08:34:56 +0000
錯誤:僅限正整數的因數分解(-20220109)
錯誤:僅限正整數的因數分解(0)
[因數分解]共花費時間(秒):0.0076819658279418945
程式結束:2022-01-11 08:34:56 +0000
20220911: [1, 97, 208463, 20220911] 時間差 16.82110297679901
20220109: [1, 7, 13, 91, 222199, 1555393, 2888587, 20220109] 時間差 16.859354972839355
#29 演算法的威力

在我們前面的範例程式,若仔細看「因數分解」程式碼,用的是最直觀方法,當n大於2時,已知1與n必然是因數,所以用for迴圈從2一路找到n-1,看能否整除n,如果能整除,就加入「因數」陣列中:
for i in 2...(n-1) {
if n % i == 0 {
因數 = 因數 + [i]
}
}

這樣算法雖然正確,但實在太慢,分解8位數就需花十幾秒,那分解100位數(密碼學應用需100位數以上)豈不是算到宇宙滅亡!有沒有更快的方法呢?

當然有,而且還不只一種。如果反覆觀察,我們可以發現「因數分解」有以下幾個特徵,可用來「加速」程式:
  • 因數都是成對出現的,如果 n / i 能整除的話(也就是 n % i == 0),除了除數 i 是因數,(n / i)的商也是因數。唯一的例外就是如果 n 是平方數,那 n 的平方根就是唯一不成對的因數。

  • 不需要從2找到(n-1)整個範圍,只要找2到n的平方根即可。

舉例來說,觀察100的因數分解,100平方根是10,所以只要找 2...10能夠整除100的,再加上配對的商數即可,也就是:

(2, 50), (4, 25), (5, 20), (10)

經過重新排序,再加上 1, 100 也是因數,最後因數分解的結果就是:

[1, 2, 4, 5, 10, 20, 25, 50, 100]

這樣一來,迴圈計算的次數從原本98次(2...99)大幅降低到9次(2...10)。對於8位數20220109來說,迴圈次數會從2千萬次降低到只需4千多次,相當於將8位數縮短成4位數的計算時間!

以下我們就根據這樣的運算方法(或稱為「演算法」),重新改寫3-8b因數分解的程式:
// 3-8c 加速因數分解
// Revised by Heman, 2022/01/11
import Foundation

enum 因數分解錯誤: Error {
case 負整數
case 等於零
}

func 因數分解(_ n: Int) async throws -> [Int] {
if n < 0 { // 排除零與負數
throw 因數分解錯誤.負整數
} else if n == 0 {
throw 因數分解錯誤.等於零
} else if n == 1 {
return [1]
}
var 因數: [Int] = [1] // 1與n是基本的因數
let 近平方根 = Int(sqrt(Double(n)))
for i in 2...近平方根 {
if n % i == 0 {
因數 = 因數 + [i]
let 商數 = Int(n / i)
if 商數 != 近平方根 {
因數 = 因數 + [商數]
}
}
}
因數 = 因數 + [n]
return 因數.sorted()
}

let 某數 = [-20220109, 0, 20220109, 20220911, 123456789012345, 135791357913579]
var 時間差: Double = 0.0

let 計時開始 = Date()
print("因數分解 -- 開始時間:\(計時開始)")
for i in 某數 {
Task {
do {
let 因數 = try await 因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
print("\(i): \(因數) 時間差 \(時間差)")
} catch 因數分解錯誤.負整數, 因數分解錯誤.等於零 {
print("錯誤:僅限正整數的因數分解(\(i))")
} catch {
print("錯誤:其他原因(\(i))")
}
}
}
時間差 = Date().timeIntervalSince(計時開始)
print("[因數分解]共花費時間(秒):\(時間差)")
print("程式結束:\(Date())")

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

在程式中,我們用了一個新的函式,sqrt() 用來求平方根,參數必須是實數,所以用Double(n)將整數n轉換為實數,算出平方根(實數)之後,再用 Int() 轉回整數:
let 近平方根 = Int(sqrt(Double(n)))

所以「近平方根」就是最接近(小於或等於)n平方根的整數。

接下來 for 迴圈只要從2算到「近平方根」,將可整除n的除數與商數加入到「因數」陣列中:
for i in 2...近平方根 {
if n % i == 0 {
因數 = 因數 + [i]
let 商數 = Int(n / i)
if 商數 != 近平方根 {
因數 = 因數 + [商數]
}
}
}

最後將 n 本身也加入「因數」陣列中,並且將整個陣列排序後回傳。排序用的是物件方法 .sorted(),可對陣列內容加以排序後,回傳「排序後的陣列」(所以用被動語態sorted):
因數 = 因數 + [n]
return 因數.sorted()

最後執行結果,對3個8位數20220109, 20220911, 20223009竟然只要0.02秒多,比起上一節3-8b用16秒足足快了約800倍,比最初版本3-8a用29.2秒近1500倍!

為了檢驗新的算法,我們還特地增加一個平方數 20223009(4497的平方),以及兩個15位數的整數,全部算出來也不過花8.9秒多而已。
因數分解 -- 開始時間:2022-01-13 00:05:16 +0000
錯誤:僅限正整數的因數分解(-20220109)
錯誤:僅限正整數的因數分解(0)
[因數分解]共花費時間(秒):0.007884979248046875
程式結束:2022-01-13 00:05:16 +0000
20223009: [1, 3, 9, 1499, 4497, 13491, 2247001, 6741003, 20223009] 時間差 0.021432995796203613
20220109: [1, 7, 13, 91, 222199, 1555393, 2888587, 20220109] 時間差 0.021718978881835938
20220911: [1, 97, 208463, 20220911] 時間差 0.0246199369430542
123456789012345: [1, 3, 5, 15, 283, 849, 1415, 3851, 4245, 11553, 19255, 57765,
1089833, 3269499, 5449165, 7552031, 16347495, 22656093, 37760155, 113280465,
2137224773, 6411674319, 10686123865, 29082871381, 32058371595, 87248614143,
145414356905, 436243070715, 8230452600823, 24691357802469,
41152263004115, 123456789012345] 時間差 8.547168970108032
135791357913579: [1, 3, 31, 37, 93, 111, 367, 1101, 1147, 1369, 3441, 4107, 11377, 13579,
34131, 40737, 42439, 127317, 420949, 502423, 1262847, 1507269, 2906161, 8718483,
15575113, 46725339, 90090991, 107527957, 270272973, 322583871,
1066561087, 3199683261, 3333366667, 3978534409, 10000100001, 11935603227,
33063393697, 39462760219, 99190181091, 118388280657, 123334566679, 370003700037,
1223345566789, 1460122128103, 3670036700367, 4380366384309,
45263785971193, 135791357913579] 時間差 8.970889925956726



只是改變一個計算方式,程式碼也沒幾行,就能加快這麼多,可見演算法的威力!

註解
  1. 在電腦科學(computer science)中,研究解決問題的方法與程序稱為「演算法(algorithm)」,是資訊科系必修課程,除了研究更快更好的方法之外,也研究計算困難的數學難題。
  2. 質因數分解與第一單元提過的費波那契數,都只用到「整數」,數學上研究整數特性的理論稱為「整數論」(或簡稱「數論」),聽起來簡單,小學生都能懂,讓人誤以為用電腦一定可以解決所有數論問題,沒什麼好研究的,其實不然。
  3. 2018年上海有位名叫「談方琳」的14歲小女孩,解開了一個數論的千年難題,就是研究「費波那契數」和「貝祖數(與最大公因數有關)」的關係,成為公認的數學天才,堪稱當代的高斯,就是最好例子。
  4. 本節的因數分解其實還不夠快,尚有很多改善空間,值得深入研究。不過演算法並非本單元課程範圍,重點還是放在「非同步模式」與「錯誤處理」。
關閉廣告
文章分享
評分
評分
複製連結

今日熱門文章 網友點擊推薦!