BÀI 10: KẾ THỪA VÀ ĐA HÌNH TRONG C#

10.1. Lý thuyết

VÍ DỤ 1: GIA ĐÌNH – DI TRUYỀN

Tình huống thực tế:

Trong một gia đình, con cái thừa hưởng (kế thừa) những đặc điểm từ cha mẹ.

text
┌─────────────────────────────────────────────────────────────┐
│                     CHA MẸ (Lớp cha)                        │
│                                                             │
│  Đặc điểm chung:                                            │
│  ├── Họ và tên (Họ)                                        │
│  ├── Màu mắt                                               │
│  ├── Màu tóc                                               │
│  ├── Chiều cao                                             │
│  ├── Nói chuyện()                                          │
│  └── Ăn uống()                                             │
└─────────────────────────────────────────────────────────────┘
                           │
                           │ Kế thừa (Inheritance)
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    CON CÁI (Lớp con)                        │
│                                                             │
│  Kế thừa từ cha mẹ:                                        │
│  ├── Họ và tên (Họ của cha)  ← Kế thừa                     │
│  ├── Màu mắt (mắt của mẹ)   ← Kế thừa                     │
│  ├── Màu tóc (tóc của cha)  ← Kế thừa                     │
│  ├── Nói chuyện()           ← Kế thừa                     │
│  └── Ăn uống()              ← Kế thừa                     │
│                                                             │
│  Thêm đặc điểm riêng (mới):                                 │
│  ├── Sở thích (chơi game)   ← Thêm mới                     │
│  ├── Hát()                  ← Thêm mới                     │
│  └── Đá bóng()              ← Thêm mới                     │
└─────────────────────────────────────────────────────────────┘

Giải thích:

  • Con cái kế thừa họ, màu mắt, màu tóc từ cha mẹ (không cần tự tạo ra)

  • Con cái có thể thêm sở thích, tài năng riêng (mở rộng)

  • Con cái có thể thay đổi cách nói chuyện (ghi đè – override)

VÍ DỤ 2: PHƯƠNG TIỆN GIAO THÔNG

Tình huống thực tế:

Xe máy, ô tô, xe tải đều là phương tiện giao thông, nhưng mỗi loại có đặc điểm riêng.

text
┌─────────────────────────────────────────────────────────────┐
│          PHƯƠNG TIỆN GIAO THÔNG (Lớp cha)                  │
│                                                             │
│  Đặc điểm chung:                                            │
│  ├── Màu sắc                                               │
│  ├── Biển số                                               │
│  ├── Số bánh xe                                            │
│  ├── Di chuyển()                                           │
│  └── Dừng lại()                                            │
└─────────────────────────────────────────────────────────────┘
           │                    │                    │
           │ Kế thừa            │ Kế thừa            │ Kế thừa
           ▼                    ▼                    ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│      XE MÁY       │ │      Ô TÔ         │ │      XE TẢI       │
├───────────────────┤ ├───────────────────┤ ├───────────────────┤
│ Kế thừa:          │ │ Kế thừa:          │ │ Kế thừa:          │
│ ├── Màu sắc       │ │ ├── Màu sắc       │ │ ├── Màu sắc       │
│ ├── Biển số       │ │ ├── Biển số       │ │ ├── Biển số       │
│ ├── Di chuyển()   │ │ ├── Di chuyển()   │ │ ├── Di chuyển()   │
│                   │ │                   │ │                   │
│ Thêm riêng:       │ │ Thêm riêng:       │ │ Thêm riêng:       │
│ ├── Loại xe máy   │ │ ├── Số ghế ngồi   │ │ ├── Tải trọng     │
│ ├── Đổ xăng()     │ │ ├── Bật điều hòa()│ │ └── Chở hàng()    │
│ └── Đội mũ bảo     │ │ └── Mở cốp xe()   │ │                   │
│    hiểm()          │ │                   │ │                   │
└───────────────────┘ └───────────────────┘ └───────────────────┘

Giải thích:

  • Kế thừa: Xe máy, ô tô, xe tải đều có màu sắc, biển số

  • Mở rộng: Mỗi loại thêm đặc điểm riêng (xe tải có tải trọng)

  • Ghi đè: Ô tô di chuyển nhanh hơn xe máy

10.1.1. Tổng quan về Kế thừa (Inheritance)

