標籤 breakcontinue 語句

Note

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異已記錄在相關的 語言設計會議(LDM)備忘錄中。

您可以在 規範的文章中深入瞭解將功能規範納入 C# 語言標準的過程。

總結

允許 breakcontinue 語句可選擇性地指定一個標籤,標示要鎖定的迴圈或 switch 語句,讓巢狀結構中的控制流程更乾淨,無需使用 goto 語句或其他扭曲,如巢狀函數、元組回傳等。

動機

在處理巢狀迴圈或包含 switch 語句的迴圈時,開發者常需要從內部情境中跳出或延續外部迴圈。 目前有兩種主要方法可達成此目標,且各有重大缺點:

使用 goto 陳述

string foundValue = null;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            goto FOUND;
    }
}
FOUND:
ProcessValue(foundValue);

雖然 goto 可行,但它需要在迴圈結構後加上標籤,且無法清楚傳達要從特定迴圈中斷出的意圖。 若繼續外環,方法會更加尷尬:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
    }
    CONTINUE_OUTER: ;
}

此模式令人困惑,因為標籤必須放置在環體末端,緊鄰閉合支撐,以便進行增量器與條件檢查。 當 break 同一外環同時需要 和 continue 時,則需兩個獨立標籤:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
        
        if (ShouldExitAll(x, y))
            goto BREAK_OUTER;
    }
    CONTINUE_OUTER: ;
}
BREAK_OUTER:
// Subsequent statements

使用旗標變數

string foundValue = null;
bool shouldBreak = false;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
        {
            shouldBreak = true;
            break;
        }
    }
    if (shouldBreak)
        break;
}
ProcessValue(foundValue);

此方法需要額外的狀態管理,增加程式碼冗長度,並模糊控制流程意圖。

建議的解決方案

標記為 breakcontinue時,程式碼會變得更清晰且易於維護:

string foundValue = null;
outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            break outer;
    }
}
ProcessValue(foundValue);

標籤直接放置在它所識別的迴圈上,而 break/continue 語句則明確指定其目標。 繼續:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
    }
}

這自然表達「繼續外環」,避免與 S 相關的 goto標籤放置混淆。 兩個操作都可以用同一個標籤:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
        
        if (ShouldExitAll(x, y))
            break outer;
    }
}

此功能在 C# 社群中被廣泛請求,討論可追溯數十年,且持續被重新提出與請求。 類似的特徵在其他幾種現代語言中也有:

在所有這些情況下,語言的運作方式與本規範相同。 也就是說,有些結構可以有標籤,並且可以從它們各自的 continue or break 陳述中引用該標籤。

詳細設計

以下更新以 C# 7 標準(statements.md)相應章節的差異呈現。 在本節中, 劃線 表示文本從現有規範中移除, 粗體 表示新增文字。 原文原文逐字引用以供背景參考。

§13.5 標註語句

請在現有段落「標籤可從 goto 該標籤範圍內的陳述(§13.10.4)中引用」後插入以下段落:

如果直接巢狀於labeled_statement中的語句switch_statement§13.8.3)或iteration_statement§13.9),則該巢狀語句稱為標示了labeled_statement識別碼break_statement§13.10.2)或continue_statement§13.10.3)可以指定此類識別碼來參考包含標籤的語句。

注意:只有直接嵌套在labeled_statement中的語句才會標示該識別碼。 例如,給定 a: b: while (…) …,僅b標記iteration_statement;a標記內層labeled_statement,而內層b: while (…) …本身並非switch_statementiteration_statement。 因此, break a; 出現在迴圈體中的 或 continue a; 並不針對該 while 陳述。 結尾註釋

§13.10.2 斷裂陳述

break_statement
    : 'break' identifier? ';'
    ;

break 陳述句會從最近的包裹 switchwhiledoforforeach 陳述句中退出。

break 語句會從最近的包裹 switch_statement§13.8.3)或 iteration_statement§13.9)退出,或者如果指定了識別 ,則會從最近的包裹 switch_statementiteration_statement 出該 識別碼 (見 §13.5)。

陳述的目標break是最近包圍switchwhile、、doforforeach陳述陳述的終點,如上所述若陳述 break 句未被 switchwhiledoforforeach 陳述句包圍,則會發生編譯時錯誤。若不存在此類封函式,則會發生編譯時錯誤。

當多個switchwhiledoforforeach 語句彼此巢狀時,break語句只會套用至最內部的語句。 若要跨多個巢狀層級轉移控制,可以使用goto語句(§13.10.4)。

語句 break 無法結束 finally 區塊 (~13.11)。 當break語句出現在finally區塊內時,break語句的目標須位於相同的finally區塊內,否則會發生編譯時期錯誤。

break語句的執行方式如下:

  • break如果語句離開一個或多個具try區塊的相關聯finally區塊,則控制權將最初傳送至最內層finally語句的try區塊。 當控件到達區塊的 finally 結束點時,控件會傳送至 finally 下一個封入 try 語句的 區塊。 此過程會重複進行,直到執行所有中間 finally 語句的 try 區塊為止。
  • 控制 轉移至 語句目標 break

因為break語句無條件地將控制權轉移到其他地方,所以無法到達break語句的終點。

範例:標記 break 的 解析為最近且標籤相符的包圍 switch_statementiteration_statement

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i * j > 20)
            break outer; // exits the outer for-loop
    }
}

結束範例

§13.10.3 繼續陳述

continue_statement
    : 'continue' identifier? ';'
    ;

