1. Giới thiệu
Giả sử tụi mình có một app để quản lý động vật trong sở thú (cái ví dụ này tụi mình đã bắt đầu phát triển từ mấy bài trước rồi đó). Tụi mình tạo một class cơ bản Animal với phương thức MakeSound, để mọi con vật đều có thể "phát ra" một âm thanh nào đó.
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Động vật phát ra âm thanh.");
}
}
Nhưng rồi phát hiện ra sư tử với vẹt kêu khác nhau hoàn toàn. Câu "Động vật phát ra âm thanh" chung chung không còn hợp nữa. Đây là lúc ghi đè phương thức xuất hiện. Bạn cần mỗi con cháu kêu theo kiểu riêng của nó!
2. Cú pháp: override hoạt động thế nào
Quy tắc cơ bản
- Muốn ghi đè phương thức, nó phải được đánh dấu là virtual (hoặc abstract) trong class cha.
- Ở class con, dùng từ khóa override trước phương thức có cùng chữ ký.
Ví dụ: sư tử và vẹt
public class Lion : Animal
{
public override void MakeSound()
{
Console.WriteLine("Gừ gừ gừ!");
}
}
public class Parrot : Animal
{
public override void MakeSound()
{
Console.WriteLine("Popka ngốc!");
}
}
Giờ nếu bạn tạo một mảng động vật và gọi MakeSound cho từng con, mỗi con sẽ "nói" theo kiểu riêng:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Animal()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
// Kết quả:
// Gừ gừ gừ!
// Popka ngốc!
// Động vật phát ra âm thanh.
3. Virtual table (v-table)
Khi bạn dùng virtual và override, compiler sẽ tạo cho class một "bảng phương thức ảo" (v-table).
Khi gọi phương thức qua tham chiếu class cha, CLR sẽ kiểm tra bảng này: phương thức đó có bị ghi đè ở class con không? Nếu có — sẽ gọi version của class con.
Đó chính là "ma thuật" của late binding, hay dynamic polymorphism.
4. Kết hợp lại: tiếp tục phát triển app của tụi mình
Thêm một class nữa nhé:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Gâu gâu!");
}
}
Và dùng nó trong mini-app quản lý động vật:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Dog()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
Giờ code mở rộng và bảo trì dễ hơn nhiều, thêm động vật mới cũng vui phết!
5. Ghi đè thuộc tính và override với giá trị trả về
Không chỉ phương thức mới ghi đè được đâu. Bạn cũng có thể ghi đè thuộc tính virtual và indexer nữa.
public class Animal
{
public virtual string Name { get; set; } = "Động vật";
}
public class Lion : Animal
{
public override string Name { get; set; } = "Sư tử";
}
Cái này cực tiện khi cần bổ sung info cho từng loài cụ thể.
6. Dùng base để gọi implementation của class cha
Đôi khi bạn muốn mở rộng hành vi của phương thức cha chứ không phải thay thế hoàn toàn.
Lúc này, gọi implementation của cha bằng từ khóa base trong phương thức ghi đè.
public class Parrot : Animal
{
public override void MakeSound()
{
base.MakeSound(); // "Động vật phát ra âm thanh."
Console.WriteLine("Popka ngốc!");
}
}
Vậy là con vẹt sẽ phát ra âm thanh "chung" trước, rồi mới tới câu "Popka ngốc!" đặc trưng của nó.
7. Ứng dụng thực tế: override dùng ở đâu?
Hiểu cơ chế override là bắt buộc trong bất kỳ project C# nghiêm túc nào dùng OOP.
- Model động vật trong sở thú? Đã làm rồi.
- Tạo custom control cho UI: Bạn ghi đè các phương thức hiển thị chuẩn, thêm logic riêng.
- Business logic linh hoạt: Cho phép xây dựng "khung" hành vi ở class cha, còn chi tiết thì để class con xử lý.
- Testing và mock: Dễ dàng "thay thế" phương thức qua class con cho unit test.
- Plugin và extension: Interface hoặc abstract base class, nhiều implementation — tất cả chạy được nhờ override đúng cách.
8. sealed override và phương thức virtual trong chuỗi kế thừa
Nếu bạn không muốn ai đó ở dưới kế thừa tiếp tục ghi đè phương thức đã override, dùng sealed override:
public class Base
{
public virtual void Foo() { }
}
public class Middle : Base
{
public sealed override void Foo() { }
}
public class Last : Middle
{
public override void Foo() {} // Lỗi — không được ghi đè!
}
Cái này giúp "khóa" chuỗi override ở chỗ bạn muốn, đảm bảo hệ thống chạy đúng.
9. Phương thức mới (từ khóa new)
Đôi khi bạn muốn khai báo ở class con một phương thức trùng chữ ký, nhưng class cha lại không virtual.
Khi đó dùng từ khóa new — nhưng đây không phải polymorphism, chỉ là "che giấu" thôi:
public class Animal
{
public void MakeSound()
{
Console.WriteLine("Tôi là động vật!");
}
}
public class Cat : Animal
{
public new void MakeSound()
{
Console.WriteLine("Tôi là mèo!");
}
}
Animal animal = new Cat();
animal.MakeSound(); // "Tôi là động vật!"
Cat cat = new Cat();
cat.MakeSound(); // "Tôi là mèo!"
Ở đây, kiểu biến lúc gọi quyết định tất cả! Nên muốn dynamic polymorphism thực sự thì luôn dùng combo virtual + override.
10. Lỗi phổ biến khi ghi đè phương thức
Lỗi số 1: cố ghi đè phương thức không được khai báo là virtual, abstract hoặc override.
Trong C#, không thể override phương thức thường. Nếu phương thức ở class cha không có modifier đặc biệt (virtual, abstract hoặc override), viết override ở class con sẽ báo lỗi compile.
Lỗi số 2: chữ ký phương thức không khớp.
Để phương thức thực sự được override, tên, kiểu trả về và tham số phải giống hệt phương thức ở class cha. Lệch một tí (ví dụ kiểu tham số khác) là tạo ra phương thức mới chứ không phải override.
Lỗi số 3: quên modifier override.
Nếu bạn định nghĩa phương thức trùng chữ ký với class cha mà không ghi override, compiler không coi đó là override. Đây gọi là ẩn phương thức (sẽ nói kỹ sau). Khi đó, gọi phương thức qua biến kiểu cha sẽ ra kết quả bất ngờ — sẽ gọi phương thức của cha chứ không phải của bạn.
GO TO FULL VERSION