C# Day 2: Class, Struct, Record, Interface, Abstract và Equality

Sau khi đã hiểu Value Type, Reference Type, Stack, Heap và Boxing ở ngày 1, ngày 2 tập trung vào một câu hỏi quan trọng hơn:

Khi cần thiết kế một type trong C#, nên dùng class, struct, record, tuple, interface hay abstract class?

Đồng thời, chúng ta cần hiểu C# xác định hai object “bằng nhau” như thế nào thông qua ==, Equals(), ReferenceEquals() và GetHashCode().

1. class là gì?

class là reference type.

public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
}

Khi assignment:

var a = new User { Name = "Vu" };
var b = a;

b.Name = "David";

Console.WriteLine(a.Name); // David

b = a không copy toàn bộ object. Nó copy reference.

a ─────┐
       ↓
    User object
       ↑
b ─────┘

Vì vậy a và b cùng trỏ tới một object.

Khi nào class phù hợp?

Một class thường hợp lý khi object có một hoặc nhiều đặc điểm:

  • Identity
  • Lifecycle
  • Mutable state
  • Behavior
  • Inheritance / Polymorphism

2. Identity là gì?

Identity trả lời câu hỏi:

Object này là ai?

Ví dụ:

User #100
User #101

Hai user có thể cùng tên nhưng vẫn là hai user khác nhau.

public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
}

Tương tự:

Order #123

Dù trạng thái thay đổi:

Pending
→ Paid
→ Completed

nó vẫn là cùng một Order #123.

Đó là identity.

3. Lifecycle là gì?

Lifecycle là vòng đời trạng thái của object.

Ví dụ:

Created
→ Pending
→ Paid
→ Completed

Một Order vẫn giữ cùng identity nhưng state thay đổi theo thời gian.

public class Order
{
    public Guid Id { get; set; }

    public OrderStatus Status { get; private set; }

    public void Pay()
    {
        Status = OrderStatus.Paid;
    }
}

4. Mutable State là gì?

Mutable state nghĩa là state của object có thể thay đổi.

var user = new User
{
    Name = "Vu"
};

user.Name = "David";

State thay đổi:

Name = Vu
↓
Name = David

Nhưng object vẫn là cùng một User.

Ngược lại, immutable object không cho thay đổi state trực tiếp sau khi được tạo.

5. Behavior là gì?

Behavior trả lời:

Object làm được gì?

Ví dụ:

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
        {
            throw new InvalidOperationException();
        }

        Balance -= amount;
    }
}

Ở đây:

State
→ Balance

Behavior
→ Withdraw()

Một mental model đơn giản:

Object = State + Behavior

Behavior giúp kiểm soát cách state thay đổi thay vì để code bên ngoài chỉnh dữ liệu tùy ý.

6. struct là gì?

struct là value type.

public struct Point
{
    public int X;
    public int Y;
}

Khi assignment:

var a = new Point { X = 10, Y = 20 };
var b = a;

b.X = 100;

Console.WriteLine(a.X); // 10

b = a copy value.

a
└── Point(10,20)

b
└── Point(10,20)

Sau khi sửa b:

a
└── Point(10,20)

b
└── Point(100,20)

Khi nào dùng struct?

Thường dùng cho dữ liệu:

  • nhỏ
  • có value semantics
  • không cần identity
  • không cần class inheritance

Ví dụ:

Point
Coordinate
Money
Guid-like value

7. Vì sao mutable struct dễ gây nhầm?

Ví dụ:

public struct Address
{
    public string City;
}
var a = new Address { City = "HCM" };
var b = a;

b.City = "HN";

a.City vẫn là:

HCM

vì b là một bản copy.

Do đó với value object, thường nên cân nhắc:

public readonly struct Money
{
    public decimal Amount { get; }

    public Money(decimal amount)
    {
        Amount = amount;
    }
}

8. record là gì?

record mặc định là record class.

public record UserDto(
    string Name,
    string Email
);

record class vẫn là reference type.

Điểm khác biệt lớn là compiler generate value-based equality.

var a = new UserDto("Vu", "a@test.com");
var b = new UserDto("Vu", "a@test.com");

Console.WriteLine(a == b); // true

Trong khi plain class:

public class UserClass
{
    public string Name { get; set; } = "";
}
var a = new UserClass { Name = "Vu" };
var b = new UserClass { Name = "Vu" };

