Menu

Tham số ref, out và in trong C#: truyền tham chiếu dễ hiểu

Cách tham số ref, out và in hoạt động trong C#: truyền chính biến thay vì bản sao, out cho kết quả phụ và mẫu TryParse, out var, tham số in chỉ đọc, quy tắc gán giá trị bắt buộc, và điều gì thay đổi khi đối số là kiểu tham chiếu.

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.

Mặc định, C# truyền mọi đối số theo giá trị (by value): phương thức nhận một bản sao, và việc gán lại tham số không ảnh hưởng đến bên gọi. Các từ khóa ref, out và in thì truyền chính biến của bên gọi.

Đầu ra:

by value: 50
by ref:   60

Từ khóa xuất hiện hai lần: trong danh sách tham số (ref int score) và ở nơi gọi (ref score). Viết nó ở nơi gọi là bắt buộc, và đó là chủ ý: ai đọc AddPoints(ref score) cũng biết score có thể thay đổi. Quên nó thì trình biên dịch báo CS1620, Argument 1 must be passed with the 'ref' keyword.

ref: đọc và ghi biến của bên gọi

Tham số ref là một tên khác (alias) của biến bên gọi. Phương thức có thể đọc giá trị hiện tại và thay thế nó. Ví dụ kinh điển là hoán đổi hai giá trị, điều không thể làm với tham số thông thường:

Đầu ra:

Linus Ada
volume 100

Quy tắc của ref:

  • Đối số phải là một biến (biến cục bộ, tham số, field hoặc phần tử mảng), không phải literal hay biểu thức. Thuộc tính cũng không được: Clamp(ref player.Volume, 0, 100) là lỗi CS0206, A non ref-returning property or indexer may not be used as an out or ref value. Hãy chép thuộc tính vào một biến cục bộ, truyền biến đó, rồi gán ngược lại.
  • Biến phải chắc chắn đã được gán trước lời gọi. int n; Clamp(ref n, 0, 10); là lỗi CS0165, Use of unassigned local variable 'n'.

out: kết quả phụ

out dành cho các giá trị mà phương thức tạo ra. Bên gọi không cần khởi tạo biến, còn phương thức bắt buộc phải gán nó trên mọi nhánh trước khi trả về:

static void Split(string fullName, out string first, out string last)
{
    // error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
    first = fullName.Split(' ')[0];
}

Bên trong phương thức, tham số out được coi là chưa gán cho đến khi bạn ghi vào, nên bạn không thể đọc giá trị cũ của bên gọi. Đó là khác biệt so với ref: ref là hai chiều, còn out chỉ một chiều từ phương thức về bên gọi.

Mẫu TryParse

Cách dùng out nổi tiếng nhất là mẫu Try trong thư viện chuẩn: trả về bool để báo thành công và đặt kết quả vào tham số out, nhờ vậy đầu vào sai không bao giờ ném ngoại lệ:

Đầu ra:

"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized

out var quantity (C# 7.0) khai báo biến ngay trong lời gọi; kiểu của nó lấy từ tham số. Khi thất bại, TryParse đặt nó về 0 và trả về false. Khi bạn chỉ quan tâm việc phân tích có thành công hay không, hãy bỏ giá trị bằng out _: bool isNumber = int.TryParse(input, out _);.

Để ý cách TryGetDiscount gán rate trên cả hai nhánh: một lần gán trực tiếp và một lần chuyển tiếp nó cho Dictionary.TryGetValue, bản thân cũng là một lời gọi out.

out hay trả về tuple

Trước C# 7, out là cách chính để trả về nhiều giá trị. Giờ đây tuple làm việc đó trực tiếp hơn:

// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);

// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);

Dùng tuple (hoặc một lớp nhỏ) cho nhiều kết quả thông thường. Giữ out cho mẫu Try, nơi giá trị trả về bool cho phép đặt lời gọi ngay trong điều kiện if.

Kiểu tham chiếu: thay đổi đối tượng hay thay thế nó

Truyền một instance của lớp theo giá trị đã cho phép phương thức thay đổi đối tượng, vì giá trị được sao chép là một tham chiếu đến cùng đối tượng. ref thêm một khả năng nữa: thay thế đối tượng mà biến của bên gọi trỏ tới.

Đầu ra:

after AddItem:      2 items
after ResetByValue: 2 items
after ResetByRef:   0 items

ResetByValue cho bản sao tham chiếu của riêng nó trỏ sang một list mới; bên gọi không hề thấy. ResetByRef thì thay đổi chính biến của bên gọi. Trong code hằng ngày, bạn hiếm khi cần ref cho tham số kiểu lớp: hãy trả về đối tượng mới thay vào đó.

Với struct, khác biệt còn lớn hơn, vì một đối số struct được sao chép từng field. Phương thức cần sửa struct của bên gọi (một Point, một struct cấu hình lớn) phải dùng ref.

in: chỉ đọc theo tham chiếu

C# 7.2 bổ sung in, truyền theo tham chiếu nhưng cấm gán. Mục đích là hiệu năng: struct lớn không bị sao chép ở mỗi lời gọi, và bên gọi được đảm bảo nó sẽ không bị thay đổi.

// C# 7.2 and later
struct Matrix4
{
    public double M11, M12, M13, M14, M21, M22, M23, M24,
                  M31, M32, M33, M34, M41, M42, M43, M44;
}

static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;

static void Reset(in Matrix4 m)
{
    m.M11 = 0;
    // error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}

Ở nơi gọi, in là tùy chọn: Trace(matrix) và Trace(in matrix) đều chạy được. Với int, double, DateTime và các kiểu nhỏ khác, in không mang lại gì và có thể chậm hơn một chút; hãy dùng nó cho các struct nhiều field mà bạn truyền thường xuyên.

Giới hạn

  • Tham số ref và out không thể là tham số tùy chọn (CS1741). Tham số in thì có thể: static void Connect(in int retries = 3) là hợp lệ.
  • Các overload không thể chỉ khác nhau giữa ref và out (hoặc in), vì ở cấp runtime chúng giống nhau. F(int x) và F(ref int x) thì có thể cùng tồn tại.
  • Phương thức async và iterator (phương thức dùng yield return) không thể có tham số ref, out hoặc in. Với chúng, hãy trả về một tuple hoặc một đối tượng kết quả.
  • Lambda hay hàm cục bộ không thể capture tham số ref, out hoặc in của phương thức bao ngoài; hãy chép nó vào một biến cục bộ trước.

So sánh ref, out và in

Bên gọi phải gán trướcPhương thức phải gánPhương thức được đọcTừ khóa ở nơi gọi
(không có)cókhôngcó (một bản sao)không
refcókhôngcóbắt buộc
outkhôngcó, trên mọi nhánhchỉ sau khi gánbắt buộc
incókhông được phépcótùy chọn

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

ref và out trong C# khác nhau thế nào?

Cả hai đều truyền chính biến của bên gọi chứ không phải bản sao. Với ref, biến phải được gán giá trị trước lời gọi, và phương thức được phép đọc lẫn thay đổi nó. Với out, biến không cần gán trước, phương thức không được đọc nó trước khi ghi, và phải gán nó trước khi trả về (nếu không sẽ gặp CS0177). out có nghĩa là "đây là một kết quả phụ".

out var trong C# có nghĩa là gì?

Từ C# 7.0 bạn có thể khai báo biến out ngay trong lời gọi: if (int.TryParse(text, out var number)). Kiểu của biến được suy ra từ tham số, và biến vẫn dùng được sau câu lệnh đó. out _ bỏ đi kết quả bạn không cần.

Trong C#, đối tượng có được truyền theo tham chiếu không?

Không, mọi đối số đều được truyền theo giá trị trừ khi bạn viết ref, out hoặc in. Với một lớp, giá trị được sao chép là một tham chiếu, nên phương thức có thể thay đổi các field của đối tượng và bên gọi thấy được thay đổi đó. Điều phương thức không làm được nếu thiếu ref là khiến biến của bên gọi trỏ sang một đối tượng khác.

Từ khóa in cho tham số trong C# là gì?

in (từ C# 7.2) truyền đối số theo tham chiếu nhưng chỉ đọc: phương thức không thể gán giá trị cho nó. Mục đích là tránh sao chép các struct lớn ở mỗi lời gọi. Với các kiểu nhỏ như int hay DateTime thì không có lợi ích gì.

Vì sao tôi gặp lỗi "Argument 1 must be passed with the 'ref' keyword"?

Phương thức khai báo tham số ref nhưng lời gọi lại bỏ sót từ khóa này. C# yêu cầu viết ref (và out) cả ở nơi gọi, để người đọc thấy biến có thể bị thay đổi: Increment(ref count);, chứ không phải Increment(count);.

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