1. Giới thiệu
Hãy tưởng tượng bạn có một class cha Animal với phương thức Speak(). Mặc định cho mọi con vật, nó in ra "Một số âm thanh". Bạn muốn tạo các class con, ví dụ Dog và Cat, để chúng không nói "Một số âm thanh" mà là câu thật của chúng ("Gâu gâu!" và "Meo meo!").
Bạn có thể viết phương thức Speak() riêng cho từng class con, nhưng nếu bạn làm việc với một mảng kiểu Animal[], khi gọi animal.Speak() thì nó vẫn gọi phương thức của Animal chứ không phải của chó hay mèo — vì C# mặc định dựa vào kiểu biến chứ không phải đối tượng thực sự phía sau.
Ví dụ dễ hiểu nè:
Animal dog = new Dog();
dog.Speak(); // Ủa, nó sẽ in ra gì đây?
Mặc định — "Một số âm thanh", chứ không phải "Gâu gâu!", dù trong Dog có phương thức Speak() riêng. Vậy phải làm sao? Cần khai báo phương thức là virtual (ở class cha) và override (ở class con).
Phương thức ảo là gì?
virtual-method — là phương thức được khai báo trong class cha với từ khóa virtual và dùng để cho phép class con ghi đè lại ( override).Khi gọi phương thức này qua tham chiếu class cha, sẽ gọi đúng phương thức của đối tượng "thực" — giống như ngoài đời: nếu bạn thuần hóa một con vật, mà nó là mèo, nó sẽ kêu "Meo meo!" chứ không phải "Một số âm thanh".
Phương thức ảo là nền tảng của đa hình trong C#.
2. Khai báo phương thức ảo
Cách khai báo phương thức ảo
Ở class cha, khai báo phương thức với từ khóa virtual:
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Một số âm thanh");
}
}
Ở class con, dùng từ khóa override:
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Gâu gâu!");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meo meo!");
}
}
Ví dụ: minh họa đa hình
// Ví dụ, trong file Program.cs
// Class cha Animal với phương thức ảo Speak
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Một số âm thanh");
}
}
// Class Dog, ghi đè hành vi Speak
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Gâu gâu!");
}
}
// Class Cat, cũng ghi đè Speak
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meo meo!");
}
}
public static class Program
{
public static void Main()
{
Animal[] animals = new Animal[]
{
new Animal(),
new Dog(),
new Cat()
};
foreach (Animal animal in animals)
{
animal.Speak(); // Sẽ in: "Một số âm thanh", "Gâu gâu!", "Meo meo!"
}
}
}
Thấy chưa, phương thức ảo thật là "ảo diệu"! Dù biến kiểu Animal, vẫn gọi đúng phiên bản phương thức của từng class cụ thể.
3. Chuyện gì xảy ra "bên trong": bảng ảo
Tóm tắt cơ chế
Khi bạn khai báo phương thức là ảo, compiler sẽ thêm nó vào một "bảng ảo" đặc biệt cho mỗi kiểu (kiểu như menu quán ăn cho từng món). Khi gọi phương thức này, C# không nhìn vào kiểu biến mà nhìn vào đối tượng thực sự — và "gắn" đúng phương thức phù hợp với kiểu thực của object.
Có thể nói, C# tự động thay công thức đặc biệt nếu class có phiên bản riêng.
So sánh: hợp đồng với "phụ lục ghi chú nhỏ"
Hãy tưởng tượng bạn có hợp đồng thuê nhà (class cha), trong đó ghi mặc định người thuê trả tiền cố định mỗi tháng (phương thức PayRent()). Nhưng bạn — chủ nhà ranh mãnh — cho phép ghi chú phụ lục (phương thức ảo). Nếu ai đó (class con) tận dụng quyền này, họ sẽ viết override riêng, và giờ tiền thuê sẽ tính theo cách đặc biệt.
4. Khi nào nên dùng phương thức ảo?
- Nếu phương thức ở class cha nên hoạt động mặc định cho hầu hết class con, nhưng một số có thể cần hành vi riêng.
- Khi xây dựng hệ thống class có thể mở rộng hoặc thay đổi một phần logic.
- Khi thiết kế kiến trúc linh hoạt, ví dụ cho business logic, nơi các loại thao tác khác nhau có thể cần hành vi riêng biệt.
Trong dự án thực tế, phương thức ảo là nền tảng của pattern Template Method, Strategy, và nói chung là nửa thế giới OOP.
So sánh với phương thức thường
| Phương thức thường | Phương thức ảo | |
|---|---|---|
| Có thể ghi đè | Không | Có (override) |
| Cách gọi | Theo kiểu biến | Theo kiểu "thực" của object |
| Đa hình | Không | Có |
Câu hỏi thường gặp
- Có làm constructor thành ảo được không?
Không nhé! Constructor không bao giờ là ảo — luôn chạy đúng kiểu object. - Phương thức ảo có thể là static không?
Không, chỉ phương thức instance mới ảo được. Static method không tham gia đa hình, vì không có object — ai mà ghi đè được? - Field có thể là ảo không?
Không, chỉ method, property và event thôi.
5. Thực hành: phát triển app "Thế giới động vật"
Trong app học tập của mình, bạn đã làm hệ thống động vật đơn giản. Giờ hãy thêm phương thức ảo mới cho động vật, để mỗi loài ăn theo cách riêng!
Ví dụ code có chú thích
// Ở class cha, khai báo phương thức ảo Eat
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Một số âm thanh");
}
public virtual void Eat()
{
Console.WriteLine("Ăn thức ăn chung chung.");
}
}
// Ở Dog, ghi đè Eat
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Gâu gâu!");
public override void Eat() => Console.WriteLine("Ăn thức ăn cho chó.");
}
// Ở Cat cũng có hành vi riêng
public class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meo meo!");
public override void Eat() => Console.WriteLine("Ăn cá.");
}
public static class Program
{
public static void Main()
{
Animal[] animals = new Animal[]
{
new Animal(),
new Dog(),
new Cat()
};
foreach (var animal in animals)
{
animal.Speak();
animal.Eat();
Console.WriteLine("---");
}
}
}
Xong, giờ mỗi boss và sen ăn theo kiểu riêng! Còn Animal gốc thì vẫn ăn món gì đó lạ lạ, đúng kiểu "default".
6. Lỗi thường gặp và lưu ý
- Có thể quên override ở class con — lúc đó phương thức class cha vẫn giữ nguyên.
- Hoặc ngược lại, quên virtual ở class cha, thì class con không thể ghi đè bằng override.
Không thể viết override nếu thiếu virtual
Compiler C# sẽ không cho ghi đè phương thức nếu nó không được khai báo là ảo (hoặc abstract) ở class cha.
Tại sao cần override
override nói rõ với compiler và người đọc code: "Tôi không chỉ viết method mới — tôi cố ý thay đổi cái đã có ở class cha". Điều này tránh lỗi khi bạn muốn "ghi đè" mà lại vô tình viết method mới trùng tên.
Nếu quên override mà viết method mới
public class Dog : Animal
{
public void Speak()
{
Console.WriteLine("Gâu gâu!");
}
}
Nhưng đây không phải ghi đè! Khi truy cập Dog qua biến kiểu Animal, sẽ gọi method của động vật ("Một số âm thanh"), chứ không phải của chó. Đúng là bất ngờ với kiểu dữ liệu!
GO TO FULL VERSION