做做Flutter-Day05:流程、函式、模式、錯誤處理

影山小麥機
18 min readJun 27, 2023

聊聊Control Flow、Function、Patterns、Error Handling

(其實有點不知道不知道patterns翻成模式合不合適)

前言

昨天本來要騎去埔鹽順澤宮拿冠軍帽,但後來路上遇到了車友就把我拐去嘉義東石了,今天腿好酸(嗚嗚)

前幾天聊了Flutter關於型別的一些基本設定,其實主要要談就是Dart語法的基本設計就是強型別,不會像是JavaScript可以在宣告型別後更改型別,所以,Dart語法強型別的規則對於寫作脈絡來說聽起來是一個比較容易維護的規則。(不知道各位客官怎麼看)

總之,在型別系統上的規則也類似於Swift的運作,如果大家有碰過iOS開發的話(笑

那今天會講的事情是流程控制,其實之前瀏覽過這幾個篇章的想法是與Swift的規範類似,那麼,我們就看下去吧!

正文

Function

關於function的使用方式,大概的表達方式會是這樣:

/// 空回傳值的function
void doSomething() {
print(“do something”);
};

/// 有回傳值的function
bool returnABool() {
return true
};

/// 有設定輸入參數值的function
void setNumbers(int numberOne, double: numberTwo) {
print("setNumbers")
};

/// 有設定輸入型別、有設定輸出型別的function
String inputStringReturnSomething(String inputTextOne, String inputTextTwo) {
return "something"
};

///多返回值的function
(String, Int) returnMutipleThings() {
return ("True", 1);
}

在前面的void代表這個function不回傳東西,只執行動作,funtion的前面有回傳型別,比如像是上面這個functino是回傳bool值,就會寫在前面,這點跟Swift有點不太一樣。

實際上調用已經定義好的function就會像下面這樣寫:

doSomething();

returnBool();

setNumbers(numberOne: 1, numberTwo: 2);

inputStringReturnSomething(inputTextOne: "one", inputTextTwo: "Two");

其實Function的寫作邏輯都差不多是這樣,如果有更多需要仔細看的,可以到官方網站裡面找找,但大致上會用的差不多都是這樣。

Control Flow

控制流的寫法其實大多的語言也都差不多,這邊會描述一下Dart的For Loop,下面基本上也就是很常見的for迴圈,這邊就拿官網的範例稍微寫一下:

for迴圈

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
message.write('!');
}

while迴圈

while (!isDone()) {
doSomething();
}

do…while迴圈

do {
printLine();
} while (!atEndOfPage());

其實上面的邏輯都是重複做某些事情,如果有什麼條件就補個條件上去,如果符合或不符合就繼續執行這個迴圈裡的事情。

迴圈還有另外一些輔助的語法叫做break跟continue:

break

while (true) {
if (shutDownRequested()) break;
processIncomingRequests();
}

如果遇到shutDownRequest()回傳true的話,就跳出迴圈的循環邏輯。

continue

for (int i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (candidate.yearsExperience < 5) {
continue;
}
candidate.interview();
}

continue就是如果條件達成就會繼續run這個迴圈的邏輯。

不過它們都算是命令語句,遇到他們就會自動執行這個命令的定義。

Branches

這邊會簡單帶過Dart在寫if else 、switch case的大概樣子。

if

if (isRaining()) {
you.bringRainCoat();
} else if (isSnowing()) {
you.wearJacket();
} else {
car.putTopDown();
}

這邊的寫法原則上跟Swift一模一樣,除了條件的描述外面需要給括弧之外,其他都一模一樣。

if -case

不過這邊也有個寫法跟Swift不一樣,它能夠支援使用pair的方式去匹配case,假如這些case剛好符合你的定義,那麼你就可以直接執行符合這個case的陳述句,像下面這樣:

if (pair case [int x, int y]) return Point(x, y);

