Chương 17: Thuộc tính
Tác giả: Sưu tầm
Khái quát
Vài tháng trước đây, tôi đang viết một vài đoạn mã, và tôi đã gặp một tình huống là một trong các trường của một lớp (Filename) có thể được dẫn xuất từ một lớp khác (Name). Do đó, tôi quyết định sử dụng thuật ngữ thuộc tính (hay mẫu thiết kế) trong C++, và viết một hàm getFileName() cho trường được dẫn xuất từ lớp khác. Sau đó tôi phải duyệt qua tất cả mã và thay đổi tham chiếu đến trường đó bằng cách gọi hàm getFileName(). Điều này mất một khoảng thời gian, vì chương trình là khá lớn.
Tôi cũng phải nhớ rằng khi tôi muốn nhận một tên tập tin, tôi phải gọi hàm thành phần getFileName() để có nó, thay vì đơn thuần tham chiếu đến thành phần tên tập tin của lớp đó. Điều này làm cho mô hình trở nên khó nắm bắt; thay vì FileName chỉ là một trường, tôi phải nhớ rằng thật sự tôi đang gọi một hàm mỗi khi tôi cần truy xuất nó.
C# xem thuộc tính như thành phần hạng nhất của nó. Thuộc tính dường như là các trường cho người sử dụng của một lớp, nhưng chúng sử dụng một hàm thành phần để nhận giá trị hiện tại và thiết đặt giá trị mới. Bạn có thể phân mô hình người sử dụng (một trường) từ mô hình cài đặt (hàm thành phần), mà nó làm giảm bớt số lượng ghép nối giữa một lớp và người sử dụng một lớp, mang lại nhiều linh hoạt hơn trong thiết kế và bảo trì.
Trong .NET Runtime, thuộc tính được cài đặt bằng cách sử dụng một mẫu được đặt tên và một phần nhỏ thêm vào dữ liệu mêta nối các hàm thành phần đến tên thuộc tính. Điều này cho phép thuộc tính được xem như thuộc tính trong một số ngôn ngữ, và đơn thuần như hàm thành phần trong một số ngôn ngữ khác.
Thuộc tính được sử dụng hầu khắp .NET Base Class Library; thực tế, có một số trường chung (nếu có).
Accessor
Một thuộc tính bao gồm một khai báo thuộc tính và một hoặc hai khối mã – được biết đến như các accessor – để xử lý nhận và đặt giá trị thuộc tính. Đây là một ví dụ đơn giản:
class Test {
private string name;
public string Name {
get {
return name;
}
set {
name = value;
}
}
}
Lớp này khai báo một thuộc tính Name, và định nghĩa cả phần nhận và thiết đặt cho thuộc tính đó. Phần nhận đơn thuần trả ra giá trị của biến private, và phần thiết đặt cập nhật biến bên trong lớp thông qua một tham số đặc biệt value. Mỗi khi phần thiết đặt được gọi, biến value chứa giá trị mà thuộc tính được thiết đặt. Kiểu của value là giống với kiểu của thuộc tính.
Thuộc tính có thể có phần nhận, phần thiết đặt, hoặc cả hai. Thuộc tính mà chỉ có phần nhận gọi là thuộc tính chỉ đọc, và một thuộc tính mà chỉ có phần thiết đặt gọi là thuộc tính chỉ ghi.
Thuộc tính và thừa kế
Như các hàm thành phần, thuộc tính cũng có thể được khai báo bằng các bổ từ virtual, override, hay astract. Các bổ từ này được đặt lên một thuộc tính và ảnh hưởng đến cả hai accessor.
Khi một lớp dẫn xuất khai báo một thuộc tính trùng tên trong lớp cơ sở, nó che đậy toàn bộ thuộc tính; nó không thể chỉ che đậy phần nhận hay phần thiết đặt.
Sử dụng thuộc tính
Thuộc tính tách giao diện của một lớp ra khởi cài đặt của lớp đó. Điều này hữu ích trong các trường hợp mà thuộc tính được dẫn xuất từ các trường khác, và cũng để thực hiện khởi tạo lười nhác và chỉ mang về một giá trị nếu người sử dụng thật sự cần đến nó.
Giả sử một người thiết kế xe ô tô muốn có thể tạo ra một báo cáo mà nó liệt kê một vài thông tin hiện nay về việc sản xuất xe.
using System;
class Auto {
public Auto(int id, string name) {
this.id = id;
this.name = name;
}
// query to find # produced
public int ProductionCount {
get {
if (productionCount == -1) {
// fetch count from database here.
}
return(productionCount);
}
}
public int SalesCount {
get {
if (salesCount == -1) {
// query each dealership for data
}
return(salesCount);
}
}
string name;
int id;
int productionCount = -1;
int salesCount = -1;
}
Cả hai thuộc tính ProductionCount và SalesCount đều được khởi tạo –1, và thao tác xa xỉ của việc tính toán chúng được trì hoãn cho đến khi nó thật sự cần thiết.
Những ảnh hưởng khi thiết đặt giá trị
Các thuộc tính cũng rất hữu ích để làm một số việc bên ngoài đơn thuần thiết đặt giá trị khi phương pháp thiết đặt được gọi. Một cửa hàng bán giỏ có thể cập nhật tổng số khi người sử dụng thay đổi một mục đếm, ví dụ:
using System;
using System.Collections;
class Basket {
internal void UpdateTotal() {
total = 0;
foreach (BasketItem item in items) {
total += item.Total;
}
}
ArrayList items = new ArrayList();
Decimal total;
}
class BasketItem {
BasketItem(Basket basket) {
this.basket = basket;
}
public int Quantity {
get {
return(quantity);
}
set {
quantity = value;
basket.UpdateTotal();
}
}
public Decimal Price {
get {
return(price);
}
set {
price = value;
basket.UpdateTotal();
}
}
public Decimal Total {
get {
// volume discount; 10% if 10 or more are purchased
if (quantity >= 10)
return(quantity * price * 0.90m);
else
return(quantity * price);
}
}
int quantity; // count of the item
Decimal price; // price of the item
Basket basket; // reference back to the basket
}
Trong ví dụ này, lớp Basket chứa một mảng BasketItem. Khi giá cả hoặc số lượng của một phần tử được cập nhật, một cập nhật được thực hiện cho lớp Basket, và duyệt qua tất cả các phần tử để cập nhật tổng sổ giỏ.
Việc duyệt này cũng có thể được cài đặt tổng quát hơn bằng các sự kiện, được đề cập đến trong Chương 23, “Sự kiện”.
Thuộc tính tĩnh
Ngoài các thuộc tính thành phần, C# cũng cho phép định nghĩa các thuộc tính tĩnh, mà nó thuộc về toàn bộ lớp hơn là để cho một thể hiện nhất định của lớp. Giống như các hàm thành phần tĩnh, thuộc tính tĩnh không được khai báo với các bổ từ virtual, abstract, hay override.
Khi trường readonly được thảo luận ở Chương 8, “Vấn đề khác về lớp”, có một trường hợp mà nó khởi tạo một số trường readonly tĩnh. Điều tương tự có thể xảy ra với thuộc tính tĩnh mà không phải khởi tạo các trường cho đến khi cần thiết. Giá trị cũng có thể được tạo dựng khi cần, chứ không lưu trữ. Nếu việc tạo một trường là tốn kém và nó có vẻ sẽ được sử dụng lại, sau đó giá trị nên được lưu trữ trong một trường private. Nếu ít tốn kém để tạo hay có vẻ sẽ không được sử dụng lại, nó có thể được tạo khi cần.
class Color {
public Color(int red, int green, int blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
int red;
int green;
int blue;
public static Color Red {
get {
return(new Color(255, 0, 0));
}
}
public static Color Green {
get {
return(new Color(0, 255, 0));
}
}
public static Color Blue {
get {
return(new Color(0, 0, 255));
}
}
}
class Test {
static void Main() {
Color background = Color.Red;
}
}
Khi người sử dụng muốn một trong những giá trị màu định nghĩa sẵn, phương pháp nhận của thuộc tính tạo một thể hiện với màu thích hợp, và trả ra thể hiện đó.
Hiệu quả của thuộc tính
Trở lại với ví dụ đầu tiên của chương này, hãy xem xét tính hiệu quả của mã khi thực thi:
class Test {
private string name;
public string Name {
get {
return name;
}
set {
name = value;
}
}
}
Đây có vẻ là một thiết kế hiệu quả, bởi vì một lời gọi hàm thành phần được thêm vào nơi mà ở đó bình thường sẽ là một truy xuất trường. Tuy nhiên, không có lý do nào để môi trường thời gian chạy bên dưới không thể thực hiện inline các accessor như nó là bất kỳ một hàm đơn giản nào khác, nên thường không có sự thực thi sự bất lợi trong việc chọn một thuộc tính thay vì một trường đơn giản. Một cơ hội để có thể xem lại cài đặt sau này mà không thay đổi giao diện có thể là vô giá, nên thuộc tính thường là lựa chọn tốt hơn trường cho các thành phần chung.
Ở đây còn lại một phần nhỏ bên dưới của việc sử dụng thuộc tính; chúng không được hỗ trợ tự nhiên bởi tất cả ngôn ngữ .NET, nên các ngôn ngữ khác có thể phải gọi trực tiếp các hàm accessor, mà nó là phức tạp hơn so với sử dụng các trường.