GT C Sharp cơ bản – Bài 10 :Chương 9: Cấu trúc (Kiểu giá trị)
Chương 9: Cấu trúc (Kiểu giá trị)
Tác giả: Sưu tầm
Khái quát
Lớp được sử dụng để cài đặt hầu hết các đối tượng. Tuy nhiên, đôi khi thật là đáng để ước mong có thể tạo một đối tượng hoạt động như một trong những kiểu dựng sẵn; kiểu dựng sẵn là đơn giản, định vị nhanh và không có sự phức tạp của các tham chiếu. Trong trường hợp này, một kiểu giá trị được sử dụng, được thực hiện bằng cách khai báo một cấu trúc trong C#.
Các cấu trúc hoạt động tương tự như các lớp, nhưng với một số hạn chế. Chúng không thể thừa kế từ bất kỳ một kiểu nào khác (tuy nhiên chúng hoàn toàn thừa kế từ object), và các lớp khác không thể thừa kế từ chúng.
Cấu trúc Point
Trong một hệ thống đồ hoạ, một lớp giá trị có thể được sử dụng để đóng gói một điểm. Đây là cách nó được khai báo:
using System;
struct Point {
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public override string ToString() {
return(String.Format(“({0}, {1})”, x, y));
}
public int x;
public int y;
}
class Test {
public static void
Point start = new Point(5, 5);
Console.WriteLine(“Start: {0}”, start);
}
}
Các thành phần x và y của Point có thể được truy xuất. Trong hàm
Lời gọi Console.WriteLine() là một điều khó hiểu. Nếu Point được định vị trong stack, lời gọi đó làm việc như thế nào?
Đóng hộp và tháo hộp
Trong C# và thời gian chạy .NET, có một ít ma thuật làm cho các kiểu giá trị xem như là các kiểu tham chiếu, và ma thuật đó được gọi là đóng hộp (boxing). Như ma thuật làm, nó khá đơn giản. Trong lời gọi Console.WriteLine(), trình biên dịch đang tìm một cách để chuyển đổi, start thành object, bởi vì kiểu của tham số thứ hai của WriteLine() là object. Đối với một kiểu tham chiếu (ví dụ, lớp), điều này là dễ dàng, bởi vì object là lớp cơ sở của mọi lớp. Trình biên dịch đơn thuần chuyển một tham chiếu object tham chiếu đến một thể hiện lớp.
Tuy nhiên, không có một thể hiện dựa trên tham chiếu cho một lớp giá trị, nên trình biên dịch C# định vị một “hộp” kiểu tham chiếu cho Point, xem cái hộp đó như đang chứa một Point, và sao chép giá trị của Point vào trong hộp. Bây giờ nó là một kiểu tham chiếu, và chúng ta có thể đối xử với nó như thể nó là một object.
Tham chiếu này sau đó được chuyển đến hàm WriteLine(), mà nó gọi hàm ToString() trong Point đã được đóng hộp, nhận được bằng cách gởi tới hàm ToString(), và mã cho ra kết quả:
Start: (5, 5)
Đóng hộp tự động xảy ra mỗi khi một kiểu giá trị được sử dụng tại vị trí yêu cầu (hoặc có thể sử dụng) một object.
Một giá trị được đóng hộp được trả lại cho một kiểu giá trị bằng cách tháo hộp nó:
int v = 123
object o = v; // đóng hộp số 123
int v2 = (int) o; // tháo hộp trả lại số nguyên 123
Việc gán cho đối tượng o giá trị 123 là đóng hộp một số nguyên, mà sau đó được lấy lại ở dòng tiếp theo. Việc chuyển đổi kiểu thành int đó là cần thiết, bởi vì đối tượng o có thể là bất kỳ kiểu của đối tượng nào đó, và chuyển đổi kiểu có thể bị lỗi. Đoạn mã này có thể được thể hiện bằng hình 9.1. Việc gán giá trị int cho kiểu object dẫn đến một hộp được định vị trong heap và giá trị được sao chép vào trong hộp. Hôp sau đó được gán nhãn với kiểu mà nó đang chứa đựng nên thời gian chạy biết kiểu của đối tượng được đóng hộp.
123 |
v
System.Int32 |
123 |
|
123 |
v2
Hình 9.1. Đóng hộp và tháo hộp một kiểu giá trị.
Trong quá trình chuyển đổi tháo hộp, kiểu phải được đối sánh chính xác; một kiểu giá trị được đóng hộp không thể tháo hộp thành một kiểu thích hợp:
object o = 15;
short s = (short) o; //lỗi, o không chứa một short
short t = (short)(int) o; // nó hoạt động
Cấu trúc và cấu tử
Cấu trúc và cấu tử có cách hoạt động khác so với lớp. Đối với lớp, một thể hiện phải được khởi tạo bằng cách gọi new trước khi đối tượng được sử dụng; nếu new không được gọi, thì sẽ không có thể hiện nào được tạo ra, và tham chiếu sẽ là null.
Tuy nhiên, không có tham chiếu nào kết hợp với một cấu trúc. Nếu new không được gọi cho một cấu trúc, một thể hiện với các trường có giá trị 0 được tạo ra. Trong một số trường hợp, người sử dụng sau đó có thể sử dụng một thể hiện mà không khởi tạo trước.
Do đó, là quan trọng để chắc chắn rằng trạng thái tất cả là 0 là một trạng thái khởi tạo hợp lệ cho tất cả các kiểu giá trị.
Một cấu tử mặc định (không có tham số) của một cấu trúc có thể thiết đặt các giá trị khác nhau hơn là trạng thái tất cả là 0, mà điều này là hành vi không mong đợi. Do đó thời gian chạy .NET cần cấu tử mặc định cho cấu trúc.
Những nguyên tắc thiết kế
Cấu trúc chỉ nên được sử dụng cho các kiểu thật sự là một mẫu dữ liệu – đối với các kiểu có thể được sử dụng tương tự các kiểu dựng sẵn. Ví dụ, một kiểu như kiểu dựng sẵn decimal, được cài đặt như một kiểu giá trị.
Dù là các kiểu phức tạp hơn có thể được cài đặt như các kiểu giá trị, chúng có lẽ không cần, vì kiểu giá trị về mặt ngữ nghĩa có lẽ không được mong đợi bởi người sử dụng. Người sử dụng mong muốn một biến có kiểu có thể có giá trị null, điều này là không thể với các kiểu giá trị.