continue 陳述句開始對最近的包圍 whiledoforforeach 陳述進行新的迭代。

continue語句會開始最近包圍 iteration_statement§13.9)的新迭代,或者如果指定了識別,則以該示標記的最近包圍iteration_statement(見 §13.5)。

陳述的continue目標是最近包圍while語句、doforforeach或陳述句的嵌入陳述的終點,iteration_statement如上確定若陳述 continue 未被 whiledoforforeach 陳述包圍,則會發生編譯時錯誤。若不存在此類封函式,則會發生編譯時錯誤。

當多個 whiledoforforeach 語句彼此巢狀時, continue 語句只會套用至最內部的語句。 若要跨多個巢狀層級轉移控制,可以使用goto語句(§13.10.4)。

語句 continue 無法結束 finally 區塊 (~13.11)。 當continue語句出現在finally區塊內時,continue語句的目標須位於相同的finally區塊內,否則會發生編譯時期錯誤。

continue語句的執行方式如下:

  • continue如果語句離開一個或多個具try區塊的相關聯finally區塊,則控制權將最初傳送至最內層finally語句的try區塊。 當控件到達區塊的 finally 結束點時,控件會傳送至 finally 下一個封入 try 語句的 區塊。 此過程會重複進行,直到執行所有中間 finally 語句的 try 區塊為止。
  • 控制 轉移至 語句目標 continue

因為continue語句無條件地將控制權轉移到其他地方,所以無法到達continue語句的終點。

範例:標記為 continue 的 解析為最近且標籤相符的包圍 iteration_statement

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (ShouldSkip(i, j))
            continue outer; // continues the outer for-loop
    }
}

結束範例

缺點/替代方案

繼續使用 goto 陳述

C# 已經支援 goto,可以實現相同的控制流程。 然而,與標示的中斷/繼續相比,有 goto 幾個缺點:

  • 需要分開與繼續情境的標籤(分開標籤放在迴圈後,繼續標籤放在結束支架前)
  • 標籤的位置比較不直覺,且會根據你是斷裂還是繼續而有所不同
  • 對於意圖的說明較少(跳到某個地點,還是打破或繼續特定循環)
  • 脆弱且易出錯:開發者必須確保標籤與目標結構之間不會意外放置陳述。 例如,當 goto END_LOOP; 後接 END_LOOP:時,維護時很容易不小心插入陳述句,破壞原本的控制流程。 有標籤的環路透過直接將標籤綁定到構造體上,避免此問題。
  • 帶有歷史上的污名,標示為「中斷/繼續避免」

使用旗標變數

如動機部分所示,旗標變數雖然有效,但增加了大量模板化,並模糊了控制流程的邏輯。

使用 break Ncontinue N 與數值等級一起使用

  • 重構時脆弱(新增或移除迴圈層級需要更新所有數值參考)
  • 較難閱讀(必須數等級才能理解目標)
  • 比起命名標籤來得不那麼明確
  • 缺乏清晰度(1基?0基?)

重構成不同的方法

雖然這通常是良好的做法,但並非總是可行或適當,有時反而會為本應簡單的控制流程帶來不必要的複雜性。

本提案整合並回應以下社群討論:

未解決的問題

標籤語意

現行規範將 的 break identifier/continue identifier 語意定義為尋找最內層適用的迴圈/交換結構,並以標準 break/continue 語意分配至該結構。 另一種形式化說法是,該 break identifier/continue identifier 用與「goto」相同的規則來識別標籤。 如果標籤直接包含一個環路/切換結構包圍中斷/繼續,那麼中斷/繼續就是該迴圈/開關所適用的。

這兩種形式化實際上是相同的,允許與禁止相同的程式集合。 本樣本所採用的方法,既是概念上的簡單,也是字面上的簡化。 它不必涵蓋標籤範圍、識別碼綁定(如 goto 同它一樣),或是必須定義外向內綁定邏輯來解決迴圈/切換與繼續/中斷語句。 相反地,它只是擴展了簡單的規範語言,根據中斷/繼續尋找適當的包圍迴圈/開關,讓它能延伸到最內層,甚至超越那個層次。

如果 LDM 覺得這更緊貼 goto+label 語意,調整規格就不難。 如果團體認為後者形式比這裡的形式更自然,我會保留這個問題。

void M()
{
  label:
  Console.WriteLine();
  
  foreach (var x in ...)
  {
    break label;
    // should this scenario fail because:
    // 1. the identifier lookup fails, or
    // 2. the label is rejected (not a valid label for a `break` since not attached to a loop construct) after being found?
  }
}

標籤 的宣告規範:

每個區塊或switch_block建立一個獨立的標籤宣告空間。 名稱透過 labeled_statements 引入此宣告空間,並透過 goto_statements 來引用名稱。 區塊的標籤宣告空間包含任何巢狀區塊。 因此,在巢狀區塊內,不可能宣告與封閉區塊中同名的標籤。

註語句的說明:

標籤的範圍是宣告標籤的整個區塊,包括任何巢狀區塊。 當兩個具有相同名稱的標籤範圍重疊時,會產生一個編譯時錯誤。
標籤可從 goto 語句(§13.10.4)在標籤範圍內被參考。

巢狀標籤

應該 a: b: while (true) continue a; 被支持嗎?

建議:不。 沒有人要求過這個。 目前尚未提出有說服力的使用案例。 大多數主流語言不允許,社群中也沒有人抱怨。 lang(和 impl)則更簡單且清晰,嚴格要求只有包含帶有標籤的直接語句標示迴圈/開關。

設計會議

TBD