1. Giới thiệu
Chủ đề hôm nay có thể hơi trừu tượng một chút, nhưng tin mình đi, không có nó thì không đi xa được đâu. Chúng ta sẽ nói về kế thừa trong lập trình. Hôm nay chỉ chạm nhẹ thôi, sau này sẽ đào sâu hơn.
Hãy tưởng tượng bạn đang viết chương trình quản lý động vật trong sở thú. Có nhiều loài động vật khác nhau: sư tử, hổ, voi, vẹt. Tất cả đều là động vật. Nhưng mỗi loài lại có đặc điểm riêng:
- Sư tử có bờm.
- Hổ có vằn.
- Voi rất to và có vòi.
- Vẹt biết nói.
Nếu mô tả từng con riêng biệt, code sẽ rất giống nhau:
// Sư tử
public class Lion
{
public string Name { get; set; }
public int Age { get; set; }
public string Species { get; set; } = "Sư tử"; // Luôn là "Sư tử"
public void Eat() { Console.WriteLine("Sư tử ăn thịt."); }
public void Sleep() { Console.WriteLine("Sư tử ngủ."); }
public void Roar() { Console.WriteLine("Sư tử gầm!"); }
public string ManeColor { get; set; } // Đặc điểm của sư tử - bờm
}
// Hổ
public class Tiger
{
public string Name { get; set; }
public int Age { get; set; }
public string Species { get; set; } = "Hổ"; // Luôn là "Hổ"
public void Eat() { Console.WriteLine("Hổ ăn thịt."); }
public void Sleep() { Console.WriteLine("Hổ ngủ."); }
public void Stride() { Console.WriteLine("Hổ rón rén."); }
public string StripePattern { get; set; } // Đặc điểm của hổ - vằn
}
Thấy không, bao nhiêu code bị lặp lại? Name, Age, Eat(), Sleep() – mấy cái này là chung cho tất cả động vật! Nếu có 100 loài thì sao? Ác mộng luôn!
Đây là lúc kế thừa cứu cánh cho chúng ta.
2. Kế thừa class trong C# là gì?
Kế thừa — là một trong những nguyên lý nền tảng của lập trình hướng đối tượng (OOP). Nó cho phép tạo class mới dựa trên class đã có. Class mới (con, class con, class dẫn xuất) sẽ nhận (kế thừa) tất cả thuộc tính (field) và hành vi (method) của class cha (class cơ sở, class cha).
Giống như ngoài đời: bạn thừa hưởng một số đặc điểm từ bố mẹ, nhưng vẫn có nét riêng của mình.
Trong C#, kế thừa được thể hiện bằng dấu hai chấm : sau tên class con:
// Class cha (cơ sở) - Animal
public class Animal
{
public string Name { get; set; } // Tên động vật
public int Age { get; set; } // Tuổi động vật
public void Eat()
{
Console.WriteLine($"{Name} ăn.");
}
public void Sleep()
{
Console.WriteLine($"{Name} ngủ.");
}
}
// Class con (dẫn xuất) - Lion kế thừa từ Animal
public class Lion : Animal // <-- Đây là kế thừa!
{
public string ManeColor { get; set; } // Thuộc tính riêng của sư tử
public void Roar() // Hành vi riêng của sư tử
{
Console.WriteLine($"{Name} gầm: RRRRRR!");
}
}
// Một class con khác - Tiger kế thừa từ Animal
public class Tiger : Animal
{
public string StripePattern { get; set; } // Thuộc tính riêng của hổ
public void Stride() // Hành vi riêng của hổ
{
Console.WriteLine($"{Name} rón rén không tiếng động.");
}
}
Giờ nếu tạo object Lion hoặc Tiger, chúng sẽ tự động có thuộc tính Name và Age, cũng như method Eat() và Sleep(), vì chúng kế thừa từ Animal.
class Program
{
static void Main(string[] args)
{
Lion simba = new Lion();
simba.Name = "Simba";
simba.Age = 5;
simba.ManeColor = "Vàng";
simba.Eat(); // Method kế thừa từ Animal
simba.Sleep(); // Method kế thừa từ Animal
simba.Roar(); // Method riêng của Lion
Console.WriteLine($"{simba.Name} - tuổi: {simba.Age}, màu bờm: {simba.ManeColor}");
Tiger shereKhan = new Tiger();
shereKhan.Name = "Shere Khan";
shereKhan.Age = 7;
shereKhan.StripePattern = "Cổ điển";
shereKhan.Eat(); // Method kế thừa từ Animal
shereKhan.Sleep(); // Method kế thừa từ Animal
shereKhan.Stride(); // Method riêng của Tiger
Console.WriteLine($"{shereKhan.Name} - tuổi: {shereKhan.Age}, hoa văn: {shereKhan.StripePattern}");
}
}
Ý tưởng chính của kế thừa:
- Tái sử dụng code (Code Reusability): Tránh lặp code bằng cách để logic chung vào class cơ sở.
- Phân cấp (Hierarchy): Tạo cấu trúc logic "chung-riêng" (Animal → Lion/Tiger).
- Đa hình (Polymorphism): (Tạm nhắc thôi, chi tiết sau) Bạn có thể làm việc với object của class con thông qua tham chiếu class cha. Ví dụ, có list Animal rồi bỏ vào đó cả sư tử, hổ, sau đó gọi method Eat() cho từng con.
3. Gọi constructor của class cơ sở (base)
Khi bạn tạo object của class con (ví dụ new Lion()), C# sẽ tự động gọi constructor của class cha (Animal) trước khi gọi constructor của class con. Điều này đảm bảo phần cơ sở của object được khởi tạo đúng.
Đôi khi bạn cần truyền tham số vào constructor của class cha. Lúc này dùng từ khóa base sau phần khai báo constructor của class con.
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
// Constructor của class cơ sở
public Animal(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine($"Constructor Animal: Đã tạo động vật {Name}, tuổi {Age}.");
}
public void Eat()
{
Console.WriteLine($"{Name} ăn.");
}
public void Sleep()
{
Console.WriteLine($"{Name} ngủ.");
}
}
public class Lion : Animal
{
public string ManeColor { get; set; }
// Constructor Lion, gọi constructor của class cơ sở Animal
public Lion(string name, int age, string maneColor) : base(name, age) // <-- Đây là 'base'!
{
ManeColor = maneColor;
Console.WriteLine($"Constructor Lion: Màu bờm {ManeColor}.");
}
public void Roar()
{
Console.WriteLine($"{Name} gầm: RRRRRR!");
}
}
class Program
{
static void Main(string[] args)
{
// Khi tạo Lion, đầu tiên gọi constructor Animal, sau đó Lion
Lion simba = new Lion("Simba", 5, "Vàng");
simba.Roar();
}
}
Chuyện gì xảy ra khi chạy new Lion("Simba", 5, "Vàng"):
- Sẽ gọi constructor Lion(string name, int age, string maneColor).
- Nhờ : base(name, age), quyền điều khiển chuyển sang constructor Animal(string name, int age).
- Code trong constructor Animal chạy, bạn sẽ thấy Constructor Animal: Đã tạo động vật Simba, tuổi 5.
- Sau đó quyền điều khiển quay lại constructor Lion.
- Code trong constructor Lion chạy, bạn sẽ thấy Constructor Lion: Màu bờm Vàng.
Điều này rất quan trọng: class cơ sở luôn được khởi tạo trước!
4. Toán tử is và as
Trong thế giới lập trình C#, bạn sẽ thường gặp tình huống cần kiểm tra kiểu object hoặc thử chuyển đổi nó sang kiểu khác. Lúc này, toán tử is và as sẽ giúp bạn. Chúng đặc biệt hữu ích khi làm việc với kế thừa và đa hình, nhưng cơ bản thì bạn đã có thể hiểu ngay bây giờ rồi.
Toán tử is (kiểm tra kiểu)
Toán tử is cho phép kiểm tra xem object có tương thích với kiểu nào đó không. Nó trả về true nếu object có thể chuyển sang kiểu chỉ định, và false nếu không.
Cú pháp: biểu_thức is Kiểu
static void AnalyzeObject(object obj)
{
if (obj is string)
{
Console.WriteLine("Đây là chuỗi!");
}
else if (obj is int)
{
Console.WriteLine("Đây là số nguyên!");
}
else
{
Console.WriteLine("Đây là cái gì khác.");
}
}
static void Main()
{
AnalyzeObject("Xin chào, thế giới!"); // Đây là chuỗi!
AnalyzeObject(123); // Đây là số nguyên!
AnalyzeObject(3.14); // Đây là cái gì khác.
AnalyzeObject(new int[] { 1, 2 }); // Đây là cái gì khác.
}
is với pattern matching:
C# hiện đại cho phép dùng is không chỉ để kiểm tra kiểu mà còn tạo luôn biến kiểu đó nếu đúng. Cái này gọi là "pattern matching" và làm code gọn hơn nhiều.
static void AnalyzeObjectWithPatternMatching(object obj)
{
if (obj is string message) // Nếu obj là chuỗi, gán cho biến message
{
Console.WriteLine($"Đây là chuỗi, độ dài: {message.Length}");
}
else if (obj is int number) // Nếu obj là int, gán cho biến number
{
Console.WriteLine($"Đây là số, nhân đôi: {number * 2}");
}
else
{
Console.WriteLine("Không nhận diện được kiểu.");
}
}
static void Main()
{
AnalyzeObjectWithPatternMatching("Ví dụ"); // Đây là chuỗi, độ dài: 5
AnalyzeObjectWithPatternMatching(50); // Đây là số, nhân đôi: 100
AnalyzeObjectWithPatternMatching(new object()); // Không nhận diện được kiểu.
}
Cách dùng is với pattern matching này thích hơn nhiều so với ép kiểu truyền thống rồi kiểm tra null, vì nó an toàn và dễ đọc hơn.
Toán tử as (ép kiểu an toàn)
Toán tử as thử chuyển object sang kiểu chỉ định. Nếu chuyển được, nó trả về object kiểu đó. Nếu không chuyển được (object không tương thích với kiểu đích), as trả về null thay vì ném exception. Vì vậy as là toán tử ép kiểu "an toàn".
Cú pháp: biểu_thức as Kiểu
static void ProcessData(object data)
{
string text = data as string; // Thử ép data sang string
if (text != null) // Kiểm tra ép kiểu thành công không
{
Console.WriteLine($"Xử lý chuỗi: {text.ToUpper()}");
}
else
{
Console.WriteLine("Dữ liệu không phải chuỗi.");
}
}
static void Main()
{
ProcessData("hello"); // Xử lý chuỗi: HELLO
ProcessData(123); // Dữ liệu không phải chuỗi.
ProcessData(null); // Dữ liệu không phải chuỗi.
}
Khi nào dùng as thay vì ép kiểu trực tiếp (Kiểu)biểu_thức:
- An toàn: Nếu không chắc object đúng kiểu, as giúp tránh InvalidCastException và trả về null để xử lý tiếp.
- Với kiểu tham chiếu: as chỉ dùng với kiểu tham chiếu (class, interface, delegate, array). Không dùng để chuyển kiểu giá trị (ví dụ int sang double).
Lưu ý: Nếu chắc chắn object luôn đúng kiểu, hoặc cần chuyển kiểu giá trị, hãy dùng ép kiểu trực tiếp (Kiểu)biểu_thức. Nếu không chuyển được, ép kiểu trực tiếp sẽ báo lỗi (InvalidCastException), điều này có thể là mong muốn nếu kiểu sai là lỗi logic chương trình.
GO TO FULL VERSION