不過上面這個算是把它做成單行的語法簡寫,如果遇到下面這種需要throw(拋出)某些特殊系統資訊的工作,下面這樣的呈現方式也比較完整一點。

if (pair case [int x, int y]) {
print('Was coordinate array $x,$y');
} else {
throw FormatException('Invalid coordinates.');
}

switch statements

這邊接下來會講switch case的使用,也大概是我在Swift很常用的語法之一,我經常會用它來明確定義一個東西,比如像條件,有時候單純的使用if…else不能講清楚整個function在做的事情的時候,我會偏好直接使用switch case去處理整段程式碼,讓它變得好讀:

var command = 'OPEN';
switch (command) {
case 'CLOSED':
executeClosed();
case 'PENDING':
executePending();
case 'APPROVED':
executeApproved();
case 'DENIED':
executeDenied();
case 'OPEN':
executeOpen();
default:
executeUnknown();
}

不過Dart的Switch case功能比較多元一點,這邊可以定義一個叫做new case的語法,讓你可以在寫條件邏輯的時候可以藉由new case去執行其他明確定義的條件邏輯,所以當你需要new case的時候,可以再直接叫它出來執行:

switch (command) {
case 'OPEN':
executeOpen();
continue newCase; // Continues executing at the newCase label.

case 'DENIED': // Empty case falls through.
case 'CLOSED':
executeClosed(); // Runs for both DENIED and CLOSED,

newCase:
case 'PENDING':
executeNowClosed(); // Runs for both OPEN and PENDING.
}

Dart的switch case的內部case也可以另外寫邏輯,比如像是下面這樣:

switch (charCode) {
case slash || star || plus || minus: // Logical-or pattern
token = operator(charCode);
case comma || semicolon: // Logical-or pattern
token = punctuation(charCode);
case >= digit0 && <= digit9: // Relational and logical-and patterns
token = number();
default:
throw FormatException('Invalid');
}

上面的舉例中,你可以看到每個內部的case都會有自己另外的邏輯條件,符合了這些邏輯,才會執行該case下面的描述句。

不過這樣的寫法會不會反而讓整段的程式碼不好讀,這可能還是要看編寫的人的程式語法功力了。

然後我覺得Dart的Switch case還有個蠻好用的地方,就是可以直接讓switch case直接變成變數,這樣可能可以少掉很多邏輯的理解在裡面:

token = switch (charCode) {
slash || star || plus || minus => operator(charCode),
comma || semicolon => punctuation(charCode),
>= digit0 && <= digit9 => number(),
_ => throw FormatException('Invalid')
};

當然,你在上面看到的這些寫作的case的語法算是縮減掉case關鍵字的寫法,對不知道switch case的開發者來說,可能不一定馬上就知道這個就是Dart的switch case,所以對我來說,我如果有個產品型的APP要用邏輯來做,可能還是會盡量減低理解一包code的難度,但如果是接案型APP,不一定需要太多人共同維護,可能就可以用簡單的寫法,但這可能也很看個人啦(笑)

switch (pair) {
case (int a, int b) when a > b:
print('First element greater');
case (int a, int b):
print('First element not greater');
}

上面這種pair的寫法其實可以照著case裡面的輸入型別去決定描述句裡面應該要執行什麼描述句。不過好用不好用,可以自己試試看(笑)先猜不錯用。

接下來會花一點篇幅在Pattern上,在Dart的官方語法上,Pattern放的比Control Flow、Branches還要前面,但因為如果對於從零開始學習Dart的人來說,可能先理解比較基礎的語法邏輯,再進到Pattern這類像是語法糖的章節好像比較好,所以這邊在編排的時候,把Pattern這個章節往後放了。

Patterns

Dart提出了一個挺有趣的概念叫做Pattern,它真正在做的事情有二:

  1. match value 配對值
  2. destructuring 解構賦值