Định nghĩa: Kế thừa là cơ chế cho phép một lớp (lớp con) kế thừa các thành phần (fields, properties, methods) từ một lớp khác (lớp cha).

Cú pháp:

csharp
public class LopCon : LopCha
{
    // Thêm thành phần mới
    // Ghi đè thành phần của lớp cha
}

Đặc điểm:

  • C# chỉ hỗ trợ đơn kế thừa (một lớp con chỉ có một lớp cha)

  • Lớp con có thể truy cập các thành phần public, protected của lớp cha

  • Lớp con có thể mở rộng hoặc ghi đè hành vi của lớp cha

  • Tất cả class trong C# đều kế thừa từ System.Object

Ví dụ:

csharp
// ============================================================
// LỚP CHA (BASE CLASS) - ĐỘNG VẬT
// ============================================================  /// <summary>
/// Lớp DongVat là lớp cha (base class) 
/// Các lớp con sẽ kế thừa từ lớp này
/// </summary>
public class DongVat
{
    // === PROPERTIES ===
    
    /// <summary>
    /// Thuộc tính Ten - Tên của động vật
    /// { get; set; } là auto-implemented property
    /// Có thể đọc (get) và gán (set) từ bên ngoài
    /// </summary>
    public string Ten { get; set; }
    
    /// <summary>
    /// Thuộc tính Tuoi - Tuổi của động vật
    /// Kiểu int (số nguyên)
    /// </summary>
    public int Tuoi { get; set; }
    
    // === METHODS (PHƯƠNG THỨC) ===
    
    /// <summary>
    /// Phương thức An() - Hành vi ăn của động vật
    /// Không có tham số, không trả về giá trị (void)
    /// Sử dụng Console.WriteLine để in ra màn hình
    /// {Ten} là biểu thức nội suy (interpolation) - lấy giá trị từ property Ten
    /// </summary>
    public void An()
    {
        // In ra: "Tên động vật đang ăn..."
        // Ví dụ: "Con chó đang ăn..."
        Console.WriteLine($"{Ten} đang ăn...");
    }
    
    /// <summary>
    /// Phương thức Ngu() - Hành vi ngủ của động vật
    /// </summary>
    public void Ngu()
    {
        // In ra: "Tên động vật đang ngủ..."
        Console.WriteLine($"{Ten} đang ngủ...");
    }
}

// ============================================================
// LỚP CON 1 (DERIVED CLASS) - CHÓ
// ============================================================

/// <summary>
/// Lớp Cho kế thừa từ lớp DongVat
/// Dấu : (hai chấm) thể hiện quan hệ kế thừa
/// Lớp Cho sẽ có tất cả các thành phần public của DongVat
/// </summary>
public class Cho : DongVat  // : DongVat nghĩa là "kế thừa từ DongVat"
{
    // === PROPERTY RIÊNG CỦA LỚP CON ===
    
    /// <summary>
    /// Thuộc tính Giong - Giống của con chó
    /// Đây là property mới, chỉ có ở lớp Cho, không có ở DongVat
    /// </summary>
    public string Giong { get; set; }
    
    // === PHƯƠNG THỨC RIÊNG CỦA LỚP CON ===
    
    /// <summary>
    /// Phương thức Sua() - Hành vi sủa của chó
    /// Đây là method mới, chỉ có ở lớp Cho
    /// Lớp DongVat không có phương thức này
    /// </summary>
    public void Sua()
    {
        // Lưu ý: {Ten} được kế thừa từ lớp DongVat
        // Lớp Cho không cần khai báo lại Ten vì đã có sẵn từ lớp cha
        Console.WriteLine($"{Ten} đang sủa...");
        // Output: "Tên chó đang sủa..."
        // Ví dụ: "Milu đang sủa..."
    }
}

// ============================================================
// LỚP CON 2 (DERIVED CLASS) - MÈO
// ============================================================

/// <summary>
/// Lớp Meo kế thừa từ lớp DongVat
/// Cũng giống như Cho, Meo có tất cả thành phần của DongVat
/// </summary>
public class Meo : DongVat  // : DongVat - kế thừa từ DongVat
{
    // === PROPERTY RIÊNG CỦA LỚP CON ===
    
    /// <summary>
    /// Thuộc tính MauLong - Màu lông của con mèo
    /// Property mới, chỉ có ở lớp Meo
    /// </summary>
    public string MauLong { get; set; }
    
