Skip to content

บทที่ 3 — Control Flow (if, switch expression, pattern matching, loop)

← บทที่ 2 | สารบัญ | บทที่ 4: Methods + Scope →

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) ไหนเลยตอนรันจริง จะ throw SwitchExpressionException

    ⚠️ สำหรับ 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" — ถ้าไม่มี null arm และไม่มี _ 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 (จุดพลาดบ่อย)

  1. switch statement ลืม break; → compile error (ยังดีกว่าการ fall-through คือไหลลง case ถัดไปโดยอัตโนมัติแบบ C)
  2. switch expression ไม่ครอบทุก case (กรณี) → warning + อาจ throw (โยน error) ตอน runtime
  3. == กับ floating point (ทศนิยม) — เพราะค่าปัดเศษเพี้ยน → ใช้ Math.Abs(a - b) < 0.001 แทน
  4. is กับ nullif (obj is null) ดีกว่า if (obj == null) เพราะตรวจตรง ๆ ไม่ผ่าน == ที่อาจถูก operator overload (กำหนดความหมายของเครื่องหมายเอง)
  5. for (int i = 0; i < list.Count; i++) ถ้า list เปลี่ยนขนาดในลูป → ระวัง
  6. foreach แล้วแก้ collection (เพิ่ม/ลบสมาชิก) ระหว่างวน → InvalidOperationException → ใช้ for หรือก๊อป (copy) ก่อน

🛠️ Checkpoint 3 — ลงมือทำ

  1. รับเกรด (0-100) → print "A/B/C/D/F" ด้วย switch expression + relational pattern
  2. ประกาศตัวแปร 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"
  3. รับ array int[] → ใช้ list pattern: [] empty, [x] single, [x, y] pair, [first, .., last] more
  4. (ข้อนี้ใช้ 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 ได้

→ บทที่ 4: Methods + Scope