โหมดมืด
บทที่ 4 — Methods + Scope (ref/out/in, params, local function)
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 ตรง ๆ โดยไม่ต้อง new2. 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 (เวอร์ชันที่ตรง) ตาม:
- จำนวนพารามิเตอร์
- ชนิดของพารามิเตอร์
- การแปลงชนิดอัตโนมัติ (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()); // Alicethis หน้าพารามิเตอร์ตัวแรก = "ฉันกำลังเพิ่มความสามารถ (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 (จุดพลาดบ่อย)
- ลืมใส่
refตอนเรียก —Inc(ref x)ต้องใส่refทั้งตอนประกาศ method และตอนเรียก - พารามิเตอร์
outไม่ได้ใส่ค่าก่อน return → compile error - ค่า default ใน method ต้องเป็นค่าที่รู้ตั้งแต่ compile —
int n = DateTime.Now.Hourใช้เป็น default ไม่ได้ - พิมพ์ชื่อ named argument ผิด → error (typo = พิมพ์ชื่อพารามิเตอร์ผิด)
- overload ที่กำกวม (ambiguous) → ใช้ cast (สั่งแปลงชนิด) ช่วยให้ compiler เลือกถูก
paramsแต่ส่ง null — จะได้ null ไม่ใช่ array ว่าง → เช็คด้วยif (nums is null or { Length: 0 })
🛠️ Checkpoint 4 — ลงมือทำ
- เขียน
bool TryParseRange(string s, out int min, out int max)ที่ parse"1-10"→ min=1, max=10 - เขียน
void Swap<T>(ref T a, ref T b)— ข้อนี้ข้ามไปได้เลย กลับมาทำหลังเรียนบท 7 (Generic) จะเข้าใจมากกว่านี้มาก - เขียน extension method
IsValidEmail(this string s)ที่ check format — แนะนำใช้System.Net.Mail.MailAddress(try/catch constructor) แทนการเขียน regex เอง เพราะ email regex ที่ถูกต้องครบถ้วนนั้นซับซ้อนมาก - method ที่รับ
params string[] namesแล้วคืน "Hello, A, B, C!" - ลอง 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 ใด ๆ ก็ได้