    // === PHƯƠNG THỨC RIÊNG CỦA LỚP CON ===
    
    /// <summary>
    /// Phương thức Keu() - Hành vi kêu của mèo
    /// Method mới, chỉ có ở lớp Meo
    /// </summary>
    public void Keu()
    {
        // {Ten} được kế thừa từ lớp DongVat
        Console.WriteLine($"{Ten} đang kêu meo meo...");
        // Output: "Tên mèo đang kêu meo meo..."
        // Ví dụ: "Tom đang kêu meo meo..."
    }
}

10.1.2. Từ khóa base và Constructor kế thừa (0.3 giờ)

a. Gọi Constructor lớp cha:

csharp
// Lớp cơ sở (base class) - đại diện cho nhân viên nói chung
public class NhanVien
{
    // Các thuộc tính (properties) của nhân viên
    public string MaNV { get; set; }        // Mã nhân viên
    public string HoTen { get; set; }       // Họ tên nhân viên
    public double LuongCoBan { get; set; }  // Lương cơ bản
    
    // Constructor của lớp NhanVien - dùng để khởi tạo đối tượng nhân viên
    // Tham số: ma - mã NV, ten - họ tên, luong - lương cơ bản
    public NhanVien(string ma, string ten, double luong)
    {
        MaNV = ma;          // Gán giá trị cho thuộc tính MaNV
        HoTen = ten;        // Gán giá trị cho thuộc tính HoTen
        LuongCoBan = luong; // Gán giá trị cho thuộc tính LuongCoBan
    }
}

// Lớp dẫn xuất (derived class) - kế thừa từ NhanVien
// Đại diện cho nhân viên văn phòng
public class NhanVienVanPhong : NhanVien
{
    // Thuộc tính riêng của nhân viên văn phòng
    public int SoNgayLam { get; set; }  // Số ngày làm việc trong tháng
    
    /* 
     * Constructor của NhanVienVanPhong
     * Tham số: ma, ten, luong, ngay
     * 
     * GIẢI THÍCH CÂU LỆNH: base(ma, ten, luong)
     * 
     * 1. "base" là từ khóa dùng để tham chiếu đến lớp cha (NhanVien)
     * 
     * 2. base(ma, ten, luong) gọi constructor của lớp cha NhanVien
     *    - Truyền 3 tham số ma, ten, luong lên cho lớp cha
     *    - Constructor cha sẽ gán các giá trị này cho MaNV, HoTen, LuongCoBan
     * 
     * 3. LÝ DO PHẢI DÙNG base():
     *    - Lớp NhanVien KHÔNG có constructor mặc định (không tham số)
     *    - Khi tạo đối tượng NhanVienVanPhong, phần "NhanVien" bên trong 
     *      cũng cần được khởi tạo đúng cách
     *    - base() đảm bảo các thuộc tính kế thừa (MaNV, HoTen, LuongCoBan) 
     *      được khởi tạo trước khi phần mở rộng (SoNgayLam) được thêm vào
     * 
     * 4. THỨ TỰ THỰC THI:
     *    Bước 1: Gọi constructor của NhanVien (thông qua base)
     *            - Gán MaNV = ma
     *            - Gán HoTen = ten  
     *            - Gán LuongCoBan = luong
     *    Bước 2: Quay lại constructor của NhanVienVanPhong
     *            - Gán SoNgayLam = ngay
     */
    public NhanVienVanPhong(string ma, string ten, double luong, int ngay) 
        : base(ma, ten, luong)  // ← GỌI CONSTRUCTOR CỦA LỚP CHA
    {
        SoNgayLam = ngay;  // Khởi tạo thuộc tính riêng của lớp con
    }
}
b. Gọi phương thức lớp cha:
csharp
// Lớp cơ sở (base class) - đại diện cho động vật nói chung
public class DongVat
{
    /* 
     * Phương thức ảo (virtual) - cho phép lớp con ghi đè (override)
     * virtual: đánh dấu phương thức có thể được tùy chỉnh lại ở lớp con
     * Mục đích: cung cấp hành vi mặc định, nhưng cho phép thay đổi nếu cần
     */
    public virtual void TiengKeu()
    {
        Console.WriteLine("Động vật kêu...");  // Hành vi mặc định
    }
}

