Skip to content

บทที่ 5 — OOP Fundamentals (class, property, indexer, constructor)

← บทที่ 4 | สารบัญ | บทที่ 6: Inheritance + Polymorphism →

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ทุกที่
privateclass นี้เท่านั้น (เป็น default ของสมาชิกใน class)
protectedclass นี้ + subclass (class ลูก)
internalassembly เดียวกัน
protected internalsubclass หรือ assembly เดียวกัน
private protectedsubclass + 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 นี้มี method Equals รูปแบบที่กำหนด) — interface อธิบายเต็มในบทที่ 6 ตอนนี้แค่รู้ว่า syntax : InterfaceName หมายถึง "class นี้รับปาก" ว่าทำตาม contract นั้น

⚠️ คำเตือน: Point ด้านล่างมี { get; set; } (แก้ค่าได้) แต่ implement IEquatable<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 (จุดพลาดบ่อย)

  1. ใช้ public field แทน property — เปลี่ยน behavior ภายหลังไม่ได้ → ใช้ property
  2. ลืม override GetHashCode() หลัง override Equals() (ดูหัวข้อ 12 — Equality) → ทำให้ dictionary/hashset (โครงสร้างที่ใช้ hash ค้นหา) พัง
  3. ใช้ property ที่แก้ค่าได้ (mutable) เป็น key ของ collectionDictionary<User, ...> ที่ key เปลี่ยนค่า → bug
  4. logic ใน constructor หนัก (เช่นเรียก DB) → ไม่ดี, ควรแยกออกเป็น factory (ตัวสร้าง object แยก)
  5. static state ที่แชร์กัน → race condition (บั๊กตอนหลายเส้นแย่งกันแก้ข้อมูลเดียวกัน)

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

  1. สร้าง BankAccount class:
    • property: Id (read-only, generate Guid), Owner (init), Balance (private set)
    • method: Deposit(decimal), Withdraw(decimal) (throw ถ้าไม่พอ)
  2. ลองสร้างเวอร์ชัน 2 ที่ใช้ primary constructor (C# 12+)
  3. เพิ่ม indexer account["history-N"] ที่คืน transaction ที่ index N
  4. override ToString() ให้สวย + override Equals/GetHashCode (เทียบ Id เท่านั้น)
  5. ลองเปลี่ยนเป็น 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 ง่าย

→ บทที่ 6: Inheritance + Polymorphism