Lớp (class) là một kiểu do bạn tự định nghĩa: nó gom dữ liệu (trường) cùng với code xử lý dữ liệu đó (phương thức). Đối tượng (object) là một instance của lớp, được tạo lúc chạy bằng new. Một lớp, nhiều đối tượng, mỗi đối tượng có giá trị riêng.
Khai báo lớp và tạo đối tượng
Phần khai báo lớp liệt kê các thành viên bên trong cặp ngoặc nhọn. Trường chứa dữ liệu, phương thức chứa hành vi. new ClassName() cấp phát một đối tượng và trả cho bạn một tham chiếu đến nó.
Đầu ra:
Maya: 120.50
Omar: 40
Withdraw 200 succeeded: False
Mỗi đối tượng có Owner và Balance riêng. Bên trong một phương thức, Balance có nghĩa là "số dư của đối tượng mà phương thức này đang được gọi trên đó", vì vậy maya.Deposit và omar.Deposit thay đổi hai con số khác nhau. Đối tượng ngầm định đó có thể được gọi bằng this khi bạn cần nhắc đến nó.
Các trường bạn không gán sẽ bắt đầu với giá trị mặc định: 0 với số, false với bool, và null với chuỗi cùng các kiểu lớp khác.
Các thành viên ở đây là public để Main truy cập được. Lớp thực tế thường giữ trường ở private và cung cấp thuộc tính thay vào đó; trang về access modifier có đủ các quy tắc. Một lớp cũng có thể khai báo hàm khởi tạo để gán giá trị cho các trường khi đối tượng được tạo, được trình bày trong trang về hàm khởi tạo.
Lớp là kiểu tham chiếu
Một biến có kiểu lớp không chứa đối tượng. Nó chứa một tham chiếu đến đối tượng nằm trên heap. Gán biến đó cho biến khác chỉ sao chép tham chiếu, nên hai biến có thể cùng trỏ đến một đối tượng.
Đầu ra:
5
6
True
False
Ở đây có ba hệ quả:
- Thay đổi đối tượng qua
secondthì cũng thấy được quafirst, vì chỉ có một đối tượng. - Truyền một đối tượng vào phương thức là truyền tham chiếu. Phương thức có thể thay đổi các trường của đối tượng và bên gọi sẽ thấy thay đổi đó. (Gán một đối tượng mới cho tham số bên trong phương thức thì không ảnh hưởng đến biến của bên gọi; muốn vậy cần dùng
ref.) ==trên hai instance của lớp mặc định so sánh tham chiếu: "đây có phải cùng một đối tượng không?", chứ không phải "chúng có cùng giá trị không?".lookalikecó dữ liệu giống hệt nhưng vẫn là một đối tượng khác.
Nếu bạn muốn phép gán tạo ra bản sao và so sánh theo giá trị, hãy xem struct, một kiểu giá trị, hoặc record, loại được so sánh theo giá trị.
null và NullReferenceException
Một biến tham chiếu có thể giữ null, nghĩa là "không có đối tượng nào". Dùng một thành viên thông qua null sẽ ném ra NullReferenceException, ngoại lệ lúc chạy phổ biến nhất trong code C#.
Đầu ra:
True
no city on file
NullReferenceException: Address was never set
Lisbon
Cách sửa gần như không bao giờ là bắt ngoại lệ này. Hãy tìm hiểu vì sao tham chiếu lại là null, rồi tạo đối tượng sớm hơn, kiểm tra trước khi dùng, hoặc dùng toán tử null-conditional ?., toán tử này dừng lại và trả về null thay vì ném ngoại lệ. Sau đó ?? cung cấp một giá trị dự phòng. Cả hai toán tử được trình bày trong trang nullable type.
Ghi đè ToString
Mọi lớp đều kế thừa từ object, lớp cung cấp ToString(). Phiên bản kế thừa trả về tên đầy đủ của kiểu (kể cả namespace, nếu có), và hiếm khi đó là thứ bạn muốn in ra. Hãy ghi đè nó:
Đầu ra:
Plain
Mug (8.50)
In cart: Mug (8.50)
Total line: Mug (8.50)
Console.WriteLine, string interpolation và phép + với chuỗi đều tự gọi ToString() cho bạn. Trình gỡ lỗi cũng hiển thị nó, nên việc ghi đè vẫn hữu ích kể cả khi bạn không bao giờ tự in đối tượng ra.
Object initializer
Object initializer gán giá trị cho các trường và thuộc tính public ngay sau khi đối tượng được tạo, trong cùng một biểu thức. Cách viết này dễ đọc hơn một loạt câu lệnh gán và rất hợp khi dùng bên trong khai báo một tập hợp.
Đầu ra:
A12: 25 (0 extras)
A13: 40 (2 extras)
Có hai chi tiết ở vé thứ hai. Price = 40m ghi đè giá trị 25m của field initializer, vì object initializer chạy sau khi đối tượng đã được tạo. Extras = { "Parking", "Poster" } không có new: nó gọi Add trên chính list mà trường đang giữ, thay vì thay thế list đó.
Object initializer không phải là hàm khởi tạo. Nó chỉ gán được những thành viên mà code gọi có quyền truy cập, và không thể bắt buộc một thành viên phải được gán. Khi một đối tượng không hợp lệ nếu thiếu một số giá trị, hãy yêu cầu các giá trị đó trong hàm khởi tạo.
Partial class
Từ khóa partial chia một lớp ra nhiều tệp. Trình biên dịch sẽ gộp các phần lại thành một lớp duy nhất.
// Invoice.cs
public partial class Invoice
{
public decimal Subtotal;
public decimal Total() => Subtotal + Tax();
}
// Invoice.Tax.cs
public partial class Invoice
{
private decimal Tax() => Subtotal * 0.2m;
}
Mọi phần đều phải ghi partial, nằm trong cùng namespace và cùng project. Cách dùng phổ biến là cho code được sinh tự động: trình thiết kế WinForms, source generator và một số ORM viết một phần và để phần còn lại cho bạn, nên việc sinh lại code không bao giờ ghi đè lên code của bạn. Chia một lớp viết tay ra nhiều tệp chỉ để nó trông ngắn hơn thường là đang che giấu một lớp lẽ ra nên tách thành hai.
Lỗi thường gặp
- Dùng đối tượng trước khi tạo. Một trường kiểu lớp là
nullcho đến khi bạn gánnew ...cho nó. Lần truy cập thành viên đầu tiên sẽ ném raNullReferenceException. - Nghĩ rằng phép gán sẽ sao chép.
var b = a;với một lớp chỉ đặt thêm một cái tên cho cùng một đối tượng. Hãy tạo đối tượng mới và sao chép giá trị khi bạn cần một bản độc lập. - So sánh đối tượng bằng
==và nghĩ rằng đó là so sánh giá trị. Với lớp, nó so sánh tham chiếu, trừ khi lớp đó nạp chồng==(nhưstring). Hãy so sánh các trường liên quan, hoặc ghi đèEquals. - In một đối tượng và chỉ nhận được tên kiểu. Hãy ghi đè
ToString(). - Khai báo mọi thứ là
public. Cách này chạy được trong ví dụ nhỏ, nhưng nó cho phép bất kỳ code nào đưa đối tượng vào trạng thái không hợp lệ, chẳng hạn số dư âm. Hãy giữ trường ở private và thay đổi chúng qua các phương thức có kiểm tra quy tắc.
Câu hỏi thường gặp
Lớp (class) trong C# là gì?
Lớp là một kiểu do bạn tự định nghĩa: một nhóm dữ liệu (trường và thuộc tính) và hành vi (phương thức) được đặt tên. class Account { public decimal Balance; public void Deposit(decimal amount) { Balance += amount; } } khai báo một lớp. Lớp là một bản thiết kế; nó không làm gì cho đến khi bạn tạo đối tượng từ nó bằng new.
Lớp và đối tượng trong C# khác nhau thế nào?
Lớp là phần định nghĩa, được viết một lần trong mã nguồn. Đối tượng là một instance của lớp đó, được tạo lúc chạy bằng new, có bản sao riêng của mọi trường instance. Gọi new Account() hai lần sẽ cho hai đối tượng cùng lớp với số dư độc lập nhau.
Làm sao tạo một đối tượng trong C#?
Dùng new theo sau là tên lớp và các đối số cho hàm khởi tạo: var account = new Account();. Bạn có thể gán các trường và thuộc tính public ngay trong cùng biểu thức bằng object initializer: new Account { Owner = "Maya" }. Biến chỉ giữ tham chiếu đến đối tượng mới, không phải chính đối tượng đó.
Tại sao tôi gặp lỗi NullReferenceException trong C#?
Bạn đã dùng một thành viên (.Name, .Length, một lời gọi phương thức) thông qua một biến đang giữ null, nên không có đối tượng nào để thao tác. Nguyên nhân thường gặp là một trường chưa bao giờ được gán (trường kiểu lớp ban đầu là null), một lần tra cứu trả về null, hoặc một phương thức trả về null khi thất bại. Hãy kiểm tra bằng if (x != null), dùng x?.Name, hoặc đảm bảo đối tượng được tạo trước khi dùng.
Làm sao ghi đè ToString trong C#?
Khai báo public override string ToString() trong lớp và trả về đoạn văn bản bạn muốn, ví dụ public override string ToString() => $"{Name} ({Price:F2})";. Nếu không có nó, Console.WriteLine(obj) và string interpolation sẽ in ra tên lớp, vì đó là thứ object.ToString() trả về.