// Lớp dẫn xuất (derived class) - kế thừa từ DongVat
// Đại diện cho loài chó
public class Cho : DongVat
{
    /* 
     * override: ghi đè (thay thế) phương thức TiengKeu của lớp cha
     * Cho phép định nghĩa lại hành vi riêng cho loài chó
     */
    public override void TiengKeu()
    {
        /* 
         * GIẢI THÍCH CÂU LỆNH: base.TiengKeu()
         * 
         * 1. "base" là từ khóa tham chiếu đến lớp cha (DongVat)
         * 
         * 2. base.TiengKeu() gọi phương thức TiengKeu của lớp cha
         *    - Thực hiện hành vi của lớp cha trước
         *    - In ra: "Động vật kêu..."
         * 
         * 3. MỤC ĐÍCH SỬ DỤNG base.TiengKeu():
         *    a) KẾ THỪA HÀNH VI: 
         *       - Tận dụng lại code của lớp cha
         *       - Không cần viết lại logic đã có
         *    
         *    b) MỞ RỘNG (EXTENSION):
         *       - Thêm hành vi mới vào sau hành vi cũ
         *       - Kết hợp cả hành vi cha và hành vi con
         *    
         *    c) Duy trì TÍNH KẾ THỪA:
         *       - Đảm bảo luồng xử lý được kế thừa đúng cách
         *       - Giữ được logic cốt lõi từ lớp cha
         * 
         * 4. SO SÁNH 2 CÁCH VIẾT:
         *    
         *    CÁCH 1: CÓ base.TiengKeu()
         *    override void TiengKeu() {
         *        base.TiengKeu();      // In: "Động vật kêu..."
         *        Console.WriteLine("Gâu gâu!"); // In: "Gâu gâu!"
         *    }
         *    → Kết quả: "Động vật kêu..." + "Gâu gâu!"
         *    
         *    CÁCH 2: KHÔNG base.TiengKeu()
         *    override void TiengKeu() {
         *        Console.WriteLine("Gâu gâu!"); // Chỉ in: "Gâu gâu!"
         *    }
         *    → Kết quả: Chỉ "Gâu gâu!"
         * 
         * 5. THỨ TỰ THỰC THI KHI GỌI:
         *    Cho conCho = new Cho();
         *    conCho.TiengKeu(); // Gọi phương thức đã ghi đè
         *    
         *    Bước 1: Vào phương thức TiengKeu() của lớp Cho
         *    Bước 2: Gọi base.TiengKeu() → vào phương thức của DongVat
         *    Bước 3: In "Động vật kêu..."
         *    Bước 4: Quay lại phương thức của Cho
         *    Bước 5: In "Gâu gâu!"
         */
        base.TiengKeu();  // ← GỌI PHƯƠNG THỨC CỦA LỚP CHA
        Console.WriteLine("Gâu gâu!");  // Thêm hành vi riêng của chó
    }
}

c. Truy cập field/property của lớp cha:

csharp
public class NhanVienVanPhong : NhanVien
{
    public override double TinhLuong()
    {
        // base.LuongCoBan - truy cập property của lớp cha
        return base.LuongCoBan + SoNgayLam * 200000;
    }
}

10.1.3. Nạp chồng (Overloading) và Ghi đè (Overriding) (0.3 giờ)

a. Nạp chồng phương thức (Method Overloading):

  • Cùng tên phương thức trong cùng lớp

  • Khác nhau về số lượng hoặc kiểu tham số

  • Không liên quan đến kế thừa

csharp
/* 
 * ============================================================
 * NẠP CHỒNG PHƯƠNG THỨC (METHOD OVERLOADING)
 * ============================================================
 * 
 * ĐỊNH NGHĨA:
 * - Nhiều phương thức có CÙNG TÊN trong cùng một lớp
 * - Nhưng KHÁC NHAU về danh sách tham số
 * 
 * ĐIỀU KIỆN NẠP CHỒNG:
 * 1. Khác số lượng tham số
 * 2. Khác kiểu dữ liệu của tham số
 * 3. Khác thứ tự các kiểu tham số
 * 
 * LƯU Ý QUAN TRỌNG:
 * - Không thể nạp chồng chỉ dựa trên kiểu trả về (return type)
 * - Không thể nạp chồng chỉ dựa trên tham số ref/out
 * - Nạp chồng xảy ra tại thời điểm biên dịch (compile-time polymorphism)
 */