Console.WriteLine(a == b); // false

Hai UserClass có dữ liệu giống nhau nhưng là hai object khác nhau.

9. record class và record struct

record class:

public record Person(string Name);

Là:

reference type
+
value-based equality

record struct:

public record struct Point(int X, int Y);

Là:

value type
+
value-based equality

Bảng tóm tắt:

Type Type semantics Equality mặc định
class Reference Reference-based
struct Value Value-based qua Equals()
record class Reference Value-based
record struct Value Value-based

10. Record làm gì để == trả về true?

Với record, compiler generate các thành phần equality cần thiết, bao gồm:

  • Equals(...)
  • GetHashCode()
  • ==
  • !=

Ví dụ:

public record User(
    string Name,
    string Email
);

Khi:

var a = new User("Vu", "a@test.com");
var b = new User("Vu", "a@test.com");

thì:

a == b

có thể được hiểu theo mental model:

a.Name == b.Name
AND
a.Email == b.Email

Vì vậy có thể xảy ra:

Console.WriteLine(ReferenceEquals(a, b)); // false
Console.WriteLine(a == b);                // true

Hai object khác nhau về reference nhưng bằng nhau về value.

11. Record không tự deep-compare mọi collection

Ví dụ:

public record Order(
    List<string> Items
);
var a = new Order(
    new List<string> { "A", "B" }
);

var b = new Order(
    new List<string> { "A", "B" }
);

Không nên mặc định rằng:

a == b

sẽ là true.

Equality của record dựa trên equality của từng member.

Nếu Items là List<string>, equality mặc định của List<T> dựa trên reference identity.

Order equality
↓
Items equality
↓
List equality
↓
reference equality

Record không tự động thực hiện deep equality cho collection.

12. with expression

Record hỗ trợ tạo object mới dựa trên object cũ.

var user = new UserDto(
    "Vu",
    "a@test.com"
);

var updated = user with
{
    Name = "David"
};

Có thể hiểu:

user
Name = Vu

↓ copy + change

updated
Name = David

Object cũ không bị thay đổi.

13. Tuple

Tuple phù hợp để gom một số giá trị tạm thời.

(string Name, int Age) user =
    ("Vu", 30);

Hoặc return nhiều giá trị:

(string Name, int Count) GetResult()
{
    return ("Vu", 10);
}

Quy tắc đơn giản:

Temporary grouping
→ Tuple

Domain concept rõ ràng
→ Named type

Nếu dữ liệu đại diện cho Customer, Order, Invoice hoặc một business concept rõ ràng, nên tạo type riêng thay vì dùng tuple.

14. Interface

Interface mô tả contract hoặc capability.

public interface IPaymentService
{
    Task PayAsync(decimal amount);
}

Implementation:

public class StripePaymentService
    : IPaymentService
{
    public Task PayAsync(decimal amount)
    {
        return Task.CompletedTask;
    }
}

Interface trả lời:

Type này làm được gì?

Không phải:

Type này chứa những dữ liệu gì?

Một class có thể implement nhiều interface.

public class MyService
    : IService,
      IDisposable,
      IAsyncDisposable
{
}

15. Abstract Class

abstract class là một base class chưa hoàn chỉnh.

public abstract class PaymentService
{
    public abstract void Pay(decimal amount);
}

Không thể tạo trực tiếp:

var service = new PaymentService();

Class con phải implement phần abstract:

public class StripePaymentService
    : PaymentService
{
    public override void Pay(decimal amount)
    {
        Console.WriteLine("Stripe");
    }
}

Abstract class có thể chứa:

  • field
  • property
  • constructor
  • state
  • method đã implement
  • abstract method
  • protected member

16. Abstract Class vs Interface

Mental model:

abstract class
→ shared base + state + behavior

interface
→ contract / capability

Ví dụ abstract class:

public abstract class PaymentService
{
    protected string ProviderName { get; }

    protected PaymentService(string providerName)
    {
        ProviderName = providerName;
    }

    public void Log()
    {
        Console.WriteLine(ProviderName);
    }

    public abstract void Pay();
}

Nó có thể chia sẻ cả state và implementation cho các class con.

Interface thường phù hợp khi chỉ muốn nói:

Type nào implement contract này
phải hỗ trợ behavior này.

