C# Day 3: Members & Modifiers

Sau khi đã học về Type System ở ngày 1 và class, struct, record, interface, abstract, equality ở ngày 2, ngày 3 tập trung vào các thành phần nằm bên trong một type.

Mục tiêu của ngày 3 là hiểu:

  • Field là gì?
  • Property là gì?
  • Backing field là gì?
  • Auto-property hoạt động thế nào?
  • get, set, init, required khác nhau ra sao?
  • Constructor dùng để làm gì?
  • const, readonly, static, static readonly khác nhau thế nào?
  • Access modifier kiểm soát quyền truy cập ra sao?
  • Base type và derived type là gì?
  • virtual, override, new, abstract, sealed khác nhau thế nào?
  • Overload khác Override ra sao?
  • this, base, partial dùng khi nào?

1. Field là gì?

Field là nơi lưu state trực tiếp của object hoặc type.

public class User
{
    private string _name = "";
    private int _age;
}

Ở đây _name và _age là các field.

Mental model:

Field
→ storage
→ nơi giữ state

2. Property là gì?

Property là API để đọc hoặc ghi state.

public class User
{
    private string _name = "";

    public string Name
    {
        get => _name;
        set => _name = value;
    }
}

Ở đây:

_name
→ storage

Name
→ API để truy cập storage

Khi gọi:

user.Name = "Vu";

có thể hình dung:

Name.set("Vu")
↓
_name = "Vu"

Khi đọc:

var name = user.Name;

mental model:

Name.get()
↓
return _name

Property nhìn giống field khi sử dụng, nhưng có thể chứa logic.


3. Backing Field là gì?

Backing field là field được dùng để lưu giá trị phía sau một property.

private string _name = "";

public string Name
{
    get => _name;
    set => _name = value;
}

_name là backing field của Name.

Mental model:

Property
   ↓
get / set
   ↓
Backing field
   ↓
Actual data

4. Auto-property

Nếu property không cần custom logic, bạn không cần viết backing field thủ công.

public string Name { get; set; } = "";

Compiler tự tạo backing storage phía sau.

Mental model:

public string Name { get; set; }

        ↓ compiler

hidden backing field
+
getter
+
setter

Điểm quan trọng:

Auto-property giúp giữ encapsulation nhưng giảm boilerplate code.


5. Vì sao C# dùng Property thay vì Public Field?

Public field:

public string Name;

cho phép caller truy cập trực tiếp storage.

Property:

public string Name { get; set; }

cho phép sau này thêm validation mà caller không phải đổi API.

Ví dụ:

private string _name = "";

public string Name
{
    get => _name;

    set
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Name is required.");

        _name = value;
    }
}

Caller vẫn dùng:

user.Name = "Vu";

Mental model:

Property
→ abstraction around state

6. Không phải Property nào cũng có Backing Field

Ví dụ computed property:

public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";

public string FullName
    => $"{FirstName} {LastName}";

FullName không cần backing field riêng.

Mỗi lần đọc user.FullName, nó tính lại từ FirstName + LastName.

Nhớ:

Auto-property
→ compiler thường tạo backing field

Computed property
→ có thể không cần backing field

7. = "" trong Property là gì?

public string Name { get; set; } = "";

Có hai phần khác nhau:

{ get; set; }
→ cách đọc / ghi property

= ""
→ initial value

= "" dùng để initialize giá trị ban đầu.

Nó không phải là set, cũng không phải init.


8. set

public string Name { get; set; } = "";

Caller có thể thay đổi property sau khi object được tạo.

var user = new User();

user.Name = "Vu";
user.Name = "David";

Mental model:

set
→ normal write
→ mutable property

9. init

public string Name { get; init; } = "";

init là init-only setter.

Cho phép:

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

Nhưng sau khi object được initialize:

// user.Name = "David"; // compile error

Mental model:

= ""
→ bắt đầu bằng giá trị gì?

init
→ caller được phép gán khi nào?

10. get; init; = "" hoạt động thế nào?

public string Name { get; init; } = "";

Có thể đọc như sau:

Name
→ property

get
→ được đọc

init
→ được ghi trong initialization phase

= ""
→ default initial value

Ví dụ:

var a = new User();

thì Name là "".

Còn:

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

thì object initializer thay giá trị mặc định bằng "Vu".


11. required

public class User
{
    public required string Name { get; init; }
}

Caller phải initialize member này.

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

Mental model:

required
→ must initialize

init
→ initialize-only write

Combination phổ biến:

public required string Name { get; init; }

12. Constructor

Constructor được gọi khi object được tạo.

public class User
{
    public Guid Id { get; }
    public string Name { get; }

    public User(Guid id, string name)
    {
        Id = id;
        Name = name;
    }
}

Một constructor tốt thường giúp đảm bảo:

new object
→ valid initial state

