1. Giới thiệu
Bắt đầu với vấn đề khi dùng trường trực tiếp nhé. Nếu bạn khai báo một trường là public, thì nó có thể bị thay đổi ngay lập tức và trực tiếp từ bất kỳ đâu trong chương trình:
public class Dog
{
public string Name;
}
Dog dog = new Dog();
dog.Name = ""; // O_o ... kiểu như "tên chó = chuỗi rỗng"!
Người dùng có thể gán cho trường Name những giá trị cực kỳ vô lý, thậm chí chẳng có ý nghĩa gì với domain của bạn: chuỗi rỗng, tên quá dài, hoặc thậm chí là null. Kiểu như ai đó lắp tủ IKEA mới của bạn mà bạn lại cho họ quyền truy cập toàn bộ nhà máy chế biến gỗ ấy.
Nhắc lại, đóng gói (encapsulation) là khi object tự kiểm soát dữ liệu của nó. Mình không tin tưởng ai cũng được đụng vào bên trong, mà chỉ cho phép qua "cửa" đặc biệt — thuộc tính (Properties), nơi bạn có thể kiểm tra, log, sửa đổi hoặc làm gì đó mỗi khi giá trị thay đổi.
2. Định nghĩa và cú pháp
Thuộc tính là thành viên đặc biệt của class, nhìn gần giống trường, nhưng thực ra bên trong là cặp method đặc biệt: getter (lấy giá trị) và setter (gán giá trị). Nhờ thuộc tính, bạn có thể:
- Cho phép (hoặc cấm) đọc/ghi dữ liệu;
- Thêm kiểm tra hợp lệ hoặc logic khi truy cập dữ liệu;
- Ẩn trường nội bộ, thậm chí lưu giá trị ở nơi khác cũng được.
Thuộc tính khai báo gần giống trường, chỉ khác là có dấu ngoặc nhọn và từ khóa get và set bên trong.
[modifikator_dostupa] kieu TenThuocTinh
{
get { ... }
set { ... }
}
Ví dụ cho class Dog của mình:
public class Dog
{
private string _name; // trường nội bộ (private!)
public string Name
{
get { return _name; } // "getter": lấy tên
set { _name = value; } // "setter": gán tên
}
}
Lưu ý: dấu gạch dưới thường dùng cho trường private (_name). Đây là chuẩn style của C#.
3. Cơ chế hoạt động của thuộc tính
Thuộc tính giống như "bảo vệ" đứng giữa dữ liệu nội bộ của object và thế giới bên ngoài. Ví dụ:
Dog dog = new Dog();
dog.Name = "Sharik";
Console.WriteLine(dog.Name);
Giải thích:
- Khi chương trình chạy đến dòng dog.Name = "Sharik"; — lúc này gọi set-method của thuộc tính, bạn có thể thêm kiểm tra (ví dụ, kiểm tra tên không rỗng).
- Khi Console.WriteLine(dog.Name); thì gọi get-method, trả về giá trị hiện tại hoặc tính toán động nếu muốn.
Nhìn thì giống trường bình thường, nhưng thực ra mọi thứ đều được kiểm soát!
4. Tại sao thuộc tính là "Best Practice"
Hầu hết trường hợp, mình không cho truy cập trực tiếp vào trường nội bộ của object. Dù hiện tại chưa cần kiểm tra gì, nhưng thói quen bọc dữ liệu bằng thuộc tính sẽ cực kỳ hữu ích khi sau này có thay đổi.
Ví dụ "kiểm tra hợp lệ" khi gán giá trị:
public class Dog
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Tên chó không được để trống!");
}
_name = value;
}
}
}
Giờ thử cái này:
dog.Name = ""; // Sẽ ném exception!
… bảo vệ object khỏi giá trị vô lý.
5. Thuộc tính: chỉ đọc, chỉ ghi và thông thường
Đôi khi chỉ muốn cho xem giá trị (ví dụ, chó chỉ có năm sinh, không đổi được), hoặc ngược lại — chỉ cho gán (hiếm, nhưng biết đâu bạn muốn làm gì đó bí ẩn).
- Chỉ đọc: chỉ viết get, bỏ set.
- Chỉ ghi: chỉ viết set, bỏ get.
Ví dụ:
public class Dog
{
private int _birthYear = 2018;
// Chỉ đọc
public int BirthYear
{
get { return _birthYear; }
}
// Chỉ ghi (rất hiếm gặp)
public string Secret
{
set { /* làm gì đó với value */ }
}
}
6. Thuộc tính vs Trường
| Trường | Thuộc tính | |
|---|---|---|
| Cú pháp | |
|
| Truy cập | Trực tiếp | Qua get/set |
| Kiểm tra hợp lệ | Không | Có thể thêm vào set/get |
| Mở rộng | Không | Có thể sửa bất cứ lúc nào |
| Tích hợp IDE | Nhìn như trường | Nhìn như thuộc tính (quan trọng cho framework) |
Minh họa: Cách thuộc tính hoạt động
sequenceDiagram
participant User as Nguoi_dung_object
participant Dog as Object Dog
participant Field as Truong_private _name
User->>Dog: dog.Name = "Ryzhik"
Dog->>Dog: set Name("Ryzhik")
Dog->>Field: _name = "Ryzhik"
User->>Dog: print(dog.Name)
Dog->>Dog: get Name()
Dog->>Field: doc _name
Dog->>User: tra ve "Ryzhik"
7. Lỗi thường gặp và lưu ý
Cùng xem thử có bẫy nào ở đây nhé.
Lỗi phổ biến — nhầm giữa trường và thuộc tính, vô tình để lộ dữ liệu private ra ngoài:
public string name; // Đây là trường! Ai cũng thấy, nguy hiểm!
Tốt hơn nên làm thế này:
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
Thuộc tính static cũng có, để lưu giá trị chung cho tất cả object. Nhưng dùng cẩn thận nhé.
Vui vui: nếu thuộc tính chỉ có get mà không có constructor nào, thì không thể thay đổi nó luôn — cái này gọi là immutable property và được dùng nhiều trong thiết kế hiện đại (mình sẽ nói kỹ hơn ở bài về record và bất biến dữ liệu).
GO TO FULL VERSION