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

#21. 變數名稱的有效範圍

在Swift程式中,每個「名稱」包括變數、常數、函式名稱等等,都有其個別的有效範圍,以範例1-5c為例,每個變數或常數的有效範圍如下圖:



說明如下:
① 下一個
「下一個」是迴圈內定義的「區域(local)常數」,每一次迴圈就會重新宣告,重新計算起始值,生命週期最短,只有一次迴圈。

② i
i 是for的「迴圈參數」,生命週期從進入迴圈到整個脫離迴圈為止。i 不能在迴圈內被修改,事實上它是一個常數,每次迴圈會重新定義,自動給予新的值。

③ m
m 是函式內定義的「區域常數」,生命週期從宣告到函式結束為止,每次函式呼叫時就會重新定義

④ n
n 是函式本身的「參數」,生命週期從進入函式到函式結束為止。大部分的參數在函式內不能被修改(稱為"Call by value"),如這個 n,但有些情況下,允許修改(稱為"Call by reference"),後面遇到再解釋。

⑤ 陣列, 最大索引
「陣列」與「最大索引」是程式的「全域(global)變數」,在整個程式範圍內都有效,生命週期從宣告起到整個程式結束,生命週期最長。


注意我們在①最內圈變更了「數列」這個全域變數,因為它的有效範圍是涵蓋整個程式,所以即便離開迴圈、離開函式,被改變的結果仍是有效的,不會被還原。

在比較複雜的程式裡面,特別是多人共同做的專案中,這樣的用法並不好,因為你不知道全域變數會在哪裡被改過,很難追蹤和debug,也就是說,「全域變數」盡量少用。但「全域常數」則沒有這個顧慮。

除了變數或常數之外,任何命名的名稱都有類似的有效範圍,如果在內圈的命名與外圈重複,會取用較內圈的變數,如下例。

// 1-5f 變數有效範圍
// Created by Heman, 2021/07/19
let i = 10

for i in 1...9 {
print(i)
}

print(i)

在for迴圈內 print(i) 取的是迴圈參數 i,所以印出1到9,而最後一行 print(i)取的是全域常數 i,印出10。

變數、常數等名稱的範圍(Scope)是程式設計很重要的概念,範圍不同,程式的邏輯會跟著不同。在不同範圍內,取相同變數名稱,容易導致邏輯錯誤,但有時又很難避免,只能靠正確的觀念,細心地去debug。
#22. Debug 迴圈

迴圈的威力強大,但也是程式裡面比較難"Debug"(除錯、偵錯)的地方,還好 Swift Playgrounds 提供了一個除錯專用的功能,稱為「逐步執行程式碼」,可以讓我們清楚看到程式執行的每一個步驟。



在除錯的過程中,也可以將個別的變數值顯示出來,這樣就更容易確認程式的邏輯是否正確。
#23. 第6課 Swift 程式語言的基本句型

學程式設計跟任何語言一樣,「練習」是不二法門,學外語有個「1,000小時定律」,就是說一個人從零基礎,持續累積1000小時的練習,才能精通某個外語,假設最少每週練習3小時的話,大約333週(6年左右),就能精通。

程式設計應該也差不多,100小時左右可以入門,1,000小時能夠精通,不會忘記,累積10,000小時就是大師了。1,000小時看起來好像很漫長,但實際上,如果有天份和興趣,這1,000小時的投資,可以讓你一輩子受用至少20年,非常划算。

Swift 既然是一種「語言」,就會有常見的「句型」跟「片語」,以我們目前所學歸納起來,有以下幾種基本句型:

表1-6a Swift基本句型
Swift基本句型 範例 說明
1. 宣告句
let n = 0
var 我的名字 = "Angela"
var 陣列: [Int]
func 因數(_ n: Int) -> [Int] { }
宣告用來定義新的變數、常數、函式、資料型態...等等
2. 指定句
新陣列 = 起始陣列 + [下一個數]
n = 費氏數列(92)
先做等號右邊的運算,再將結果指定給等號左邊
3. 條件句
if i < 100 { } else { }
根據邏輯運算式的結果,選擇執行的 { } 段落,else 子句可省略
4. 迴圈句
for i in 1...100 { }
while i < 100 { }
repeat { } while i < 100
for, while, repeat 三種迴圈
5. 函式呼叫
print()
注意函數的參數與回傳值
6. 其他指令
return
控制程式執行流程

