โหมดมืด
บทที่ 3 — Control Flow (if, switch expression, pattern matching, loop)
C# มี control flow (การควบคุมลำดับการทำงาน — if, loop ฯลฯ) พื้นฐานเหมือนภาษาตระกูล C (C-family = กลุ่มภาษาที่มี syntax คล้ายภาษา C เช่น Java, JavaScript, C++ — ใช้ {} ครอบบล็อก, if/for/while แบบเดียวกัน) — แต่ตัวเด็ดของบทนี้คือ switch expression + pattern matching (การจับคู่รูปแบบ) ที่ทันสมัย
ตัวอย่างเล็ก ๆ ก่อนเข้าเนื้อหา — เปรียบเทียบโค้ดเดิม vs pattern matching:
💡 ตัวอย่างด้านล่างใช้ method declaration (การประกาศฟังก์ชัน) แบบย่อ — เรียนเต็มในบทที่ 4 ตอนนี้แค่ดูรูปแบบได้เลย
csharp
int n = 5;
// แบบเดิม — if/else ซ้อน ๆ
string label;
if (n < 0) label = "negative";
else if (n == 0) label = "zero";
else label = "positive";
// แบบ switch expression + pattern matching — สั้น อ่านไหลเป็นนิพจน์เดียว
string label2 = n switch
{
< 0 => "negative",
0 => "zero",
_ => "positive"
};แค่นี้พอเห็นหน้าตา — เดี๋ยวจะค่อย ๆ อธิบายว่ารูปแบบ (pattern) แต่ละชนิดมีอะไรบ้าง
1. if / else
การตัดสินใจพื้นฐานใน C# ใช้ if/else if/else — เหมือนภาษาตระกูล C ทั่วไป (เงื่อนไขในวงเล็บ, ปีกกาครอบ block ถ้ามีหลายบรรทัด):
csharp
int score = 75;
if (score >= 80)
Console.WriteLine("A");
else if (score >= 70)
Console.WriteLine("B");
else
Console.WriteLine("C");Ternary expression:
csharp
string grade = score >= 80 ? "A" : "B";2. switch — แบบเก่า (statement)
switch statement แบบดั้งเดิมเลือกทำตามค่า — ต่างจากภาษาอื่นตรงที่ C# บังคับให้แต่ละ case จบด้วย break (กัน fall-through = ไหลลง case ถัดไป โดยไม่ตั้งใจ) ส่วน switch expression แบบใหม่อยู่หัวข้อถัดไป:
csharp
int status = 200; // ประกาศตัวแปรก่อน — เปลี่ยนเป็นค่าอื่นเพื่อทดสอบ
switch (status)
{
case 200:
case 201:
Console.WriteLine("ok");
break;
case 404:
Console.WriteLine("not found");
break;
default:
Console.WriteLine("other");
break;
}⚠️ ต้อง
break;ทุก case (ไม่มี fall-through แบบ C++)
3. switch expression (C# 8+) — ใหม่และดี
csharp
int status = 404; // ใช้ status จากตัวอย่างข้างบน หรือประกาศใหม่ได้
string label = status switch
{
200 or 201 => "ok",
404 => "not found",
>= 500 => "server error",
_ => "other" // _ = กรณีอื่น ๆ ที่เหลือ (default)
};ความต่างจาก statement:
- คืนค่า ออกมาตรง ๆ
- ใช้
=>แทน: - ใช้
,คั่น - ไม่มี
break; - บังคับให้ครบทุกกรณี (exhaustive) — ถ้าไม่ครบ compiler จะเตือน (warning CS8509) ตอน compile และถ้ามีค่าที่ไม่ตรง arm (= แขนง/กรณีหนึ่งของ switch expression — แต่ละบรรทัด
value => resultคือ 1 arm) ไหนเลยตอนรันจริง จะ throwSwitchExpressionException⚠️ สำหรับ open type (เช่น
int,string) compiler อาจไม่เตือนเสมอไปถ้าไม่มี_arm — ควรใส่_ => ...ไว้เสมอ เพื่อกันSwitchExpressionExceptionที่เกิดขึ้นเงียบ ๆ ตอนรัน
4. Pattern Matching (การจับคู่รูปแบบ) — สุดยอดของ C# modern
"Pattern matching" = การตรวจว่าค่าตรงกับ "รูปแบบ" ที่เรากำหนดไหม (เช่น เป็นชนิดนี้ไหม, อยู่ในช่วงนี้ไหม, มี property แบบนี้ไหม) แล้วดึงค่าข้างในออกมาใช้ได้ในตัว
4.1 Type pattern (จับตามชนิด)
csharp
object obj = 42;
if (obj is int n)
{
Console.WriteLine(n * 2); // ถ้า obj เป็น int → ได้ตัวแปร n ใช้ในบล็อกนี้
}
// switch expression
string describe = obj switch
{
int i => $"int {i}",
string s => $"string '{s}'",
null => "null",
_ => "other"
};
// หมายเหตุ: pattern ใน switch expression ตรวจจากบนลงล่าง — ลำดับสำคัญ
// arm ที่กว้าง (เช่น _ หรือ { ... } ที่ match ทุกอย่าง) ต้องอยู่ล่างสุด4.2 Relational pattern (จับตามการเปรียบเทียบ มากกว่า/น้อยกว่า; C# 9+)
csharp
int age = 25; // ประกาศตัวแปรก่อน — เปลี่ยนค่าเพื่อทดสอบ
string size = age switch
{
< 13 => "child",
>= 13 and < 20 => "teen",
>= 20 and < 65 => "adult",
_ => "senior"
};4.3 Property pattern (จับตามค่า property ข้างใน object)
csharp
// record = class แบบสั้น (เรียนเต็มบท 10) — ตอนนี้แค่รู้ว่าเป็น object ที่มี Name, Age, City
// สร้าง object ด้วย new Person("Alice", 17, "Bangkok") ได้เลย — ค่าถูกส่งตามลำดับ
public record Person(string Name, int Age, string City);
var person = new Person("Alice", 17, "Bangkok");
string describe = person switch
{
null => "ไม่มีคน", // กัน NullReferenceException
{ Age: < 18 } => "minor",
{ City: "Bangkok", Age: >= 18 } => "adult bkk",
{ Name: var n } => $"someone named {n}",
_ => "unknown" // กันเหนียว
};⚠️ ระวัง property pattern แบบ
{ ... }จะ match เฉพาะ object ที่ "ไม่ใช่ null" — ถ้าไม่มีnullarm และไม่มี_arm แล้วส่งpersonที่เป็น null เข้ามา จะได้SwitchExpressionExceptionตอนรัน อีกวิธีคือเขียนnot null { Age: < 18 }ให้ชัด
4.4 Tuple pattern (จับหลายค่าพร้อมกัน)
csharp
string Classify(int x, int y) => (x, y) switch
{
(0, 0) => "origin",
(_, 0) => "on x-axis",
(0, _) => "on y-axis",
var (a, b) when a == b => "diagonal",
_ => "other"
};4.5 List pattern (จับตามรูปร่างของ list/array; C# 11+)
csharp
int[] arr = { 1, 2, 3 };
string desc = arr switch
{
[] => "empty", // ว่าง
[var x] => $"single {x}", // มีตัวเดียว
[_, _] => "pair", // มีสองตัว
[1, .., 9] => "starts 1 ends 9", // ขึ้นต้นด้วย 1 ลงท้ายด้วย 9 (.. = ตรงกลางกี่ตัวก็ได้)
[var first, .. var rest] => $"first={first}, rest count={rest.Length}"
};💡 Pattern matching = "ภาษาย่อยในภาษา" — ใช้เป็นจะลด if/else เป็นกอง ๆ ให้เหลือ expression (นิพจน์) สั้น ๆ
5. Loops
C# มี loop ครบทุกแบบ — for (รู้จำนวนรอบ), foreach (วนสมาชิกใน collection), while/do-while (วนตามเงื่อนไข) พร้อม break/continue คุมการวน:
for
csharp
for (int i = 0; i < 10; i++)
Console.WriteLine(i);foreach
csharp
string[] names = { "Alice", "Bob" };
foreach (var name in names)
Console.WriteLine(name);
// ใช้ได้กับทุกอย่างที่เป็น IEnumerable<T> (= 'สัญญา' ว่า object นี้วนทีละตัวได้ เช่น array, List, Dictionary ล้วนทำตามสัญญานี้ทั้งนั้น)
// ตัว <T> = generic ไว้บอกว่าเป็นรายการของ type อะไร เช่น <int> = รายการ int, <string> = รายการ string — เรียนเต็มบท 7
var scores = new List<int> { 80, 90, 70 };
foreach (var s in scores)
Console.WriteLine(s);while / do-while
csharp
int i = 0;
while (i < 10) { i++; }
do { /*...*/ } while (i < 10);break / continue
csharp
int[] numbers = { -1, 50, 200 }; // ประกาศ array ก่อนใช้
foreach (var n in numbers)
{
if (n < 0) continue; // ข้ามรอบนี้ วนต่อ
if (n > 100) break; // หยุดวนทั้งหมด
Console.WriteLine(n);
}Range + index (การตัดช่วงและนับจากท้าย; C# 8+)
csharp
int[] nums = { 0, 1, 2, 3, 4 };
foreach (var n in nums[1..4]) // 1,2,3 (slice = ตัดช่วงย่อย)
Console.WriteLine(n);
var last = nums[^1]; // 4 (^ = นับจากท้าย, ^1 คือตัวสุดท้าย)
var lastTwo = nums[^2..]; // 3, 4💡 หมายเหตุ:
nums[1..4]บน array จะ allocate array ใหม่ (สำเนา ไม่ใช่ view) — ถ้าอยากได้แบบไม่ก๊อปสำหรับโค้ดที่เน้น performance ใช้nums.AsSpan()[1..4]แทน ⚠️Span<T>เป็น stack-only type — เก็บใน field ของ class หรือใช้ข้ามawaitไม่ได้ เหมาะเฉพาะโค้ด synchronous ที่เน้นประสิทธิภาพ — ถ้าต้องใช้ใน async code ใช้Memory<T>แทน
6. Exception handling (การจัดการข้อผิดพลาด — เกริ่นก่อน, สอนเต็มบท 8)
เมื่อโค้ดเจอข้อผิดพลาด (เช่นแปลง string ที่ไม่ใช่ตัวเลข) มันจะโยน "exception" (ข้อผิดพลาดที่เกิดตอนรัน) — ห่อด้วย try/catch เพื่อดักจับและจัดการแทนการปล่อยให้โปรแกรมล่ม (รายละเอียดเต็มอยู่บทที่ 8):
csharp
try
{
int n = int.Parse(input);
}
catch (FormatException ex)
{
Console.WriteLine($"Bad number: {ex.Message}");
}
catch (IOException ex) when (ex.HResult == -2147024784) // ตัวอย่าง: HResult = disk full (Windows-specific)
{
// filter expression = เงื่อนไขเพิ่มเติมว่าจะจับ exception นี้ไหม (when = ตัวกรอง, เรียนเต็มบท 8)
// ❌ อย่าใช้ ex.Message.Contains("...") กรอง — ข้อความ exception เปลี่ยนตาม locale/version → เปราะมาก
// ให้ใช้ ErrorCode, HResult (= รหัสตัวเลขของ error บน Windows สืบทอดมาจาก COM
// ค่า -2147024784 = 0x80070070 = ERROR_DISK_FULL บน Windows เท่านั้น — Linux/Mac ใช้ค่าต่างออกไป
// ดูค่าได้จาก docs.microsoft.com หรือ winerror.h), หรือ property อื่น ๆ ของ exception แทน
// ⚠️ HResult เป็น Windows-specific — code ที่กรองด้วย HResult จะไม่ portable ข้ามแพลตฟอร์ม
// วิธีที่ portable กว่าคือสร้าง custom exception subclass และกรองตาม type แทน
}
finally
{
Console.WriteLine("always run"); // ทำเสมอ ไม่ว่าจะ error หรือไม่
}7. using statement — การคืนทรัพยากร (resource cleanup)
ทรัพยากรที่ต้องคืน (ไฟล์, stream คือสายข้อมูล, DB connection คือการเชื่อมต่อฐานข้อมูล) ควรปิดเสมอแม้เกิด error
using ช่วยจัดการให้อัตโนมัติ — object ที่เป็น IDisposable (= object ที่ถือทรัพยากรไว้และต้องคืนเมื่อเลิกใช้; เดี๋ยวบท 8 อธิบาย interface เต็ม) จะถูก dispose (คืนทรัพยากร/ปิด) เมื่อจบ scope (ขอบเขตบล็อก) อัตโนมัติ ไม่ต้องเขียน finally เอง
csharp
// using statement (C# 8+)
// ⚠️ data.txt ต้องอยู่ที่ working directory (โฟลเดอร์ที่รันโปรแกรม)
// ถ้าไม่มีจะได้ FileNotFoundException — กันก่อนด้วย File.Exists ถ้าจำเป็น
if (File.Exists("data.txt"))
{
using var file = File.OpenRead("data.txt");
// file ถูก dispose (ปิด/คืน) อัตโนมัติเมื่อจบ scope
}
// using block (แบบเก่า) — ต้องระวังว่าไฟล์ต้องมีอยู่ก่อน มิฉะนั้นได้ FileNotFoundException
if (File.Exists("data.txt"))
{
using (var file2 = File.OpenRead("data.txt"))
{
// ...
} // คืนทรัพยากร (dispose) ตรงนี้
}ใช้ using ได้กับ object ที่เป็น IDisposable ส่วนใหญ่ — file, stream, DB connection, lock, etc.
🔴 ยกเว้น
HttpClient— ห้ามห่อusingต่อ request แม้มันจะเป็นIDisposableก็ตาม การสร้างHttpClientใหม่ทุกครั้งแล้ว dispose จะทำให้ TCP socket (ช่องเชื่อมต่อเครือข่าย) ถูก exhausted (ใช้จนหมด) ใน productionวิธีที่ถูกสำหรับ console app / โปรแกรมเล็ก ๆ — ใช้
static readonly HttpClientตัวเดียวทั้ง app:csharp// ✅ ประกาศครั้งเดียว แชร์ทั้ง app private static readonly HttpClient _http = new(); // จากนั้นเรียกใช้ได้เลย — ไม่ต้อง new และไม่ต้อง using var html = await _http.GetStringAsync("https://example.com");วิธีที่ถูกสำหรับ ASP.NET Core — ใช้
IHttpClientFactoryซึ่งต้องเรียนเรื่อง DI (Dependency Injection) ก่อน — เรียนในส่วน dotnet/ บท ASP.NET Core ⏭️
8. Local function (preview)
C# ให้ประกาศฟังก์ชัน "ซ้อน" ภายในฟังก์ชันอื่นได้ — เหมาะกับ helper ที่ใช้เฉพาะในฟังก์ชันนั้น เรียนรายละเอียดเต็มใน บทที่ 4 หัวข้อ 6:
csharp
int Sum(int[] arr)
{
int total = 0;
foreach (var n in arr) Add(n);
return total;
// local function — มองเห็นตัวแปรของฟังก์ชันแม่ (outer)
void Add(int n) => total += n;
}9. goto + label (มีอยู่นะ แต่อย่าใช้!)
csharp
goto retry;
retry:
// ...จุดเดียวที่พอใช้ได้ = กระโดด (jump) ออกจาก nested loop (ลูปซ้อนลูป):
csharp
foreach (var x in xs)
foreach (var y in ys)
if (Match(x, y)) goto found;
found:
// ...⚠️ ในเกือบทุกเคส แยกเป็น method (ปรับโครงสร้างโค้ด = refactor) แทนจะดีกว่า
10. Common Pitfalls (จุดพลาดบ่อย)
switchstatement ลืมbreak;→ compile error (ยังดีกว่าการ fall-through คือไหลลง case ถัดไปโดยอัตโนมัติแบบ C)switch expressionไม่ครอบทุก case (กรณี) → warning + อาจ throw (โยน error) ตอน runtime==กับ floating point (ทศนิยม) — เพราะค่าปัดเศษเพี้ยน → ใช้Math.Abs(a - b) < 0.001แทนisกับ null —if (obj is null)ดีกว่าif (obj == null)เพราะตรวจตรง ๆ ไม่ผ่าน==ที่อาจถูก operator overload (กำหนดความหมายของเครื่องหมายเอง)for (int i = 0; i < list.Count; i++)ถ้า list เปลี่ยนขนาดในลูป → ระวังforeachแล้วแก้ collection (เพิ่ม/ลบสมาชิก) ระหว่างวน → InvalidOperationException → ใช้forหรือก๊อป (copy) ก่อน
🛠️ Checkpoint 3 — ลงมือทำ
- รับเกรด (0-100) → print "A/B/C/D/F" ด้วย switch expression + relational pattern
- ประกาศตัวแปร
object x = 42;(หรือค่าอื่น ๆ) แล้ว describe ด้วย pattern matching:int n when n > 0→ "positive int"int n when n < 0→ "negative int"string s→ "string len={s.Length}"null→ "null"
- รับ array
int[]→ ใช้ list pattern:[]empty,[x]single,[x, y]pair,[first, .., last]more - (ข้อนี้ใช้ class/record ที่เรียนในบท 5 และ 10 — ข้ามได้ กลับมาทำหลังอ่านบท 5 แล้วค่อยทำ) สร้าง
record Order(decimal Total, string Status)→ คำนวณ shipping ตาม property pattern:- Total >= 1000 + Status="paid" → free
- Total >= 500 → 50
- else → 100
สรุปบทที่ 3
if/else+switchแบบ C-family- switch expression + pattern matching = highlight ของ C# modern
- pattern: type, relational, property, tuple, list — ลด if/else ที่ซับซ้อน
- foreach ใช้กับทุก
IEnumerable<T> usingทุก IDisposable- range/index
[1..4],[^1]ใช้ slice ได้