Menu

Tham số tùy chọn và đối số có tên trong C#: giá trị mặc định

Cách tham số tùy chọn và đối số có tên hoạt động trong C#: giá trị mặc định và quy tắc hằng số lúc biên dịch, thứ tự tham số, bỏ qua đối số bằng tên, tham số tùy chọn so với nạp chồng, các thuộc tính caller info và cái bẫy phiên bản khi giá trị mặc định được ghi sẵn vào nơi gọi.

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.

Tham số tùy chọn (optional parameter) có giá trị mặc định ngay trong khai báo phương thức, nên nơi gọi có thể bỏ qua nó. Đối số có tên (named argument) truyền giá trị theo tên tham số thay vì theo vị trí. Kết hợp lại, chúng cho phép một phương thức phục vụ nhiều kiểu gọi mà không cần cả đống bản nạp chồng.

Đầu ra:

to ana@mail.com: (no subject), retries 3
to ben@mail.com: Invoice #1042, retries 3
to cy@mail.com: (no subject) [URGENT], retries 3
to dev@mail.com: Build failed, retries 0

Lời gọi thứ ba bỏ qua subject và đặt urgent theo tên; nếu không có đối số có tên, nó sẽ phải truyền lại "(no subject)" chỉ để đến được vị trí thứ ba. Lời gọi cuối truyền mọi đối số theo tên, với thứ tự khác với khai báo, và điều đó hoàn toàn hợp lệ.

Quy tắc cho giá trị mặc định

Giá trị mặc định phải là thứ mà trình biên dịch tính được:

  • một hằng số (3, "INFO", true, 1.5m, một trường const, một thành viên enum)
  • null cho kiểu tham chiếu hoặc kiểu nullable
  • default(T), hoặc new T() với một kiểu giá trị T

Mọi thứ được tính lúc chạy đều bị từ chối. Trường hợp kinh điển là ngày tháng:

static void Schedule(string task, DateTime at = DateTime.Now) { }
// error CS1736: Default parameter value for 'at' must be a compile-time constant

Cách xử lý chuẩn là một tham số nullable có giá trị mặc định là null, rồi xác định giá trị thật trong thân hàm:

Đầu ra:

backup at 2026-01-01 09:00, tags: 0
report at 2026-03-15 18:30, tags: 0
deploy at 2026-01-01 09:00, tags: 2

Mẹo này cũng áp dụng cho các collection: giá trị mặc định new List<string>() không phải hằng số, nên hãy để mặc định là null và tạo danh sách bên trong. Cách này cũng tránh được bug giá trị mặc định có thể thay đổi bị dùng chung kiểu Python, vốn bị C# loại bỏ ngay từ đầu vì chỉ cho phép hằng số.

Quy tắc về thứ tự

Tham số bắt buộc đứng trước, tham số tùy chọn đứng sau, và mảng params (nếu có) đứng cuối cùng:

static void Log(string message, string level = "INFO", params string[] tags) { }   // OK

static void Log(string level = "INFO", string message) { }
// error CS1737: Optional parameters must appear after all required parameters

Tham số ref và out không thể là tham số tùy chọn.

Ở phía gọi, các đối số theo vị trí lấp các tham số từ đầu. Đối số có tên có thể theo sau chúng với thứ tự bất kỳ. Từ C# 7.2, một đối số có tên cũng có thể đứng trước một đối số theo vị trí, nhưng chỉ khi nó nằm đúng vị trí của mình (SendEmail("a@b.c", subject: "Hi", true)); ở các phiên bản cũ hơn, đối số có tên phải đứng hết ở cuối. Bỏ sót một tham số bắt buộc, kể cả khi đã đặt tên các tham số khác, là lỗi biên dịch.

Đối số có tên giúp code dễ đọc

Đối số có tên hữu ích ngay cả khi không có tham số nào tùy chọn. Các giá trị true, false, null và con số viết thẳng chẳng nói lên điều gì ở nơi gọi:

ResizeImage(photo, 800, 600, true, false);                                   // which is which?
ResizeImage(photo, width: 800, height: 600, keepAspect: true, upscale: false);

Đổi tên một tham số sẽ trở thành thay đổi phá vỡ tương thích với những nơi gọi có dùng tên đó, điều đáng nhớ khi viết thư viện công khai.

Tham số tùy chọn so với nạp chồng

Trước C# 4, để có cùng độ linh hoạt bạn cần một bản nạp chồng cho mỗi tổ hợp. Tham số tùy chọn gộp tất cả vào một phương thức:

// overloads
static void Connect(string host) => Connect(host, 443);
static void Connect(string host, int port) => Connect(host, port, 30);
static void Connect(string host, int port, int timeoutSeconds) { /* ... */ }

// one method with optional parameters
static void Connect(string host, int port = 443, int timeoutSeconds = 30) { /* ... */ }

Khi cả hai cùng tồn tại, việc chọn bản nạp chồng ưu tiên ứng viên không cần điền giá trị mặc định nào:

Đầu ra:

Greet()
Greet(string) with Lena