總之,patterns的寫法算是讓編寫者在使用語法的時候可以具有更高的便利性去做原本語法就可以做到的事情,或許這也是寫Flutter的迷人之處(笑)

switch (number) {
// Constant pattern matches if 1 == number.
case 1:
print('one');
}

比如像是上面這樣的寫法,你可以明確的讓數字number在某個case就可以做某件事情,然後也可以這樣寫:

const a = 'a';
const b = 'b';
switch (obj) {
// List pattern [a, b] matches obj first if obj is a list with two fields,
// then if its fields match the constant subpatterns 'a' and 'b'.
case [a, b]:
print('$a, $b');
}

作為switch case的寫作方式,其實當然的可以配合下面這些的數值去進行匹配,這算是蠻多switch case的寫作邏輯常會有的寫法,這邊就不多做描述了:

switch (obj) {
// Matches if 1 == obj.
case 1:
print('one');

// Matches if the value of obj is between the constant values of 'first' and 'last'.
case >= first && <= last:
print('in range');

// Matches if obj is a record with two fields, then assigns the fields to 'a' and 'b'.
case (var a, var b):
print('a = $a, b = $b');

default:
}

然後這邊switch case有下面的寫法,或稱為對列表的模式匹配(list pattern matching)

switch (list) {
case ['a' || 'b', var c]:
print(c);
}

在這個例子中,switch語句使用list作為條件進行匹配,並在case中檢查是否存在一個包含兩個元素的列表,第一個元素是'a''b'(使用邏輯運算符||),第二個元素是一個變數c

看起來這個概念就是可以讓a、b兩種的儲存值可以藉由你的物件宣告去印出你給的數值。這種寫法我好像還沒在Swift中寫過。

不過我真正覺得酷的是下面這種的寫法:

Destructuring解構賦值

var numList = [1, 2, 3];
// List pattern [a, b, c] destructures the three elements from numList...
var [a, b, c] = numList;
// ...and assigns them to new variables.
print(a + b + c);

誠如上面的範例,我可以讓三個數值的list去分出三種的值,讓它可以直接變成是你宣告的名稱,這看起來頗有趣R。

然後變數的宣告也有像是下面這樣對應的寫法,你可以針對不同的賦值進行直接的對應,某種程度上算是給了賦值這件事情比較高延伸性,這樣的寫法挺有趣的!

var (a, [b, c]) = ('str', [1, 2]);

然後如果要針對賦值進行順序的更換,我們也有另一種寫法:

var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "right left".

還有一種pattern的寫作方式我也覺得蠻特別的:

它可以直接針對一個變數值進行switch case的分類,然後最後把返回值直接儲存進去宣告的變數或常數裡面,也就是可以直接把邏輯綁在變數裡面:

var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false
};

不過覺得這個區塊很多的範例其實蠻細碎的,我目前覺得缺乏組織性,如果要一一列舉可能不太必要,建議想學習Dart的朋友可以直接進到官網去看看Pattern的兩者個章節:

overview&usage

pattern type

Error Handling錯誤處理

Dart provides Exception and Error types, as well as numerous predefined subtypes. You can, of course, define your own exceptions. However, Dart programs can throw any non-null object—not just Exception and Error objects—as an exception.

Dart在官方語法裡面有特地的講到錯誤處理的部分,內建了Exception、Error兩種型別,其實這個內建蠻多語言都有的,像行之有年的Swift也就有這個東西

不過下面就是簡單的示範一下throw:

///這個是有Exception type的 throw
throw FormatException("Expected at least 1 section");

///這個是純粹throw字串
throw 'Out of llamas!';

Catch

然後我們很多時候會需要處理特定的狀況,所以會有下面的寫法,我們可能就會提供一個例外的情形在on後面:

try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}

不過,當然寫法上,我們可能還是會有沒有特別指定的型別,所以可能這邊就會用catch去捕捉這種特定的型別,像下面這樣:

try {
breedMoreLlamas();
} on OutOfLlamasException {
// A specific exception
buyMoreLlamas();
} on Exception catch (e) {
// Anything else that is an exception
print('Unknown exception: $e');
} catch (e) {
// No specified type, handles all
print('Something really unknown: $e');
}

如果有遇到沒有定義的特別案例,可能就會用catch去捕獲這個特別的狀態,然後也有一種特別的語法叫做rethrow,如下面這樣,它可以將當前的異常重新拋出如下:

void misbehave() {
try {
dynamic foo = true;
print(foo++); // Runtime error
} catch (e) {
print('misbehave() partially handled ${e.runtimeType}.');
rethrow; // Allow callers to see the exception.
}
}

void main() {
try {
misbehave();
} catch (e) {
print('main() finished handling ${e.runtimeType}.');
}

那,我自己是有個問題,所以throw跟rethrow有什麼差別呢?
ChatGPT給了我這樣的答案:

rethrowthrow 在 Dart 中的使用方式和效果有一些不同:

  1. throw 用於在異常發生的地方創建並拋出一個新的異常。你可以使用 throw 陳述式來拋出自定義的異常,或是拋出內建的異常類型,如 ExceptionError。通常,在 throw 後面會跟著一個異常對象,例如 throw Exception('Something went wrong.');
  2. rethrow 用於在異常處理程式碼(catch 區塊)內重新拋出異常。當你使用 rethrow 時,它會將當前捕獲的異常重新拋出,而不是創建一個新的異常。這樣做的目的是讓異常能夠繼續傳播到上層的異常處理程序。

簡而言之,throw 用於創建並拋出新的異常,而 rethrow 用於在異常處理程序內重新拋出異常。

在你提供的程式碼中,throw 並沒有被使用,而是使用了 rethrow。這是因為在 misbehave() 方法內部,異常已經被捕獲並部分處理,而我們希望該異常能夠被上層的異常處理程序(在 main() 方法中)感知到。因此,使用 rethrow 來重新拋出該異常,使其能夠繼續傳播到 main() 方法的 catch 區塊中進行處理。

Finally

最後,錯誤處理有另外一個語法叫做Finally,這邊有個比較好的詮釋是這樣:

finally 子句允許你定義一段程式碼,無論是否發生異常,都會被執行。這對於確保一些必要的清理操作、關閉資源或回收資源非常有用。即使異常被傳播到更高層級,你也可以確保 finally 子句中的程式碼得到執行,以確保程式的正確運行和資源的正確管理。

以下是直接使用finally的用法,可以直接進行在finally中直接進行這段程式碼一定要使用的工作,比如在下面提到的Always clean up, even if an exception is thrown

try {
breedMoreLlamas();
} finally {
// Always clean up, even if an exception is thrown.
cleanLlamaStalls();
}

所以就算catch了該catch的東西,還是會進到finally裡面去執行某些程式碼L,比如這邊可能就是要清理一些資源:

try {
breedMoreLlamas();
} catch (e) {
print('Error: $e'); // Handle the exception first.
} finally {
cleanLlamaStalls(); // Then clean up.
}

後記

Well,這篇林林總總的講了蠻多東西,有一些語法甚至是新接觸的概念,感覺Dart相對於Swift來說在語法規範上比較多伸展度,但也不知道這些彈性是不是會造成寫的人的困難。

下一篇大概會實際上的講物件導向了,雖然還沒有仔細的看Dart對於物件的設計邏輯,但從基本邏輯上看,似乎沒有像Swift那樣具有Struct的call by value的設計,所有事情都是reference的,不過到底是不是這樣,我們就繼續看下去吧。

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

影山小麥機
影山小麥機

Written by 影山小麥機

本職為Mobile工程師,熱愛分享視野,也樂意站在ChatGPT的肩膀上。訂閱小麥機,收割技術、職涯、人生的難題。

No responses yet

Write a response