1. Giới thiệu
Ngoài đời nhiều hành động giống như dao đa năng Thụy Sĩ ấy: cùng một lệnh nhưng có thể làm việc với nhiều bộ công cụ khác nhau. Ví dụ, tưởng tượng bạn dùng cây ATM: nếu bạn nhét thẻ vào — ATM hỏi mã PIN; nếu nhập số điện thoại — ATM đợi mã xác nhận từ SMS. Hành động là "kiểm tra người dùng", nhưng cách làm thì khác nhau.
Trong lập trình cũng vậy, nhiều lúc bạn cần thực hiện một thao tác, nhưng dữ liệu lại có thể khác kiểu hoặc khác số lượng tham số. Ví dụ, hàm của bạn cần in lời chào cho người hoặc cho thú, hoặc cộng hai, ba, thậm chí mười số nguyên.
Tất nhiên, bạn có thể đặt tên hàm khác nhau: SumTwo, SumThree, SumArray. Nhưng dev tụi mình lười lắm (người ta bảo lười là động lực phát triển mà). Với lại, code như vậy nhìn chán lắm.
Nạp chồng phương thức
Nạp chồng phương thức — là cách "bắt" một phương thức làm việc với nhiều bộ tham số khác nhau, mà tên hàm vẫn giữ nguyên. Đây là một dạng của đa hình, nhưng không liên quan đến kế thừa nha.
Nạp chồng phương thức nghĩa là bạn có thể tạo trong một class (hoặc struct) nhiều phương thức cùng tên, nhưng khác danh sách tham số (khác kiểu, số lượng và/hoặc thứ tự).
Chữ ký phương thức
Chữ ký của phương thức trong C# là tên hàm cộng với kiểu và thứ tự tham số. Kiểu trả về không nằm trong chữ ký đâu nha! Nhiều bạn hay bị lỗi chỗ này (mình sẽ nói kỹ hơn ở dưới).
2. Nạp chồng thực tế: ví dụ đơn giản
Giờ mình tạo class Greeter để chào user theo nhiều cách: chỉ tên, tên và tuổi, hoặc không tham số luôn.
public class Greeter
{
// Chào không tham số
public void Greet()
{
Console.WriteLine("Xin chào, thế giới!");
}
// Chào với tên
public void Greet(string name)
{
Console.WriteLine($"Xin chào, {name}!");
}
// Chào với tên và tuổi
public void Greet(string name, int age)
{
Console.WriteLine($"Xin chào, {name}! Bạn đã {age} tuổi rồi hả? Ghê thật!");
}
}
Giờ bạn có thể gọi bất kỳ hàm nào, compiler C# sẽ tự chọn đúng phiên bản — dựa vào số lượng và kiểu tham số truyền vào.
var greeter = new Greeter();
greeter.Greet(); // Xin chào, thế giới!
greeter.Greet("Anya"); // Xin chào, Anya!
greeter.Greet("Pyotr", 23); // Xin chào, Pyotr! Bạn đã 23 tuổi rồi hả? Ghê thật!
3. Phân biệt theo kiểu và số lượng tham số
Nạp chồng hoạt động nếu các phương thức khác nhau về:
- số lượng tham số,
- kiểu của ít nhất một tham số,
- thứ tự kiểu tham số (nhưng cái này phải cẩn thận).
Thử thêm một overload nữa, chỉ nhận tuổi:
public void Greet(int age)
{
Console.WriteLine($"Từng này tuổi là đỉnh lắm rồi! ({age} tuổi)");
}
Giờ gọi thử:
greeter.Greet(10); // Từng này tuổi là đỉnh lắm rồi! (10 tuổi)
Nhớ nha: nếu các phương thức chỉ khác kiểu trả về, không nạp chồng được. Ví dụ này sẽ lỗi:
// Lỗi biên dịch!
public int Foo(string s) { ... }
public double Foo(string s) { ... }
Compiler sẽ báo lỗi: "Đã có phương thức Foo(string) rồi, nghĩ cái gì khó hơn đi!"
4. Nạp chồng và thư viện chuẩn C#
Nạp chồng không chỉ có ở Greet của mình đâu. Mở tài liệu .NET cho Console.WriteLine thử xem:
| Chữ ký | Mục đích |
|---|---|
|
In dòng trống |
|
In chuỗi |
|
In số nguyên |
|
In số thực |
|
Định dạng chuỗi với một tham số |
|
Định dạng với nhiều tham số |
Tất cả đều là overload của WriteLine. Giờ bạn hiểu vì sao luôn có thể làm thế này:
Console.WriteLine("Chỉ là chuỗi thôi");
Console.WriteLine(123);
Console.WriteLine(2.5);
Console.WriteLine("Tổng: {0}", 42);
Và compiler luôn hiểu đúng ý bạn!
5. Compiler chọn overload nào?
Rất nghiêm túc: nó nhìn vào kiểu và số lượng tham số thực tế truyền vào. Nhìn bảng này cho dễ hiểu:
| Gọi | Phiên bản nào chạy? |
|---|---|
|
|
|
|
Nếu bị mơ hồ thì sao?
Đôi khi mọi thứ vượt khỏi tầm kiểm soát. Ví dụ overload mơ hồ — compiler không biết chọn cái nào
public void Print(int a, double b) { ... }
public void Print(double a, int b) { ... }
printer.Print(5, 10);
// Lỗi: mơ hồ — gọi Print nào đây? (cả hai đều hợp lý)
Compiler sẽ báo lỗi mơ hồ. Trường hợp này nên tránh overload có cùng số tham số và kiểu gần giống nhau, dễ làm compiler rối.
6. params — số lượng tham số linh hoạt
Giả sử bạn muốn tạo hàm nhận số lượng số không xác định. Hãy dùng từ khóa params.
public void SumAll(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
Console.WriteLine($"Tổng: {sum}");
}
Giờ bạn có thể gọi:
SumAll(1, 2, 3); // Tổng: 6
SumAll(10, 20); // Tổng: 30
SumAll(); // Tổng: 0
Hàm dùng params có thể kết hợp với overload, nhưng nhớ đừng tạo overload khiến compiler không biết bạn muốn gọi cái nào.
7. Nạp chồng và modifier tham số (ref, out, in)
C# phân biệt phương thức theo modifier tham số (tức là, chữ ký void Foo(int a) khác void Foo(ref int a), và cả hai cùng tồn tại trong một class):
public void SetValue(int a)
{
a = 42;
}
public void SetValue(ref int a)
{
a = 100;
}
Gọi không có ref sẽ vào phiên bản đầu, có ref sẽ vào phiên bản sau:
int n = 5;
SetValue(n); // n vẫn là 5 (giá trị được copy)
SetValue(ref n); // n thành 100
8. Sơ đồ: nạp chồng là gì
+----------+
| MyClass |
+----------+
|
| (một số phương thức)
+-----------------------+
| void Foo() |
| void Foo(int a) |
| void Foo(string s) |
| void Foo(int a, int b) |
+-----------------------+
Nếu viết code thì:
// Gọi các phiên bản overload của Foo():
var mc = new MyClass();
mc.Foo(); // void Foo()
mc.Foo(5); // void Foo(int)
mc.Foo("Hello"); // void Foo(string)
mc.Foo(2, 3); // void Foo(int, int)
9. Ví dụ: nạp chồng phương thức trong app của mình
Tiếp tục phát triển app học tập, thêm overload vào hệ động vật.
public class Animal
{
public string Name { get; set; }
// Phương thức phát ra âm thanh
public virtual void MakeSound()
{
Console.WriteLine("Âm thanh gì đó khó hiểu...");
}
// Overload: âm thanh với độ lớn chỉ định
public void MakeSound(int volume)
{
Console.WriteLine($"Động vật phát ra âm thanh với độ lớn {volume} dB.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Gâu!");
}
// Overload: gâu với độ lớn
public void MakeSound(int volume)
{
Console.WriteLine($"Gâu! (độ lớn: {volume} dB)");
}
}
Thử gọi như này:
Dog rex = new Dog();
rex.MakeSound(); // Gâu!
rex.MakeSound(75); // Gâu! (độ lớn: 75 dB)
Lưu ý: trong class con (Dog) mình overload MakeSound(int volume), nên giờ nó có cả hai phiên bản: có và không có tham số.
10. Lỗi thường gặp khi nạp chồng phương thức
Lỗi #1: cố nạp chồng chỉ khác kiểu trả về.
Không được đâu — kiểu trả về không nằm trong chữ ký. Nạp chồng phải khác về số lượng hoặc kiểu tham số đầu vào, chứ không phải void hay int.
Lỗi #2: overload mơ hồ làm compiler rối.
Overload cùng số tham số và kiểu gần giống nhau (ví dụ int và double) dễ làm compiler không biết chọn cái nào. Ví dụ: Print(int a, double b) và Print(double a, int b) — gọi Print(1, 1) sẽ lỗi mơ hồ.
Lỗi #3: params bị trùng với overload khác.
Hàm có params có thể bắt luôn cả lệnh gọi dành cho overload khác. Nếu kiểu giống nhau, compiler có thể chọn nhầm hàm bạn không mong muốn.
Lỗi #4: quên rằng ref và out nằm trong chữ ký.
Hàm Do(ref int x) và Do(out int x) là hai overload khác nhau. Nếu không để ý, dễ gọi nhầm phiên bản.
GO TO FULL VERSION