• 9

有人對 Swift/SwiftUI 程式設計有興趣嗎?

#29 struct 結構化資料類型 v.s. 函式

前面我們提到 struct 定義的資料類型,指定值的語法與函式呼叫很類似:
商湯 = 商王(名號: "大乙湯", 即位: -1558, 在位: 12)

那麼,struct 類型與函式有何差別呢?我們用實體世界的事物來比喻看看。

函式有點像「自動販賣機」,裡面怎麼運作的不用管,我們只要知道投錢進去加上選擇,就能得到一瓶想要的飲料。函式的參數就像投入的錢與選擇,飲料就像函式回傳值。

struct 定義的資料類型,則比較像有結構的「模具」,呼叫一次這個類型,就會產出一個「物件」,例如玩具或手機,有其結構及屬性(如可更換顏色或配件),產出的是一個可變更屬性的物件,而不是一個單純的東西(值)。

在下面的範例中,我們對struct資料類型「商王」(模具)連續使用30次,就會產出30個變數(物件),每個變數都有不同的屬性,然後將這30個變數的屬性分別列印出來。

// 1-7b struct: 商王年表
// Created by Heman, 2021/07/25
// Reference: https://zh.wikipedia.org/wiki/商朝

struct 商王 {
var 名號: String
var 即位: Int //西元年
var 在位: Int //年
}
let 商湯 = 商王(名號: "大乙湯", 即位: -1558, 在位: 12)
let 商王世系: [商王] = [
商王(名號: "大乙湯", 即位: -1558, 在位: 12),
商王(名號: "外丙勝", 即位: -1546, 在位: 2),
商王(名號: "仲壬庸", 即位: -1544, 在位: 4),
商王(名號: "大甲至", 即位: -1540, 在位: 12),
商王(名號: "沃丁絢", 即位: -1528, 在位: 19),
商王(名號: "大庚辯", 即位: -1509, 在位: 5),
商王(名號: "小甲高", 即位: -1504, 在位: 17),
商王(名號: "大戊密", 即位: -1487, 在位: 75),
商王(名號: "雍己伷", 即位: -1412, 在位: 12),
商王(名號: "中丁莊", 即位: -1400, 在位: 9),
商王(名號: "外壬發", 即位: -1391, 在位: 10),
商王(名號: "河亶甲整", 即位: -1381, 在位: 9),
商王(名號: "祖乙滕", 即位: -1372, 在位: 19),
商王(名號: "祖辛旦", 即位: -1353, 在位: 14),
商王(名號: "沃甲踰", 即位: -1339, 在位: 5),
商王(名號: "祖丁新", 即位: -1334, 在位: 9),
商王(名號: "南庚更", 即位: -1325, 在位: 6),
商王(名號: "陽甲和", 即位: -1319, 在位: 4),
商王(名號: "盤庚旬", 即位: -1315, 在位: 28),
商王(名號: "小辛頌", 即位: -1287, 在位: 3),
商王(名號: "小乙斂", 即位: -1284, 在位: 10),
商王(名號: "武丁昭", 即位: -1274, 在位: 59),
商王(名號: "祖庚曜", 即位: -1215, 在位: 11),
商王(名號: "祖甲載", 即位: -1204, 在位: 33),
商王(名號: "廩辛先", 即位: -1171, 在位: 4),
商王(名號: "庚丁囂", 即位: -1167, 在位: 8),
商王(名號: "武乙瞿", 即位: -1159, 在位: 35),
商王(名號: "文丁托", 即位: -1124, 在位: 13),
商王(名號: "帝乙羨", 即位: -1111, 在位: 9),
商王(名號: "帝辛紂", 即位: -1102, 在位: 52)]

for 帝王 in 商王世系 {
print(帝王.名號, 帝王.即位, 帝王.在位)
}

執行結果如下圖。


從這個輸出結果,我們可以明顯看出來,「商王世系」其實就是一個30筆資料的表格,表格的每一筆資料就是一個變數,變數每一個屬性,就是表格的欄位,所以這個表格有3個欄位: 商王名號、即位年代、在位時間。

許多的資料表格集合在一起,就稱為「資料庫」,類似這樣可組成表格的資料,就稱為「結構化資料」。結構化資料最適合電腦處理,因為不管多少筆資料,用一個迴圈都能搞定。

在網路時代,結構化資料無所不在,從學校成績單、社團會員名單、網路購物商品列表到預約疫苗登記表....,太多了,整個網際網路幾乎就是一個無比龐大的資料庫!

所以說,struct 太重要了!

註解
什麼是「模具」?看過夜市賣的車輪餅或章魚燒嗎?做車輪餅的鐵製容器就是模具,將材料、餡料倒進去,就會「複製」產出相同外觀的車輪餅。

鴻海公司代工的蘋果手機,其金屬外殼也是透過模具做的,鴻海最早的本業就是做精密模具。我們現在使用的金屬或塑膠製品,大都是用模具大量製作的。

模具(或模子)的歷史非常久,前面提過的商王寶藏「三節提梁卣」就是用模具做的。古代的模具,用木頭做的稱為「模」,陶土做的稱為「範」,「模範」的意思就是從模具引申出來的。
xattacker

用struct 有幾個很重要的特性:一是它不能被繼承,二是它是value copy,三是預設提供memberwise initializer,一般教學文件都會拿它跟class 出來對比差異

2021-08-14 23:25
#30 空字串與空陣列

在第6課範例程式1-6a中,我們在寫「整數分解()」函式中,用了一個宣告句:
var 數列: [Int] = []

定義變數「數列」是一個整數陣列的型態 [Int],後面為什麼還要指定 = [] 呢?這個值 [] 稱為空陣列,類似的做法,就像:
var 名號: String = ""

指定字串變數一個 "" 空字串的值。

為什麼會有空陣列和空字串?這牽涉到一個非常重要的觀念:變數或常數的「初始化」。以下面程式為例:
// 1-7c 空陣列與空字串
// Created by Heman, 2021/07/26

var 名號: String
var 數列: [Int]

名號 = 名號 + "大乙湯"
數列 = 數列 + [0, 1, 1, 2, 3]

print(名號)
print(數列)

我們知道陣列或字串可以做「加法」,如果我們直接將沒有給過值的變數做加法,會產生導致App閃退的錯誤,還好Swift Playgrounds 能幫我們檢查出來。


當我們用 var/let 宣告變數或常數時,第一次指定值的動作,就稱為給變數或常數「初始化」。變數與常數最大區別,就是變數可以在初始化之後,再指定(變更)其他的值,而常數初始化之後,就不能再變更。

如果我們在變數或常數初始化之前,就使用它們來運算,就會導致整個程式無法繼續執行的錯誤,非常嚴重。

所以空陣列和空字串的目的,就是提供一個沒有實質內容的初始值,就像數學的「空集合」一樣,讓初始化之後的變數與常數可以開始使用在運算式中。

所以上述範例程式只要稍微修改,就能正常執行了:
// 1-7d 空陣列與空字串
// Created by Heman, 2021/07/26

var 名號: String = ""
var 數列: [Int] = []

名號 = 名號 + "大乙湯"
數列 = 數列 + [0, 1, 1, 2, 3]

print(名號)
print(數列)



為什麼要先初始化為空陣列或空字串?為什麼不直接給有內容的值?這是因為大部分的程式,資料未必是寫在程式裡面,而是由外部的檔案、資料庫、甚至網路傳進來的,所以先初始化為空陣列或空字串,再跟傳進來的資料結合。在第2單元我們就會遇到這樣的情況。

另外還有一個小地方要注意:
var 名號: String = ""    //Good.

空字串的宣告初始化可以省略寫成:
var 名號 = ""    //Good.

但空陣列的初始化宣告,不能寫為:
var 數列 = []    //No Good!

因為光寫一個空陣列,陣列裡面的資料類型還是無法判定,可能是字串陣列,可能是整數陣列,可能是其他類型陣列,這違背「Swift必須在變數或常數宣告的時候,就確定資料類型」,所以必須明確宣告空陣列的類型:
var 數列: [Int] = []    //Good.

當然,如果初始值不是空陣列,而是有實際內容的,就可以省略寫出:
var 數列 = [0, 1]    //Good.

很清楚可以判定是整數陣列,就可以省略資料類型。
#31 第8課 物件、物件、物件!

前一課我們提到所有結構化的資料都可以利用 struct 來定義,將實體世界的人、事、物映射到虛擬世界中,例如學校學生在校務系統中,可能包括姓名、學號、出生年月日、科系、年級、修業成績....等等,用 struct 來設計就非常容易。這裡所說的映射就像這樣,將實體能夠「數位化」的資料抽離出來。

一個物品能夠數位化的資料越多,描述物品的屬性就越多,當然這個虛擬化的結果就越真實。

但是 struct 的本事還不只這些。除了在 struct 的大括號 { } 裡面定義結構化資料,也就是變數或常數之外,還可以定義「函式」!

為什麼要在結構化資料裡面定義函式呢?我們先來看一個範例程式。
// 1-8a struct: 商王年表v3
// Created by Heman, 2021/07/26
// Reference: https://zh.wikipedia.org/wiki/商朝

struct 商王 {
var 名號: String
var 即位: Int //西元年
var 在位: Int //年

func 自我介紹() -> String {
let 西元年 = 即位 < 0 ? "西元前\(abs(即位))年" : "西元\(即位)年"
let 介紹詞 = "我是商王「\(名號)」,\(西元年)即位,在位\(在位)年。"
return 介紹詞
}
}

let 商湯 = 商王(名號: "大乙湯", 即位: -1558, 在位: 12)
print(商湯.名號, 商湯.即位, 商湯.在位)
print(商湯.自我介紹())

在宣告「商王」的資料類型中,我們增加了一個函式,名稱為「自我介紹」,函式裡面沿用第5課1-5a範例程式裡面的「西元年」運算式:
        let 西元年 = 即位 < 0 ? "西元前\(abs(即位))年" : "西元\(即位)年"

然後組成一個「介紹詞」:
        let 介紹詞 = "我是商王「\(名號)」,\(西元年)即位,在位\(在位)年。"

目的就是將這個「介紹詞」字串當函式回傳值。

