Interface là một hợp đồng: một danh sách thành viên có tên mà một kiểu hứa sẽ cung cấp. Nó nói kiểu đó làm được gì và không nói gì về cách làm. Lớp và struct triển khai interface, và code viết dựa trên interface sẽ làm việc được với mọi bản triển khai, kể cả những bản được viết sau này.
Khai báo và triển khai một interface
Interface khai báo các thành viên không có phần thân. Theo quy ước, tên của nó bắt đầu bằng I. Một lớp triển khai interface bằng cách liệt kê nó sau dấu hai chấm và cung cấp một thành viên public cho từng thành viên của interface:
Đầu ra:
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice không biết gì về PercentOff hay FixedOff. Nó chỉ phụ thuộc vào IDiscount, nên thêm một lớp BuyOneGetOne sau này không cần sửa gì ở nó. Sự tách rời đó là toàn bộ lý do interface tồn tại.
Một vài quy tắc khi khai báo:
- Thành viên của interface mặc định là public, và các thành viên triển khai cũng phải là public (trừ khi triển khai tường minh, xem bên dưới).
- Interface có thể khai báo phương thức, thuộc tính, indexer và sự kiện. Nó không thể khai báo trường instance hay hàm khởi tạo.
- Một lớp bỏ sót thành viên sẽ không biên dịch được: lỗi CS0535, "'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'".
- Bạn không thể tạo một interface bằng
new. Bạn tạo một lớp triển khai và có thể giữ nó trong một biến kiểu interface:IDiscount d = new FixedOff(5);.
Interface như một kiểu
Biến, tham số, trường hay kiểu trả về đều có thể khai báo là interface. Khi đó nó nhận mọi đối tượng triển khai interface đó, và chỉ để lộ các thành viên của interface. Đây là cách bạn viết code không quan tâm mình nhận bản triển khai nào, và là cách các bài kiểm thử thay vào một bản giả:
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
Các container dependency injection trong ASP.NET Core được xây dựng trên ý tưởng này: service được đăng ký và yêu cầu theo interface.
Triển khai nhiều interface
Một lớp có tối đa một lớp cơ sở nhưng có thể triển khai bao nhiêu interface cũng được. Lớp cơ sở đứng trước, rồi đến các interface, ngăn cách bằng dấu phẩy:
Đầu ra:
INVOICE Studio rent March: 950.00
True
True
Interface cũng có thể kế thừa từ interface khác: interface IRepository<T> : IReadRepository<T> thêm thành viên vào những thành viên nó kế thừa, và một lớp triển khai IRepository<T> phải cung cấp cả hai nhóm.
Triển khai interface tường minh
Đôi khi hai interface khai báo một thành viên trùng tên nhưng khác ý nghĩa, hoặc một thành viên interface không có nghĩa gì trên giao diện public của chính lớp đó. Hãy triển khai nó tường minh (explicit) bằng cách đặt tên interface phía trước và bỏ access modifier:
Đầu ra:
Desk lamp,34.90
Desk lamp for 34.90
Thành viên được triển khai tường minh chỉ truy cập được qua interface. Framework dùng cách này để giữ các kiểu thông dụng gọn gàng: mảng triển khai IList.Add một cách tường minh (nó ném NotSupportedException, vì mảng có kích thước cố định), nên phương thức này không hiện ra khi bạn gõ array..
Các interface của framework bạn sẽ triển khai
Triển khai một interface chuẩn sẽ gắn kiểu của bạn vào code có sẵn của framework. Có ba interface xuất hiện liên tục.
IComparable<T> cho một kiểu thứ tự tự nhiên, thứ mà List<T>.Sort(), Array.Sort và Max() sử dụng. CompareTo trả về một số âm, số không hoặc một số dương:
Đầu ra:
1.4, 2.9, 2.10, 10.0
Sắp xếp cùng các phiên bản đó dưới dạng chuỗi sẽ cho 1.4, 10.0, 2.10, 2.9. Không có IComparable<T>, Sort() ném InvalidOperationException vì nó không có cách nào so sánh hai đối tượng Version.
IEnumerable<T> giúp một kiểu dùng được trong foreach và với LINQ. Cách dễ nhất để triển khai GetEnumerator là dùng yield return, được trình bày ở trang IEnumerable và yield.
IDisposable đánh dấu một kiểu đang giữ thứ cần được giải phóng (một file handle, một kết nối, một bộ hẹn giờ). Phương thức duy nhất của nó, Dispose, là thứ mà câu lệnh using gọi khi khối lệnh kết thúc, kể cả khi có ngoại lệ được ném ra:
Đầu ra:
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
Phương thức mặc định của interface (C# 8)
Trước đây, thêm một thành viên vào interface đã phát hành sẽ làm hỏng mọi lớp triển khai nó. Từ C# 8, một thành viên interface có thể có phần thân, và các lớp triển khai sẽ kế thừa phần thân đó trừ khi tự cung cấp phiên bản riêng:
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
Dòng cuối cùng là phần khiến nhiều người bất ngờ: phương thức mặc định không được đưa vào các thành viên của chính lớp, nên chỉ gọi được qua kiểu interface. Phương thức mặc định cũng không dùng được trường instance (interface không có trường nào); chúng hoạt động thông qua các thành viên khác của interface. Tính năng này cần .NET Core 3.0 trở lên và không có trên .NET Framework.
C# 8 cũng cho phép thành viên static trong interface, và C# 11 bổ sung thành viên static abstract, cho phép code generic gọi một phương thức static hoặc toán tử trên T. Đó là nền tảng của generic math trong .NET 7 (INumber<T>, where T : INumber<T>).
Interface hay lớp trừu tượng?
Hãy dùng interface cho một khả năng mà các kiểu không liên quan có thể dùng chung, 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. Hãy dùng lớp trừu tượng khi các bản triển khai 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. Trang về lớp trừu tượng có bảng so sánh chi tiết. Nhiều thiết kế dùng cả hai: một interface cho bên sử dụng, và một lớp cơ sở trừu tượng triển khai sẵn những phần nhàm chán cho bên triển khai.
Lỗi thường gặp
- Thiếu bản triển khai hoặc bản triển khai không public. Mỗi thành viên interface cần một thành viên public có chữ ký khớp, hoặc một bản triển khai tường minh. Nếu không sẽ gặp CS0535 (hoặc CS0737 khi phương thức có tồn tại nhưng không public).
- Interface chỉ có một bản triển khai "phòng khi cần". Interface chỉ đáng có khi có nhiều bản triển khai hoặc một bản giả cho kiểm thử. Nếu không, nó chỉ là thêm một tệp phải lần tìm.
- Interface quá lớn. Một interface có hai mươi thành viên buộc mọi lớp triển khai phải viết hai mươi thành viên. Hãy tách nó thành các vai trò nhỏ hơn (
IReader,IWriter) để các lớp kết hợp. - Ép kiểu interface ngược về lớp.
((StripeGateway)gateway).Refund()phá bỏ sự tách rời. Nếu nơi gọi cầnRefund, phương thức đó nên nằm trong interface. - Mong gọi phương thức mặc định của interface trên lớp. Nó chỉ gọi được qua kiểu interface.
Câu hỏi thường gặp
Interface trong C# là gì?
Interface là một tập hợp thành viên có tên (phương thức, thuộc tính, sự kiện, indexer) mà một kiểu hứa sẽ cung cấp, không có trạng thái instance. interface IDiscount { decimal Apply(decimal price); } nói rằng "bất cứ thứ gì là IDiscount đều áp dụng được lên một mức giá". Lớp và struct triển khai nó bằng dấu hai chấm, và code sau đó có thể làm việc với bất kỳ kiểu nào trong số đó thông qua kiểu interface.
Làm sao triển khai một interface trong C#?
Liệt kê nó sau dấu hai chấm trong phần khai báo lớp (sau lớp cơ sở, nếu có) và cung cấp một thành viên public cho mỗi thành viên của interface: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. Thiếu một thành viên là lỗi CS0535. Visual Studio và Rider có thể tự sinh khung code bằng quick fix.
Một lớp C# có thể triển khai nhiều interface không?
Có, bao nhiêu cũng được, ngăn cách bằng dấu phẩy: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. Đây là câu trả lời của C# cho đa kế thừa: một lớp có một lớp cơ sở nhưng có thể đóng nhiều vai trò. Nếu hai interface khai báo cùng một thành viên, một phương thức public có thể đáp ứng cả hai, hoặc bạn có thể dùng triển khai tường minh để mỗi interface có phiên bản riêng.
Triển khai interface tường minh trong C# là gì?
Là triển khai một thành viên với tên interface đứng trước và không có access modifier: string IExportable.Format() { ... }. Khi đó thành viên này chỉ gọi được qua một biến kiểu interface, không gọi được qua lớp. Cách này dùng để giải quyết xung đột tên giữa hai interface và để giữ các thành viên interface ít dùng ra khỏi giao diện public của lớp.
Phương thức mặc định của interface trong C# là gì?
Từ C# 8, một thành viên interface có thể có phần thân: void Error(string m) => Write("ERROR: " + m);. Các lớp triển khai nhận nó miễn phí và có thể tự cung cấp phiên bản riêng. Tính năng này ra đời để tác giả thư viện thêm thành viên vào một interface đã phát hành mà không làm hỏng mọi lớp triển khai. Phương thức mặc định được gọi qua kiểu interface, không qua lớp.
Tại sao tên interface trong C# bắt đầu bằng I?
Đó là quy ước đặt tên của .NET (IEnumerable, IDisposable, IComparable<T>): một chữ I viết hoa theo sau là tên dạng PascalCase, thường là một tính từ kết thúc bằng "-able". Trình biên dịch không bắt buộc, nhưng làm theo quy ước giúp nhận ra kiểu interface ngay lập tức, nhất là trong phần khai báo lớp nơi lớp cơ sở và các interface nằm chung một danh sách.