โหมดมืด
บทที่ 5 — OOP Fundamentals (class, property, indexer, constructor)
C# เป็นภาษา OOP (object-oriented programming — เขียนโปรแกรมแบบยึด object เป็นหลัก) ที่เขียนได้กระชับกว่า Java มาก
object คืออะไร? object = ตัวแทนของสิ่งหนึ่งในโปรแกรม เช่น User, Order, Car — มีข้อมูล (เช่น ชื่อ, ยอดเงิน) และความสามารถ (เช่น Withdraw, Greet) อยู่ในตัว
ฟีเจอร์เด่นที่ช่วยให้สั้นลง: property, record, primary constructor (มี syntax sugar = ไวยากรณ์ลัดที่เขียนสั้นแต่ความหมายเหมือนเดิม) — ค่อย ๆ ทำความรู้จักทีละตัวในบทนี้
1. Class basics
class คือแม่แบบ (blueprint หรือ "พิมพ์เขียว") สำหรับสร้าง object — เป็นรากฐานของ OOP ใน C# ประกอบด้วย 4 ส่วน:
- field — ตัวแปรเก็บข้อมูล
- property — ช่องเข้าถึงข้อมูลที่ควบคุมได้ (get/set + validation)
- constructor — ตัวสร้าง object (ตั้งค่าเริ่มต้น)
- method — พฤติกรรม/ฟังก์ชันที่ทำกับข้อมูล
ตัวอย่าง:
csharp
public class Person
{
// field (ตัวแปรเก็บข้อมูลภายใน)
private string _name;
private int _age;
// constructor (ตัวสร้าง object)
public Person(string name, int age)
{
_name = name;
_age = age;
}
// method
public string Greet() => $"Hi, {_name}!";
}
var p = new Person("Alice", 30);
Console.WriteLine(p.Greet());2. Property — สำคัญที่สุดของ C#
property = ช่องเข้าถึงข้อมูลที่หน้าตาเหมือน field แต่ควบคุมการอ่าน/เขียนได้ ใช้แทน getter/setter (method สำหรับอ่าน/เขียนค่า) ใน Java ด้วย syntax สั้น ๆ:
CS8618 = รหัส warning ที่บอกว่า non-nullable reference type property อาจไม่ได้รับค่าตอนสร้าง object — เกิดกับ reference type เช่น
stringเท่านั้น (int/boolไม่ได้รับผลกระทบ เพราะมีค่าเริ่มต้นของตัวเองอยู่แล้ว) แก้ได้ด้วยการใส่ค่าเริ่มต้น (เช่น= "") หรือใช้required(อธิบายเต็มในหัวข้อ 5 — ตอนนี้รู้แค่ว่ามันบังคับให้ระบุค่าตอนสร้าง)
csharp
public class Person
{
// 1. auto-property — compiler สร้างที่เก็บค่า (backing field) ให้อัตโนมัติ
// ภายใต้ NRT (บท 2) ต้องมี required หรือค่าเริ่มต้น มิฉะนั้นจะ warning CS8618
public string Name { get; set; } = "";
public int Age { get; set; }
// 2. read-only property (อ่านอย่างเดียว)
public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow; // ใช้ UtcNow เสมอสำหรับ timestamp — DateTime.Now ใช้เวลาเครื่อง (timezone) ซึ่งต่างกันในแต่ละประเทศ/server ทำให้เทียบเวลาข้ามเครื่องผิดพลาดได้
// 3. private set — เขียนค่าได้แค่ภายใน class
public string Id { get; private set; } = "";
// 4. init-only (C# 9+) — เขียนค่าได้แค่ตอนสร้าง object
public required string Email { get; init; }
// 5. computed property (property ที่คำนวณค่าทุกครั้งที่อ่าน)
public bool IsAdult => Age >= 18;
// 6. property แบบเต็ม (มี backing field คือที่เก็บค่าจริง)
private int _score;
public int Score
{
get => _score;
set
{
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), value, "Score ต้องไม่ติดลบ");
// ArgumentOutOfRangeException ถูกต้องกว่า ArgumentException สำหรับค่าตัวเลขที่เกินช่วงที่อนุญาต (BCL convention)
_score = value;
}
}
}
// ใช้ (Person ในตัวอย่างนี้คือเวอร์ชันขยายของหัวข้อนี้ — ไม่ใช่ Person เดิมใน §1)
// ⚠️ ถ้าจะรันโค้ดจริง อย่าใส่ Person ทั้งสองเวอร์ชันในไฟล์เดียวกัน — จะ error ชื่อซ้ำ (duplicate class)
var p = new Person { Name = "Alice", Age = 30, Email = "a@b.c" };
p.Name = "Bob"; // ได้
// p.Email = "x"; // ❌ init-only (เขียนได้แค่ตอนสร้าง)
// p.CreatedAt = ...; // ❌ read-only (อ่านอย่างเดียว)
Console.WriteLine(p.IsAdult);💡 ใช้ property แทน field สาธารณะ(public)เสมอ — เปลี่ยน behavior (พฤติกรรม) ภายหลังได้โดยไม่ทำลาย API (ในที่นี้หมายถึง interface ของ class ที่เปิดให้คนอื่นใช้ — ไม่ใช่ Web API)
3. Object initializer
"Object initializer" (การกำหนดค่าเริ่มต้นให้ object) ให้กำหนดค่า property ตอนสร้าง object ในวงเล็บปีกกาได้เลย — กระชับกว่าการเรียก setter ทีละบรรทัด และอ่านง่ายว่า object มีค่าอะไรบ้าง:
csharp
// class ที่ใช้ในตัวอย่างนี้ (ต้องประกาศก่อนใช้)
public class Customer { public string Name { get; set; } = ""; }
public class Order
{
public Customer? Customer { get; set; }
public decimal Total { get; set; }
}
// แทนการเรียก setter ทีละตัว
var p = new Person
{
Name = "Alice",
Age = 30,
Email = "a@b.c"
};
// ซ้อนกัน (nested)
var order = new Order
{
Customer = new Customer { Name = "Alice" },
Total = 100
};
// collection initializer (กำหนดสมาชิกตอนสร้าง collection)
var list = new List<int> { 1, 2, 3 };
var dict = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 };4. Constructor
csharp
public class Account
{
public string Owner { get; }
public decimal Balance { get; private set; }
// constructor หลัก
public Account(string owner, decimal initialBalance)
{
Owner = owner;
Balance = initialBalance;
}
// overload (เวอร์ชันอื่น)
// `: this(owner, 0)` = constructor chaining — เรียก constructor อีกตัวของ class นี้เองก่อน
// ทำให้ไม่ต้องเขียนโค้ด initialize ซ้ำ
public Account(string owner) : this(owner, 0) { }
// static constructor — รันครั้งเดียวก่อนใช้งาน class ครั้งแรก
static Account()
{
Console.WriteLine("Account type initialized");
}
}Primary Constructor (constructor แบบติดที่หัว class; C# 12+)
csharp
// เขียนสั้นลงมาก
public class Account(string owner, decimal initialBalance)
{
public string Owner { get; } = owner;
public decimal Balance { get; private set; } = initialBalance;
public void Deposit(decimal amount) => Balance += amount;
}(string owner, decimal initialBalance) ที่ติดอยู่หลังชื่อ class = พารามิเตอร์ของ ctor (constructor) — ใช้เป็นตัวแปรในทุก method ของ class ได้
5. required modifier (C# 11+)
required (C# 11+) บังคับให้ผู้สร้าง object ต้องกำหนดค่า property นั้นเสมอตอน initialize — ถ้าลืม compile ไม่ผ่าน ช่วยกันลืมตั้งค่าสำคัญโดยไม่ต้องเขียน constructor:
csharp
public class User
{
public required string Email { get; init; }
public required string Name { get; init; }
public int? Age { get; init; }
}
// ต้องระบุ Email และ Name ตอน construct
var u = new User { Email = "a@b.c", Name = "Alice" };
// var u2 = new User(); // ❌ compile error (ลืมใส่ Email/Name)ใช้แทน constructor ที่มีพารามิเตอร์เยอะ — ยืดหยุ่นกว่า
6. Access modifiers
access modifier = ตัวกำหนดว่า "ใครเรียกใช้สมาชิกตัวนี้ได้บ้าง"; subclass = class ลูกที่สืบทอด (เดี๋ยวบท 6); assembly = หน่วยรวมโค้ดที่ build ออกมาเป็นไฟล์ .dll/.exe หนึ่งชุด
| Modifier | เห็นจากไหน |
|---|---|
public | ทุกที่ |
private | class นี้เท่านั้น (เป็น default ของสมาชิกใน class) |
protected | class นี้ + subclass (class ลูก) |
internal | assembly เดียวกัน |
protected internal | subclass หรือ assembly เดียวกัน |
private protected | subclass + assembly เดียวกัน |
file (C# 11+) | ไฟล์เดียวกัน |
💡 ส่วนใหญ่ใช้แค่
public/private+ บางทีinternal(สำหรับ library คือชุดโค้ดที่แจกให้คนอื่นใช้)
7. static class + member
csharp
// static class — สร้าง instance (object) ไม่ได้ มีแต่สมาชิกแบบ static
public static class MathHelper
{
public static double Square(double x) => x * x;
public static double Pi { get; } = 3.14159;
}
MathHelper.Square(5); // 25ใช้สำหรับ:
- Utility/helper (ฟังก์ชันช่วยเหลือทั่วไป)
- Extension method
- Factory (ตัวสร้าง object) แบบเก่า ๆ — สมัยใหม่นิยมใช้ DI (Dependency Injection = การส่ง dependency เข้ามาจากภายนอกแทนการสร้างเองใน class) แทน — จะเรียนแนวคิดเบื้องต้นในบท 6 และรายละเอียดเต็มในส่วน dotnet/ (ASP.NET Core)
⚠️ Static state (ข้อมูลที่แชร์กันทั้งโปรแกรม) มีปัญหาหลายข้อ: ทดสอบยาก และเสี่ยง threading bug (บั๊กตอนทำงานหลายเส้นพร้อมกัน)
เก็บ static ไว้แค่สำหรับ pure function เท่านั้น (pure function = ฟังก์ชันที่ไม่มีผลข้างเคียง ให้ผลตามอินพุตเสมอ เช่น
Math.Sqrt)
8. Indexer — เข้าถึงด้วย [...]
"Indexer" ทำให้ object ของเราเข้าถึงด้วย syntax obj[key] เหมือน array/dictionary — กำหนดด้วย this[...] พร้อม get/set เหมาะกับ type ที่ทำตัวเหมือน collection:
csharp
public class Inventory
{
private readonly Dictionary<string, int> _items = new();
public int this[string sku]
{
get => _items.TryGetValue(sku, out var n) ? n : 0;
set => _items[sku] = value;
}
}
var inv = new Inventory();
inv["apple"] = 10;
Console.WriteLine(inv["apple"]); // 10
Console.WriteLine(inv["orange"]); // 0ทำให้ object ใช้ syntax [] ได้ — เหมาะกับ type ที่ทำตัวเหมือน collection (collection-like)
9. Operator overload
C# ให้เรานิยามความหมายของตัวดำเนินการ (+, -, ==) สำหรับ type ของเราเองได้ — เช่นบวก Vec2 สองตัวด้วย a + b ทำให้โค้ดที่ทำงานกับ type คณิตศาสตร์อ่านเป็นธรรมชาติ (ใช้เท่าที่สมเหตุผล):
csharp
// record struct = record ที่เป็น value type (เรียนเต็มบท 10) — ตอนนี้แค่รู้ว่า X, Y เป็น field ที่ตั้งชื่อผ่าน parameter (เหมือน primary constructor ในหัวข้อ 4)
public record struct Vec2(double X, double Y)
{
public static Vec2 operator +(Vec2 a, Vec2 b) => new(a.X + b.X, a.Y + b.Y);
public static Vec2 operator *(Vec2 v, double k) => new(v.X * k, v.Y * k);
// record struct สร้าง == / != ให้อัตโนมัติ (value equality) — ห้าม override ซ้ำ
// ถ้าต้องการเทียบแบบ "ใกล้เคียงพอ" (เพราะ double มีปัญหา rounding — ดูบท 3) ใช้ method แยก:
public bool ApproxEquals(Vec2 other, double eps = 1e-9)
=> Math.Abs(X - other.X) < eps && Math.Abs(Y - other.Y) < eps;
}
var sum = new Vec2(1, 2) + new Vec2(3, 4); // (4, 6)⚠️ อย่า override
==บน record struct — compiler สร้างให้แล้ว ถ้าเขียนซ้ำจะ error CS0111 (member already defined)⚠️ อย่าใช้
==กับdouble/floatตรง ๆ — ใช้Math.Abs(a - b) < epsilonหรือเปลี่ยนเป็นdecimal(ดูบท 3)
ใช้กับ math type — ไม่ใช่ทุก class
10. partial class
แบ่ง class เป็นหลายไฟล์:
csharp
// User.cs
public partial class User
{
public string Name { get; set; } = "";
}
// User.Validation.cs
public partial class User
{
public bool IsValid() => !string.IsNullOrEmpty(Name);
}ใช้สำหรับ generated code (โค้ดที่เครื่องสร้างให้อัตโนมัติ) — แยกโค้ดที่เครื่องสร้าง (code-gen) ออกจากโค้ดที่คนเขียน เช่น:
- EF Core (Entity Framework Core — ไลบรารีเชื่อมกับฐานข้อมูล จะเรียนในส่วน dotnet/) สร้างโค้ด SQL ให้อัตโนมัติ
- gRPC (โปรโตคอล API ประสิทธิภาพสูง) สร้างโค้ด networking ให้
- Source Generator (.NET 5+) — เครื่องมือที่ generate code ตอน compile (เช่น System.Text.Json ใช้ generate serializer ให้อัตโนมัติ รองรับ AOT)
ใน 95% ของงานทั่วไปไม่ต้องเขียน partial เอง — compiler tools ใช้เอง
11. sealed — ห้าม inherit
sealed ปิดไม่ให้ class อื่นมาสืบทอด (inherit) — ใช้เมื่อออกแบบ class ที่ไม่ต้องการให้ขยายต่อ ช่วยให้ปลอดภัยและ compiler optimize ได้ดีขึ้น:
csharp
public sealed class FinalConfig
{
// class นี้ห้ามมี subclass (class ลูก)
}JIT (ตัวแปลโค้ดตอนรัน) ปรับให้เร็วขึ้นได้ผ่านการ devirtualize (compiler เรียก method ตรง ๆ ไม่ต้องไล่หา method ของ class จริงตอนรัน)
หมายเหตุ: .NET 8+ มี dynamic PGO ที่ devirtualize ให้อัตโนมัติแม้ไม่ใส่
sealed— ประโยชน์ด้านความเร็วของsealedจึงน้อยลง คุณค่าหลักของsealedตอนนี้คือ บอกเจตนาในการออกแบบ API ว่าไม่อนุญาตให้สืบทอด
12. Equality — value vs reference
csharp
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
var a = new Point { X = 1, Y = 2 };
var b = new Point { X = 1, Y = 2 };
Console.WriteLine(a == b); // False — เทียบที่อยู่ (reference equality)
Console.WriteLine(a.Equals(b)); // False — ค่าเริ่มต้นก็เทียบที่อยู่💡 ต่างจาก
string(บท 2) ที่ override==ให้เทียบค่าตัวอักษรเป็น default — class ทั่วไปไม่ทำแบบนั้น
ถ้าอยากเทียบ "ค่าข้างใน" (value equality) ต้อง override (เขียนทับ) เอง:
💡
: IEquatable<Point>คือ interface (สัญญาว่า class นี้มี methodEqualsรูปแบบที่กำหนด) — interface อธิบายเต็มในบทที่ 6 ตอนนี้แค่รู้ว่า syntax: InterfaceNameหมายถึง "class นี้รับปาก" ว่าทำตาม contract นั้น
⚠️ คำเตือน:
Pointด้านล่างมี{ get; set; }(แก้ค่าได้) แต่ implementIEquatable<T>— ห้ามใช้ mutable object ที่ implement GetHashCode() เป็น key ของ Dictionary/HashSet เพราะถ้าแก้ค่าหลังจาก insert แล้ว hash จะเปลี่ยนและ entry จะหายไป ถ้าต้องการ value equality จริง ๆ ให้ใช้{ get; init; }(แก้ได้แค่ตอนสร้าง) หรือrecord(ดูด้านล่าง)
csharp
public class Point : IEquatable<Point>
{
public int X { get; set; }
public int Y { get; set; }
public bool Equals(Point? other) => other is not null && X == other.X && Y == other.Y;
public override bool Equals(object? obj) => Equals(obj as Point);
public override int GetHashCode() => HashCode.Combine(X, Y);
}ทางลัด — ใช้ record:
csharp
public record Point(int X, int Y);
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b); // True ✅ (record = เทียบค่า + แก้ไม่ได้โดย default)(เดี๋ยวบท 10 อธิบาย record เต็ม ๆ)
13. ToString() override
ทุก object มี method ToString() ที่ถูกเรียกเวลาพิมพ์ — ค่าเริ่มต้นแสดงแค่ชื่อ type (ไม่มีประโยชน์) ควร override ให้แสดงข้อมูลที่อ่านเข้าใจ มีประโยชน์มากเวลา debug และ log:
csharp
public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
public override string ToString() => $"User(Name={Name}, Age={Age})";
}Debug print, log, exception message — ทุกอย่างเรียก ToString() → override ให้แสดงข้อมูลที่มีประโยชน์ (informative)
14. Common Pitfalls (จุดพลาดบ่อย)
- ใช้ public field แทน property — เปลี่ยน behavior ภายหลังไม่ได้ → ใช้ property
- ลืม override
GetHashCode()หลัง overrideEquals()(ดูหัวข้อ 12 — Equality) → ทำให้ dictionary/hashset (โครงสร้างที่ใช้ hash ค้นหา) พัง - ใช้ property ที่แก้ค่าได้ (mutable) เป็น key ของ collection —
Dictionary<User, ...>ที่ key เปลี่ยนค่า → bug - logic ใน constructor หนัก (เช่นเรียก DB) → ไม่ดี, ควรแยกออกเป็น factory (ตัวสร้าง object แยก)
- static state ที่แชร์กัน → race condition (บั๊กตอนหลายเส้นแย่งกันแก้ข้อมูลเดียวกัน)
🛠️ Checkpoint 5 — ลงมือทำ
- สร้าง
BankAccountclass:- property:
Id(read-only, generate Guid),Owner(init),Balance(private set) - method:
Deposit(decimal),Withdraw(decimal)(throw ถ้าไม่พอ)
- property:
- ลองสร้างเวอร์ชัน 2 ที่ใช้ primary constructor (C# 12+)
- เพิ่ม indexer
account["history-N"]ที่คืน transaction ที่ index N - override
ToString()ให้สวย + overrideEquals/GetHashCode(เทียบ Id เท่านั้น) - ลองเปลี่ยนเป็น
recordแทน — สังเกตว่า equals ทำให้ทำงานยังไง
สรุปบทที่ 5
- class = blueprint ของ reference type
- property (
{ get; set; },{ get; init; }) แทน getter/setter — ใช้แทน public field - primary constructor (C# 12+) ลดโค้ดมาก
required= บังคับให้ระบุตอน initialize- access modifier: public/private/internal เพียงพอใน 95% ของเคส
- equality default = reference — ต้อง override สำหรับ value equality (หรือใช้ record)
- ToString override ทำให้ debug ง่าย