CodeGym /Các khóa học /C# SELF /Mối liên hệ giữa tính đa hình và phương thức abstract

Mối liên hệ giữa tính đa hình và phương thức abstract

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Quay lại với sở thú của tụi mình nhé. Mình có class cơ sở Animal (Động vật) và các class con: Dog (Chó), Cat (Mèo), Fish (Cá).

Ở các bài trước, tụi mình đã thêm vào Animal một phương thức virtual tên là MakeSound():

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public virtual void MakeSound() // Phương thức virtual
    {
        Console.WriteLine("Một động vật nào đó phát ra âm thanh."); // Mặc định
    }

    public void Sleep() // Phương thức thường
    {
        Console.WriteLine($"{Name} đang ngủ.");
    }
}

public class Dog : Animal
{
    public override void MakeSound() // Ghi đè âm thanh cho chó
    {
        Console.WriteLine("Gâu-gâu!");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // Ghi đè âm thanh cho mèo
    {
        Console.WriteLine("Meo!");
    }
}

Cái này chạy ngon luôn! Khi tạo Dog hoặc Cat rồi gọi MakeSound(), tụi mình nghe được tiếng đặc trưng của tụi nó. Nhưng nếu tạo một Animal bình thường thì sao?

Animal genericAnimal = new Animal();
genericAnimal.Name = "Sinh vật không xác định";
genericAnimal.MakeSound(); // Sẽ in: "Một động vật nào đó phát ra âm thanh."

Nghe cũng hợp lý. Nhưng đôi khi, việc có một thực thi mặc định ở class cha lại không hợp lý chút nào. Nếu Animal chỉ là một khái niệm chứ không phải động vật cụ thể thì sao? "Động vật" nói chung đâu có phát ra âm thanh cụ thể, chỉ từng loài mới có. Hoặc tưởng tượng có class Shape (Hình dạng) với method CalculateArea() (TínhDiệnTích). "Hình dạng" chung chung thì tính diện tích kiểu gì? Không tính được! Chỉ có Hình Tròn, Hình Vuông mới có diện tích, chứ "Hình dạng" thì chịu.

Trong mấy trường hợp như vậy, khi class cha không thể (hoặc không nên) cung cấp thực thi mặc định có ý nghĩa, nhưng lại bắt buộc các class con phải tự cài đặt method đó, thì tụi mình cần tới phương thức abstractclass abstract.

2. Class abstract: Khi bản vẽ chưa phải là nhà

Tưởng tượng bạn là kiến trúc sư, bạn vẽ bản thiết kế một ngôi nhà mẫu. Nhưng không phải nhà cụ thể, mà là "Nhà Khái Niệm". Nó có mấy điểm chung: tường, mái, móng. Nhưng bạn chưa biết nó là biệt thự một tầng hay cao ốc nhiều tầng. Một số phần bản vẽ sẽ cụ thể (ví dụ chiều cao trần tầng 1), còn một số chỉ là gợi ý (ví dụ "số tầng" sẽ xác định sau).

Trong thế giới C#, "Nhà Khái Niệm" này gọi là class abstract.
Class abstract là class được đánh dấu bằng từ khóa abstract.


public abstract class Animal // Animal giờ là class abstract
{
    // ...
}
Khai báo class abstract

Điểm đặc biệt của class abstract:

  • Không thể tạo trực tiếp. Bạn không thể viết new Animal(). Tại sao? Vì Animal giờ chỉ là khái niệm, không phải thực thể cụ thể. Bạn không thể xây "Nhà Khái Niệm", chỉ xây được biệt thự hoặc cao ốc thôi.
    Nếu thử new Animal(), compiler sẽ nhắc nhở bạn ngay:
    Cannot create an instance of the abstract type or interface 'Animal'
    (Không thể tạo instance của abstract type hoặc interface 'Animal')
    Đây là giới hạn rất quan trọng!
  • Có thể chứa thành phần abstract. Đây mới là phần thú vị nhất!

3. Phương thức abstract: hợp đồng không có thực thi

Nếu class abstract là "Nhà Khái Niệm", thì phương thức abstract là phần trên bản vẽ ghi "phải làm cái này", nhưng chưa có hướng dẫn cụ thể "làm sao". Ví dụ, "Xây móng" là bắt buộc, nhưng kích thước, vật liệu móng thì tùy từng loại nhà.

Phương thức abstract là method mà:

  • Được đánh dấu bằng từ khóa abstract.
  • Không có thân hàm (không có block {}). Kết thúc bằng dấu chấm phẩy ;.
  • Chỉ được khai báo bên trong class abstract.

Giờ tụi mình sẽ biến method MakeSound() thành abstract:


public abstract class Animal // Animal giờ là abstract
{
    public string Name { get; set; }
    public int Age { get; set; }

    public abstract void MakeSound(); // Đây, method abstract! Không có thân hàm!