而所謂的「片語」是構成句子的一部分,在 Swift 裡面通常稱為「運算式(Expression)」或表達式,本身並不是完整的句子。歸納如下表:

表1-6b Swift運算式
Swift運算式 例句 說明
1. 算術運算式
x * x + 2 * (x + 1)
先乘除後加減,括號最優先
2. 邏輯運算式
i == 99
n < 0
結果為 true 或 false
3. 字串運算式
陣列運算式
"你好" + "我是Angel" + "很高興認識你"
[0, 1] + [1, 2, 3, 5]
數字以外的資料類型,有些也可以用來做算術運算或邏輯運算
4. { } 段落
{
let pi = 3.14, i = 10
var sum = 0
sum = sum + i * pi * 2
}
{ } 是零或多個句子形成的「段落」,當成「子句」,放在句子的最後。{ } 裡面,任何句型都可以寫。

另外「函式呼叫(function call)」比較特別,函式可能是一個新的指令,可以當作單獨的句子,如 print();若函式有回傳值,也可當作是運算式的一部分,例如 if 費氏數列(n) < 10000 { print(n) }

所以,就像學習寫作文一下,先學單字(變數、常數、保留字)、再學造詞(運算式)造句、最後練習作文。

用以上這些的句型,來分析一下範例1-5c程式,如下圖。

這個小程式由(最外層)4個句子組成,其中的第③句函式宣告又是由⑹個子句組成,其中包含一個for迴圈句,迴圈中有⒉個子句。

所以程式的結構就像這樣,由基本句型一層一層組合而成,學會基本句型,了解語法規則,就能根據解決問題的邏輯,寫出完整的程式來,不管再複雜的問題,都可以解決。
#24. 整數分解為費波那契數之和

我們都知道任何正整數都可以分解為質因數之乘積,稱為質因數分解。同樣的,任何正整數也都可以分解為「相異的費波那契數之和」,而且可能有多種組合,每種組合的費波那契數都不同。例如:
101 = F(1) + F(4) + F(6) + F(11)
= 1 + 3 + 8 + 89

2021 = F(7) + F(9) + F(14) + F(17)
= 13 + 34 + 377 + 1597

20210722 = F(5) + F(7) + F(9) + F(12) + F(14) + F(16) + F(19) + F(21) + F(25) + F(28) + F(31) + F(33) + F(36)
= 5 + 13 + 34 + 144 + 377 + 987 + 4181 + 10946 + 75025 + 317811 + 1346269 + 3524578 + 14930352

之前提過,費波那契數的密度比質數低很多,到19位整數也不過92個費波那契數。19位整數相當於10億的10億倍,這麼多數字裡面只有92個費波那契數,可見之稀少,但卻只要這92個費波那契數,就能夠組合出10億的10億倍的數字,不由得讓人吃驚。所以老子說:「道生一,一生二,二生三,三生萬物」,的確很有道理。

我們用程式來驗證一下,看任意整數是否都能分解為相異費波那契數之和。

// 1-6a 整數分解(費波那契數之和)
// Created by Heman, 2021/07/22
var 數列 = [0, 1]
var 最大索引 = 1

func 費氏數列(_ n: Int) -> Int {
if n < 0 { return -1 }
if n <= 最大索引 {
return 數列[n]
}
let m = 最大索引 + 1
for i in m...n {
let 下一個 = 數列[i-1] + 數列[i-2]
數列 = 數列 + [下一個]
}
最大索引 = n
return 數列[n]
}

func 整數分解(_ n: Int) -> [Int] {
var 數列: [Int] = []
var 索引 = 0
if n <= 0 { return [] }
while 費氏數列(索引) < n {
索引 = 索引 + 1
}
if 費氏數列(索引) == n { return [n] }
let 最近費數 = 費氏數列(索引-1)
let 差值 = n - 最近費數
數列 = 整數分解(差值) + [最近費數]
return 數列
}

print(整數分解(2021))


