聚會需要徽章標籤依名稱順序排列。 每位與會者索取一份或多份,每個標籤上都會包含與會者角色的訊息。 在這個教學中,你會結合一些小型 C# 技巧,產生標籤,而不需要獨立的格式化字串集合。
本教學適合熟悉基本 C# 方法、迴圈與集合的開發者。 你可以選擇短操作的歸屬位置,明確設定輔助依賴關係,並決定程式何時產生結果。 關於這些選擇背後的理由,請參見「建置軟體」。
Prerequisites
- .NET 10 SDK。
- 一個文字編輯器和一個終端機。
每位參加者先貼一個標籤
建立一個名為
BadgeLabels的資料夾,裡面有一個名為Program.cs的檔案。請將以下章節的完整來源複製至
Program.cs。 每個步驟和實驗都用這個檔案:#define BADGE_SORT #define BADGE_COPIES #define BADGE_FORMAT #define BADGE_COUNT // #define BADGE_DIAGNOSTICS // #define BADGE_REPEAT // #define BADGE_SNAPSHOT Attendee[] attendees = [ new("Ben", "Host", 1), new("Ada", "Guest", 2) ]; #if BADGE_SORT #if BADGE_DIAGNOSTICS Console.WriteLine("Before query definition."); #endif var orderedAttendees = attendees.OrderBy( #if BADGE_DIAGNOSTICS attendee => { Console.WriteLine($"Sort key: {attendee.Name}"); return attendee.Name; }, StringComparer.Ordinal); #else attendee => attendee.Name, StringComparer.Ordinal); #endif #if BADGE_DIAGNOSTICS Console.WriteLine("After query definition."); #endif #else var orderedAttendees = attendees; #endif #if BADGE_SNAPSHOT var attendeesToPrint = orderedAttendees.ToList(); #else var attendeesToPrint = orderedAttendees; #endif int passes = 1; #if BADGE_REPEAT passes = 2; #endif for (int pass = 0; pass < passes; pass++) { #if BADGE_COUNT int expected = attendees.Sum(attendee => attendee.Copies); int printed = 0; #endif foreach (Attendee attendee in attendeesToPrint) { #if BADGE_COPIES var labels = CreateLabels(attendee); #if BADGE_DIAGNOSTICS Console.WriteLine($"Sequence created: {attendee.Name}"); #endif foreach (string label in labels) { Console.WriteLine(label); #if BADGE_COUNT printed++; #endif } #else Console.WriteLine($"{attendee.Name} | {attendee.Role}"); #endif } #if BADGE_COUNT if (printed != expected) { throw new InvalidOperationException("Label count doesn't match the request."); } Console.WriteLine($"Printed {printed} of {expected} requested labels."); #endif } #if BADGE_COPIES static IEnumerable<string> CreateLabels(Attendee attendee) { #if BADGE_DIAGNOSTICS Console.WriteLine($"Iterator started: {attendee.Name}"); #endif ArgumentOutOfRangeException.ThrowIfNegativeOrZero(attendee.Copies); #if BADGE_FORMAT string message = attendee.Role switch { "Guest" => "Welcome!", "Host" => "Ask me for help", _ => throw new ArgumentException( $"Unsupported role: {attendee.Role}.", nameof(attendee)) }; #endif for (int copy = 1; copy <= attendee.Copies; copy++) { #if BADGE_FORMAT yield return FormatLabel( attendee.Name, attendee.Role, message, copy, attendee.Copies); #else yield return $"{attendee.Name} | {attendee.Role} [{copy}/{attendee.Copies}]"; #endif } #if BADGE_FORMAT static string FormatLabel( string name, string role, string message, int copy, int total) => $"{name} | {role} | {message} [{copy}/{total}]"; #endif } #endif record Attendee(string Name, string Role, int Copies);在檔案頂端,將四行作用中的
#define行註解掉。 另外三條實驗線也請保留註解狀態。從那個資料夾裡,執行 基於檔案的應用程式:
dotnet run --file Program.cs
該程式輸出:
Ben | Host
Ada | Guest
檔案包含每個階段。 從現在開始,只更改最上方的 #define 行,不要更改方法本體或宣告。
#if指令只有在其符號被定義時才包含代碼;#else分支則提供替代方案。 參見 條件編譯。
在每個步驟取消註解一條額外的階段行,並保持先前的階段行啟用:
| Stage | 作用中階段符號 |
|---|---|
| 每位參加者一張標籤 | None |
| 排序參加者 | BADGE_SORT |
| 建立副本 |
BADGE_SORT,BADGE_COPIES |
| 格式訊息 |
BADGE_SORT、BADGE_COPIES、BADGE_FORMAT |
| 確認計數 |
BADGE_SORT、BADGE_COPIES、BADGE_FORMAT、BADGE_COUNT |
下載的原始碼啟用所有四個階段符號,因此預設會執行最終程式。 另外三個符號控制可選實驗,並在正常進行過程中保持關閉。 這些符號使用 BADGE_ 前綴以避免使用 SDK 定義的符號,例如 TRACE。
以下摘錄為你檔案中已包含的程式碼。 不要把它們複製到整個程式中。
Attendee[] attendees =
[
new("Ben", "Host", 1),
new("Ada", "Guest", 2)
];
Attendee 宣告定義了 一種記錄,這是一種將相關資料分組並提供值相等性的類型。 每位參與者有一個 Name、Role,以及所要求數量的 Copies。 記錄之所以符合這個小型資料模型,是因為程式會在各項操作之間傳遞參加者資料,而不加以更改。 欲了解更多此類資訊,請參閱記錄。
固定輸入使用 陣列與集合表達式。 在每次new(...)呼叫中,C# 都是從陣列元素類型推論出Attendee;參見目標型別的new。
頂層陳述式 讓您能在不寫 Main 方法的情況下執行範例。
迴foreach圈會讀取每位參加者,並用插值字串將其值插入輸出。 迴圈目前忽略 Copies。
在開發程式時,請保留這個小小的輸入:Ben 要求一份,Ada 要求兩份。 只有 Guest 和 Host 是受支援的角色,且複製數量必須是正數。
用 lambda 排序
若要將參加者按姓名順序排列,請取消註解 #define BADGE_SORT。 其他符號就註解掉。排序分支定義了此查詢:
var orderedAttendees = attendees.OrderBy(
#if BADGE_DIAGNOSTICS
attendee =>
{
Console.WriteLine($"Sort key: {attendee.Name}");
return attendee.Name;
}, StringComparer.Ordinal);
#else
attendee => attendee.Name, StringComparer.Ordinal);
#endif
關閉診斷時, #else 分支會供應短 lambda。 循環列印已排序的與會者:
foreach (Attendee attendee in attendeesToPrint)
{
#if BADGE_COPIES
var labels = CreateLabels(attendee);
#if BADGE_DIAGNOSTICS
Console.WriteLine($"Sequence created: {attendee.Name}");
#endif
foreach (string label in labels)
{
Console.WriteLine(label);
#if BADGE_COUNT
printed++;
#endif
}
#else
Console.WriteLine($"{attendee.Name} | {attendee.Role}");
#endif
}
執行同樣的指令。 輸出現在將 Ada 放在首位:
Ada | Guest
Ben | Host
attendee => attendee.Name 是一個 lambda 運算式,一個在程式碼需要某種行為時提供的未命名函式。 此處,lambda 選擇要排序的名稱。 lambda 會保持OrderBy,直接寫在 呼叫中,因為它很短且目的明確。 你可以把選取內容擷取到具名的輔助方法,但方法名稱無法更好地解釋這段單一屬性的選取內容。 參見 Lambda 表達式。
OrderBy 是 語言整合查詢 (LINQ) 的一部分,後者是一組用於資料查詢的 C# 功能和 .NET API。 請參見 LINQ 查詢。
此範例使用 方法語法,直接呼叫 LINQ 方法。 相較之下,查詢語法 使用 C# 子句,例如 from、orderby 和 select:
var orderedAttendees =
from attendee in attendees
orderby attendee.Name
select attendee;
編譯器會將查詢語法轉換成方法呼叫。 查詢語法可以讓包含多個子句的查詢更易閱讀;方法語法讓這個單一排序保持簡短,並讓您可將比較子傳遞給 OrderBy。 查詢語法範例使用預設字串比較器,因此無法保證跨文化的順序相同。 保留此程式使用的方法語法版本。 請參見 LINQ 方法與查詢語法。
為了讓輸出不受機器的地區設定影響,樣本會用 Ordinal 來比較名稱。 這裡的短名字順序是按字母順序排列的。 請參見 LINQ 排序資料。
var宣告讓編譯器推斷查詢變數的型別。 這個 orderedAttendees 變數描述的是查詢,而非已完成的排序集合。 列 舉 序列的意思是一次只讀取一個元素。 當 foreach 首次請求某個元素時,OrderBy 會讀取並排序所有與會者。
注意執行順序
延遲執行是指執行會在程式碼列舉序列時開始,而非在程式碼定義查詢時開始。 請參閱 執行查詢。 使用診斷輸出來觀察差異:
在關卡符號中,僅保留
BADGE_SORT為啟用狀態。在同一個檔案頂端取消註解
#define BADGE_DIAGNOSTICS。 診斷分支以訊息包圍查詢定義,並使用 lambda 來列印每個排序鍵:#if BADGE_SORT #if BADGE_DIAGNOSTICS Console.WriteLine("Before query definition."); #endif var orderedAttendees = attendees.OrderBy( #if BADGE_DIAGNOSTICS attendee => { Console.WriteLine($"Sort key: {attendee.Name}"); return attendee.Name; }, StringComparer.Ordinal); #else attendee => attendee.Name, StringComparer.Ordinal); #endif #if BADGE_DIAGNOSTICS Console.WriteLine("After query definition."); #endif #else var orderedAttendees = attendees; #endif再次執行
dotnet run --file Program.cs。 在讀取輸出之前,先預測Sort key:會出現在After query definition.之前還是之後。
輸出為:
Before query definition.
After query definition.
Sort key: Ben
Sort key: Ada
Ada | Guest
Ben | Host
兩個查詢定義訊息之間不會出現排序鍵。 lambda 只有在 foreach 請求第一個結果時才會執行。 兩個關鍵訊息都會出現在第一個標籤之前,因為 OrderBy 需要每個鍵才能判斷哪位與會者排在前面。 鍵依輸入順序排列,先是 Ben,接著是 Ada;結果依照排序順序排列,先是 Ada,接著是 Ben。
診斷 lambda 需要訊息與回傳鍵,因此它使用帶有大括號和 的return,取代原本的單一表達式主體。
開啟診斷時,試試同一個檔案裡的兩個實驗開關。 不需要對迴圈或查詢做任何更改:
取消註解
#define BADGE_REPEAT,然後再跑一次:Before query definition. After query definition. Sort key: Ben Sort key: Ada Ada | Guest Ben | Host Sort key: Ben Sort key: Ada Ada | Guest Ben | Host第二輪重複這個排序。 查詢變數不會快取結果。
保持
BADGE_REPEAT啟用,取消註解#define BADGE_SNAPSHOT,然後再跑一次:Before query definition. After query definition. Sort key: Ben Sort key: Ada Ada | Guest Ben | Host Ada | Guest Ben | Host這兩個關鍵訊息現在只會在
ToList期間出現一次,而且是在任一遍開始列印標籤之前。 兩張通行證都讀取已儲存的參加者。
這些開關會選擇要列舉的序列及通過次數:
#if BADGE_SNAPSHOT
var attendeesToPrint = orderedAttendees.ToList();
#else
var attendeesToPrint = orderedAttendees;
#endif
int passes = 1;
#if BADGE_REPEAT
passes = 2;
#endif
即時執行意味著操作會立即執行,而非等待後續列舉。
ToList 立即執行查詢並將結果儲存在清單中。 現在兩個階段都能讀取儲存的出席者,無需重新執行排序。 相較之下,定義 OrderBy 查詢仍會延遲排序:只有在列舉開始時才讀取並緩衝所有輸入。 緩衝並不代表查詢在你定義時就會執行。 需要重複使用結果且不重新計算時,使用快照;當你想讓每個列舉讀取當前輸入時,保留延遲查詢。 請參見 「使用快取結果建立查詢」。
在繼續之前,請再次將 BADGE_DIAGNOSTICS、BADGE_REPEAT 和 BADGE_SNAPSHOT 註解掉。 保持 BADGE_SORT 啟用。 執行應用程式確認只有兩個已排序的標籤出現。
用反覆運算器產生所需的副本
若要將每位參與者擴展為所需的標籤數量,請取消 #define BADGE_COPIES 的註解,使其與 BADGE_SORT 一同使用。 迴圈現在選擇先前顯示的 BADGE_COPIES 分支,並呼叫檔案中已存在的迭代器:
#if BADGE_COPIES
static IEnumerable<string> CreateLabels(Attendee attendee)
{
#if BADGE_DIAGNOSTICS
Console.WriteLine($"Iterator started: {attendee.Name}");
#endif
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(attendee.Copies);
#if BADGE_FORMAT
string message = attendee.Role switch
{
"Guest" => "Welcome!",
"Host" => "Ask me for help",
_ => throw new ArgumentException(
$"Unsupported role: {attendee.Role}.", nameof(attendee))
};
#endif
for (int copy = 1; copy <= attendee.Copies; copy++)
{
#if BADGE_FORMAT
yield return FormatLabel(
attendee.Name, attendee.Role, message, copy, attendee.Copies);
#else
yield return $"{attendee.Name} | {attendee.Role} [{copy}/{attendee.Copies}]";
#endif
}
#if BADGE_FORMAT
static string FormatLabel(
string name, string role, string message, int copy, int total) =>
$"{name} | {role} | {message} [{copy}/{total}]";
#endif
}
#endif
再執行一次應用程式:
Ada | Guest [1/2]
Ada | Guest [2/2]
Ben | Host [1/1]
CreateLabels 是一個 迭代器,一個一次產生序列元素的函數。 其 IEnumerable<T> 回傳型別,記作 IEnumerable<string>,代表一串字串。 每個 yield return 將一個標籤提供給內層 foreach 迴圈,內層 foreach 迴圈先列印該標籤,再申請下一個標籤。 參見 迭代器。
迴for圈會將副本數從 1 計算到 attendee.Copies。 一位參加者會依照 Copies 的請求數量產生相同數量的字串。 集合也可以,但呼叫者每個標籤只需要一次。 這個迭代器避免了獨立的標籤集合,並將 copy number 迴圈保持在一起。 外圈與內 foreach 圈將參加者的訂單與標籤製作分開;呼叫端仍負責印刷。
呼叫 CreateLabels(attendee) 不會執行其主體。 與 LINQ 查詢類似,迭代器使用延遲執行。 內部 foreach 會開始此作業,包括正向複製檢查。 新的列舉作業會再次走訪迭代器。 與 OrderBy 不同,該迭代器會在不建立其餘標籤的情況下提供一個標籤。
要觀察迭代器的執行順序,請再次取消註解 #define BADGE_DIAGNOSTICS 。 讓 BADGE_REPEAT 和 BADGE_SNAPSHOT 保持關閉。 診斷現在同時顯示排序和迭代器:
Before query definition.
After query definition.
Sort key: Ben
Sort key: Ada
Sequence created: Ada
Iterator started: Ada
Ada | Guest [1/2]
Ada | Guest [2/2]
Sequence created: Ben
Iterator started: Ben
Ben | Host [1/1]
對每位參與者而言,由 sequence 建立的訊息會出現在由 iterator 啟動的訊息及標籤之前。 建立序列不會進入迭代器本體;請求第一個元素則是。 繼續之前,請再次將 BADGE_DIAGNOSTICS 註解掉。
相同的實驗對照組在後期階段仍然可用。
BADGE_REPEAT 會將參加者列舉兩次,並重新建立每位參加者的標籤序列。
BADGE_SNAPSHOT 它只快取已排序的出席者,而非標籤字串,因此可避免再次排序,但不會阻止迭代器在每次遍歷時再次執行。
ThrowIfNegativeOrZero 檢查會報告無效輸入,而不是默默地不產生任何標籤。 例外會回報中斷正常執行的失敗;請參見建立並拋出例外。 此時,無效的複製數量會停止該程式,而非默默跳過該參加者。
計算訊息值並命名格式步驟
每個職位都需要不同的訊息。 取消註解 #define BADGE_FORMAT,並讓 BADGE_SORT 和 BADGE_COPIES 保持啟用。 同一迭代器現在包含訊息計算:
#if BADGE_FORMAT
string message = attendee.Role switch
{
"Guest" => "Welcome!",
"Host" => "Ask me for help",
_ => throw new ArgumentException(
$"Unsupported role: {attendee.Role}.", nameof(attendee))
};
#endif
它也會選擇 yield return FormatLabel(...) 分支,並包含以下本地輔助工具:
#if BADGE_FORMAT
static string FormatLabel(
string name, string role, string message, int copy, int total) =>
$"{name} | {role} | {message} [{copy}/{total}]";
#endif
再執行一次應用程式:
Ada | Guest | Welcome! [1/2]
Ada | Guest | Welcome! [2/2]
Ben | Host | Ask me for help [1/1]
該 switch 表達 式會為每位參加者計算一次訊息,並在複製迴圈之前完成計算。 表達式之所以適合,是因為每個角色會選擇一個值來指派給 message。
if
/
else 串聯或 switch 語句都可行,但會在每個分支中重複該指派。
模式匹配 是將表達式與指定的模式進行測試。 此處,attendee.Role 是輸入表達式,而 常數模式"Guest" 和 "Host" 會測試其求值結果,以選擇相符的訊息。
棄置模式_會匹配任何其他角色並拋出例外,避免對未支援的角色產生誤導性的預設訊息。 請參見 模式比對。
nameof 運算子會將參數名稱 attendee 提供給例外。 與硬編碼字串不同,只要用重構工具重新命名參數,這個名稱依然正確。
FormatLabel 是一個 本地函式,是另一個函式中宣告的命名函數。 因為只有 CreateLabels 需要格式化操作,將其置於 CreateLabels 內,會將輔助函式限制於使用它的該操作。 類別中的私有成員方法會讓該類別的其他成員也能使用該輔助器。 如果有多個操作需要相同的格式化操作,請選擇更廣泛的範圍。 參見 本機函式。
這個名稱 FormatLabel 讓你可以直接跟著複製流程走,不用看格式細節。 插值字串直接留在 yield return 裡面也可行,就像前一步一樣。 現在這個名稱變得更有用,因為標籤包含了特定角色的訊息。 輔助程式的 => 運算式主體 會傳回該格式化字串。
所有格式輸入皆經由參數傳入。 非靜態的本地函式可以 擷取或使用其周圍作用域的變數。
static修飾符會防止該擷取發生,因此其依賴關係保持明確。 詳見 參數與捕獲狀態。
由於 CreateLabels 仍是迭代器,角色檢查也僅在列舉開始時執行。 對於這種受控輸入,這些檢查應歸入標籤產生流程。 該計畫並未承諾在列印標籤前驗證所有參加者的身份。
驗證總數並執行完整程式
若要比較實際標籤產量與請求數量,請取消註解 #define BADGE_COUNT。 四個階段符號現在全部已啟用。 將三個實驗符號保持為註解狀態,以作為正常的最終輸出。
計數設定會先於參加者循環進行:
#if BADGE_COUNT
int expected = attendees.Sum(attendee => attendee.Copies);
int printed = 0;
#endif
Sum 使用 eager 執行來加總輸入陣列中的複製次數。 與 OrderBy不同,它回傳一個數字,而非日後列舉的序列。 它的 lambda attendee => attendee.Copies,會直接選取複製次數。 就像名稱選擇一樣,使用獨立的輔助方法無法釐清這個簡短的表達式。 帶有累加器的迴路也可以; Sum 直接說明你想要一個總額。 參見 常見的 LINQ 方法。
在 label 迴圈中的 printed++ increment 只會在迴圈收到標籤時增加計數器。 迴圈結束後,if 陳述式會將該計數與請求的總數比較,若兩者不符則拋出例外:
#if BADGE_COUNT
if (printed != expected)
{
throw new InvalidOperationException("Label count doesn't match the request.");
}
Console.WriteLine($"Printed {printed} of {expected} requested labels.");
#endif
你已經進入一開始複製的完整程式的預設設定。 不需要變更的方法或宣告:
#define BADGE_SORT
#define BADGE_COPIES
#define BADGE_FORMAT
#define BADGE_COUNT
// #define BADGE_DIAGNOSTICS
// #define BADGE_REPEAT
// #define BADGE_SNAPSHOT
用 dotnet run --file Program.cs 來執行。 輸出為:
Ada | Guest | Welcome! [1/2]
Ada | Guest | Welcome! [2/2]
Ben | Host | Ask me for help [1/1]
Printed 3 of 3 requested labels.
輸入陣列已經存在於記憶體中。
Sum 立即讀取,OrderBy 當參加者迴圈開始時排序,並且 CreateLabels 會隨著內部迴圈推進而產生字串。 程式從不建立獨立的標籤集合。 如需更廣泛的查詢指引,請參見 LINQ 與集合。
若要重新檢視最終程式的診斷,請在實驗符號中只啟用 BADGE_DIAGNOSTICS。 輸出為:
Before query definition.
After query definition.
Sort key: Ben
Sort key: Ada
Sequence created: Ada
Iterator started: Ada
Ada | Guest | Welcome! [1/2]
Ada | Guest | Welcome! [2/2]
Sequence created: Ben
Iterator started: Ben
Ben | Host | Ask me for help [1/1]
Printed 3 of 3 requested labels.
如果你也啟用 BADGE_REPEAT,每次執行都會檢查並報告各自的計數。 啟用時 BADGE_SNAPSHOT ,排序鍵只會出現一次,但迭代器的訊息、標籤和計數報告仍會在每次執行中出現。 將實驗符號註解掉以恢復正常的最終輸出。
Summary
每種技術都服務於同一任務的不同部分:
- Lambda 和 LINQ 會內嵌選取名稱和計數,以用於排序和總數。
- 迭代器 表達自訂一對多的產生,並將標籤的處理留給呼叫者。
-
一個
switch表達式 計算角色特定的值,並明確表示無支援的角色。 - 本地函式為格式化步驟命名、限制其作用範圍,並陳述其輸入。
當這些形式的範圍、依賴性及執行順序符合操作時,請選擇這些形式。 直接迴圈、共享方法或已完成的集合能更適合其他呼叫者。