    public void Sleep() // Method này vẫn là thường, "cụ thể"
    {
        Console.WriteLine($"{Name} đang ngủ.");
    }
}

Nhìn xem MakeSound() đã thay đổi thế nào! Không còn dấu ngoặc nhọn và thực thi mặc định nữa. Nó chỉ nói: "Bất kỳ động vật nào bắt buộc phải biết phát ra âm thanh. Còn phát kiểu gì thì class con tự quyết."

Luật quan trọng: Nếu class của bạn kế thừa từ class abstract mà không phải là abstract, thì bắt buộc phải override (dùng override) tất cả method abstract của class cha. Không phải lựa chọn, mà là bắt buộc, hợp đồng! Compiler C# rất nghiêm vụ này. Nếu bạn quên, nó nhắc ngay:

public class Dog : Animal // Class thường, không abstract
{
    // LỖI BIÊN DỊCH!
    // 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
    // (Class 'Dog' chưa cài đặt thành phần abstract kế thừa 'Animal.MakeSound()')
    // Compiler đòi bạn phải có MakeSound() với override!
}

Để hết lỗi, DogCat phải override MakeSound():

public class Dog : Animal
{
    public override void MakeSound() // Bắt buộc override!
    {
        Console.WriteLine("Gâu-gâu!");
    }
}

public class Cat : Animal
{
    public override void MakeSound() // Ở đây cũng vậy!
    {
        Console.WriteLine("Meo!");
    }
}

So sánh virtual, abstract và override

Đặc điểm virtual method abstract method override keyword
Vị trí Trong class thường hoặc class abstract Chỉ trong class abstract Trong class con (kế thừa)
Thân hàm Có thân hàm (thực thi mặc định) Không có thân hàm (kết thúc bằng ;) Có thân hàm (thực thi mới)
Mục đích Cung cấp thực thi mặc định, cho phép class con thay đổi Khai báo hợp đồng: class con bắt buộc phải tự cài đặt Cung cấp thực thi riêng cho virtual hoặc abstract method của class cha
Tạo instance class cha? Có (nếu class cha không abstract) Không (nếu class cha abstract) N/A (liên quan tới method, không phải class)
Bắt buộc ở class con Có thể override (override) hoặc không Bắt buộc override (override), nếu class con không abstract N/A

4. Đa hình thực chiến với phương thức abstract

Giờ mới là phần hấp dẫn nè! Tụi mình biết đa hình cho phép làm việc với object của nhiều class con thông qua reference tới class cha. Và điều này vẫn chạy ngon kể cả khi class cha là abstract!

Dù không thể tạo instance Animal trực tiếp (nhớ nha, new Animal() sẽ lỗi), tụi mình vẫn dùng kiểu Animal làm reference cho object của class con. Cực kỳ mạnh luôn!

Tiếp tục với sở thú. Giả sử có một trang trại, trong đó có nhiều động vật khác nhau. Tụi mình muốn mỗi con phát ra tiếng của nó.


using System;

// Class abstract Animal
public abstract class Animal
{
    public string Name;
    public int Age;
    public Animal(string name, int age) { Name = name; Age = age; }
    public abstract void MakeSound();
    public void Sleep() { Console.WriteLine($"{Name} đang ngủ."); }
}

public class Dog : Animal
{
    public Dog(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Gâu-gâu!"); }
}

public class Cat : Animal
{
    public Cat(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Meo!"); }
}

public class Fish : Animal
{
    public Fish(string name, int age) : base(name, age) { }
    public override void MakeSound() { Console.WriteLine("Bõm-bõm"); }
}

class Program
{
    static void Main()
    {
        Animal[] animals = {
            new Dog("Sharik", 3),
            new Cat("Murzik", 5),
            new Fish("Nemo", 1)
        };

        foreach (Animal animal in animals)
        {
            Console.WriteLine($"\nXin chào, tôi là {animal.Name}, tôi {animal.Age} tuổi.");
            animal.MakeSound();
            animal.Sleep();
        }
    }
}

Chuyện gì xảy ra trong code này?

  1. Tụi mình khai báo Animalabstract class. Compiler hiểu: "Class này là mẫu, không tạo instance được, nhưng có thể kế thừa".
  2. Khai báo public abstract void MakeSound(); trong Animal. Nghĩa là: "Bất kỳ class nào kế thừa Animal (không phải abstract) bắt buộc phải cài đặt method MakeSound()". Đây là hợp đồng!
  3. Dog, Cat, và Fish đều tuân thủ hợp đồng, override MakeSound() với thực thi riêng. Nếu quên ở bất kỳ class nào, compiler sẽ không cho qua.
  4. Trong method Main, tụi mình tạo mảng Animal[]. Dù Animal là abstract, mảng này vẫn chứa reference tới object của class con (Dog, Cat, Fish), vì tụi nó Animal!
  5. Khi duyệt mảng bằng foreach và gọi animal.MakeSound(), nhờ đa hình, C# "biết" phải gọi MakeSound() của class nào: Dog.MakeSound(), Cat.MakeSound() hay Fish.MakeSound(). Nó gọi method đúng với kiểu thực tế của object mà reference Animal đang trỏ tới, không phải kiểu reference. Đó chính là sức mạnh của đa hình!
  6. Còn animal.Sleep() thì gọi thực thi cụ thể từ class cha Animal, vì method này không được đánh dấu virtual hay abstract, và cũng không override ở class con.

5. Ứng dụng thực tế để làm gì?

"Ok, sở thú, động vật... Nhưng khi code app cho ngân hàng hay shop thì dùng kiểu gì?" — bạn hỏi. Câu hỏi quá hay! Class và method abstract là công cụ cực mạnh để thiết kế hệ thống linh hoạt, dễ mở rộng.

Bắt buộc thực thi hợp đồng: Đây là lợi ích lớn nhất. Giả sử bạn viết framework cho hệ thống thanh toán. Có class cha abstract class PaymentProcessor (Xử lý thanh toán). Bạn biết chắc bất kỳ bộ xử lý thanh toán nào cũng phải có ProcessPayment() (Xử lý thanh toán), RefundPayment() (Hoàn tiền) và CheckStatus() (Kiểm tra trạng thái). Nhưng cách làm với PayPal, thẻ ngân hàng hay Bitcoin thì khác nhau hoàn toàn.
Bạn khai báo mấy method này là abstract trong PaymentProcessor.


public abstract class PaymentProcessor
{
    public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
    public abstract bool RefundPayment(string transactionId);
    public abstract string CheckStatus(string transactionId);
    // ... các method khác có thể là cụ thể, ví dụ log
    public void LogTransaction(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public class PayPalProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // Ở đây là logic phức tạp với API PayPal
        Console.WriteLine($"PayPal: xử lý {amount} {currency}...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "Hoàn thành"; }
}

public class CreditCardProcessor : PaymentProcessor
{
    public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
    {
        // Ở đây là logic với ngân hàng
        Console.WriteLine($"CreditCard: {amount} {currency} từ thẻ {cardNumber.Substring(0,4)}XXXX...");
        return true;
    }
    public override bool RefundPayment(string transactionId) { /* ... */ return true; }
    public override string CheckStatus(string transactionId) { /* ... */ return "Đang xử lý"; }
}

Giờ bất kỳ dev nào muốn tạo bộ xử lý thanh toán mới (ví dụ BitcoinProcessor) sẽ bắt buộc phải cài đặt đủ ba method này. Không thể quên RefundPayment() được, vì compiler không cho qua! Điều này đảm bảo hệ thống của bạn luôn nhất quán.

Linh hoạt và dễ mở rộng: Bạn có thể viết code làm việc với PaymentProcessor mà không cần biết cụ thể class nào sẽ dùng. Ví dụ, trong code giỏ hàng của shop online, bạn chỉ cần gọi currentProcessor.ProcessPayment(), hệ thống tự chọn đúng processor theo phương thức thanh toán user chọn. Ngày mai có thêm phương thức mới — chỉ cần tạo class mới kế thừa PaymentProcessor, cài đặt các method abstract, code chính không cần sửa gì luôn!

Tránh thực thi rỗng: Nếu dùng virtual thay vì abstract, bạn phải cho thực thi mặc định (có thể là rỗng), dễ gây misleading (gây hiểu nhầm). abstract nói rõ: "Không có thực thi ở đây, phải tự làm ở class con!"

Cải thiện kiến trúc code: Class abstract giúp tách rõ logic chung và logic riêng. Logic chung (ví dụ LogTransaction trong PaymentProcessor) nằm ở class cha, còn logic riêng (như ProcessPayment) ở class con. Code dễ đọc, dễ bảo trì, dễ test. Giúp designer framework xác định "điểm mở rộng" mà user bắt buộc phải tự cài đặt.

6. Lỗi phổ biến và lưu ý

Lỗi #1: cố tạo instance class abstract.
Luôn là lỗi biên dịch. Class abstract là khái niệm, không phải object cụ thể. Bạn dùng nó làm kiểu reference được, nhưng không thể viết new Animal().

Lỗi #2: quên override method abstract.
Nếu class con không phải abstract, nó phải cài đặt tất cả method abstract của class cha. Không thì lỗi biên dịch. Cách duy nhất để né là làm class con cũng abstract, nhưng thường không phải điều bạn muốn.

Lỗi #3: nhầm giữa abstractvirtual.
abstract bắt buộc phải override, không có thân hàm. virtual có thực thi mặc định và có thể override. Không được khai báo method abstract có thân hàm hoặc virtual mà không có thân hàm — lỗi cú pháp.

Lỗi #4: dùng new thay vì override.
Nếu bạn không dùng override mà chỉ tạo method trùng tên ở class con, bạn không override mà chỉ ẩn method của class cha. Điều này dẫn tới hành vi bất ngờ khi gọi đa hình: sẽ gọi method của class cha.


public class Base
{
    public void DoSomething() { Console.WriteLine("Base"); }
}

public class Derived : Base
{
    public new void DoSomething() { Console.WriteLine("Derived"); }
}

Base obj = new Derived();
obj.DoSomething(); // Sẽ in: Base
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Khái niệm về đa hình
Đa hình và nạp chồng phương thức
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION