Menu

Lớp trừu tượng C#: abstract method, ví dụ, so với interface

Lớp trừu tượng (abstract class) trong C# là gì: một lớp cơ sở không thể tạo instance, có thể khai báo các thành viên abstract mà lớp dẫn xuất bắt buộc phải cài đặt. Bài viết đề cập phương thức và thuộc tính abstract, hàm khởi tạo, mẫu template method và khi nào nên chọn interface.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Lớp trừu tượng (abstract class) là lớp chỉ tồn tại để được kế thừa. Bạn không thể tạo instance của nó, và nó có thể khai báo các thành viên abstract: phương thức và thuộc tính không có thân hàm mà mọi lớp dẫn xuất cụ thể đều phải cài đặt. Ngoài điều đó ra, nó là một lớp bình thường: có thể có trường, hàm khởi tạo và các phương thức đã cài đặt đầy đủ để lớp dẫn xuất kế thừa.

Khai báo một abstract class

Đánh dấu lớp bằng abstract, và đánh dấu abstract cho từng thành viên mà lớp dẫn xuất phải cung cấp, kết thúc bằng dấu chấm phẩy thay vì thân hàm:

Đầu ra:

Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00

Describe chỉ được viết một lần, trong lớp cơ sở, và gọi Area cùng Perimeter, hai phương thức hoàn toàn không có phần cài đặt trong Shape. Đó chính là ý nghĩa của thành viên abstract: lớp cơ sở có thể dựa vào nó, và trình biên dịch đảm bảo mọi hình cụ thể đều cung cấp nó. Một lớp dẫn xuất quên cài đặt một trong hai sẽ không biên dịch được:

class Triangle : Shape
{
    public Triangle() : base("Triangle") { }
    public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'

Cách sửa là cài đặt Perimeter, hoặc đánh dấu chính Triangle là abstract nếu nó là lớp cơ sở trung gian cho các lớp khác.

Quy tắc cho thành viên abstract