C# chỉ cho một class kế thừa một base class, nhưng có thể implement nhiều interface.

17. Inheritance

Inheritance nghĩa là type con là một dạng của type cha.

public class Animal
{
    public void Eat()
    {
    }
}

public class Dog : Animal
{
}

Mental model:

Dog is an Animal

Dog có thể sử dụng behavior được kế thừa từ Animal.

18. Polymorphism

Polymorphism hay đa hình nghĩa là:

Cùng một base type hoặc interface nhưng runtime có thể chạy implementation khác nhau.

Ví dụ:

public abstract class PaymentService
{
    public abstract void Pay();
}
public class StripePaymentService
    : PaymentService
{
    public override void Pay()
    {
        Console.WriteLine("Stripe");
    }
}
public class PaypalPaymentService
    : PaymentService
{
    public override void Pay()
    {
        Console.WriteLine("Paypal");
    }
}

Sử dụng:

PaymentService service =
    new StripePaymentService();

service.Pay();

Compile-time type:

PaymentService

Runtime type:

StripePaymentService

Runtime gọi:

StripePaymentService.Pay()

Có thể nhớ:

abstract
→ phải có behavior này

override
→ implementation của class con

polymorphism
→ runtime chọn implementation thực tế

19. Reference Equality

Reference equality hỏi:

Hai variable có trỏ tới đúng cùng một object không?

Ví dụ:

var a = new User();
var b = a;

Console.WriteLine(
    ReferenceEquals(a, b)
); // true

Trong khi:

var a = new User();
var b = new User();

Console.WriteLine(
    ReferenceEquals(a, b)
); // false

20. Equals()

Equals() hỏi:

Theo equality logic của type, hai object/value có được xem là bằng nhau không?

Plain class mặc định thường dựa trên reference equality.

Nhưng có thể override:

public class User
{
    public string Email { get; init; } = "";

    public override bool Equals(object? obj)
    {
        return obj is User other &&
               Email == other.Email;
    }

    public override int GetHashCode()
    {
        return Email.GetHashCode();
    }
}

Lúc này:

var a = new User
{
    Email = "a@test.com"
};

var b = new User
{
    Email = "a@test.com"
};

Console.WriteLine(a.Equals(b)); // true

Hai object khác reference nhưng được coi là equal theo rule của type.

21. == khác Equals() thế nào?

== là operator.

Equals() là method.

Không nên mặc định chúng luôn có cùng behavior.

Với plain class:

== thường kiểm tra reference equality

Với record:

== được compiler generate theo value equality

Với custom struct:

Equals()
→ có value equality mặc định

==
→ không tự động tồn tại nếu type không định nghĩa operator

22. ReferenceEquals()

ReferenceEquals() chỉ trả lời:

Có phải cùng một object không?

Nó không quan tâm business equality.

Ví dụ record:

var a = new UserDto("Vu", "a@test.com");
var b = new UserDto("Vu", "a@test.com");

Console.WriteLine(a == b);                // true
Console.WriteLine(a.Equals(b));           // true
Console.WriteLine(ReferenceEquals(a, b)); // false

Đây là ví dụ rất tốt để phân biệt reference equality và value equality.

23. GetHashCode()

Các collection như HashSet<T> và Dictionary<TKey,TValue> sử dụng hash code để tìm dữ liệu hiệu quả.

Mental model:

GetHashCode()
↓
tìm bucket
↓
Equals()
↓
xác định có thực sự equal không

Quy tắc quan trọng:

Nếu a.Equals(b) là true, thì a.GetHashCode() và b.GetHashCode() phải bằng nhau.

Nhưng chiều ngược lại không đúng.

Hai object có thể cùng hash nhưng vẫn không equal.

Đó gọi là hash collision.

24. Khi nào nên dùng type nào?

Có thể dùng mental model sau:

Có identity / lifecycle / behavior?
→ class

Dữ liệu nhỏ, value semantics?
→ struct

Reference type nhưng muốn value equality?
→ record class

Value type + generated value equality?
→ record struct

Chỉ gom data tạm thời?
→ tuple

Muốn contract/capability?
→ interface

Muốn shared base có state/behavior?
→ abstract class

25. Ví dụ trong Backend

Entity

public class Order
{
    public Guid Id { get; set; }

    public OrderStatus Status { get; private set; }
}

