Skip to content

บทที่ 4 — Methods + Scope (ref/out/in, params, local function)

← บทที่ 3 | สารบัญ | บทที่ 5: OOP Fundamentals →

Method ใน C# คือฟังก์ชันที่อยู่ใน class/struct — ตรงไปตรงมา แต่มี modifier (= คีย์เวิร์ดพิเศษที่เปลี่ยนวิธีส่งค่าเข้า method) เพิ่มเข้ามา 4 ตัวคือ ref, out, in, params ทำให้ยืดหยุ่นกว่าหลายภาษา

💡 method กับ top-level statement: ตั้งแต่บทที่ 1 เราเขียนโค้ดใน Program.cs แบบ top-level statement (เขียนลอย ๆ โดยไม่ต้องมี class ห่อ) ซึ่งใช้ method ได้เช่นกัน — แต่ในบทนี้จะเรียนรู้ method ที่อยู่ใน class/struct ซึ่งเป็นรูปแบบหลักที่ใช้จริงในโปรเจกต์ทุกประเภท


1. Method พื้นฐาน

Method คือหน่วยโค้ดที่ทำงานเฉพาะอย่าง — ระบุ access modifier (ตัวกำหนดว่าใครเรียกได้ เช่น public), return type (ชนิดของค่าที่คืน), ชื่อ, และพารามิเตอร์ ใน C# method ต้องอยู่ใน class/struct เสมอ (ไม่มีฟังก์ชันลอย ๆ แบบบางภาษา):

csharp
public class Calculator
{
    // ชนิดที่คืน ชื่อMethod(พารามิเตอร์)
    public int Add(int a, int b)
    {
        return a + b;
    }

    // expression-bodied (เขียนตัวเดียวด้วย =>; C# 6+) — สำหรับ method บรรทัดเดียว
    public int Multiply(int a, int b) => a * b;

    // void = ไม่คืนค่า
    public void Print(string s) => Console.WriteLine(s);

    // static = เรียกได้โดยไม่ต้องสร้าง instance (ตัวอย่างของ object)
    public static double Pi() => 3.14159;
}

// new Calculator() = สร้าง object (instance) ของ class Calculator ขึ้นมาก่อน
// จึงจะเรียก method ที่ไม่ใช่ static ได้ — รายละเอียดเต็มในบท 5
var c = new Calculator();
int n = c.Add(1, 2);
double p = Calculator.Pi();  // static เรียกผ่านชื่อ class ตรง ๆ โดยไม่ต้อง new

2. Parameter modifiers

C# มี modifier 4 ตัว — แต่ละตัวเปลี่ยน semantics (ความหมายเชิงพฤติกรรม) ของการส่ง argument (ค่าที่ส่งเข้า method)

2.1 ปกติ (by value = ส่งสำเนาค่า)

csharp
void Inc(int n) { n++; }

int x = 5;
Inc(x);
Console.WriteLine(x);  // 5 — ไม่กระทบ (เพราะส่งสำเนา)

2.2 ref — by reference (ส่งตัวจริงเข้าไป แก้ได้)

csharp
void IncRef(ref int n) { n++; }

int x = 5;
IncRef(ref x);  // ต้องใส่ ref ทั้งตอนประกาศและตอนเรียก
Console.WriteLine(x);  // 6

ตัวแปรต้องมีค่าก่อนส่ง (assign ก่อน)

2.3 out — สำหรับส่งค่ากลับออกมาอย่างเดียว (output only)

csharp
bool TryDivide(int a, int b, out int result)
{
    if (b == 0) { result = 0; return false; }
    result = a / b;
    return true;
}

if (TryDivide(10, 3, out int r))
    Console.WriteLine(r);  // 3

นิยมตั้งชื่อตาม naming convention TryXxx — สื่อว่า "method นี้คืนได้ทั้งสถานะสำเร็จ/ไม่สำเร็จ + ค่าผลลัพธ์" ในเคสนี้ดีกว่า tuple

2.4 in — reference แบบอ่านอย่างเดียว (readonly; C# 7.2+)

csharp
public struct BigStruct { public int X; }

void Process(in BigStruct data)
{
    // data.X = 5;  // ❌ error: in = อ่านได้อย่างเดียว แก้ไม่ได้
    Console.WriteLine(data.X);
}

ใช้กับ struct ขนาดใหญ่ — ส่งโดยอ้างอิงตัวเดิม (by reference) เพื่อเลี่ยงการก๊อปทั้งก้อน แต่ห้ามแก้

2.5 ref vs out vs in (สรุป)

Caller assign ก่อนส่ง?Method แก้ค่าได้?
ปกติใช่ไม่ (copy)
refใช่ได้
outไม่จำเป็นต้องแก้ก่อน return
inใช่ไม่ได้

3. params — รับ argument ได้ไม่จำกัดจำนวน (variable arguments)

csharp
int Sum(params int[] nums)
{
    int total = 0;
    foreach (var n in nums) total += n;
    return total;
}

Sum();              // 0
Sum(1);             // 1
Sum(1, 2, 3, 4);    // 10
Sum(new[] { 1, 2 }); // 3 — ส่ง array ตรง ๆ ก็ได้

ต้องเป็นพารามิเตอร์ตัวสุดท้าย

💡 เกร็ดขั้นสูง (ข้ามได้): ตั้งแต่ C# 13 / .NET 9 ขึ้นไป params รองรับ collection ประเภทอื่นด้วย เช่น params ReadOnlySpan<T>, params List<T>, params IEnumerable<T> — แบบ ReadOnlySpan<T> ไม่ต้อง allocate array ใหม่ → เร็วกว่า เหมาะกับโค้ดที่ต้องเร็วเป็นพิเศษ (Span จะอธิบายเต็มในบท 10 — ตอนนี้รู้แค่ว่าเป็นการอ้างถึงช่วงของ array โดยไม่สร้างก้อนใหม่)

ต้องตั้งค่า project เป็น <TargetFramework>net9.0</TargetFramework> (หรือสูงกว่า) — SDK จะตั้ง LangVersion ถูกต้องให้อัตโนมัติ ไม่ต้องตั้งเอง ตัวอย่างก่อนหน้านี้ (params int[]) ใช้ได้กับทุก .NET


4. Default parameter + named argument

C# ให้กำหนดค่า default ให้พารามิเตอร์ (ไม่ส่งก็ใช้ค่าเริ่มต้น) และเรียกแบบระบุชื่อ argument (port: 443) — ทำให้ข้ามพารามิเตอร์กลาง ๆ ได้และโค้ดอ่านชัดว่าค่าไหนคืออะไร:

csharp
void Connect(string host = "localhost", int port = 80, bool ssl = false)
{
    // ...
}

Connect();                              // ใช้ค่า default ทุกตัว
Connect("example.com");                 // เปลี่ยนแค่ host
Connect("example.com", 443, true);      // ใส่ครบ
Connect(port: 443, ssl: true);          // named — ข้าม host ได้
Connect(host: "x.com", ssl: true);      // named — สลับลำดับได้

💡 default + named argument = ลดการต้องเขียน overload (method ชื่อเดิมหลายเวอร์ชัน) ลงเยอะ


5. Method overloading (method ชื่อเดียวกันแต่หลายเวอร์ชัน)

csharp
public class Logger
{
    public void Log(string msg) { /*...*/ }
    public void Log(string msg, int code) { /*...*/ }
    public void Log(double seconds, string msg) { /*...*/ }
}

Compiler เลือก overload (เวอร์ชันที่ตรง) ตาม:

  1. จำนวนพารามิเตอร์
  2. ชนิดของพารามิเตอร์
  3. การแปลงชนิดอัตโนมัติ (implicit conversion) — เช่น int แปลงเป็น double ได้

⚠️ ระวังกรณีกำกวม (ambiguous) — Log("x", null) → compiler ไม่รู้จะเลือกเวอร์ชันไหน


6. Local function (C# 7+)

csharp
int SumSquares(int n)
{
    int total = 0;
    for (int i = 0; i < n; i++)
        total += Square(i);
    return total;

    // local function (ฟังก์ชันซ้อนในฟังก์ชัน)
    int Square(int x) => x * x;
}

ข้อดีของ local function เทียบกับการส่ง method เป็นค่า (lambda = ฟังก์ชันที่ไม่ต้องตั้งชื่อ เขียนสั้น ๆ แล้วส่งต่อเป็นค่าได้ทันที เช่น x => x * 2 — เรียนเต็มบท 9):

  • ไม่จองหน่วยความจำเพิ่ม (ไม่ต้อง allocate delegate)
  • มองเห็นตัวแปรของฟังก์ชันแม่ (outer)
  • มีชื่อ → ข้อความ stack trace (รอยทาง error) อ่านง่าย
  • รายละเอียดเปรียบเทียบกับ lambda อยู่ในบท 9

ใช้แทน lambda เมื่ออยากเก็บ helper (ฟังก์ชันช่วย) ให้อยู่ในที่เดียว


7. Static local function

ถ้า local function ไม่จำเป็นต้องใช้ตัวแปรของฟังก์ชันแม่ ให้ใส่ static — บังคับห้าม capture outer variable ทำให้ปลอดภัยกว่าและเร็วกว่า (ไม่ต้องสร้าง closure):

csharp
int Calc(int n)
{
    return Helper(n) * 2;

    static int Helper(int x) => x + 1;  // static = ห้ามมองเห็นตัวแปรของฟังก์ชันแม่
}

static บน local function = ห้าม capture (ยืมใช้) ตัวแปรของฟังก์ชันแม่ → ปลอดภัย + เร็ว

ตัวอย่าง capture ให้เห็นภาพ: ถ้า Helper ไม่ใส่ static แล้วเขียน Helper() => n + 1 (ใช้ n ของ Calc ตรง ๆ โดยไม่ผ่านพารามิเตอร์) — โค้ดจะ compile ผ่านเพราะ local function "มองเห็น" ตัวแปร n ของฟังก์ชันแม่ได้เลย นี่คือ closure (= การที่ฟังก์ชันจดจำ/ยืมตัวแปรรอบข้างไว้ใช้ แม้ฟังก์ชันแม่จะทำงานจบไปแล้ว) การใส่ static คือการบอก compiler ว่า "ห้ามทำแบบนี้" — บังคับให้ส่งค่าทุกอย่างผ่านพารามิเตอร์ชัดเจน จะได้ไม่งงว่าตัวแปรมาจากไหน (รายละเอียดเต็มเรื่อง closure จะเรียนอีกทีตอน lambda ในบท 9)


8. Expression-bodied member

ถ้า method มีแค่บรรทัดเดียว เขียนย่อด้วย => (expression-bodied) แทนปีกกาเต็มได้ — สั้นและอ่านง่ายขึ้นสำหรับ logic เรียบ ๆ:

csharp
public class Greeter
{
    // method แบบเต็ม
    public string Greet(string name)
    {
        return $"Hi, {name}!";
    }

    // method แบบ expression-bodied — เหมือนกัน แต่สั้นกว่า
    public string GreetShort(string name) => $"Hi, {name}!";

    // void ก็ใช้ได้
    public void Print(string msg) => Console.WriteLine(msg);
}

ใช้เมื่อ method สั้น 1-2 บรรทัด

💡 => ยังเขียนย่อให้ property แบบคำนวณ, constructor และ operator overload ได้ด้วย — เดี๋ยวเจอครบในบท 5 (OOP) ตอนนี้เน้น method ก่อนพอ


9. Extension method — เพิ่ม method ให้ type ที่มีอยู่แล้ว

csharp
public static class StringExtensions
{
    // เพิ่ม method ชื่อ ToTitleCase ให้ string
    // ต้องเป็น static class + static method + ใส่ this หน้าพารามิเตอร์ตัวแรก
    public static string ToTitleCase(this string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        // แบ่งเป็นขั้น ๆ เพื่อให้อ่านง่าย
        char firstChar = s[0];                        // ตัวแรก
        string rest = s.Substring(1);                 // ที่เหลือ (s[1..] ก็ได้ถ้ารู้ range operator)
        // ToUpperInvariant/ToLowerInvariant = ตัวพิมพ์แบบไม่อิง culture
        // (ถ้าใช้ ToUpper/ToLower เฉย ๆ จะเจอบั๊กใน locale แปลก ๆ เช่น Turkish I)
        return char.ToUpperInvariant(firstChar) + rest.ToLowerInvariant();
        // ⚠️ ใช้ได้กับ ASCII/Latin-1 — สำหรับ Unicode ที่มี emoji หรืออักขระพิเศษ
        // ควรใช้ CultureInfo.InvariantCulture.TextInfo.ToTitleCase(s) แทน
    }
}

// ใช้
string name = "alice";
Console.WriteLine(name.ToTitleCase());  // Alice

this หน้าพารามิเตอร์ตัวแรก = "ฉันกำลังเพิ่มความสามารถ (extend) ให้ type นี้"

ใช้ได้กับทุก type — รวม sealed (ห้ามสืบทอด) และ third-party (ของคนอื่น) เขียน source ไม่ได้ เทคนิคนี้คือสิ่งที่ทำให้ LINQ (= ชุดเครื่องมือ query ข้อมูลแบบ SQL-style ใน C# — เรียนเต็มบท 9) ทำงานได้ (IEnumerable<T>.Where() ก็เป็น extension method)

⚠️ อย่าใช้บ่อยเกินจำเป็น — ทำให้งงว่า method มาจากไหน → ดูแลยาก


10. ref return + ref local (C# 7+)

⏭️ โซนขั้นสูง — มือใหม่ข้ามได้

หัวข้อนี้เป็นเทคนิคเน้น performance ที่ใช้น้อยมาก มือใหม่ข้ามไปหัวข้อ 11 ได้เลย ไม่กระทบความเข้าใจบทต่อ ๆ ไป

ปกติ C# คืนค่าแบบก๊อป แต่ ref return คืน "การอ้างอิง" (reference) ไปยังตัวจริง (เช่น ช่อง slot ใน array) ทำให้แก้ค่าต้นฉบับผ่านตัวแปรที่รับมาได้ — ใช้น้อย เฉพาะโค้ดที่เน้น performance:

csharp
// คืน reference ไปยัง slot (ช่อง) ของ array
ref int Find(int[] arr, int target)
{
    for (int i = 0; i < arr.Length; i++)
        if (arr[i] == target) return ref arr[i];
    throw new ArgumentException("not found");
}

int[] nums = { 1, 2, 3 };
ref int slot = ref Find(nums, 2);
slot = 999;  // แก้ใน array โดยตรง!
Console.WriteLine(nums[1]);  // 999

ใช้น้อย — ส่วนใหญ่อยู่ในโค้ดที่ต้องเร็วเป็นพิเศษ (performance-critical = โค้ดที่ต้องเร็วเป็นพิเศษ)


11. Variable scope

ตัวแปรใน C# มี "block scope" — มองเห็นได้เฉพาะใน block ({ }) ที่ประกาศ พอออกจาก block ก็หายไป (รวมถึงตัวแปร loop ตั้งแต่ C# 7+) เหมือน Java/C++:

csharp
void Demo()
{
    int outer = 1;

    {
        int inner = 2;
        Console.WriteLine(outer + inner);  // 3
    }
    // inner = error: หลุดออกนอก scope แล้ว

    for (int i = 0; i < 3; i++)
        Console.WriteLine(i);
    // i = error: หลุดออกนอก scope ตั้งแต่ C# 7+
}

C# = block scope (เหมือน Java/C++)


12. Method group + delegate (preview ของบท 9)

⏭️ โซน preview — ข้ามได้ถ้าต้องการ

หัวข้อนี้ใช้ syntax ที่จะเรียนเต็มในบท 7 (Generic) และบท 9 (Lambda + LINQ) ตอนนี้แค่รู้แนวคิดว่า method ส่งต่อกันได้เหมือนค่าทั่วไป — ไม่ต้องเข้าใจทุกบรรทัดก็ไปบทต่อไปได้

ใน C# method เป็น "ค่า" ที่ส่งต่อได้ — เก็บ method ไว้ในตัวแปร delegate (ตัวแปรที่ชี้ไปยัง method) หรือส่งเป็น argument ได้ (เรียกว่า "method group"):

csharp
int Add(int a, int b) => a + b;

// method group → delegate
// Func<int, int, int> = ตัวแปรเก็บ method ที่รับ int 2 ตัว แล้วคืน int
// (generic + Func เรียนเต็มบท 7 และ 9 — ตอนนี้รู้แค่หน้าตา อย่าเพิ่งกังวล)
Func<int, int, int> f = Add;
int result = f(1, 2);  // 3

จะลงลึกในบทที่ 9 (Lambda + LINQ)


13. Common Pitfalls (จุดพลาดบ่อย)

  1. ลืมใส่ ref ตอนเรียกInc(ref x) ต้องใส่ ref ทั้งตอนประกาศ method และตอนเรียก
  2. พารามิเตอร์ out ไม่ได้ใส่ค่าก่อน return → compile error
  3. ค่า default ใน method ต้องเป็นค่าที่รู้ตั้งแต่ compileint n = DateTime.Now.Hour ใช้เป็น default ไม่ได้
  4. พิมพ์ชื่อ named argument ผิด → error (typo = พิมพ์ชื่อพารามิเตอร์ผิด)
  5. overload ที่กำกวม (ambiguous) → ใช้ cast (สั่งแปลงชนิด) ช่วยให้ compiler เลือกถูก
  6. params แต่ส่ง null — จะได้ null ไม่ใช่ array ว่าง → เช็คด้วย if (nums is null or { Length: 0 })

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

  1. เขียน bool TryParseRange(string s, out int min, out int max) ที่ parse "1-10" → min=1, max=10
  2. เขียน void Swap<T>(ref T a, ref T b) — ข้อนี้ข้ามไปได้เลย กลับมาทำหลังเรียนบท 7 (Generic) จะเข้าใจมากกว่านี้มาก
  3. เขียน extension method IsValidEmail(this string s) ที่ check format — แนะนำใช้ System.Net.Mail.MailAddress (try/catch constructor) แทนการเขียน regex เอง เพราะ email regex ที่ถูกต้องครบถ้วนนั้นซับซ้อนมาก
  4. method ที่รับ params string[] names แล้วคืน "Hello, A, B, C!"
  5. ลอง overload Print(int), Print(double), Print(string) แล้ว observe ว่า compiler เลือกอะไรเมื่อส่ง Print(1)

สรุปบทที่ 4

  • Method = static หรือ instance, expression-bodied สำหรับ one-liner
  • ref/out/in = pass by reference (แก้ได้/output only/readonly)
  • params = variable args
  • default parameter + named argument = ลด overload
  • Local function ดีกว่า lambda เมื่อ encapsulate helper
  • Extension method = เพิ่ม method ให้ type ใด ๆ ก็ได้

→ บทที่ 5: OOP Fundamentals