public class Calculator
{
    // ========================================================
    // 1. Nạp chồng dựa trên KIỂU DỮ LIỆU của tham số
    // ========================================================
    
    /* Phương thức Add với 2 tham số int
     * - Dùng cho phép cộng số nguyên
     * - Kiểu trả về: int
     */
    public int Add(int a, int b) => a + b;
    
    /* Phương thức Add với 2 tham số double
     * - Dùng cho phép cộng số thực
     * - Kiểu trả về: double
     * - KHÁC với phương thức trên ở kiểu dữ liệu tham số (int vs double)
     */
    public double Add(double a, double b) => a + b;
    
    // ========================================================
    // 2. Nạp chồng dựa trên SỐ LƯỢNG tham số
    // ========================================================
    
    /* Phương thức Add với 3 tham số int
     * - Dùng để cộng 3 số nguyên
     * - KHÁC với phương thức Add(int a, int b) ở số lượng tham số (2 vs 3)
     */
    public int Add(int a, int b, int c) => a + b + c;
    
    // ========================================================
    // 3. Nạp chồng dựa trên KIỂU DỮ LIỆU khác (string)
    // ========================================================
    
    /* Phương thức Add với 2 tham số string
     * - Dùng để nối chuỗi (concatenation)
     * - KHÁC với các phương thức trên ở kiểu dữ liệu tham số (string)
     * - Lưu ý: đây là phép nối chuỗi, không phải cộng số học
     */
    public string Add(string a, string b) => a + b;
    
    // ========================================================
    // CÁC VÍ DỤ BỔ SUNG (Comment để tham khảo)
    // ========================================================
    
    /* Có thể nạp chồng với tham số có giá trị mặc định
     * public int Add(int a, int b, int c = 0) => a + b + c;
     * Tuy nhiên sẽ gây mơ hồ với Add(int a, int b) 
     * nên C# sẽ báo lỗi
     */
    
    /* KHÔNG THỂ nạp chồng chỉ khác kiểu trả về
     * public int Add(int a, int b) => a + b;
     * public double Add(int a, int b) => (double)(a + b); // ❌ LỖI
     * Vì C# không phân biệt được phương thức nào được gọi
     */
    
    /* Có thể nạp chồng với tham số params
     * public int Add(params int[] numbers) => numbers.Sum();
     * public int Add(int a, int b) => a + b; // ✅ HỢP LỆ
     */
}

// ============================================================
// LỚP PROGRAM ĐỂ MINH HỌA CÁCH SỬ DỤNG
// ============================================================
class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        
        // ====================================================
        // VÍ DỤ SỬ DỤNG CÁC PHƯƠNG THỨC ĐÃ NẠP CHỒNG
        // ====================================================
        
        // 1. Gọi phương thức Add(int, int)
        int ketQua1 = calc.Add(5, 10);
        Console.WriteLine($"5 + 10 = {ketQua1}");        // Output: 15
        
        // 2. Gọi phương thức Add(double, double)
        double ketQua2 = calc.Add(5.5, 10.2);
        Console.WriteLine($"5.5 + 10.2 = {ketQua2}");    // Output: 15.7
        
        // 3. Gọi phương thức Add(int, int, int)
        int ketQua3 = calc.Add(5, 10, 15);
        Console.WriteLine($"5 + 10 + 15 = {ketQua3}");   // Output: 30
        
        // 4. Gọi phương thức Add(string, string)
        string ketQua4 = calc.Add("Hello ", "World");
        Console.WriteLine(ketQua4);                       // Output: Hello World
        
        // ====================================================
        // CÁCH TRÌNH BIÊN DỊCH QUYẾT ĐỊNH GỌI PHƯƠNG THỨC NÀO
        // ====================================================
        // 1. Dựa vào SỐ LƯỢNG tham số truyền vào
        // 2. Dựa vào KIỂU DỮ LIỆU của tham số truyền vào
        // 3. Ưu tiên phương thức có kiểu khớp chính xác nhất
        // 4. Nếu có nhiều phương thức phù hợp → LỖI (ambiguous)
        
        // Ví dụ gây lỗi nếu có 2 phương thức mơ hồ:
        // calc.Add(5, 5); // Nếu có cả int và double, sẽ gọi int (khớp chính xác hơn)
    }
}

// ============================================================
// SO SÁNH NẠP CHỒNG VỚI CÁC KHÁI NIỆM KHÁC
// ============================================================
/* 
 * NẠP CHỒNG (Overloading)         vs    GHI ĐÈ (Overriding)
 * --------------------------------------------------------------
 * Xảy ra trong cùng lớp                  Xảy ra giữa lớp cha - lớp con
 * Thời điểm biên dịch                    Thời điểm chạy
 * Không dùng từ khóa override            Dùng từ khóa override
 * Không liên quan đến kế thừa            Phụ thuộc vào kế thừa
 * Cùng tên, khác tham số                 Cùng tên, cùng tham số
 */

// ============================================================
// LỢI ÍCH CỦA NẠP CHỒNG PHƯƠNG THỨC
// ============================================================
/* 
 * 1. Tăng TÍNH NHẤT QUÁN: Cùng tên phương thức cho các thao tác tương tự
 * 2. Tăng KHẢ NĂNG ĐỌC: Dễ hiểu, dễ nhớ
 * 3. LINH HOẠT: Có thể gọi cùng phương thức với nhiều kiểu dữ liệu khác nhau
 * 4. TÁI SỬ DỤNG: Mở rộng chức năng mà không cần đặt tên mới
 * 5. GỌN GÀNG: Giảm số lượng tên phương thức trong lớp
 */

Bảng tóm tắt các phương thức đã nạp chồng:

 
 
Phương thứcSố tham sốKiểu tham sốKiểu trả vềChức năng
Add(int a, int b)2int, intintCộng 2 số nguyên
Add(double a, double b)2double, doubledoubleCộng 2 số thực
Add(int a, int b, int c)3int, int, intintCộng 3 số nguyên
Add(string a, string b)2string, stringstringNối 2 chuỗi

b. Ghi đè phương thức (Method Overriding):

  • Lớp con viết lại phương thức của lớp cha

  • Sử dụng từ khóa virtual ở lớp cha và override ở lớp con

  • Áp dụng tính đa hình

csharp
// ============================================================
// LỚP NHÂN VIÊN (CHA) - ĐẠI DIỆN CHUNG
// ============================================================
public class NhanVien
{
    // Phương thức ảo (virtual) - cho phép lớp con ghi đè
    // virtual: đánh dấu phương thức có thể được tùy chỉnh ở lớp con
    public virtual double TinhLuong()
    {
        // Hành vi mặc định: lương = lương cơ bản
        return LuongCoBan;
    }
}

// ============================================================
// LỚP NHÂN VIÊN VĂN PHÒNG (CON) - KẾ THỪA TỪ NHANVIEN
// ============================================================
public class NhanVienVanPhong : NhanVien
{
    // override: ghi đè (thay thế) phương thức của lớp cha
    // Áp dụng cách tính lương riêng cho nhân viên văn phòng
    public override double TinhLuong()
    {
        // Lương = Lương cơ bản + (Số ngày làm × 200.000đ/ngày)
        // base.TinhLuong() sẽ trả về LuongCoBan
        return LuongCoBan + SoNgayLam * 200000;
    }
}

// ============================================================
// LỚP NHÂN VIÊN SẢN XUẤT (CON) - KẾ THỪA TỪ NHANVIEN
// ============================================================
public class NhanVienSanXuat : NhanVien
{
    // override: ghi đè phương thức của lớp cha
    // Áp dụng cách tính lương riêng cho nhân viên sản xuất
    public override double TinhLuong()
    {
        // Lương = Lương cơ bản + (Số sản phẩm × 50.000đ/sp)
        return LuongCoBan + SoSanPham * 50000;
    }
}

📊 SO SÁNH 3 CÁCH TÍNH LƯƠNG:

 
 
LớpPhương thứcCách tínhGhi chú
NhanVien (Cha)virtual TinhLuong()return LuongCoBanHành vi mặc định
NhanVienVanPhong (Con)override TinhLuong()LuongCoBan + SoNgayLam × 200.000Thêm thưởng theo ngày làm
NhanVienSanXuat (Con)override TinhLuong()LuongCoBan + SoSanPham × 50.000Thêm thưởng theo sản phẩm

10.1.4. Lớp trừu tượng (Abstract Class) (0.3 giờ)

Định nghĩa:

  • Lớp trừu tượng là lớp không thể khởi tạo (không thể new)

  • Dùng làm lớp nền cho các lớp con kế thừa

  • Có thể chứa phương thức trừu tượng (chỉ khai báo, không triển khai)

