1. Đa hình trong thực tế
Trong lập trình, đa hình giống như cái remote đa năng ấy: bạn bấm một nút "Tăng âm lượng+", mà nó điều khiển TV, dàn loa hay điều hòa — mỗi thiết bị phản ứng khác nhau, nhưng giao diện thì chỉ có một! Tương tự, các object khác loại có thể phản ứng khác nhau với cùng một method call, miễn là method đó được định nghĩa ở cha chung với từ khóa virtual.
Ở C#, đa hình xuất hiện khi biến kiểu lớp cơ sở (hoặc interface) có thể "giữ" object của bất kỳ lớp con nào, và khi gọi virtual methods trên biến đó thì sẽ chạy đúng implementation thực tế — cái mà object đó định nghĩa. Đây là nền tảng cho các kiến trúc mà logic thay đổi linh hoạt.
Có cần thiết ngoài sách giáo khoa không?
Có chứ! Gần như dự án nào mà làm việc với nhiều object khác nhau nhưng giống nhau về bản chất — từ động vật trong sở thú đến các thành phần giao diện, từ event handler đến hệ thống tài liệu.
- Cho phép tạo thuật toán tổng quát — code làm việc với các thực thể trừu tượng, không cần quan tâm chi tiết implementation cụ thể.
- Đảm bảo khả năng mở rộng — thêm cả trăm loại "động vật", "hình", "handler" cũng không phải sửa code cũ.
- Giảm phụ thuộc giữa các thành phần chương trình (cái này rất hay bị hỏi khi phỏng vấn và thiết kế kiến trúc!).
2. Cú pháp cơ bản và cơ chế hoạt động
Nhớ lại và chỉnh sửa các class Animal, Dog, Cat để demo đa hình. Đã bắt đầu với app "Sở thú ảo" thì mình nâng cấp nó luôn nhé.
Lớp cơ sở với virtual methods
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Phương thức ảo — có thể override
public virtual void MakeSound()
{
Console.WriteLine($"{Name} phát ra một âm thanh nào đó...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Gâu-gâu!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Meo!");
}
}
Dùng đa hình: ví dụ với danh sách động vật
Giả sử bạn có một list động vật — cả thú cưng lẫn không — và muốn tất cả "phát ra âm thanh". Nếu không có đa hình thì phải check type rồi lặp code nhiều lần. Có đa hình thì mọi thứ gọn gàng, đẹp đẽ!
// Tạo mảng động vật các loại
Animal[] animals = new Animal[]
{
new Dog("Bobik"),
new Cat("Murka"),
new Dog("Sharik"),
new Cat("Barsik"),
};
// Lặp qua mảng và yêu cầu từng con phát ra âm thanh
foreach (var animal in animals)
{
animal.MakeSound(); // Sẽ gọi method của Dog hoặc Cat, không phải Animal!
}
Kết quả:
Bobik nói: Gâu-gâu!
Murka nói: Meo!
Sharik nói: Gâu-gâu!
Barsik nói: Meo!
Đó, magic là vậy: một đoạn code tổng quát — kết quả khác nhau tùy loại object thực tế.
Sơ đồ: đa hình hoạt động thế nào
Animal (lớp cơ sở)
/ \
Dog Cat
Khi gọi animal.MakeSound(), với Animal có thể chứa instance của Dog hoặc Cat, môi trường .NET lúc runtime sẽ tự "biết" phải gọi đúng method MakeSound() nào.
3. Giải quyết các bài toán điển hình với đa hình
Ví dụ 1: Danh sách tổng quát, hành động khác nhau
Giả sử bạn làm game. Có class gốc GameObject, các class con là enemy, friend, obstacle. Tất cả đều có thể di chuyển, đều có method Update(), nhưng mỗi loại lại khác nhau.
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("Kẻ địch đang tấn công!");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("Đồng minh đang giúp đỡ!");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// Sẽ in ra:
// Kẻ địch đang tấn công!
// Đồng minh đang giúp đỡ!
// Kẻ địch đang tấn công!
Ví dụ 2: Truyền object vào method
Có thể nhận tham số kiểu cha, nhưng dùng bất kỳ loại con nào cũng được. Rất tiết kiệm công sức, nhất là khi object sẽ có thêm nhiều lớp con mới.
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
Console.WriteLine("Và được cho ăn.");
}
FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Sima"));
// Kết quả:
// Rex: Rex nói: Gâu-gâu!
// Và được cho ăn.
// Sima: Sima nói: Meo!
// Và được cho ăn.
Lưu ý: viết method như này mà không có đa hình thì cực kỳ mệt — phải check loại động vật, if else các kiểu rồi gọi method đúng bằng tay.
Lưu ý quan trọng: binding lúc runtime
Đa hình hoạt động nhờ việc gọi virtual methods được thực hiện động, tức là lúc chạy chương trình. Cái này gọi là late binding. Dù biến là Animal, method vẫn sẽ gọi đúng cái được định nghĩa ở object thực tế.
Nếu method không có virtual, thì luôn gọi code của biến (không phải object). Nên đừng tiếc từ khóa virtual nếu muốn code linh hoạt nhé!
4. Thực hành: mở rộng ứng dụng của mình
Giả sử bạn muốn thêm tính năng mới cho sở thú ảo: mỗi động vật không chỉ MakeSound() mà còn phải di chuyển (Move()). Nhưng mỗi loài lại khác nhau.
1. Thêm virtual method vào lớp cơ sở
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} phát ra một âm thanh nào đó...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} di chuyển theo cách không xác định...");
}
}
2. Ở lớp con, tự implement Move()
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Gâu-gâu!");
}
public override void Move()
{
Console.WriteLine($"{Name} chạy theo cây gậy.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} nói: Meo!");
}
public override void Move()
{
Console.WriteLine($"{Name} rón rén bằng chân mềm.");
}
}
3. Dùng cả hai method trong collection
Animal[] animals = new Animal[]
{
new Dog("Bim"),
new Cat("Lusia")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
Kết quả:
Bim nói: Gâu-gâu!
Bim chạy theo cây gậy.
Lusia nói: Meo!
Lusia rón rén bằng chân mềm.
Trong ứng dụng thực tế, kiểu này giúp viết module cực mạnh và tái sử dụng. Ví dụ, trong game thì đây là nền tảng cho mọi object manager — từ NPC đến hiệu ứng.
5. Bài tập thực tế: vẽ các hình khác nhau
Xem ví dụ không phải sở thú mà là đồ họa nhé. Tạo class cơ sở Shape với virtual method Draw(), rồi mở rộng nó.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Vẽ một hình không xác định.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Vẽ hình tròn.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Vẽ hình chữ nhật.");
}
}
// Collection các hình
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
Ở đây Draw() được gọi trên biến kiểu Shape, nhưng thực tế lại chạy method của Circle hoặc Rectangle. Trong các editor đồ họa hoặc thư viện như WinForms, WPF — cũng hoạt động y chang vậy.
6. Tổng kết: viết thuật toán tổng quát
Đa hình biến code của bạn thành công cụ linh hoạt và mở rộng. Ví dụ, collection có thể chứa không chỉ Dog, Cat mà còn Hamster, Parrot, bất kỳ ai — mà bạn không phải viết thêm dòng nào để xử lý chúng theo cách chung.
Ngoài ra, bạn dễ dàng truyền object con vào method nhận kiểu cha:
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"Tên: {animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Khoma");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Dùng method của Animal
PrintAnimalInfo(dog); // Dùng version của Dog
7. Một số lưu ý hay gặp
Câu hỏi thường gặp và bẫy newbie
Nhiều bạn mới học tưởng rằng tạo object Dog rồi khai báo biến Dog myDog hay Animal myDog thì như nhau. Thực ra, nếu khai báo là Animal, nó chỉ "thấy" những gì có trong Animal (trừ các method override), còn khai báo Dog thì thấy hết: cả Bark() và thuộc tính riêng.
Cũng cần nhớ: nếu method ở lớp cha không có virtual, thì không override được. Nếu bạn thử viết override cho method đó ở lớp con — compiler sẽ báo lỗi ngay.
À, nếu bạn thực sự muốn thay thế method không virtual, dùng từ khóa new, nhưng cái này dành cho trường hợp nâng cao (và dễ gây rối!).
Tại sao phỏng vấn hay hỏi về đa hình?
Vì đa hình giống như dao đa năng cho coder: hiểu và dùng được nó thì bạn xây hệ thống mở rộng, dễ bảo trì, code không chỉ chạy mà còn "sống lâu". Ví dụ, bạn được yêu cầu xử lý nhiều loại thanh toán (kinh điển: BankCard, PayPal, Bitcoin) — và họ mong bạn tạo interface chung (hoặc abstract class) với method Pay(), sau đó client chỉ cần gọi Pay(BankCard), Pay(PayPal), Pay(Bitcoin) — mà không cần biết bên trong hoạt động ra sao.
8. Lỗi newbie hay gặp
Một lỗi phổ biến là cố gọi method riêng của lớp con qua biến kiểu cha. Ví dụ:
Animal animal = new Dog("Tuzik");
animal.Bark(); // Lỗi! Animal không có method Bark.
Tại sao không chạy? Vì biến kiểu Animal chỉ "thấy" những gì được khai báo trong Animal, dù thực tế là Dog. Chỉ gọi được những gì có ở lớp cha — và override (override) ở lớp con.
Nếu bạn thực sự cần gọi method chỉ có ở Dog — phải ép kiểu:
Animal animal = new Dog("Tuzik");
if (animal is Dog dog)
{
dog.Bark();
}
Nhưng thường nếu bạn làm vậy, tức là kiến trúc có vấn đề (hoặc bạn lạm dụng kế thừa).
GO TO FULL VERSION