Menu

Hàm khởi tạo C#: mặc định, có tham số, this() và base()

Cách hàm khởi tạo (constructor) hoạt động trong C#: hàm khởi tạo mặc định ngầm định và khi nào nó biến mất, hàm khởi tạo có tham số và nạp chồng, gọi nối bằng this(...) và base(...), thứ tự thực thi, hàm khởi tạo static và private, và primary constructor của C# 12.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Hàm khởi tạo (constructor) là đoạn code chạy khi bạn tạo một đối tượng bằng new. Nó có cùng tên với lớp, không có kiểu trả về, và nhiệm vụ của nó là đưa đối tượng mới vào trạng thái hợp lệ trước khi bất kỳ ai dùng đến.

Đầu ra:

Mug: 8.50
Rejected: price

Vì hàm khởi tạo từ chối giá âm, không thể tồn tại một Product nào có giá âm. Sự đảm bảo đó là lý do chính để viết hàm khởi tạo thay vì gán trường từ bên ngoài.

Hàm khởi tạo mặc định, và khi nào nó biến mất

Nếu một lớp không khai báo hàm khởi tạo nào, trình biên dịch sẽ cung cấp một hàm khởi tạo public không tham số, không làm gì ngoài việc chạy các field initializer. Đó là lý do new BankAccount() dùng được với một lớp chỉ có trường.

Ngay khi bạn khai báo bất kỳ hàm khởi tạo nào, hàm ngầm định đó không còn được sinh ra nữa:

class Product
{
    public string Name;
    public Product(string name) { Name = name; }
}

var p = new Product();   // error CS7036: There is no argument given that corresponds to the required parameter 'name'

Điều này hay gây bất ngờ khi một serializer, một ORM hay ràng buộc generic new() cần một hàm khởi tạo không tham số. Nếu muốn có cả hai, hãy tự khai báo hàm khởi tạo không tham số:

public Product() { }                       // or
public Product() : this("Unnamed") { }     // delegate to the other one (next section)

Hàm khởi tạo có thể là public, internal, protected hoặc private. Phạm vi truy cập của nó quyết định ai được tạo đối tượng bằng nó.

Nạp chồng và gọi nối bằng this(...)

Một lớp có thể có nhiều hàm khởi tạo với danh sách tham số khác nhau. Để khỏi chép cùng một loạt lệnh gán vào từng hàm, hãy gọi nối chúng: : this(...) gọi một hàm khởi tạo khác của cùng lớp trước khi thân hiện tại chạy.

Đầu ra:

new Pizza():
  full constructor: medium, classic, 0
  size-only constructor body
  parameterless constructor body
Result: medium classic with 0 toppings

Chuỗi gọi nối chạy hàm khởi tạo đầy đủ nhất trước, rồi lần ngược về hàm mà bạn đã gọi. Hãy đặt phần việc thực sự (kiểm tra, gán giá trị) vào một hàm khởi tạo và để các hàm còn lại chỉ cung cấp giá trị mặc định.

Tham số tùy chọn là một cách khác: public Pizza(string size = "medium", string crust = "classic", int toppings = 0) cho một hàm khởi tạo duy nhất bao quát cả ba lời gọi. Gọi nối vẫn phù hợp hơn khi các hàm khởi tạo ngắn hơn cần tính toán gì đó, hoặc khi việc đổi giá trị mặc định không được buộc bên gọi phải biên dịch lại (giá trị mặc định của tham số tùy chọn được chép vào code của bên gọi lúc biên dịch).

Gọi hàm khởi tạo của lớp cơ sở bằng base(...)

Lớp dẫn xuất không kế thừa hàm khởi tạo. Mọi hàm khởi tạo của lớp dẫn xuất đều phải chạy một hàm khởi tạo của lớp cơ sở trước. Nếu bạn không viết gì, trình biên dịch tự chèn lời gọi tới hàm khởi tạo không tham số của lớp cơ sở; nếu lớp cơ sở không có hàm đó, bạn phải chọn một hàm bằng : base(...).

Thứ tự thực thi của mọi thứ đáng để xem qua một lần:

Đầu ra:

Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles

Field initializer chạy trước, lớp dẫn xuất trước lớp cơ sở, rồi các thân hàm khởi tạo chạy từ lớp cơ sở trở xuống. Vì vậy khi thân của Truck chạy, Plate đã được gán. Cái bẫy duy nhất trong thứ tự này: nếu hàm khởi tạo của lớp cơ sở gọi một phương thức virtual mà Truck ghi đè, phiên bản ghi đè sẽ chạy trước thân hàm khởi tạo của Truck, và thấy Axles vẫn là 0. Tránh gọi phương thức virtual từ hàm khởi tạo.

Static constructor

Một static constructor khởi tạo kiểu chứ không phải một đối tượng. Nó không có tham số và không có access modifier, và runtime gọi nó đúng một lần, ngay trước khi kiểu đó được dùng lần đầu.

Đầu ra:

Program started
Loading tax table (runs once)
123.00
119.00

Runtime đảm bảo static constructor an toàn với đa luồng: kể cả khi nhiều thread cùng chạm vào kiểu đó, nó chỉ chạy một lần. Nếu nó ném ngoại lệ, mọi lần dùng kiểu đó sau này đều ném TypeInitializationException cho đến hết vòng đời tiến trình, nên hãy giữ nó đơn giản và tránh các thao tác I/O có thể thất bại.

Hàm khởi tạo và object initializer

Một object initializer, new Pizza("large") { Toppings = 3 }, không phải là hàm khởi tạo thứ hai. Trình biên dịch biến nó thành "chạy hàm khởi tạo, rồi gán các thành viên này". Hãy dùng mỗi thứ cho đúng việc:

  • Tham số của hàm khởi tạo cho những giá trị mà thiếu chúng thì đối tượng không hợp lệ. Mọi bên gọi phải truyền chúng, và hàm khởi tạo có thể kiểm tra chúng.
  • Initializer cho các thiết lập tùy chọn có giá trị mặc định hợp lý.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };

Từ C# 11, một thuộc tính được đánh dấu required bắt buộc phải được gán trong initializer, nhờ đó cú pháp initializer có được một phần sự đảm bảo của hàm khởi tạo. Nội dung này được trình bày trong trang về thuộc tính.

Private constructor

Một hàm khởi tạo private nghĩa là chỉ chính lớp đó mới tạo được instance. Có hai mẫu thiết kế dùng nó: static factory method, nơi lớp tự kiểm soát việc tạo đối tượng và có thể trả về một đối tượng đã lưu sẵn hoặc null, và singleton:

Đầu ra:

21
100

Lỗi biên dịch khi gọi new Temperature(5m) bên ngoài lớp là CS0122, cùng lỗi mà access modifier gây ra với mọi thành viên private. Các factory method có tên giải quyết một hạn chế thực tế: hai hàm khởi tạo không thể cùng nhận một tham số decimal, nhưng FromCelsius và FromFahrenheit thì được. Một lớp chỉ có thành viên static (lớp tiện ích) nên được khai báo là static thay vì giấu hàm khởi tạo của nó.

Hàm khởi tạo dạng biểu thức

Một hàm khởi tạo có thân chỉ gồm một câu lệnh có thể dùng =>:

public Product(string name) => Name = name;

Với tuple, bạn còn có thể gán nhiều trường trên một dòng: public Point(int x, int y) => (X, Y) = (x, y);.

Primary constructor (C# 12)

C# 12 cho phép một class hoặc struct khai báo tham số hàm khởi tạo ngay trên kiểu. Các tham số này dùng được trong mọi thành viên:

public class Customer(string name, int id)
{
    public string Name { get; } = name;          // copy into a property
    public string Label => $"#{id} {Name}";      // or read a parameter directly

    public Customer(string name) : this(name, 0) { }   // other constructors must chain to it
}

Có hai điểm khác với record hay gây bất ngờ. Tham số primary constructor của một class không trở thành thuộc tính public; bạn phải tự công khai chúng. Ngoài ra, một tham số được dùng bên trong thành viên sẽ được lưu vào một trường ẩn vẫn có thể thay đổi, nên name = "x"; bên trong một phương thức vẫn biên dịch được. Primary constructor hợp nhất với dependency injection, khi các tham số là những service mà lớp chỉ việc gọi:

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public Order Get(int id) => repo.Find(id);
}