Phù hợp với class vì Order có identity và lifecycle.

DTO

public record OrderResponse(
    Guid Id,
    string Status
);

Record phù hợp vì đây chủ yếu là data transfer.

Value Object

public readonly record struct Money(
    decimal Amount,
    string Currency
);

Có value semantics.

Temporary Result

(string Email, int Count)

Tuple là đủ.

Service Contract

public interface IEmailService
{
    Task SendAsync(string email);
}

Interface mô tả capability.

Shared Base

public abstract class NotificationService
{
    public abstract Task SendAsync(
        string message
    );
}

Abstract class phù hợp khi nhiều implementation có chung base behavior hoặc state.

Những hiểu nhầm phổ biến

record là Value Type

Sai.

record mặc định là record class, tức reference type.

Reference Type không thể có Value Equality

Sai.

record class là ví dụ rõ nhất:

Reference Type
+
Value Equality

Custom struct luôn dùng được ==

Sai.

struct có value semantics nhưng operator == phải được type hỗ trợ.

Interface và Abstract Class giống nhau

Không.

Interface chủ yếu mô tả contract/capability.

Abstract class có thể cung cấp shared state và implementation.

Equals() và ReferenceEquals() giống nhau

Không.

Equals() có thể đại diện cho value/business equality.

ReferenceEquals() chỉ kiểm tra hai reference có trỏ tới cùng object hay không.

Interview Questions

1. class và struct khác nhau quan trọng nhất ở đâu?

class là reference type, còn struct là value type.

Assignment class copy reference; assignment struct copy value.

2. record class là Value Type hay Reference Type?

Reference Type.

3. Tại sao hai record class khác object vẫn có thể == true?

Vì compiler generate value-based equality.

4. record struct khác record class thế nào?

record struct là value type, còn record class là reference type.

Cả hai đều có generated value equality.

5. Interface khác Abstract Class thế nào?

Interface chủ yếu định nghĩa contract/capability.

Abstract class có thể cung cấp shared state, constructor và implementation.

6. Polymorphism là gì?

Cùng một base type/interface nhưng runtime có thể chọn implementation khác nhau.

7. == khác Equals() thế nào?

== là operator, Equals() là method.

Behavior cụ thể phụ thuộc type.

8. ReferenceEquals() kiểm tra gì?

Hai reference có trỏ tới cùng object hay không.

9. Vì sao override Equals() phải quan tâm GetHashCode()?

Để các hash-based collection như HashSet<T> và Dictionary<TKey,TValue> hoạt động nhất quán.

10. Record có tự deep-compare List<T> không?

Không.

Equality của record phụ thuộc equality implementation của từng member.

Key Takeaways

1. class = reference type.

2. struct = value type.

3. record mặc định = record class.

4. record class = reference type + value equality.

5. record struct = value type + value equality.

6. interface = contract/capability.

7. abstract class = shared base + state + behavior.

8. inheritance = "is-a" relationship.

9. polymorphism = runtime chọn implementation thực tế.

10. == là operator.

11. Equals() là equality method.

12. ReferenceEquals() chỉ kiểm tra cùng object.

13. GetHashCode() phải nhất quán với Equals().

14. Record equality không tự động deep-compare collection.

Bài thực hành

Bài 1 — Class

var a = new User { Name = "Vu" };
var b = a;

b.Name = "David";

Console.WriteLine(a.Name);

Giải thích vì sao kết quả là David.

Bài 2 — Struct

var a = new Point
{
    X = 10,
    Y = 20
};

var b = a;
b.X = 100;

Console.WriteLine(a.X);

Giải thích vì sao kết quả vẫn là 10.

Bài 3 — Record

var a = new UserDto(
    "Vu",
    "a@test.com"
);

var b = new UserDto(
    "Vu",
    "a@test.com"
);

Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));
Console.WriteLine(ReferenceEquals(a, b));

Đáp án:

true
true
false

Hãy giải thích vì sao ba kết quả này không mâu thuẫn.

Bài tiếp theo

Sau khi hiểu chắc phần này, bước tiếp theo nên đi sâu vào:

OOP Members & Modifiers
↓
field vs property
↓
const vs readonly
↓
static
↓
virtual / override / new
↓
sealed
↓
access modifiers

Đây là phần nối tự nhiên từ thiết kế type sang cách các member hoạt động trong C#.