我們比較兩行列印的結果:
print(商湯.名號, 商湯.即位, 商湯.在位)    //大乙湯 -1558 12
print(商湯.自我介紹()) //我是商王「大乙湯」,西元前1558年即位,在位12年。

後者結果才是我們要的。列印個別的屬性,如「商湯.名號」,只是為了確認資料的正確性。而呼叫「商王.自我介紹()」,有點像讓「商王自我介紹」的意思。注意「商王.自我介紹()」,呼叫函式跟使用屬性一樣,用句號 . 連接,而即使不用給參數,代表函式的 () 還是要寫出來。


所以在 struct { } 裡面定義常數或變數,是在描述這個物品的「屬性」,而定義函式,則是描述物品的「功能」或「動作」,所以如此一來,虛擬化的物品不只是靜態資料,還包括原來物品相對應的功能或我們想設計的動作。

像這樣包含結構化資料以及功能函式的資料類型,我們就正式稱為「物件(Object)」,所謂「物件化導向」的程式設計方法,基本上就是盡量將資料轉化成這樣的「物件」類型。

事實上,Swift 就是一種物件化導向的程式語言,裡面所有的資料類型,甚至包括我們學過的 Int, String, Float, Double, Bool, Array [ ], ....事實上都是「物件」類型。

定義在物件類型 struct { } 裡面的常數或變數,稱為物件「屬性(Property)」,而函式則稱為物件「方法(Method)」。中文「方法」這個詞意比較不容易體會,其實就是物件的「功能」或「動作」。

而使用物件類型定義出來的常數或變數,稱為「物件實例(Instance)」,如上面程式的「商湯」,就是「商王」物件類型的實例,物件實例其實就像模具複製出來的物品。

在Swift中,有兩種定義「結構型物件」的方法,另一種稱為 class,是過去40年物件導向的傳統做法,而本課介紹的 struct 是相對比較新的設計方式,也是第2單元 SwiftUI 的基礎,我們先以 struct 為主,class 等到未來需要時再介紹。

前面曾經比喻 struct 資料類型就像「模具」,可以複製結構化的物品(變數或常數)。增加了函式之後,就如同給物品增加活動零件,讓靜態的樂高積木可以活動起來一樣。所以用這樣的模具複製出來的物品,都是動態可以活動的,當然會比較好玩啦。
#32 商王App

一般來說,要把事情做好,有兩種哲學,一種是「第一次就把它做對」,好的開始是成功的一半,就像建造橋梁,一旦打下基礎,後面想改設計就很難了,所以一定要謀定而後動,一次就做到位。

另一種比較適合程式設計的哲學是「逐步改善」。軟體的東西很難一次就做到位,LINE, Youtube 這些一開始的功能都相當簡單,經過多年的改善才逐漸成熟,而且未來也還會繼續改進,沒有極限。

想像設計一個App程式,可瀏覽30個商王角色,當使用者點選某位商王「武丁」時,虛擬角色「武丁」就跳出來介紹他的時代,講述他與妻子「婦好」征戰四方的故事,以及從各地收集的寶藏,最後再和使用者來一場樸克牌比大小,可以贏得他的寶物,讓使用者在遊戲中學習歷史。

設計App通常從這樣天馬行空的「概念」開始,先設計粗略的原型,包括物件的資料格式以及簡略的互動流程,然後再逐步改善。

目前我們先只做個「自我介紹」功能,列印一段文字,等以後我們能夠處理語音、影片、圖形等多媒體資料時,就能夠加入更多的動作與功能,逐步改善便能完成一個App。

// 1-8b struct: 商王年表v3
// Created by Heman, 2021/07/26
// Reference: https://zh.wikipedia.org/wiki/商朝

struct 商王 {
var 名號: String
var 即位: Int //西元年
var 在位: Int //年

func 自我介紹() -> String {
let 西元年 = 即位 < 0 ? "西元前\(abs(即位))年" : "西元\(即位)年"
let 介紹詞 = "我是商王「\(名號)」,\(西元年)即位,在位\(在位)年。"
return 介紹詞
}
}

let 商王世系: [商王] = [
商王(名號: "大乙湯", 即位: -1558, 在位: 12),
商王(名號: "外丙勝", 即位: -1546, 在位: 2),
商王(名號: "仲壬庸", 即位: -1544, 在位: 4),
商王(名號: "大甲至", 即位: -1540, 在位: 12),
商王(名號: "沃丁絢", 即位: -1528, 在位: 19),
商王(名號: "大庚辯", 即位: -1509, 在位: 5),
商王(名號: "小甲高", 即位: -1504, 在位: 17),
商王(名號: "大戊密", 即位: -1487, 在位: 75),
商王(名號: "雍己伷", 即位: -1412, 在位: 12),
商王(名號: "中丁莊", 即位: -1400, 在位: 9),
商王(名號: "外壬發", 即位: -1391, 在位: 10),
商王(名號: "河亶甲整", 即位: -1381, 在位: 9),
商王(名號: "祖乙滕", 即位: -1372, 在位: 19),
商王(名號: "祖辛旦", 即位: -1353, 在位: 14),
商王(名號: "沃甲踰", 即位: -1339, 在位: 5),
商王(名號: "祖丁新", 即位: -1334, 在位: 9),
商王(名號: "南庚更", 即位: -1325, 在位: 6),
商王(名號: "陽甲和", 即位: -1319, 在位: 4),
商王(名號: "盤庚旬", 即位: -1315, 在位: 28),
商王(名號: "小辛頌", 即位: -1287, 在位: 3),
商王(名號: "小乙斂", 即位: -1284, 在位: 10),
商王(名號: "武丁昭", 即位: -1274, 在位: 59),
商王(名號: "祖庚曜", 即位: -1215, 在位: 11),
商王(名號: "祖甲載", 即位: -1204, 在位: 33),
商王(名號: "廩辛先", 即位: -1171, 在位: 4),
商王(名號: "庚丁囂", 即位: -1167, 在位: 8),
商王(名號: "武乙瞿", 即位: -1159, 在位: 35),
商王(名號: "文丁托", 即位: -1124, 在位: 13),
商王(名號: "帝乙羨", 即位: -1111, 在位: 9),
商王(名號: "帝辛紂", 即位: -1102, 在位: 52)]

for 帝王 in 商王世系 {
print(帝王.自我介紹())
}

在這個範例程式中,幾個重要的「物件」概念,包括物件類型(x1)、物件屬性(x3)、物件方法(x1)以及物件實例(x30),分別標示如下圖。
#33 物件的初始化

前面第3課提過,「函式」的作用是當作一般化的工具,就像自動販賣機,不需瞭解內部怎麼運作,只要知道怎麼用就行。「物件類型」也有類似的作用,而且物件類型的作用更多樣,更豐富。

例如,我們可以將1-8a「商王年表」物件類型加以一般化,成為「歷代帝王年表」,讓App內容不拘限於商朝,能學習更多歷史。我們也利用這個機會,學習物件另一個重要觀念:物件初始化。

// 1-8c struct: 歷代帝王年表
// Revised by Heman, 2021/07/28

struct 帝王 {
var 朝代: String
var 名號: String
var 即位: Int //西元年
var 在位: Int //年

init(_ 朝: String = "", 名: String = "", 即: Int = 0, 在: Int = 0) {
朝代 = 朝
名號 = 名
即位 = 即
在位 = 在
}

func 自我介紹() -> String {
let 西元年 = 即位 < 0 ? "西元前\(abs(即位))年" : "西元\(即位)年"
let 介紹詞 = "我是\(朝代)代帝王「\(名號)」,\(西元年)即位,在位\(在位)年。"
return 介紹詞
}
}

let 乞丐 = 帝王()
let 商鞅 = 帝王("商")
let 商湯 = 帝王("商", 名: "大乙湯", 即: -1558, 在: 12)

for 角色 in [乞丐, 商鞅, 商湯] {
print(角色.自我介紹())
}


在資料的結構部分,我們暫只增加一個屬性:朝代,將商王年表推廣為歷代帝王年表。

另外增加了一個非常特別的「函式」稱為 init(),這是 "initializer" (初始化函式)的簡寫。這個函式特別的地方,在於「不用 func 宣告,而且不要任何回傳值」。

初始化函式 init() 最重要的功能,就是必須把所有的物件屬性「初始化」,也就是指定初始值。

所以這裡的 init() 需要4個參數:朝、名、即、在,分別對應4個屬性:朝代、名號、即位、在位。而這4個參數我們都給了「預設值」,前面提過,如果函式參數有預設值,那麼在呼叫時,這個參數是可以省略的。

所以我們可以這樣來使用物件類型:
let 乞丐 = 帝王()

不需輸入任何參數,全部用預設值來初始化物件的屬性。也不用特別寫出 init(),每次使用物件類型就會自動呼叫初始化函式。

而且再注意到 init() 第一個參數名稱前面有加底線,這代表呼叫時,這個參數名稱可省略,所以只要給參數值就可以:
let 商鞅 = 帝王("商")

如果要帶入所有的參數值,最完整的呼叫方式就類似之前的做法,但是改用 init() 的參數名稱,而不是物件的屬性名稱,同時第一個參數名稱可省略:
let 商湯 = 帝王("商", 名: "大乙湯", 即: -1558, 在: 12)

所以,如果呼叫物件類型時,提供完整的參數,物件屬性就依照參數值來初始化,如果沒有提供參數,就依照預設值初始化。這樣一來,物件的功能更一般化,使用上也更多彈性了。



要特別注意的是,如果用 struct 定義物件類型時,提供了init()初始化函式,在複製(產出)物件實例時,就一定會呼叫這個初始化函式,不能再用以前(沒有init()時)的方式。

一個物件類型可以寫多個 init() 初始化函式,但是彼此的參數名稱、個數或類型必須有差異,呼叫時才知道找哪個 init()。
#34 第9課 埃及聖書體文字

西元前1274年左右,與商王武丁同一時期的埃及法老拉美西斯二世(Ramesses II)遠征西臺王國,大獲全勝,年輕的拉美西斯二世非常得意,特地將此事以「聖書體」文字刻在五座神殿牆上,包括拉美西斯神殿(Ramesseum)、卡納克(Karnak)、路克索(Luxor)、阿拜多斯(Abydos)以及阿布辛貝(Abu Simbel)神殿。

Battle of Kadesh https://www.britannica.com/biography/Ramses-II-king-of-Egypt

很可惜「聖書體」後來不再使用,許多神殿也湮沒在黃沙之下,這般豐功偉業沈寂了一千多年。

直到西元1799年,剛好是1899年發現甲骨文的100年前,法國皇帝拿破崙遠征埃及,一隊士兵在地中海沿岸的小村莊羅塞塔(Rosetta)發現牆上一塊石板,記載三種不同文字,希臘文、埃及世俗體以及聖書體,因為紀錄同一件事情,經過多年的努力,終於破解聖書體文字的含義。

聖書體以前稱為埃及象形文字,但其實只有一小部分是「表意」,大部分是「表音」。例如:
𓇳 太陽(表意)
𓈋 山(表意)
𓂀 荷魯斯之眼(表意)
𓇳(Re)𓄟(me)𓋴(s)𓋴(s) Ramesses 拉美西斯(表音)
𓅲(Tu)𓏏(t)𓋹(ankh)𓇋(i)𓏠(m)𓈖(n) Tutankhamun圖坦卡門(表音)

現在我們用Swift寫個小程式就能列印所有聖書體字母,共1,072個,領略一下古埃及文明的魅力。
// 1-9a Egyptian Hieroglyphs
// Revised by Heman, 2021/07/28
// https://zh.wikipedia.org/wiki/聖書體
import Foundation

let 聖書體編碼 = 0x13000...0x1342F
for i in 聖書體編碼 {
let 字元 = UnicodeScalar(i)!
print("\(字元)", terminator: " ")
}

這段程式只有5句,但其中4句是新用法,分別說明如下。



① import Foundation

懂得使用「物件」對程式設計幫助很大,因為Apple原廠提供了大量物件給我們使用,這些官方物件按照功能,組合成一個個「物件倉庫」,官方術語稱為 "Framework" (框架)或 "Package" (套件),在本課程我們通稱為「物件庫」。

當要使用某個「物件庫」裡面的物件時,先要下一個指令:import (匯入),例如,我們會用到一個物件類型 UnicodeScalar,是在 Foundation 物件庫裡面,因此要下 import Foundation 指令。

② let 聖書體編碼 = 0x13000...0x1342F

根據參考資料,聖書體字母的「編碼」範圍在「16進位」的13000到1342F之間,所以我們用 ... 來設定範圍,這個「範圍」可以指定給一個常數。

在整數類型 Int 中,我們正常使用的數字是10進位,如20210728,最大值可以到10進位的19位數。

但有些情況下,會用16進位數字來表示整數,跟10進位數字區別的方法,就是數字前面加上零x "0x",所以 0x12 代表16進位的 12,與10進位的轉換方法為:

0x12 = 1*16¹ + 2*16⁰ = 16 + 2 = 18

16進位的每個數字,需要有16個組合,因此用0, 1, 2,...9, A, B, C, D, E, F 來表示每一位數,A 相當於十進位的10,B相當於11,....F 相當於15。例如:

0x13000 = 1*16⁴ + 3*16³ + 0*16² + 0*16¹ + 0*16⁰
= 1*65536 + 3*4096 + 0*256 + 0*16 + 0*1
= 77824

0x1342F = 1*16⁴ + 3*16³ + 4*16² + 2*16¹ + 15*16⁰
= 1*65536 + 3*4096 + 4*256 + 2*16 + 15*1
= 78895

通常我們不需要去換算,只要知道0x開頭的是16進位的整數就行。

③ for i in 聖書體編碼 { }

for 迴圈句我們已經熟悉,其中的範圍會代入「聖書體編碼」常數所表示的0x13000...0x1342F

也就是說,這樣寫:
let 聖書體編碼 = 0x13000...0x1342F
for i in 聖書體編碼 { }

相當於:
for i in 0x13000...0x1342F { }

也等於:
for i in 77824...78895 { }

整數的形式用16進位或10進位都可以。

④ let 字元 = UnicodeScalar(i)!

我們真正想用的物件就是這個 UnicodeScalar,前面我們學過物件類型的初始化,與呼叫函式很類似,其實就是呼叫該物件類型的 init(),物件UnicodeScalar的初始化函式 init() 會將整數 i 轉成對應的 Unicode 字元,也就是我們要的聖書體字母。

什麼是 Unicode 呢?簡單地說,就是國際通用的文字編號(或稱編碼),例如,第一個聖書體字母 𓀀 對應的編碼就是整數77824(等於16進位的0x13000)。

要注意 UnicodeScalar(i)! 最後必須加一個驚嘆號 !,這是Swift語言的特別語法,稱為 Optional 資料類型,後面會用範例程式來說明。

⑤ print("\(字元)", terminator: " ")

函式print() 我們常用到,但是第一次用 terminator: " ",這其實是一個可省略的參數,因為有預設值「換行」。terminator 是參數名稱,意思為結束符號,也就是說,每次 print() 列印要怎麼結束,通常就是「換下一行」,但這次我們不要換行,而是空一格就好,所以參數改成 terminator: " "。

執行結果放大如下圖。


註解
1. 拉美西斯二世遠征西臺王國的戰役稱為Battle of Kadesh,有趣的是,當時西臺王國對此戰役也留下文字記載,同樣宣稱獲得大勝。考古學家認為兩敗俱傷,但各自宣稱己方勝利。
2. 拉美西斯二世死後的木乃伊於1881年發現,三千年來仍保存良好,原是開羅的埃及博物館鎮館之寶,2021年移至新落成的吉薩博物館。
#35 什麼是 Optional 資料類型?

前面曾經提過關於資料(變數、常數)以及物件的「初始化」,雖然只是指定初始值這個小動作,但這對Swift程式設計來說,卻是個非常重要的觀念。

考慮以下情況:

(1) 任何資料或物件都必須初始化之後才能使用,否則程式無法繼續執行,App會閃退
(2) 很多資料可能來源在程式外部,例如檔案系統、資料庫、網路等等
(3) 外部資料並不保證一定會成功取得,例如檔案可能不存在,資料庫抓不到,網路無法連線等等因素

如果資料不確定能否初始化成功,但又必須使用(例如先顯示「圖片下載中」),這時候宣告方式就有點特別,要在資料類型最後加上問號 ? (中間不可空格),這時變數會先指定為 nil,表示還未初始化,但已可以使用。例如:
// Optional data type
// 概念碼,非實際程式碼
var 網路圖片: Image? // Optional Image 資料類型
if 抓取(網路圖片) == nil {
顯示("圖片下載中") //還未初始化成功
} else {
顯示(網路圖片) //已抓到圖片,初始化成功
}

也就是說,任何資料類型都可以設定為 Optional(只要加上?)。

我們用最近的疫苗現象來寫一個範例程式。
// 1-9b Optional data type
// Created by Heman, 2021/07/29
// 疫苗名稱與數量均為虛構

struct 疫苗 {
var 庫存量: Int
var 可用狀態: Bool
init(_ n: Int, 三期試驗: Bool, 緊急授權: Bool) {
if 三期試驗 && 緊急授權 {
庫存量 = n
可用狀態 = true
} else {
庫存量 = 0
可用狀態 = false
}
}
mutating func 訂購(_ n: Int) {
if 可用狀態 == true {
庫存量 = 庫存量 + n
}
}
mutating func 施打(_ n: Int) {
if 可用狀態 == true && 庫存量 >= n {
庫存量 = 庫存量 - n
}
}
}

var AZ = 疫苗(4000000, 三期試驗: true, 緊急授權: true)
var 莫德納 = 疫苗(1000000, 三期試驗: true, 緊急授權: true)
var 高端: 疫苗?

AZ.訂購(2000000)
AZ.施打(4101387)
莫德納.訂購(3000000)
莫德納.施打(2911743)

for 新冠疫苗 in [AZ, 莫德納, 高端] {
print(新冠疫苗?.庫存量)
}

for 新冠疫苗 in [AZ, 莫德納] {
print(新冠疫苗.庫存量)
}

高端 = 疫苗(5000000, 三期試驗: false, 緊急授權: true)
print(高端!.庫存量)

若不確定某個變數是否能順利初始化,就將它設為Optional (類型名稱後面加?),例如:
var 高端: 疫苗?

使用時,變數名稱也須加上?,但是如果確定已經初始化,變數名稱後面可改為驚嘆號!,強制取值。
print(高端?.庫存量)
print(高端!.庫存量)

執行結果如下,注意Optional資料類型的顯示結果(前3行),與正常資料不同。


所以簡單的說,Optional 是一種容錯機制、防止意外的保險,如果事先知道資料有可能初始化失敗,就改用這個類型,讓程式能繼續執行,不會閃退。

所以回到前面1-9a範例的 UnicodeScalar(i)! ,原來 UnicodeScalar() 回傳值的資料類型就是 Optional (String?),因為 Unicode 的編碼範圍有限,如果傳入的參數 i 不在範圍之內,就可能傳回 nil。而我們傳入的參數 i 確定在範圍之內,所以用驚嘆號 ! 來強制取值。

註解
1. nil 字面上也是空、零、虛無的意思,但和空字串、空陣列不同,如果變數指定為空字串或空陣列,表示已經初始化,所以空字串或空陣列是一種資料值。但 nil 不是資料值,比較像是一種「狀態」,還未初始化的狀態。

2. 在定義 struct 疫苗 { } 裡面,有兩個「物件方法」宣告為
mutating func 訂購(_ n: Int) { }
mutating func 施打(_ n: Int) { }

mutating func 是什麼意思?這是表示函式內會修改物件屬性(此處為「庫存量」),mutate 意思是變更、變異,通常指性質(屬性)上的改變,就像曬太陽讓皮膚變黑一樣。

為什麼修改物件屬性的函式要特別標明呢?簡單地說,這是 Swift 為了安全特性設計的語法規則,這樣比較不容易產生bug或意外。
#36 什麼是 Unicode ?

Unicode 稱為「萬國碼」或統一碼,是目前電腦內部用來表示全世界「文字與符號」(通稱「字元」或「字符」)的編碼標準。

所謂編碼就是給每一個字元一個整數編號,例如英文空格 " " 編號 32 (16進位0x20),小寫字母 "z" 編號122(16進位0x7A),第一個漢字「一」編號19968(16進位0x4E00),第一個表情符號😀編號128512(16進位0x1F600)⋯⋯等。是的,表情符號(Emoji) 也是 Unicode 的一部分。

表1-9a Unicode 字符編碼(編號)範例
字符 Unicode編號(10進位) Unicode編號(16進位) 註解
" " 32 0x20 英文模式空格
"z" 122 0x7A 英文小寫 z
"一" 19968 0x4E00 第一個漢字
"😀" 128512 0x1F600 第一個Emoji

Unicode 編號習慣使用16進位表示。

除了文字與符號以外,Unicode 某些編號是「控制字元」,例如「換行」「跳頁」,有些用來組合成一個字,例如韓文(諺文)或阿拉伯文,但大多是一個編號對應一個字元,少部分字元由多個編號組成。

Unicode v1.0 在 1991年發表時,收錄24種文字,而經過30多年發展,目前最新版v14.0預計2021年9月發表,包含159種文字、14.4萬多個字元(其中漢字佔9萬多)。這14.4萬多個字元就是電腦能夠處理的所有文字與符號。

當然,要顯示出來還需要「字型」,字元沒有相應的字型便無法顯示,這時須安裝額外的字型檔案。

Unicode 的編碼細分成3百多個連續編號的區段("Unicode Block"),可以根據這些區段的編碼範圍列印字元。詳細列表可參考維基百科:https://zh.wikipedia.org/wiki/Unicode區段

就像程式1-9a顯示聖體書字母一樣,只要知道字元編號的範圍,就能列印出全世界的文字與符號。
// 1-9c Unicode & Emoji
// Revised by Heman, 2021/07/29
// https://zh.wikipedia.org/wiki/Unicode區段
import Foundation

let 英文字母 = 0x20...0x7E
let 拉丁字母 = 0xA0...0x2AF
let 希臘字母 = 0x370...0x3FF
let 斯拉夫字母 = 0x400...0x52F
let emoji =
[0x1F300...0x1F5FF,
0x1F600...0x1F64F,
0x1F680...0x1F6FF,
0x1F900...0x1F9FF]

for 區段 in [英文字母, 拉丁字母, 希臘字母, 斯拉夫字母] + emoji {
for i in 區段 {
let 字元 = UnicodeScalar(i)!
print(字元, terminator: " ")
}
print("\n")
}



Unicode 支援9萬多個漢字,分散在不同區段,這裡漢字指的是中、日、韓、越、港、澳、台、新等地區共用的漢字字元,裡面包含各地特有及古籍的異體字。根據教育部統計,中文常用字加次常用字大約11,000字左右。

以下範例,我們列印最前面512個漢字。
// 1-9d CJK Unified Ideographs 中日韓越通用漢字
// Revised by Heman, 2021/07/30
// https://zh.wiktionary.org/zh-hant/
import Foundation

// let CJK = 0x4E00...0x9FFF
let 基本漢字 = 0x4E00...0x4FFF
for i in 基本漢字 {
let 字元 = UnicodeScalar(i)!
let 十六進位字串 = String(format: "%4X", i)
print("\(十六進位字串) \(字元)", terminator: " ")
}

範例1-9d中,有一個新的用法:
let 十六進位字串 = String(format: "%4X", i)
這是將整數 i 轉成16進位的字串,例如 0x4E00 (整數) 轉成 "4E00" (字串),以方便列印,否則只會印出10進位的格式。轉換的方法是呼叫 String(),這並不是一個函式,而是物件!

我們學過 String 是字串類型,但其實在 Swift 內部,String 也是物件型態,可以利用它的物件初始化函式 init() 來產出一個字串物件,在此,我們將整數轉換成字串。平常如果使用:
let 十進位字串 = String(i)
String(i) 會將整數 i 轉成10進位的字串。如果 i = 0x4E00,會轉換為字串 "19968"。
let 十六進位字串 = String(format: "%4X", i)
如果 i 前面增加一個參數 format: "%4X",則會依照要求的格式(format)轉換,"%4X"要求格式為4個大寫16進位數字的字串。如果 i = 0x4E00,會轉換成 "4E00"。

這樣我們就得到一個Unicode編碼與漢字字元的對照表。


註解

1. String(format:) 用來將數字轉換為字串,格式用百分號"%"起頭,後接一個字母來指定數字類型,包括:

%d -- 正負整數,十進位
%u -- 正整數(unsigned integer),十進位
%f -- 實數(浮點數),十進位
%x -- 正整數,十六進位
%e -- 實數,十進位,轉為科學記號表示法

百分號與字母中間,可以加數字指定位數(可省略,位數非強制),下表為幾種常見的用法:


範例程式:
import Foundation

let x = String(format: "0x%X = %d, or %.3e", 12345, 12345, 12345.0)
print(x)

輸出結果:
0x3039 = 12345, or 1.234e+04
#37 第10課 時間(Date)

上一課我們第一次從「物件庫」裡面取出「物件」,這些物件代表已經數位化的事物,可能是對應實體世界的某個物品,也可能是虛擬抽象的事物。Apple原廠提供了上百個物件庫,包含上萬個物件給程式設計師使用。

再加上每個人都可以貢獻自己寫的物件公開在網路上,這些由第三方提供的開放原始碼物件庫,等於有數不盡的物件供我們使用。就像阿拉伯「一千零一夜」故事中,阿里巴巴打開盜賊寶庫說的通關密語:「芝麻開門」,學會「物件」的使用,就等於掌握了「芝麻開門」一樣。

本課是第1單元最後一課,整個第1單元的課程,其實就是為了學會「芝麻開門」,知道如何從「物件庫」取用物件,就能夠打開Apple原廠的寶庫。

我們再次練習從寶庫中取出「時間」這個物件。
// 1-10a 時間(Date)
// Revised by Heman, 2021/07/31
import Foundation

let 剛剛 = Date()
print(剛剛)

let 現在 = Date()
let 時間差 = 現在.timeIntervalSince(剛剛)
print(時間差)


1-10a範例程式共6句程式碼,import Foundation 不必再說明,就是匯入 Foundation 物件庫,因為時間的物件 Date 在裡面。

Date 是時間的物件類型名稱,我們呼叫兩次 Date() 定義兩個物件實例:「剛剛」和「現在」,物件實例的時間值就是呼叫 Date() 那瞬間的時間。

將物件實例列印出來,會列印出「格林威治標準時間」(國際時區以英國格林威治天文臺的時間為基準):
print(剛剛)
2021-07-31 03:03:17 +0000

台灣的時區是格林威治 +8 小時,所以相當於台灣時間 2021-07-31 11:03:17 +0800

注意物件使用上的細微差別,Date 是物件類型,而 Date() 則是呼叫該物件類型的初始化函式 init()。我們曾比喻「物件類型」就像模具,加上括號 () 如Date(),就相當於按下模具「複製」的開關,複製產出一個物件實例。

所以「剛剛」和「現在」雖然都是呼叫 Date(),但兩者按下開關的時間並不會一樣,我們再用這物件的一個「方法」(還記得嗎?就是物件的函式或功能),稱為 timeIntervalSince() 來取得時間差,單位為「秒」。
let 時間差 = 現在.timeIntervalSince(剛剛)

相當於
時間差 = 「現在」的時間值 - 「剛剛」的時間值
= 0.009415030479431152(秒)



學會了嗎?希望大家都像阿里巴巴一樣,入寶山絕不空手而回。
#38 計算程式執行時間

當我們掌握了「時間」物件,就可以做出很多應用。例如,第5課曾經提過,若要質因數分解非常大(幾百位數)的數目,會花很長時間,有些加密方法就利用這原理來保障密碼的安全。

我們實際來測試看看,到底質因數分解要花多少時間。
// 1-10b 時間差(Date.timeIntervalSince)
// Revised by Heman, 2021/07/31
import Foundation

func 是質數嗎(_ n: Int) -> Bool {
if n < 2 {
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] = []
for i in 2...n {
if n % i == 0 {
因數 = 因數 + [i]
}
}
return 因數
}

func 質因數分解(_ n: Int) -> [Int] {
let 因數 = 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}
let n = 20210730
let 剛剛 = Date()
let 質因數 = 質因數分解(n)
let 時間差 = Date().timeIntervalSince(剛剛)
print("\(n)的質因數分解為:\(質因數)")
print("共花費\(時間差)秒")

程式定義了三個函式「是質數嗎()」「因數分解()」「質因數分解()」,基本上就是利用第5課所學的知識,就不再詳細解釋。本課討論的是最後6句程式碼:

let n = 20210731
let 剛剛 = Date()
let 質因數 = 質因數分解(n)
let 時間差 = Date().timeIntervalSince(剛剛)
print("\(n)的質因數分解為:\(質因數)")
print("共花費\(時間差)秒")

句型跟1-10a 計算時間差很類似,在這裏,我們要計算:
let 質因數 = 質因數分解(20210731)
前後的時間差,也就相當於計算「質因數分解(20210731)」所需要的時間。

結果分解這8位數的整數 20210731 就需要15秒,當然,可能因為筆者用的 Mac mini (late 2014) 有點年紀了,還有另外一個原因,就是我們採用的「計算方法」有點笨,網路上有很多更快更聰明的質因數分解法,這些讓電腦更快解決問題的方法,在學術上稱為「演算法(Algorithm)」,對程式設計非常重要,但本單元入門課程就不多介紹。

無論如何,質因數分解的確是個計算繁重的工作,想想看如果要分解 Int 類型最大整數19位數(8位數的1000億倍),需要多久呢?千萬不要輕易嘗試啊!

註解
Swift 任何資料類型都是物件類型,所以整數類型 Int 也是,這個物件有個「屬性」(即物件內部的變數或常數)max,可以取得64位元 Int 所能表示的最大整數。用法如下:
let n = Int.max
print(n)
  • 9
內文搜尋
X
評分
評分
複製連結
請輸入您要前往的頁數(1 ~ 9)
Mobile01提醒您
您目前瀏覽的是行動版網頁
是否切換到電腦版網頁呢?