Greet() khớp với cả hai phương thức, và trình biên dịch chọn phương thức không có tham số tùy chọn bị bỏ qua. Trộn hai kỹ thuật trên cùng một tên chủ yếu tạo ra những lời gọi khó đoán đích đến, nên hãy chọn một cách cho mỗi phương thức.

Chọn nạp chồng khi các biến thể cần code khác nhau hoặc kiểu tham số khác nhau, và chọn tham số tùy chọn khi chúng chỉ khác nhau ở giá trị mặc định.

Giá trị mặc định được ghi sẵn vào nơi gọi

Giá trị mặc định không được tra cứu lúc chạy. Trình biên dịch sao chép nó vào từng nơi gọi khi biên dịch code gọi. Connect("api.shop.com") được biên dịch thành Connect("api.shop.com", 443, 30).

Điều đó kéo theo một hệ quả cho các thư viện. Giả sử phiên bản 1 của một package có Connect(string host, int timeoutSeconds = 30), và phiên bản 2 đổi giá trị mặc định thành 10. Một ứng dụng được biên dịch với phiên bản 1 vẫn tiếp tục truyền 30 sau khi bạn thay DLL phiên bản 2 vào, cho đến khi chính ứng dụng đó được biên dịch lại. Thêm một tham số tùy chọn mới vào một phương thức công khai sẵn có cũng làm hỏng những nơi gọi đã biên dịch, vì chữ ký của phương thức đã đổi mà chúng vẫn tìm chữ ký cũ (một MissingMethodException lúc chạy).

Trong một ứng dụng được biên dịch trọn vẹn cùng lúc, điều này không bao giờ thành vấn đề. Với API công khai trong các package NuGet, nạp chồng (giữ giá trị mặc định bên trong thư viện) hoặc giá trị mặc định null được xử lý trong thân hàm sẽ tránh được vấn đề này.

Giá trị mặc định và kiểu được khai báo

Cũng vì quy tắc lúc biên dịch đó, khi cả interface lẫn lớp đều khai báo giá trị mặc định, giá trị mặc định đến từ kiểu của biến mà bạn gọi qua, không phải từ đối tượng:

Đầu ra:

printing "report" x5
printing "report" x1

Cùng một đối tượng, hai giá trị mặc định khác nhau. Hãy giữ giá trị mặc định giống hệt nhau giữa interface và các lớp hiện thực nó, hoặc chỉ khai báo chúng ở một chỗ.

Các thuộc tính caller information

Tham số tùy chọn cũng là nền tảng cho các thuộc tính caller info trong System.Runtime.CompilerServices. Trình biên dịch tự điền vào chúng thông tin về nơi gọi:

Đầu ra:

[Main:18] starting
[SaveOrder:13] order saved

Đây là cách các thư viện ghi log biết một thông báo đến từ đâu mà nơi gọi không cần tự gõ, và cách các lớp hiện thực INotifyPropertyChanged lấy được tên thuộc tính. [CallerFilePath] thêm đường dẫn tệp mã nguồn theo cách tương tự.

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

Làm sao tạo tham số tùy chọn trong C#?

Gán cho nó một giá trị mặc định ngay trong khai báo: static void Log(string message, string level = "INFO"). Khi đó nơi gọi có thể viết Log("started") hoặc Log("failed", "ERROR"). Tham số tùy chọn phải đứng sau mọi tham số bắt buộc, và giá trị mặc định phải là hằng số lúc biên dịch.

Đối số có tên trong C# là gì?

Là đối số được truyền kèm tên tham số: SendEmail(to: "ana@mail.com", urgent: true). Chúng cho phép bạn bỏ qua các tham số tùy chọn ở giữa, truyền đối số theo thứ tự bất kỳ, và giúp những lời gọi có true/false hay con số viết thẳng dễ đọc ngay từ cái nhìn đầu tiên.

Vì sao không dùng được DateTime.Now làm giá trị mặc định của tham số?

Giá trị mặc định phải là hằng số lúc biên dịch, còn DateTime.Now được tính lúc chạy, nên trình biên dịch báo lỗi CS1736, Default parameter value for 'at' must be a compile-time constant. Hãy dùng một tham số nullable thay thế: DateTime? at = null, rồi viết DateTime time = at ?? DateTime.Now; trong thân hàm.

Nên dùng tham số tùy chọn hay nạp chồng phương thức trong C#?

Tham số tùy chọn đơn giản hơn khi các biến thể chỉ khác nhau ở giá trị mặc định. Nạp chồng tốt hơn khi các biến thể cần logic hoặc kiểu khác nhau, và trong thư viện công khai, vì giá trị mặc định được biên dịch vào từng nơi gọi và việc đổi nó về sau không tác động tới code đã biên dịch.

Lỗi "Optional parameters must appear after all required parameters" nghĩa là gì?

Lỗi CS1737: một tham số có giá trị mặc định đứng trước một tham số không có. Hãy đưa các tham số bắt buộc lên trước: (string to, bool urgent = false), không phải (bool urgent = false, string to). Sau một tham số tùy chọn chỉ có thể là các tham số tùy chọn khác hoặc một mảng params.

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