1. Giới thiệu
Khi newbie nghe từ kế thừa, có thể nghĩ mọi thứ đơn giản lắm: chỉ cần lấy một class có sẵn, mở rộng hoặc sửa đổi một chút — thế là xong! Nhưng thực tế thì có nhiều chi tiết nhỏ. Lỗi khi kế thừa có thể xuất hiện theo nhiều cách: signature sai, quên từ khóa, thiết kế hierarchy tệ — tất cả dẫn đến bug khó chịu.
Có lúc lỗi xuất hiện ngay: code không compile được. Nhưng cũng có lúc bug chỉ xuất hiện khi chạy, khi SuperMegaLogger của bạn ghi log không đúng như user mong đợi, hoặc thậm chí không ghi gì luôn. Buồn, thất vọng, debug cả đêm — quen không?
Cùng nhau điểm qua những lỗi phổ biến nhất liên quan đến kế thừa và ghi đè method, và "chữa lành" code của mình trên đường đi nhé.
2. Quên virtual: tại sao không ghi đè được method?
Vấn đề
Trong C#, chỉ có thể ghi đè (override) những method mà ở class cha được khai báo với từ khóa virtual, hoặc abstract, hoặc chính nó là override (trong chuỗi kế thừa). Nếu method không có từ này, khi bạn viết override ở class con sẽ bị lỗi compile.
class Animal
{
public void Speak()
{
Console.WriteLine("Động vật nói gì đó.");
}
}
class Cat : Animal
{
// Lỗi compile! Method ở class cha không phải virtual, abstract hay override.
public override void Speak()
{
Console.WriteLine("Meo!");
}
}
Lỗi sẽ kiểu như này: "'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override".
Làm sao tránh dính lỗi này?
Muốn ghi đè method, nhớ khai báo method đó ở class cha là virtual. Nếu bạn thiết kế class để mở rộng, hãy nghĩ kỹ method nào nên cho phép override.
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Động vật phát ra âm thanh.");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meo!");
}
}
Tại sao cần làm vậy?
Khai báo method là virtual nghĩa là bạn cho phép class con thay đổi hành vi, và compiler sẽ giúp bạn — không cho override nhầm method.
3. Lỗi với từ khóa override và new
Đôi khi lại ngược lại: tác giả class con muốn "ghi đè" method, nhưng ở class cha nó không phải virtual. Lúc này compiler không cho dùng override, nhưng lại cho dùng new. Nhưng hành vi này hoàn toàn khác!
class Dog : Animal
{
// Đây không phải override, mà là hiding method của class cha.
public new void Speak()
{
Console.WriteLine("Gâu!");
}
}
Nếu gọi method này qua biến kiểu Dog — ok luôn:
Dog dog = new Dog();
dog.Speak(); // "Gâu!"
Nhưng nếu gọi qua biến kiểu class cha, sẽ gọi method của class cha:
Animal dog2 = new Dog();
dog2.Speak(); // "Động vật phát ra âm thanh."
Giải thích:
Từ khóa new KHÔNG ghi đè method, mà chỉ che method của cha. Gọi là "hiding". Cách này dễ gây nhầm lẫn khi behavior polymorphic không như bạn mong đợi.
Làm sao tránh bẫy new vs override?
Nếu muốn override chuẩn với hỗ trợ polymorphism — dùng virtual/override. Nếu bạn thêm tính năng hoàn toàn mới (hoặc cố tình muốn che behavior của cha, hãy cẩn thận!), thì dùng new.
| Tình huống | Từ khóa | Polymorphism hoạt động? | Hành vi khi gọi qua kiểu cha |
|---|---|---|---|
| Thay đổi hành vi | override | Có | Gọi method của class con |
| Che/thay thế method | new | Không | Gọi method của class cha |
4. Signature method không khớp
Newbie, thậm chí dev lâu năm, cũng hay nhầm khi đổi kiểu tham số hoặc kiểu trả về ở method class con. Ví dụ, nếu method cha là public virtual void Print(string message), mà ở class con lại "override" public override void Print(object message), thì KHÔNG phải override, mà là method mới hoàn toàn.
class Printer
{
public virtual void Print(string msg)
{
Console.WriteLine("Printer cha: " + msg);
}
}
class SmartPrinter : Printer
{
// Lỗi compile! Signature không khớp với method cha.
public override void Print(object msg)
{
Console.WriteLine("Smart printer: " + msg);
}
}
Gợi ý:
Cần khớp cả tên method, kiểu trả về, và tham số (kiểu, số lượng, thứ tự).
Nếu bạn lỡ đổi kiểu tham số hoặc gõ nhầm tên — compiler sẽ báo ngay.
5. Modifiers truy cập không khớp
Một bẫy nữa mà newbie hay dính — modifiers truy cập. Method ở class con không được private hơn method cha. Ví dụ code tệ:
public class Vehicle
{
public virtual void StartEngine() { /* ... */ }
}
public class Car : Vehicle
{
// Lỗi! Modifier 'private' chặt hơn 'public' của cha.
private override void StartEngine() { /* ... */ }
}
Làm gì đây?
Modifier truy cập ở method con phải bằng hoặc mở hơn method cha. Thường là public hoặc protected.
6. Quên/thừa method abstract
Nếu ở class cha method là abstract, thì ở class con BẮT BUỘC phải có override, nếu không class con cũng thành abstract (và không tạo được object).
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
// Lỗi! Chưa implement method abstract Area()
}
Giải pháp:
Cần implement method này:
class Circle : Shape
{
public override double Area()
{
return 3.14 * 2 * 2; // Gần đúng...
}
}
7. Gọi implementation của cha: base.Method()
Đôi khi bạn không muốn thay đổi hoàn toàn method, mà chỉ muốn thêm gì đó. Lúc này nhiều người quên (hoặc không biết) là trong method con có thể gọi method cha bằng từ khóa base.
class Logger
{
public virtual void Log(string msg)
{
Console.WriteLine("Log cha: " + msg);
}
}
class FancyLogger : Logger
{
public override void Log(string msg)
{
// Thêm "gia vị" và gọi method cha:
Console.WriteLine("[FANCY] " + msg);
base.Log(msg);
}
}
Giải thích:
Nếu không gọi base.Log(msg), logic của class cha sẽ bị mất hoàn toàn.
8. Gọi constructor cha (base)
Nếu class cha yêu cầu tham số bắt buộc trong constructor, class con phải gọi constructor đó bằng từ khóa base.
class Engine
{
public Engine(int cylinders)
{
Console.WriteLine("Engine với số xi lanh: " + cylinders);
}
}
class RaceEngine : Engine
{
// Lỗi compile! Engine không có constructor mặc định.
public RaceEngine() { }
}
// Sửa lại:
class RaceEngine2 : Engine
{
public RaceEngine2() : base(8) // Gọi constructor cha rõ ràng
{
Console.WriteLine("Động cơ đua đã sẵn sàng!");
}
}
9. Quên đánh dấu method là sealed
Đôi khi cần cấm override tiếp method ở class con — dùng từ khóa sealed cùng với override. Nếu không dùng, ai đó có thể override method của bạn, phá vỡ logic bạn mong muốn.
class Hero
{
public virtual void Attack() => Console.WriteLine("Anh hùng tấn công!");
}
class Warrior : Hero
{
public sealed override void Attack() => Console.WriteLine("Chiến binh ra đòn!");
}
class Mutant : Warrior
{
// Lỗi! Method Attack đã sealed ở trên.
// public override void Attack() { ... }
}
Giải thích:
Như vậy, bạn "niêm phong" implementation ở level này và class con không thể thay đổi nữa.
10. Overload thừa hoặc sai thay vì override
Nhiều dev nhầm overload method (overloading) với override (overriding). Overload là định nghĩa method cùng tên nhưng khác signature (ví dụ khác số tham số), và không liên quan gì đến polymorphism.
class Animal
{
public virtual void Eat()
{
Console.WriteLine("Động vật ăn.");
}
}
class Panda : Animal
{
// Đây KHÔNG phải override! Chỉ là method overload mới.
public void Eat(string what)
{
Console.WriteLine("Gấu trúc ăn: " + what);
}
}
...
Animal a = new Panda();
a.Eat(); // Nếu method override — sẽ gọi implementation của class con. Nếu không — dùng method cha
// a.Eat("tre"); // Lỗi compile: Animal không có method này
Muốn có behavior polymorphic, phải override chứ không chỉ overload.
11. Lỗi thiết kế hình thức và phi hình thức
Khi class thiết kế tệ, danh sách kế thừa sẽ rối tung:
- vòng lặp override bên trong mà không dùng base. đúng cách,
- dùng modifier không nhất quán,
- hierarchy quá sâu hoặc khó hiểu,
- method mà logic bị "chia nhỏ" giữa các level kế thừa,
- thiếu comment cho method virtual/abstract,
- side effect khó lường (ví dụ gọi method virtual trong constructor cha khi class con chưa khởi tạo xong).
Lời khuyên:
Nếu cảm thấy rối — đừng ngại, vẽ ra giấy (old school mà hiệu quả!) hierarchy và ghi chú method nào ở đâu, ai override ai, ai gọi ai, và chỗ nào cần gọi base..
Trong project thực tế, nhớ ghi chú trong tài liệu về expectation khi override từng method.
GO TO FULL VERSION