13. Constructor Overload

public User()
{
}

public User(string name)
{
}

Cùng tên constructor nhưng khác parameter list.

Đây là overload.


14. Method Overload

public void Send(string message)
{
}

public void Send(string message, string email)
{
}

Cùng method name nhưng khác parameters.

Compiler chọn overload phù hợp tại compile time.


15. const

public const int MaxRetry = 3;

const là compile-time constant.

Đặc điểm:

  • giá trị phải biết tại compile time
  • không thể thay đổi
  • implicitly static

Ví dụ:

public class Config
{
    public const int MaxRetry = 3;
}

Dùng:

Config.MaxRetry

Không cần tạo instance.

Mental model:

const
→ compile-time constant
→ belongs to type

16. Vì sao const là implicitly static?

const không có giá trị khác nhau theo từng object.

Ví dụ:

public class Config
{
    public const int MaxRetry = 3;
}

Dùng trực tiếp:

Config.MaxRetry

Do đó không viết:

public static const int MaxRetry = 3;

vì const đã mang semantics thuộc type.


17. readonly

public readonly Guid Id;

Khác const, readonly có thể được initialize bằng runtime value.

public User()
{
    Id = Guid.NewGuid();
}

Sau initialization, field không được reassign.

Mental model:

const
→ compile-time

readonly
→ runtime init được
→ no reassignment afterward

18. readonly không có nghĩa Immutable

public readonly List<string> Names = new();

Không được reassign:

// Names = new List<string>();

Nhưng vẫn được:

Names.Add("Vu");
Names.Clear();

Vì readonly chỉ ngăn thay field reference.

Nó không làm object được reference trỏ tới trở thành immutable.

Mental model:

readonly reference
→ reference không đổi

referenced object
→ vẫn có thể mutable

19. Nested Object với readonly

public class Address
{
    public string City { get; set; } = "";
}

public class User
{
    public readonly Address Address = new();
}

Hợp lệ:

user.Address.City = "HCM";

Không hợp lệ:

// user.Address = new Address();

readonly không lan xuống toàn bộ object graph.


20. static

public class User
{
    public static int TotalUsers;
}

Static member thuộc về type.

User.TotalUsers++;

Mental model:

instance member
→ mỗi object một bản

static member
→ cả type dùng chung một bản

21. static readonly

public static readonly Guid ApplicationId =
    Guid.NewGuid();

Kết hợp:

static
→ thuộc type

readonly
→ runtime initialize được
→ không reassign sau đó

Ví dụ:

public static readonly DateTime StartedAt =
    DateTime.UtcNow;

DateTime.UtcNow chỉ biết ở runtime nên không thể dùng const.


22. const vs readonly vs static readonly

Concept const readonly static readonly
Thuộc type instance type
Runtime init không có có
Reassign sau init không không không
Shared có không có

23. Access Modifiers

public

Accessible từ mọi nơi phù hợp với type visibility.

private

Chỉ trong chính type.

protected

Trong type hiện tại và derived types.

internal

Accessible trong cùng assembly.

protected internal

protected OR internal

private protected

protected AND internal

24. Base Type và Derived Type

public class Animal
{
}

public class Dog : Animal
{
}

Ở đây:

Animal
→ base type

Dog
→ derived type

Mental model:

Dog is an Animal

Do đó:

Animal animal = new Dog();

là hợp lệ.


25. protected và Derived Type

public class Animal
{
    protected string Name = "";
}

Derived type:

public class Dog : Animal
{
    public void PrintName()
    {
        Console.WriteLine(Name);
    }
}

Dog truy cập được Name vì Dog là derived type.


26. virtual

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal");
    }
}

virtual nghĩa là:

Derived class được phép override behavior này.

Nó có default implementation.


27. override

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog");
    }
}
Animal animal = new Dog();

animal.Speak();

Kết quả:

Dog

Vì:

Compile-time type = Animal
Runtime type      = Dog

virtual/override
→ runtime dispatch
→ Dog.Speak()

28. new và Method Hiding

public class Animal
{
    public void Speak()
    {
        Console.WriteLine("Animal");
    }
}

public class Dog : Animal
{
    public new void Speak()
    {
        Console.WriteLine("Dog");
    }
}

Nếu:

Dog dog = new Dog();
dog.Speak();

kết quả là Dog.

Nhưng:

Animal animal = new Dog();
animal.Speak();

kết quả là Animal.

Vì new không override method của base class.

Nó chỉ hide member.

Mental model:

new
→ compile-time type quyết định member

29. override vs new

Với:

Animal animal = new Dog();

Nếu Speak() dùng override:

runtime type = Dog
→ Dog.Speak()

Nếu Speak() dùng new:

compile-time type = Animal
→ Animal.Speak()

Chốt:

override
→ runtime dispatch

new
→ member hiding
→ compile-time binding

