瑣碎的想法(五)for 的前世今生

Kwanwooo發表於2023-01-11

for

起因

記得大學上C語言的課,第一次遇到的問題就是迴圈結構裡面的 for

選擇結構的 if 非常易懂,和日常生活的判斷沒有區別。

迴圈結構的 while 同樣比較好理解。

本質上是一個判斷

  • 如果為真,繼續迴圈。
  • 如果不假,則退出迴圈。

for 會稍微複雜一些。

for (init-expr; test-expr; update-expr)
    body-statement
  1. 初始化表示式只執行一次
  2. 判斷表示式執行
    • 判斷為真執行迴圈體
    • 判斷為假退出迴圈體
  3. 執行更新表示式

在實際的語義等同於(唯一區別是init-expr一個是內部變數,一個是外部變數)

    init-expr
    while (test-expr) {
        body-statement
        update-expr
    }

那麼我們為什麼要設計一個不那麼好理解的迴圈結構呢?

因為這時候才入了程式設計的門————抽象,以及約定。

如果我們再往底層挖,會發現在組合語言中是不存在whilefor 關鍵字的。

最開始的程式總是從左到右,從上到下一條路走到黑的。

後面程式設計人員意識到編寫重複的程式碼過於麻煩才創造了 loop

所以最開始需要人工寫一個for或者while迴圈。

while 好理解在於和自然語言(英語)完全符合。

    當 條件滿足 時, {
        執行 流程;
    }

for 迴圈的好處在於規範了 while 的使用。

  1. 初始化語句(init-expr)一般只用於迴圈,所以放在內部,便於回收變數。
  2. 迴圈條件(test-expr)一般配合更新語句一起使用(update-expr),實現迴圈有限次數。
  3. 三者的拆分使編寫大段的迴圈或者巢狀迴圈時,更易讀。
// 傳統while迴圈
    int i = 0;
    while (i < 10) {
        handleX();
        i++;
        int j = 0;
        while (j < 5) {
            handleY();
            j++;
        }
    }
// for迴圈
    for (int i = 0; i < 10; i++) {
        handleX();
        for (int j = 0; j < 5; j++) {
            handleY();
        }
    }

所以 for 迴圈的出現也意味著程式設計人員開始在意的不僅僅是功能,而且看重可讀性。

然而這並不會被滿足。

之後還出現了

  1. 增強 for,部分語言的for-each,for...in
  2. lamdba 表示式中的 forEach() 方法。

注意:以下按 Java 實現的 foreach 舉例。(其他程式語言不太熟悉)

增強 for

foreach 的規則

  1. 所有使用 foreach 的集合都必須實現 Iterable 介面
  2. 透過 iterator() 獲取 iterator 物件
  3. 透過 iterator.hasNext() 判斷是否存在元素。
  4. 透過 iterator.next() 獲取下一個元素。
  5. 透過 iterator.remove() 移除返回的元素。(可選)

增強 for 的語法

    List<String> list = Arrays.asList("1", "2", "3", "4", "5");
    // for 版本
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    // foreach 版本
    for (String e : list) {
        System.out.println(e);
    }
    // 去"糖"後的while版本
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }

可以看出,foreach 對於 for、while 來說,好處是更加簡單,符合直覺。

  1. 無需判斷集合個數和中間變數減少程式碼出錯可能性,統一透過 hasNext() 處理。
  2. 不用分析每個集合類如何獲取元素,統一透過 next() 處理。
  3. 透過語法糖隱藏了 hasNext(), next() 邏輯,程式碼更易讀。

forEach()

Java 7/8 受到了函式式語言的影響,實現了更簡練的寫法。

    // Iterable<T> 內實現的forEach
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    List<String> list = Arrays.asList("1", "2", "3", "4", "5");
    // forEach()
    list.forEach(i -> System.out.print(i));
    // 方法引用
    list.forEach(System.out::print);

可以看出,forEach()的好處顯而易見。

  1. 程式碼量比 foreach 更少,只關注遍歷元素,甚至連元素型別都可以省略。
  2. 使用了方法引用後更進一步,我們關注的是這個集合執行了哪些操作,遍歷每一次的含義在forEach()的方法已經體現了,甚至不需要寫遍歷的元素。

總結

可以看出,程式設計人員一直追尋的是更簡單,更易讀的程式碼。

  • 他們不滿足於組合語言一遍遍的寫同一行程式碼,創造了 while
  • 不滿足於 複雜或多層 while 的不可讀, 創造了 for
  • 不滿足於 for 迴圈每一次定義的中間變數,創造了 foreach
  • 不滿足於 foreach 需要迴圈每一次的元素,利用了lamdba 的 Consumer, 去掉了元素。

相關文章