Cú pháp:

csharp
public abstract class HinhHoc
{
    // Abstract method - bắt buộc lớp con triển khai
    public abstract double TinhDienTich();
    public abstract double TinhChuVi();
    
    // Non-abstract method - có triển khai
    public void HienThi()
    {
        Console.WriteLine($"Diện tích: {TinhDienTich():F2}");
        Console.WriteLine($"Chu vi: {TinhChuVi():F2}");
    }
}

public class HinhTron : HinhHoc
{
    public double BanKinh { get; set; }
    
    public override double TinhDienTich()
    {
        return Math.PI * BanKinh * BanKinh;
    }
    
    public override double TinhChuVi()
    {
        return 2 * Math.PI * BanKinh;
    }
}

Quy tắc:

  • Abstract method không có body

  • Lớp con phải override tất cả abstract methods

  • Có thể có non-abstract methods

  • Không thể tạo instance của abstract class

0.1.5. Interface

Định nghĩa:

  • Interface là hợp đồng (contract) quy định các phương thức mà lớp thực thi phải triển khai

  • Tương tự abstract class nhưng chỉ chứa khai báo, không có triển khai

  • Một lớp có thể implement nhiều interface

Cú pháp:

csharp
// Định nghĩa Interface
public interface IPrintable
{
    void Print();
    string GetSummary();
}

public interface ISerializable
{
    string Serialize();
    void Deserialize(string data);
}

// Lớp implement Interface
public class SinhVien : IPrintable, ISerializable
{
    public void Print()
    {
        Console.WriteLine($"Sinh viên: {HoTen}");
    }
    
    public string GetSummary()
    {
        return $"Mã: {MaSV}, Tên: {HoTen}, Điểm: {DiemTB}";
    }
    
    public string Serialize()
    {
        return $"{MaSV}|{HoTen}|{DiemTB}";
    }
    
    public void Deserialize(string data)
    {
        string[] parts = data.Split('|');
        MaSV = parts[0];
        HoTen = parts[1];
        DiemTB = double.Parse(parts[2]);
    }
}

So sánh Abstract Class vs Interface:

 
 
Tiêu chíAbstract ClassInterface
Từ khóaabstractinterface
Kế thừaĐơn kế thừaĐa kế thừa (một class implement nhiều interface)
FieldCó thể cóKhông có
PropertyCó thể cóCó thể có (không có field)
Method triển khaiCó thể cóKhông thể có (trừ default method C# 8+)
ConstructorCó thể cóKhông có
Access ModifierTất cảChỉ public (mặc định)
Khi nào dùngMối quan hệ “is-a”Mối quan hệ “can-do”

10.1.6. Đa hình (Polymorphism)

Định nghĩa: Đa hình là khả năng một đối tượng có thể thể hiện nhiều hình thái khác nhau.

Các hình thức đa hình:

a. Đa hình tại thời điểm biên dịch (Compile-time):

  • Method Overloading

  • Operator Overloading

b. Đa hình tại thời điểm chạy (Runtime):

  • Method Overriding

  • Sử dụng Abstract Class và Interface

Ví dụ đa hình runtime:

csharp
public abstract class NhanVien
{
    public abstract double TinhLuong();
}

public class NhanVienVanPhong : NhanVien
{
    public override double TinhLuong() => 10_000_000;
}

public class NhanVienSanXuat : NhanVien
{
    public override double TinhLuong() => 8_000_000;
}

// Đa hình
List<NhanVien> nhanViens = new List<NhanVien>();
nhanViens.Add(new NhanVienVanPhong());
nhanViens.Add(new NhanVienSanXuat());

foreach (NhanVien nv in nhanViens)
{
    // Mỗi đối tượng tự biết cách tính lương của mình
    Console.WriteLine($"Lương: {nv.TinhLuong():N0}");
}

Lợi ích của đa hình:

  • Code linh hoạt, dễ mở rộng

  • Giảm dependency

  • Dễ bảo trì

  • Áp dụng được Design Patterns

Bài tập Thực hành

Bài 1: Xây dựng các lớp nhân viên

Bài 2: Xây dựng Interface cho hệ thống

Bài 3: Xây dựng Interface IComparable để sắp xếp

Bài 4: Xây dựng QuanLyNhanVien

Bài 5: Chương trình Quản lý Nhân sự