1. Giới thiệu
Trong C#, mặc định tất cả tham số của method được truyền theo giá trị, nghĩa là method nhận được một bản sao, không phải bản gốc.
Nhưng nếu bạn muốn thay đổi biến bên ngoài từ trong method thì sao? Hoặc trả về nhiều giá trị cùng lúc? Hay bạn cần truyền một struct to đùng vào method mà không muốn copy cả cái "vali không tay cầm" đó, chỉ cần truyền tham chiếu để tiết kiệm RAM thôi?
Tất cả những thứ đó là về bộ sửa đổi tham số. Chúng cho bạn quyền kiểm soát "đường đi" của dữ liệu giữa code gọi và method.
Các loại bộ sửa đổi
| Bộ sửa đổi | Truyền dữ liệu | Có cần khởi tạo trước khi gọi? | Có đọc được bên trong? | Có ghi được bên trong? | Tình huống dùng phổ biến |
|---|---|---|---|---|---|
| ref | Hai chiều | Có | Có | Có | Truyền biến để đọc và/hoặc sửa |
| out | Chỉ ra ngoài | Không | Không (trước khi gán) | Có (bắt buộc!) | Trả về nhiều giá trị từ method |
| in | Chỉ vào trong | Có | Có | Không | Truyền struct lớn chỉ để đọc, tiết kiệm RAM |
Đừng lo, giờ mình sẽ đi qua từng cái một. Dễ hơn bạn nghĩ nhiều!
2. Bộ sửa đổi ref: Truyền theo tham chiếu: vừa đọc vừa ghi
Hãy tưởng tượng: bạn mang cho bạn một cốc cà phê và bảo — "Đây, cậu muốn uống, hâm nóng hay cho thêm gì cũng được". Bạn cầm cốc đó muốn làm gì thì làm, và sau đó cái còn lại (hoặc không còn gì) sẽ trả về cho bạn. Đó chính là ref.
void DoSomething(ref int x)
{
x = x + 10; // Thay đổi tham số đầu vào
}
Để truyền tham số kiểu ref, bạn phải khai báo nó với bộ sửa đổi này cả khi định nghĩa lẫn khi gọi method:
int myNumber = 5;
DoSomething(ref myNumber);
Console.WriteLine(myNumber); // sẽ in ra 15
Bắt buộc phải là biến, không thể truyền biểu thức theo tham chiếu. Code kiểu này sẽ không chạy:
DoSomething(ref 10); // ở đây phải là biến!
- Trước khi gọi: biến phải được khởi tạo (có giá trị rồi).
- Trong method: có thể đọc và sửa giá trị.
- Sau khi gọi: giá trị thay đổi được giữ lại.
Ví dụ: Đổi chỗ giá trị hai biến
Đôi khi bạn cần đổi chỗ giá trị của hai biến. Nhưng có một điểm: trong C#, kiểu giá trị (ví dụ int) mặc định truyền theo giá trị. Nghĩa là khi truyền vào method, chỉ copy giá trị, không phải tham chiếu đến biến. Để method có thể thay đổi biến gốc, bạn phải dùng từ khóa ref.
Ví dụ không dùng ref — không hoạt động:
// Thử đổi giá trị — không thành công
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(x, y); // Truyền bản sao giá trị
Console.WriteLine($"{x}, {y}"); // → 10, 20 — không thay đổi gì!
Trong hàm Swap, biến a và b là bản sao của x và y. Bạn chỉ đổi bản sao, bản gốc không bị ảnh hưởng.
Ví dụ dùng ref — biến đổi chỗ thành công
// Đổi giá trị hai biến dùng ref
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(ref x, ref y); // Truyền tham chiếu đến biến
Console.WriteLine($"{x}, {y}"); // → 20, 10 — đã đổi chỗ đúng
ref cho phép truyền tham chiếu đến biến, không phải giá trị.
Vì vậy hàm Swap thao tác trực tiếp với x và y, đổi giá trị của chúng.
Khi nào dùng ref?
- Cần thay đổi biến truyền vào (ví dụ tăng giá trị, thay thế).
- Không cần trả về giá trị mới (như với return), vì muốn sửa trực tiếp tham số.
- Đừng dùng để "xả" cả đống giá trị — cái đó để bộ sửa đổi khác lo.
3. Bộ sửa đổi out: Truyền ra ngoài
Giờ tưởng tượng: bạn đến nhà bạn, cầm cốc rỗng và bảo: "Đổ vào đây cái gì cũng được, cà phê hay trà cũng ok!" Sau đó bạn của bạn bắt buộc phải đổ gì đó vào, nếu không compiler sẽ la làng.
out được tạo ra để method trả về nhiều giá trị khác nhau mà không cần dùng return. Tham số này bắt buộc phải được gán giá trị trong method.
void GetCoordinates(out int x, out int y)
{
x = 5;
y = 10; // Không có dòng này là lỗi!
}
- Trước khi gọi: biến không cần khởi tạo, thậm chí có thể viết luôn out int x trong lời gọi.
- Trong method: bắt buộc phải gán giá trị trước khi thoát, không thì compiler đình công.
- Sau khi gọi: biến chứa giá trị mới.
Ví dụ: Trả về hai giá trị cùng lúc từ method
void ParseNameAndAge(string input, out string name, out int age)
{
string[] parts = input.Split(','); // chuyển chuỗi thành mảng chuỗi - tách theo ','
name = parts[0];
age = int.Parse(parts[1]);
}
string userInput = "Ivan,25";
ParseNameAndAge(userInput, out string userName, out int userAge);
Console.WriteLine($"{userName} — {userAge} tuổi");
Khi nào dùng out?
- Khi cần trả về nhiều giá trị từ method (ví dụ tách chuỗi như trên).
- Khi kiểu trả về không xác định trước (ví dụ thử chuyển chuỗi thành số).
Ví dụ trong .NET: Method int.TryParse(string, out int) — kinh điển!
string input = "123";
if (int.TryParse(input, out int parsedNumber))
{
Console.WriteLine($"Đã chuyển: {parsedNumber}");
}
else
{
Console.WriteLine("Lỗi chuyển đổi!");
}
Method int.TryParse trả về true nếu chuyển chuỗi thành số thành công và false nếu thất bại. Giá trị số thực tế trả về qua out.
Lỗi phổ biến và tình huống hài khi dùng ref và out
- Quên khởi tạo biến cho ref — compiler sẽ nổi cáu.
- Không gán giá trị cho out — compiler còn cáu hơn.
- Quên ghi rõ bộ sửa đổi khi gọi: viết DoSomething(x) thay vì DoSomething(ref x).
- Dùng ref hoặc out với hằng số hoặc literal không được! Chỉ biến thôi. Chỉ biến mới có địa chỉ trong RAM để tạo tham chiếu.
4. Bộ sửa đổi in: Chỉ đọc, và chỉ truyền theo tham chiếu
Đôi khi bạn cần truyền vào method một object to và phức tạp. Thường thì object này truyền theo tham chiếu, nhưng như vậy method có thể vô tình sửa nó — vì bạn đã cho nó quyền truy cập đầy đủ.
Lý thuyết thì bạn có thể copy toàn bộ object rồi truyền vào. Như vậy bản gốc không bị sửa. Nhưng nếu object quá lớn, bạn sẽ tốn RAM vô ích. Tốt nhất là truyền object vào và nhờ compiler đảm bảo không ai được sửa nó.
in — bộ sửa đổi mới, cho phép truyền struct theo tham chiếu, nhưng chỉ để đọc. Kiểu như "trưng bày thanh kiếm quý sau kính": nhìn thì được, cầm thì không!
void PrintPoint(in Point pt)
{
pt.X = 5; // Lỗi! Chỉ đọc thôi.
Console.WriteLine($"Điểm: {pt.X}, {pt.Y}"); //Cái này thì ok
}
- Trước khi gọi: biến phải được khởi tạo.
- Trong method: chỉ đọc, không được sửa.
- Tiết kiệm RAM: struct không bị copy, truyền theo tham chiếu.
Dùng cho mảng struct lớn để tránh copy thừa. Nhưng với class (kiểu tham chiếu) thì in không có tác dụng gì đặc biệt.
Ví dụ: Truyền struct lớn theo tham chiếu để khỏi copy
struct BigData
{
public int A, B, C, D, E;
}
void PrintBigData(in BigData data)
{
data.A = 10; // không được - chỉ đọc thôi!
Console.WriteLine(data.A + data.B + data.C + data.D + data.E);
}
BigData myData = new BigData { A = 10, B = 20, C = 30, D = 40, E = 50 };
PrintBigData(in myData);
5. Những câu hỏi hay gặp:
Có dùng ref/out/in với mảng và kiểu tham chiếu được không?
Có nhé! Nhưng nhớ: mảng vốn đã là kiểu tham chiếu. Nếu truyền mảng bằng ref, bạn có thể đổi luôn tham chiếu. Thường thì cái này hay dùng cho struct hơn.
Khác biệt giữa ref và out là gì, nếu cả hai đều lấy được giá trị?
Với ref biến phải được khởi tạo trước khi dùng. Với out thì không cần, nhưng trong method bắt buộc phải gán giá trị ít nhất một lần.
Nếu mình thử dùng literal (ví dụ số 5) với ref/out/in thì sao?
Compiler sẽ báo lỗi ngay. Chỉ dùng biến thôi!
Có giới hạn bao nhiêu tham số ref/out/in không?
Bao nhiêu cũng được, không giới hạn! Nhưng đừng lạm dụng để code dễ đọc: nếu có hơn hai-ba cái, nên nghĩ đến trả về tuple, struct hoặc class.
GO TO FULL VERSION