利用上一課1-5c的「費氏數列()」函式,我們只需要再增加一個函式「整數分解(n)」,傳回n分解為費波那契數的數列。

基本的想法很直覺,就是先找到最接近(但小於或等於)參數n的費波那契數,這用一個 while 迴圈句就能搞定。
    while 費氏數列(索引) < n {
索引 = 索引 + 1
}

當脫離迴圈時,「索引」所指的費波那契數會大於或等於n。接下來用一個條件句:
    if 費氏數列(索引) == n { return [n] }

如果n剛好是費波那契數,直接命中,那就傳回 [n] 這個小陣列,不用再加別的費波那契數了。否則的話:
    let 最近費數 = 費氏數列(索引-1)
let 差值 = n - 最近費數
數列 = 整數分解(差值) + [最近費數]

最接近的費波那契數應該是「索引-1」的那個數,這樣就找到數列的最後一個元素了。然後將差值再傳入「整數分解(差值) 」函式中,傳回結果再與前次找到的最後一個元素的陣列,相加(陣列合併)在一起。

這裡函式有個特殊的用法,就是在函式的宣告裡面呼叫自己,像這樣:
func 整數分解(_ n: Int) -> [Int] {
....
整數分解(差值)
....
}

這叫做「遞迴」呼叫(Recursive call),有點像佛教裡的輪迴,呼叫上一輩子的自己,問他上輩子的事情。以上面程式中「整數分解(2021)」為例,遞迴的過程如下圖所示。


遞迴和迴圈類似,要特別注意debug,每次遞迴的條件要有差異,並想好脫離遞迴的條件,這樣才不會無限輪迴下去,沒有覺醒的一天。


知道上面這個程式,脫離遞迴的關鍵是哪一句嗎?答案就是這個條件句:
    if 費氏數列(索引) == n { return [n] }

將整數n拆分到最後,差值必然會落到某個費波那契數上面,你相信嗎?自己動手做做實驗,用上一課提到的「逐步執行程式碼」觀察看看就知道。
#25. 程式「模組化」

前面第3課曾經提過,「函式」的目的是要作為一般化工具,也就是將特定問題的解,推廣為一般化的解。

例如在範例1-5c寫的「費氏數列()」函式,可用在1-6a整數分解的程式裡面,費氏數列() 就變成一般化工具,所以我們在程式裡面複製了一份「費氏數列()」程式碼與其所需的全域變數「數列」「最大索引」。

不過這樣一來,有兩個問題,一是函式用得越多,程式碼就需要複製越多,二是如果以後這個函式「費氏數列()」有更好的解法,就需要修改所有引用這個函式的地方。這樣對於程式設計的長久發展很不利,要如何改善呢?

改善的方法簡單的說,就是「模組化」,Swift Playgrounds 提供了簡單(對初學者足夠用)的模組化功能。

做法就是將要一般化的函式,放入到 Swift Playgrounds 左側欄下面的「原始碼」->「UserModule」裡面,這樣就能讓所有頁面的程式共用。


但是將「費氏數列()」程式碼剪貼到 SharedCode.swift 裡面之後,原來的程式會出現錯誤:


這是因為它們已經不在同一個頁面,所以在有效範圍內會找不到「費氏數列()」名稱,解決的方法就是將共用的「費氏數列()」宣告為 "public":


這樣在所有頁面就可看到「費氏數列()」,共用這個函式,減少程式碼的複製,而且還有一個好處,以後如果修改「費氏數列()」(例如有更好的解法),也只要在這一個地方修改就行。

除此之外,如果需要用別人寫的程式模組,也可以這樣使用,在 UserModule 旁邊按 ⨁「新增」程式碼,就可以放更多程式碼進來,讓所有頁面分享共用。

操作過程影片如下:
#26 台灣給世界的禮物

許多人第一次學習程式設計的時候,寫的第一個程式都是 "Hello, World!",這個慣例來自於 C 語言的作者 Kernighan and Ritchie ,40多年來一直被其他程式語言的作者沿用至今,"Hello, World!" 代表我們學習程式設計的第一步,也代表我們希望透過電腦與世界建立的第一個連結。

在全球化的現代社會中,自己與世界的連結非常重要,只有清楚自己的定位,才能在世界上謀得一個位置。西元2000年,有一篇刊登於 Nature 期刊的論文,為台灣在世界中的角色,做了一個很好的定位,篇名就叫 "Taiwan's gift to the world",作者是暢銷書《槍炮、病菌與鋼鐵》的賈德.戴蒙(Jared Diamond)。
https://www.nature.com/articles/35001685

在這篇文章中,作者戴蒙推論:「台灣是南島語系的故鄉」,所謂「南島語系」包含了一千多種語言,地區涵蓋東南亞到太平洋、印度洋諸島,東西橫跨2萬多公里(半個地球),目前約2億多人使用。這一千多種語言,都是在大約西元前4300左右(大坌坑文化),從大陸華南的原住民(非漢人)移民到台灣之後,慢慢往海洋擴張,在島嶼隔離的情況下,逐漸演化成不同語言,最後一個擴張的地盤是在西元1300年左右到達紐西蘭東方的查坦群島,前後歷時5千多年。

中研院也有一篇文章,利用台灣常見植物「構樹」的基因圖譜,分析南島語族各島嶼的構樹(竟然也都是從台灣移植出去的),佐證了上述說法,刊登在「泛科學」,相當值得一看。

https://pansci.asia/archives/140261

所以台灣帶給全世界的禮物,就是五千多年來造就的語言多樣性。

目前全世界還在使用中的語言(稱為「自然語言」,以別於程式語言)大約有6000~7000種,根據不同的分類,可分為10~20個「語系」,比較大的包括印歐語系、漢藏語系、南島語系等,台灣最多人使用的三種語言為國語、閩南語和客家話,正好落在漢藏語系中漢語的三大分支(難怪歧異度較高)。另外值得注意的是,在使用羅馬拼音以前,大多數的自然語言並沒有記載的文字,例如閩南語或所有南島語系,其實語言本身就是溝通的媒介,不需要再透過文字。

相對於自然語言有數萬年的發展歷史,程式語言只有不到一百年歷史,這些人造語言必須仰賴「文字」撰寫程式,經過編譯後才能跟電腦溝通。所以嚴格說起來,程式語言跟口說的自然語言大不相同,理論上是屬於一種「符號語言」或稱為「形式語言」。

目前還在使用中的程式語言,約有上百種,追溯起來,Swift 語言是為了取代 Objective-C,而 Objective-C 則受到 Smalltalk 與 C 語言的影響,這兩者最終又可以追溯到早期的FORTRAN語言,如下圖所示。

https://www.researchgate.net/figure/history-of-high-level-programming-languages-evolution-form-1954-2002_fig3_268277381

在這張圖中,筆者早期學過的程式語言包括FORTRAN, COBOL, BASIC, C/C++, Pascal, LISP等,除此之外,因為筆者從事的工作是網路相關的資訊服務,在工作中需要整合各式各樣的軟硬體,會接觸到Assembly, Perl, Shell script, Tcl/Tk, Java, Javascript, HTML, PHP, SQL, VB, Powershell, Python等,對這些程式語言也算粗淺了解。

從這兩年學習 Swift 的過程中,筆者發現 Swift 多了許多現代程式語言的要素,比起 Objective-C 或 C++ 來說,完全是不同世代的語言,非常不一樣,不管是語法的易寫程度與可讀性,或是程式的執行效率與安全性,都比以前的程式語言好很多,這也是筆者為何挑選Swift 來教高中生,而不是Python, Java, C++的原因。

不過,這並不是說Swift就比Python, Java, C++更好,因為每一種程式語言都有其存在的原因,對不同的人來說,會有不同的優缺點,例如Swift的開放原始碼資源就沒有Python累積得多,而 Java 有很好的跨平台優勢,C++是學術標準等等,所以就看什麼情境,挑選合適的語言而已。下表是依照筆者的了解,粗略比較這幾種程式語言。