  • Thành viên abstract không có thân hàm và ngầm định là virtual: lớp dẫn xuất cài đặt nó bằng override.
  • Chỉ abstract class mới được chứa thành viên abstract. Đặt một thành viên abstract trong lớp thường sẽ gây lỗi biên dịch.
  • Thành viên abstract không thể là private (không ai ghi đè được) hay static trong một lớp.
  • Phương thức, thuộc tính, indexer và event có thể là abstract. Trường và hàm khởi tạo thì không.
  • Abstract class không bắt buộc phải có thành viên abstract nào. Chỉ đánh dấu một lớp là abstract đã đủ để ngăn tạo instance, hữu ích cho lớp cơ sở chỉ có ý nghĩa thông qua các lớp con.

Thuộc tính abstract khai báo những accessor mà lớp dẫn xuất phải cung cấp:

abstract class Plan
{
    public abstract decimal MonthlyPrice { get; }
    public abstract string Name { get; }
}

class ProPlan : Plan
{
    public override decimal MonthlyPrice => 12.99m;
    public override string Name => "Pro";
}

Hàm khởi tạo trong abstract class

Abstract class có thể có hàm khởi tạo. Không thể gọi chúng bằng new, nhưng mọi hàm khởi tạo của lớp dẫn xuất đều chạy một hàm khởi tạo của lớp cơ sở thông qua : base(...), nên đây là nơi khởi tạo và kiểm tra trạng thái dùng chung. Hãy khai báo chúng là protected, vì từ khóa này nói đúng điều đó: chỉ lớp dẫn xuất mới dùng được.

Mẫu template method

Lý do phổ biến nhất để viết một abstract class là cố định thứ tự của một thuật toán trong lớp cơ sở và để lớp dẫn xuất điền vào từng bước riêng. Phương thức cơ sở chạy các bước đó không phải virtual, nên không lớp con nào có thể đổi thứ tự hay bỏ qua bước nào.

Đầu ra:

item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20

| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |

Total: 26.20

Mỗi dòng dữ liệu là một tuple có tên, và đầu ra được ghép bằng StringBuilder. Hãy để ý cách kết hợp: Header và Row là abstract vì định dạng nào cũng phải tự quyết định hai phần này, còn Footer là virtual với giá trị mặc định rỗng vì hầu hết định dạng không cần gì ở đó. Các bước đều là protected, nên code bên ngoài chỉ có thể gọi Export. Các lớp của framework cũng dùng ý tưởng này: Stream và TextWriter là các abstract class cơ sở, có rất nhiều phương thức tiện ích được xây dựng trên vài thành viên do lớp dẫn xuất cung cấp (Read và Write với stream, Write(char) với text writer).

Abstract class và interface

Cả hai đều cho phép bạn viết code dựa trên một kiểu mà không cần biết lớp cụ thể. Chúng khác nhau ở những gì có thể chứa và số lượng một lớp có thể có:

Abstract classInterface
Số lượng một lớp có thể kế thừa hoặc cài đặtmộtnhiều
Trường instance (trạng thái)cókhông
Hàm khởi tạocókhông
Phương thức có thân hàmcócó, từ C# 8 (default interface method)
Thành viên staticcócó, từ C# 8; thành viên static abstract từ C# 11
Phạm vi truy cập của thành viêntùy ýmặc định là public; các mức khác được phép từ C# 8
Struct có dùng được khôngkhôngcó
Thêm thành viên về sauan toàn nếu không phải abstractlàm hỏng các lớp cài đặt, trừ khi có thân mặc định

Default interface method (C# 8) thu hẹp khoảng cách, nhưng không xóa được khác biệt cốt lõi:

public interface ILogger
{
    void Write(string message);
    void Error(string message) => Write("ERROR: " + message);   // default implementation: C# 8, .NET Core 3.0+
}

Một phương thức mặc định vẫn không thể truy cập trường instance, vì interface không có trường nào; nó chỉ có thể gọi các thành viên khác của interface.

Cách chọn trong thực tế:

  • Interface khi bạn mô tả một khả năng mà các kiểu không liên quan đến nhau đều có thể có (IComparable<T>, IDisposable, IShippingProvider), khi struct cũng cần tham gia, hoặc khi một lớp cần đóng nhiều vai trò như vậy. Phần lớn code dependency injection được viết dựa trên interface vì lý do này.
  • Abstract class khi các kiểu dẫn xuất thực sự là những biến thể của cùng một thứ và dùng chung trạng thái hoặc một thuật toán cố định, như các exporter ở trên.
  • Cả hai cùng lúc là chuyện thường gặp: một interface để bên gọi phụ thuộc vào, và một abstract class cơ sở cài đặt interface đó để các lớp cài đặt khỏi phải viết lại phần dùng chung. Trang về interface sẽ trình bày chi tiết phía interface.

Lỗi thường gặp

  • Cố tạo instance của lớp cơ sở. new Shape(...) gây lỗi CS0144. Hãy tạo instance của một lớp dẫn xuất.
  • Quên override ở phần cài đặt. Viết public double Area() trong lớp dẫn xuất không cài đặt thành viên abstract; trình biên dịch báo lỗi CS0534 vì thiếu phần cài đặt, kèm cảnh báo rằng phương thức mới đang che phương thức được kế thừa.
  • Hàm khởi tạo public trên abstract class. Không gây hại nhưng dễ gây hiểu nhầm. Hãy dùng protected.
  • Abstract class không có code dùng chung. Nếu lớp cơ sở chỉ có thành viên abstract và không có trạng thái, một interface cũng diễn đạt được điều đó mà không chiếm mất vị trí lớp cơ sở duy nhất.
  • Gọi thành viên abstract từ hàm khởi tạo của lớp cơ sở. Phần cài đặt ở lớp dẫn xuất chạy trước thân hàm khởi tạo của lớp dẫn xuất, nên các trường được gán trong thân đó vẫn đang là 0 hoặc null.

Câu hỏi thường gặp

Abstract class trong C# là gì?

Một lớp được đánh dấu abstract chỉ dùng làm lớp cơ sở: bạn không thể tạo instance của nó bằng new (lỗi CS0144). Lớp này có thể chứa trường, hàm khởi tạo và phương thức bình thường, cộng thêm các thành viên abstract không có thân hàm mà mọi lớp dẫn xuất không trừu tượng đều phải cài đặt.

Abstract class và interface trong C# khác nhau thế nào?

Một lớp chỉ kế thừa được một abstract class nhưng có thể cài đặt nhiều interface. Abstract class có thể lưu trạng thái (trường instance), có hàm khởi tạo và thành viên với mọi mức truy cập, nên là nơi phù hợp cho phần cài đặt dùng chung. Interface mô tả một khả năng và không có trạng thái instance; từ C# 8, interface có thể có thân phương thức mặc định, nhưng vẫn không thể có trường instance hay hàm khởi tạo.

Phương thức abstract và virtual trong C# khác nhau thế nào?

Phương thức virtual có thân hàm và lớp dẫn xuất có thể ghi đè nếu muốn. Phương thức abstract không có thân hàm và lớp dẫn xuất bắt buộc phải ghi đè, nếu không thì chính lớp đó cũng là lớp trừu tượng (lỗi CS0534 với một lớp cụ thể). Phương thức abstract chỉ có thể nằm trong abstract class.

Abstract class trong C# có hàm khởi tạo được không?

Có. Hàm khởi tạo này chạy khi một lớp dẫn xuất được tạo, thông qua : base(...), và thường dùng để khởi tạo các trường dùng chung. Hãy khai báo nó là protected: một hàm khởi tạo public trên abstract class gợi ý rằng bên ngoài có thể gọi nó, trong khi thực tế là không thể.

Tại sao tôi không thể tạo instance của abstract class?

Vì lớp đó có thể có các thành viên abstract chưa được cài đặt: gọi new Shape().Area() sẽ không có code nào để chạy. Trình biên dịch báo lỗi CS0144 ("Cannot create an instance of the abstract type or interface"). Hãy tạo instance của một lớp dẫn xuất cụ thể, rồi lưu vào biến có kiểu trừu tượng nếu muốn: Shape s = new Circle(2);.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu