1. Giới thiệu
Kế thừa trong lập trình rất giống với kế thừa ngoài đời thật. Ví dụ, bạn có thể "thừa hưởng" từ bố mẹ màu mắt, hình dáng mũi hoặc thậm chí là tài năng vẽ. Bạn nhận được những đặc điểm này "mặc định", không cần tự tạo lại từ đầu. Nhưng bạn vẫn có thể phát triển những nét riêng của mình mà bố mẹ không có.
Trong lập trình, nó cũng hoạt động kiểu như vậy:
- Một class (ta sẽ gọi là class cơ sở hoặc class cha, đôi khi là superclass) định nghĩa các đặc điểm chung (thuộc tính) và hành vi (phương thức) mà tất cả "con cháu" của nó đều có.
- Một class khác (gọi là class dẫn xuất hoặc class con, đôi khi là subclass) kế thừa tất cả các đặc điểm chung đó từ cha. Nó tự động nhận được tất cả thuộc tính và phương thức public và protected của class cơ sở. Không cần khai báo lại.
- Class dẫn xuất có thể thêm thuộc tính và phương thức riêng, độc đáo mà class cha không có.
- Đôi khi class dẫn xuất còn có thể thay đổi hành vi của các phương thức kế thừa (nhưng cái này sẽ nói ở bài sau, gọi là đa hình).
Khái niệm then chốt của kế thừa được thể hiện bằng cụm "là một cái gì đó" (is-a relationship). Giả sử bạn đang viết game có pháp sư, chiến binh và cung thủ:
- ChiếnBinh là NhanVat.
- PhapSu là NhanVat.
- CungThu là NhanVat.
Nếu ChiếnBinh là NhanVat, thì nó phải có tất cả những gì NhanVat có, cộng thêm cái gì đó riêng biệt cho ChiếnBinh.
2. Cú pháp kế thừa
Giờ thử xem ví dụ và code một chút nhé.
Cơ bản về cú pháp
// Lớp cơ sở
public class DongVat
{
public string Ten { get; set; }
public void DiChuyen()
{
Console.WriteLine($"{Ten} di chuyển.");
}
}
// Lớp con
public class Cho : DongVat
{
public void Sua()
{
Console.WriteLine($"{Ten} sủa: Gâu!");
}
}
Chú ý dấu hai chấm sau tên class Cho. Ở đây ta nói: Cho kế thừa mọi thứ từ DongVat.
Tất cả public và protected của DongVat sẽ xuất hiện ở Cho!
Sử dụng kế thừa
Thử dùng các class này trong ứng dụng console của mình nhé:
var cho = new Cho();
cho.Ten = "Sharik"; // thuộc tính Ten kế thừa từ DongVat
cho.DiChuyen(); // phương thức DiChuyen kế thừa từ DongVat
cho.Sua(); // phương thức riêng của Cho
// Kết quả:
// Sharik di chuyển.
// Sharik sủa: Gâu!
Có thể tạo thêm mèo cho "sở thú" đa dạng hơn:
public class Meo : DongVat
{
public void KeuMeo()
{
Console.WriteLine($"{Ten} kêu: Meo!");
}
}
var meo = new Meo();
meo.Ten = "Murka";
meo.DiChuyen();
meo.KeuMeo();
3. Kế thừa giúp tiết kiệm công sức như thế nào
Khi bạn có nhiều thực thể giống nhau, không cần copy code lặp đi lặp lại. Kế thừa giống như bản master: bạn định nghĩa logic một lần, tất cả con cháu đều nhận được.
Minh họa: Cây kế thừa
. DongVat
/ \
Cho Meo
| Class | Thuộc tính Ten | Phương thức DiChuyen | Phương thức riêng |
|---|---|---|---|
| DongVat | + | + | - |
| Cho | + | + | Sua() |
| Meo | + | + | KeuMeo() |
Ví dụ nữa — mở rộng ứng dụng
Giả sử bạn muốn tạo hệ thống quản lý nhiều loại người khác nhau. Nhớ lại cách làm cũ:
public class Nguoi
{
public string Ho { get; set; }
public string Ten { get; set; }
}
// Thêm nhân viên
public class NhanVien
{
public string Ho { get; set; }
public string Ten { get; set; }
public string ViTri { get; set; }
}
Không tiện lắm! Dữ liệu tên bị lặp lại. Dùng kế thừa sẽ chuẩn hơn:
public class Nguoi
{
public string Ho { get; set; }
public string Ten { get; set; }
}
public class NhanVien : Nguoi
{
public string ViTri { get; set; }
}
public class SinhVien : Nguoi
{
public int Lop { get; set; }
}
Giờ NhanVien và SinhVien đều đã có Ho và Ten — không cần viết lại lần nữa. Bạn có thể dễ dàng mở rộng hệ thống cho nhiều vai trò khác nhau.
Đó chính là "ma thuật" của kế thừa: mô tả đặc điểm chung ở class cơ sở một lần, rồi chỉ cần "mở rộng" ở class con bằng cách thêm chi tiết riêng.
4. Lợi ích của kế thừa
Tái sử dụng code (Code Reusability): Đây là lợi ích rõ ràng nhất. Không cần viết lại cùng một đoạn code ở nhiều nơi. Viết một lần — dùng nhiều lần. Code của bạn sẽ "khô" và sạch hơn (dân dev hay nói "Don't Repeat Yourself" — DRY, "Đừng lặp lại chính mình").
Dễ bảo trì (Easier Maintenance): Nếu cần thay đổi logic di chuyển, bạn chỉ sửa ở một chỗ — class cơ sở DongVat. Tất cả class con tự động nhận thay đổi đó. Hãy tưởng tượng tiết kiệm bao nhiêu thời gian cho dự án lớn!
Tạo hệ thống phân cấp (Creating Hierarchies): Kế thừa giúp mô hình hóa quan hệ "là một cái gì đó" ngoài đời, tạo cấu trúc logic, dễ hiểu. ChiếnBinh là NhanVat, PhapSu là NhanVat. Code của bạn sẽ có cấu trúc rõ ràng, dễ đọc.
Nền tảng cho đa hình: Dù chưa học kỹ đa hình, kế thừa là nền tảng của nó. Đa hình cho phép bạn làm việc với các object khác nhau của class con, nhưng dùng chung interface của class cha. Ví dụ, bạn có thể có danh sách tất cả DongVat (dù là Cho, Meo hay con gì khác) và gọi phương thức DiChuyen mà không cần quan tâm loại cụ thể. Cực kỳ mạnh, và chắc chắn sẽ học tiếp!
5. Lưu ý và đặc điểm của kế thừa
Kế thừa đơn trong C#: Khác với một số ngôn ngữ khác (ví dụ C++), C# chỉ hỗ trợ kế thừa đơn. Nghĩa là một class chỉ kế thừa từ một class cơ sở. Bạn không thể vừa kế thừa DongVat vừa PhuongTien (nếu có class đó).
Mọi class đều ngầm kế thừa từ object: Thú vị phết! Nếu bạn không chỉ rõ class cha, class của bạn tự động kế thừa từ System.Object. Đây là class gốc nhất trong .NET, cung cấp các phương thức cơ bản như ToString(), Equals(), GetHashCode(). Nên khi bạn override ToString() trong class, thực ra là override phương thức kế thừa từ object!
Constructor không được kế thừa: Class con không kế thừa constructor của class cha. Bạn phải tự định nghĩa constructor cho class con. Tuy nhiên, nếu class cha có constructor có tham số, bạn BẮT BUỘC phải gọi một constructor của class cha từ constructor của class con. Dùng từ khóa base để làm điều này.
Thành viên private không truy cập được: Thành viên private của class cha không truy cập trực tiếp từ class con. Chúng vẫn tồn tại trong object, nhưng bạn không thể gọi tên từ code class con. Nếu muốn thành viên chỉ cho class con dùng mà không public ra ngoài, hãy dùng protected.
6. Minh họa hệ thống phân cấp
Để dễ hình dung quan hệ kế thừa, người ta hay dùng sơ đồ. Một trong những sơ đồ phổ biến là sơ đồ class UML (Unified Modeling Language). Với ví dụ động vật, nó sẽ như sau:
classDiagram
class DongVat {
+ string Ten
+ DiChuyen()
}
class Cho {
+ Sua()
}
class Meo {
+ KeuMeo()
}
DongVat <|-- Cho : inherits
DongVat <|-- Meo : inherits
Trên sơ đồ này, mũi tên tam giác rỗng (từ Cho đến DongVat, từ Meo đến DongVat) nghĩa là quan hệ kế thừa: "là một cái gì đó" (is-a relationship).
7. Ứng dụng thực tế của kế thừa
Kế thừa không chỉ là lý thuyết, nó là một trong những trụ cột của lập trình hiện đại, dùng rất nhiều trong dự án thực tế:
Phát triển giao diện đồ họa (GUI): Trong WPF, WinForms, MAUI (framework tạo app desktop .NET), hầu hết các control (nút bấm, textbox, cửa sổ) đều kế thừa từ class cơ sở Control hoặc UIElement. Nhờ đó, chúng có thuộc tính chung như kích thước, vị trí, hiển thị, và phương thức chung như xử lý sự kiện.
Ví dụ, Button là Control, TextBox là Control.
Game engine: Kế thừa cực hợp để tạo hệ thống phân cấp object game: GameObject → NhanVat → NguoiChoi/KeThu hoặc PhuongTien → OTo/XeMay.
Làm việc với database (ORM): Trong các framework như Entity Framework Core, bạn thường định nghĩa class cơ sở cho tất cả entity, ví dụ BaseEntity, chứa thuộc tính như Id (mã bản ghi DB) hoặc CreatedAt (ngày tạo bản ghi). Tất cả entity cụ thể (User, Product, Order...) sẽ kế thừa BaseEntity, tự động có các thuộc tính này.
Testing: Kế thừa dùng trong framework test (xUnit, NUnit...), nơi bạn tạo class test cơ sở với logic khởi tạo/chốt chung, rồi các class test cụ thể kế thừa lại.
Thư viện và framework: Chính thư viện chuẩn .NET cũng dùng kế thừa rất nhiều. Nhiều collection và kiểu dữ liệu kế thừa từ class cơ sở chung hoặc implement interface chung (cái này sẽ học sau).
Khi phỏng vấn C#/.NET, câu hỏi về kế thừa, nguyên lý (DRY, "is-a"), và so sánh với các khái niệm khác (như composition, interface) là cơ bản và rất hay gặp. Biết giải thích, lấy ví dụ thực tế hoặc code là kỹ năng cực kỳ giá trị.
Thấy chưa, kế thừa không chỉ là "tính năng" của ngôn ngữ, mà là công cụ mạnh để tổ chức code, giảm lặp lại và tạo hệ thống logic liên kết. Đó là chìa khóa để viết code mở rộng, dễ bảo trì.
GO TO FULL VERSION