表1-6c Swift 與其他程式語言的比較
Swift Scratch Java Python C++
發明者
(任職單位)
Chris Lattner
(美國蘋果公司)
Mitchel Resnick
(美國MIT)
James Gosling
(美國昇陽公司)
Guido van Rossum
(荷蘭CWI)
Bjarne Stroustrup
(美國貝爾實驗室)
發表年度 2014 2006 1994 1991 1983
優點 易讀、易寫
適合入門~專業
主流的軟體環境
未來發展潛力佳
適合啟蒙(幼稚園到小學)
視覺化(遊戲導向)
適合專業人士
跨平台能力佳
適合初學~專業
模組資源豐富(如人工智慧、大數據、物聯網等熱門應用)
物件導向標準語言
ISO國際標準(2018)
資工系必學
缺點 初期版本變動較大 缺乏實質生產力 不適合初學者 不適合開發App 編寫效率較低

Swift 程式語言以及我們用的 Swift Playgrounds App 主要作者都是 Chris Lattner,他稱得上是當代最優秀的電腦語言學家。就在賈德.戴蒙發表 "Taiwan's gift to the world" 論文的2000年,22歲的 Chris Lattner 進入伊利諾州大學香檳校區唸研究所,2002年左右發表了一篇研究編譯程式的論文,並實作出"LLVM",2005年取得博士學位後即被蘋果公司聘用,直到2017年離職為止,成功將蘋果公司的軟體開發語言,從Objective-C轉成新一代的 Swift語言,LLVM也成為Swift Playgrounds與Xcode背後的編譯引擎。

Chris Lattner 將 LLVM 的原始碼完全開放,而蘋果公司也將 Swift 程式語言交由一個非營利基金會負責維護與發展,因此,Swift 除了開發蘋果產品軟體之外,也有許多人將 Swift推廣到其他平台,例如微軟Windows及Linux,也就是說 Swift 除了用來設計App之外,也能開發跨平台的應用軟體,前途不可限量。

如果說南島語系是台灣帶給世界的禮物,那麼,Swift 可算是蘋果公司帶給世界(最好)的禮物。
程式碼內的函數 跟 變數等命名用中文喔?!
我個人覺得這樣不是很適當

要是工作上遇到這樣的程式碼 會覺得原作者很雷
畢竟在從事這行業的人 還是有最低程度英文閱讀能力才是

我也看過網路上不少程式教學,幾乎沒看過用中文命名的
雪白西丘斯
謝謝您的建議,我調查看看。
#27 命名用中文? or 英文?

很多人留言說用中文命名很不恰當,從沒看過,事實上,筆者自己30年也沒看過用中文寫程式的,一個都沒有,已經習慣英文模式的人,看到程式裡面(甚至包括註解)用中文,會覺得很LOW,如果是同儕,我可能也會鄙視他。

但是用中文來教學真的不好嗎?我相信學習是一個過程,特別是一開始,門檻應該越低越好,一旦入門了,就可以靠自己提升能力,英文程度夠的話,全部改用英文撰寫,更不是問題了。

我並不會堅持這一點,如果大家覺得命名用英文比較好,之後可以全部改用英文。

請試試看以下英文版的費波那契數程式,與1-6a中文版程式比較一下,看是否比較容易閱讀與理解?請回覆您的建議。

// 1-6c Sum of Fibonacci Numbers
// Revised by Heman, 2021/07/24
var fibSequence = [0, 1]
var maxIndex = 1

func fibonacciNumber(_ n: Int) -> Int {
if n < 0 { return -1 }
if n <= maxIndex {
return fibSequence[n]
}
let m = maxIndex + 1
for i in m...n {
let nextFibNumber = fibSequence[i-1] + fibSequence[i-2]
fibSequence = fibSequence + [nextFibNumber]
}
maxIndex = n
return fibSequence[n]
}

func sumOfFibonacci(_ n: Int) -> [Int] {
var sequence: [Int] = []
var index = 0
if n <= 0 { return [] }
while fibonacciNumber(index) < n {
index = index + 1
}
if fibonacciNumber(index) == n { return [n] }
let approximateFibonacci = fibonacciNumber(index-1)
let difference = n - approximateFibonacci
sequence = sumOfFibonacci(difference) + [approximateFibonacci]
return sequence
}

let n = 20210724
print("\(n) = sum of ")
print(sumOfFibonacci(n))


以上的6課內容,我不知道所設定的目標對象「高中程度零基礎的初學者」能夠看懂多少,如果剛好是(或接近)這樣的讀者,請私訊回饋意見給我,這會給我很大幫助,謝謝。
中文加一,因爲初學者較容易暸解
#28 第7課 定義新資料類型(struct)

大自然中,蜜蜂能夠透過肢體語言,告訴同伴哪裡可以採花蜜;鯨魚利用聲波,可以呼叫千里外的同伴;新幾內亞天堂鳥有一套繁複的求偶舞蹈,這些智慧生物的溝通能力,有時真令人讚嘆,但這些都比不上人類的語言,人類是大自然中,唯一能夠溝通複雜事物甚至抽象情境的生物。

賈德戴蒙在《人類大歷史》一書中說,地球生物演化史上,只有現代智人「有能力聊八卦」。現代智人在四萬年前就能用語言描述具體事物,並在一萬年前開始發展農業,到五千年前發明文字,累積智慧,這是人類文明發展至今的源頭。

如何用程式描述實體世界的事物呢?我們已經在網路上或是遊戲中,體會過虛擬世界溝通感官的能力,有些將實體世界模擬得惟妙惟肖,有些是完全想像的虛擬事物,這些怎麼做到的?關鍵之一就是本課的內容:用來定義新資料類型的指令,稱為 "struct",這是 "structure" (結構、構造)的簡寫。

我們用前面的「商王世系」為例,在1-4b程式中,以兩個陣列來分別表示「名號」與「即位年代」,如果要再增加「在位時間」資料,就得再多一個陣列,如果商王有10種屬性(名號、即位年代、在位時間、父子關係、主要事件....)就得用10個陣列,這顯然不是一個好方法。

更好的辦法就是用 struct 把所有的屬性集合在一起,定義為一個複合型的資料類型,然後用一個陣列就可以包含所有商王及其屬性資料。
// 1-7a struct: 商王年表
// Created by Heman, 2021/07/24

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

let 商湯 = 商王(名號: "大乙湯", 即位: -1558, 在位: 12)
let 盤庚 = 商王(名號: "盤庚旬", 即位: -1315, 在位: 28)
let 武丁 = 商王(名號: "武丁昭", 即位: -1274, 在位: 59)
print(商湯)
print(盤庚)
print(武丁)

for 帝王 in [商湯, 盤庚, 武丁] {
print(帝王.名號, 帝王.即位, 帝王.在位)
}

注意程式中 struct 的用法:
struct 商王 { }

這是一個宣告句,用來定義複合型的「資料類型」,也就是說,「商王」定義為一個資料類型,可以再用來宣告變數或常數,例如:
let 商湯: 商王

定義「商湯」常數屬於「商王」類型。這時的變數或常數如何指定值呢?指定的方式跟函式呼叫很類似:
商湯 = 商王(名號: "大乙湯", 即位: -1558, 在位: 12)

就像函式每個參數名稱都要寫出來,用冒號(注意不是等號): 隔開資料值,這裡的冒號 : 有「對應」的意思,而不是從屬於類型,如「名號: "大乙湯"」名稱對應資料值。

struct { } 裡面的常數或變數稱為「屬性(property)」。

所以「商王」這個資料類型有3個屬性,「名號」「即位」「在位」,凡是商王類型的變數或常數,就必須依照這樣的語法給予3個屬性的值。

那如何單獨取用某一個屬性呢?就用一個句號 . 來連接,就像「商湯.名號」,中間不能有空白,這個句號 . 相當於「的」意思,用「商湯.名號」就能取得「商湯的名號」這個屬性。

所以在最後一個for迴圈句中:
for 帝王 in [商湯, 盤庚, 武丁] {
print(帝王.名號, 帝王.即位, 帝王.在位)
}

用商湯、盤庚、武丁三個變數組成的陣列當作迴圈參數「帝王」的範圍,在迴圈{ }裡面,取用「帝王」的3個屬性列印出來。

執行結果如下圖。


這一課 struct 是本單元最重要的內容,也是第2單元 SwiftUI 的基礎,非常重要!

struct 的複合結構可以從很簡單到很複雜,我們會由簡而繁,按部就班逐步解說,希望大家能充分理解。
關閉廣告
文章分享
評分
評分
複製連結
請輸入您要前往的頁數(1 ~ 9)

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