30. abstract

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

Abstract member không có implementation.

Concrete derived class phải override:

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

31. virtual vs abstract

virtual:

public virtual void Speak()
{
    Console.WriteLine("Default");
}
có default implementation
override optional

abstract:

public abstract void Speak();
không implementation
concrete derived class phải override

32. sealed

Sealed class:

public sealed class UserService
{
}

Không thể derive tiếp.

Sealed override:

public sealed override void Speak()
{
}

Derived class phía dưới không được override tiếp method này.

Mental model:

sealed
→ stop extension here

33. partial

public partial class UserService
{
}

Cho phép định nghĩa cùng một type trong nhiều file.

Compiler ghép chúng thành một type.

Thường gặp ở generated code, source generators và WinForms.


34. this

this tham chiếu tới instance hiện tại.

public class User
{
    private string _name;

    public User(string name)
    {
        this._name = name;
    }
}

35. base

base dùng để truy cập base class hoặc gọi base constructor.

public class Dog : Animal
{
    public Dog(string name)
        : base(name)
    {
    }
}

36. private set

public string Name { get; private set; }

Bên ngoài đọc được nhưng không set trực tiếp được.

Bên trong class vẫn có thể sửa.

Khác với init:

private set
→ class có thể mutate sau này

init
→ chỉ cho assignment trong initialization phase

Bảng Tổng Kết

Concept Mental model
Field storage
Property controlled access
Backing field storage phía sau property
Auto-property compiler tạo backing field
= value initial value
set normal write
init initialization-only write
required must initialize
Constructor valid initial state
const compile-time constant
readonly runtime init, no reassignment
static belongs to type
static readonly shared runtime value
protected type + derived type
virtual cho phép override
override runtime dispatch
new member hiding
abstract no implementation
sealed stop inheritance/override
partial chia type nhiều file

Câu Hỏi Ôn Tập + Đáp Án

1. Field khác Property thế nào?

Field là storage trực tiếp. Property là API để kiểm soát việc đọc và ghi state.

2. Backing field là gì?

Là field lưu giá trị phía sau một property.

3. Auto-property là gì?

Là property mà compiler tự tạo backing storage và accessor cơ bản.

4. Mọi property đều có backing field không?

Không. Computed property có thể tính giá trị mà không cần storage riêng.

5. = "" trong property làm gì?

Initialize giá trị ban đầu của backing storage.

6. set khác init thế nào?

set cho phép ghi bình thường sau khi object được tạo. init chỉ cho phép ghi trong object initialization.

7. required làm gì?

Bắt caller phải initialize member.

8. const là gì?

Compile-time constant và implicitly static.

9. readonly có phải compile-time constant không?

Không. readonly có thể được initialize bằng runtime value.

10. readonly List<T> có .Add() được không?

Có. readonly ngăn reassign field, không làm object bên trong immutable.

11. static nghĩa là gì?

Member thuộc về type và được dùng chung giữa các instance.

12. static readonly khác const thế nào?

const phải biết giá trị ở compile time. static readonly có thể được initialize ở runtime.

13. Derived type là gì?

Là type kế thừa từ base type. Ví dụ Dog : Animal, Dog là derived type.

14. protected cho ai truy cập?

Class hiện tại và derived classes.

15. virtual dùng để làm gì?

Cho phép derived class override implementation.

16. override chọn implementation theo compile-time hay runtime type?

Runtime type.

17. new method hiding chọn member theo type nào?

Compile-time type.

18. abstract khác virtual thế nào?

virtual có default implementation. abstract không có implementation và concrete derived class phải override.

19. sealed làm gì?

Ngăn inheritance hoặc ngăn override tiếp.

20. Overload khác Override thế nào?

Overload:

same name
different parameters
compile-time selection

Override:

inheritance
virtual/abstract
runtime dispatch

Key Takeaways

Field
→ storage

Property
→ controlled access

Backing field
→ storage behind property

Auto-property
→ compiler-generated backing storage

= value
→ initial value

set
→ normal mutable setter

init
→ initialization-only setter

required
→ must initialize

const
→ compile-time constant

readonly
→ runtime initialization, no reassignment

static
→ belongs to type

static readonly
→ shared runtime value

derived type
→ inherits from base type

virtual
→ allow override

override
→ runtime type decides

new
→ compile-time type decides

abstract
→ no implementation

sealed
→ stop extension

Kết Luận

Ngày 3 không chỉ là học keyword.

Mục tiêu quan trọng hơn là hiểu:

state được lưu ở đâu
↓
state được expose thế nào
↓
ai được mutate state
↓
member thuộc instance hay type
↓
inheritance cho phép extension thế nào
↓
compiler hay runtime quyết định behavior

Nếu hiểu được chuỗi này, các modifier của C# sẽ không còn là những keyword phải học thuộc riêng lẻ.