Lỗi thường gặp

  • Đánh mất hàm khởi tạo không tham số. Thêm một hàm khởi tạo có tham số sẽ xóa hàm ngầm định (lỗi CS7036 ở mọi chỗ new X()).
  • Quên : base(...) khi lớp cơ sở không có hàm khởi tạo không tham số. Trình biên dịch không thể chèn lời gọi ngầm định và báo rằng không có đối số cho tham số bắt buộc của hàm khởi tạo lớp cơ sở.
  • Gọi phương thức virtual trong hàm khởi tạo. Phiên bản ghi đè chạy trước thân hàm khởi tạo của lớp dẫn xuất.
  • Làm việc nặng trong hàm khởi tạo. Gọi mạng, đọc tệp hay bất cứ thứ gì chậm đều khiến việc tạo đối tượng tốn kém và ngoại lệ khó xử lý. Hãy dùng static factory method, hoặc một phương thức khởi tạo async, cho những việc đó.
  • Viết kiểu trả về. public void Product() là một phương thức bình thường tên Product, và trình biên dịch từ chối nó vì một thành viên không được trùng tên với kiểu chứa nó (CS0542).

Câu hỏi thường gặp

Hàm khởi tạo (constructor) trong C# là gì?

Hàm khởi tạo là một phương thức đặc biệt chạy khi một đối tượng được tạo bằng new. Nó có cùng tên với lớp và không có kiểu trả về: public Product(string name) { Name = name; }. Nhiệm vụ của nó là đưa đối tượng mới vào trạng thái hợp lệ, thường bằng cách gán các trường từ tham số và từ chối dữ liệu đầu vào sai.

C# có tự động tạo hàm khởi tạo mặc định không?

Chỉ khi lớp không khai báo hàm khởi tạo nào. Khi đó trình biên dịch thêm một hàm khởi tạo public không tham số, để mọi trường giữ giá trị từ initializer hoặc giá trị mặc định. Ngay khi bạn viết bất kỳ hàm khởi tạo nào, chẳng hạn một hàm nhận tên, hàm ngầm định đó biến mất và new Product() không còn biên dịch được (lỗi CS7036). Hãy tự khai báo public Product() { } nếu bạn vẫn cần nó.

Làm sao gọi một hàm khởi tạo từ hàm khởi tạo khác trong C#?

Dùng : this(...) sau danh sách tham số của hàm khởi tạo: public Product(string name) : this(name, 0m) { }. Hàm khởi tạo được gọi tới chạy trước, rồi đến thân của hàm khởi tạo bạn đã gọi. Để gọi hàm khởi tạo của lớp cơ sở, dùng : base(...) theo cách tương tự.

Static constructor trong C# là gì?

Là hàm khởi tạo được đánh dấu static, không có tham số và không có access modifier, chạy một lần cho mỗi kiểu trước khi instance đầu tiên được tạo hoặc thành viên static đầu tiên được dùng. Nó dùng để khởi tạo các trường static cần nhiều hơn một initializer một dòng. Bạn không thể tự gọi nó, và nếu nó ném ngoại lệ, kiểu đó sẽ không dùng được trong suốt phần còn lại của chương trình (TypeInitializationException).

Nên dùng hàm khởi tạo hay object initializer?

Dùng tham số của hàm khởi tạo cho những giá trị mà thiếu chúng thì đối tượng không hợp lệ, vì trình biên dịch buộc mọi bên gọi phải truyền vào và hàm khởi tạo có thể kiểm tra chúng. Dùng object initializer (new Product("Mug") { Color = "blue" }) cho các thiết lập tùy chọn. Initializer chạy sau khi hàm khởi tạo kết thúc.

Primary constructor trong C# 12 là gì?

Là các tham số được viết ngay trên phần khai báo lớp, class Customer(string name, int id) { ... }, và dùng được trong mọi thành viên của lớp. Khác với tham số vị trí của record, chúng không trở thành thuộc tính public: hãy tự công khai chúng bằng public string Name => name; hoặc public string Name { get; } = name;.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu