Kế thừa (inheritance) cho phép một lớp xây dựng dựa trên một lớp khác. Lớp dẫn xuất nhận các thành viên của lớp cơ sở, thêm thành viên riêng, và có thể thay thế những hành vi mà lớp cơ sở cho phép. Kết hợp với các phương thức virtual, kế thừa mang lại đa hình (polymorphism): code viết cho lớp cơ sở sẽ chạy đúng hành vi của lớp dẫn xuất mà không cần biết đó là lớp dẫn xuất nào.
Kế thừa một lớp
Đặt lớp cơ sở sau dấu hai chấm. Lớp dẫn xuất có mọi thứ lớp cơ sở có, cộng với những gì nó tự khai báo:
Đầu ra:
TX-19: 20 km, 2 fares
True
Những gì lớp dẫn xuất nhận được và không nhận được:
- Được kế thừa: trường, thuộc tính, phương thức, sự kiện và kiểu lồng nhau. Tất cả đều thực sự nằm trong đối tượng.
- Truy cập được: chỉ những gì lớp cơ sở cho phép: thành viên
public,protectedvàinternal. Một thành viênprivatecủaVehicletồn tại trong mọiTaxinhưng code củaTaxikhông gọi tên nó được. Đó là lý doKmcóprivate set:Taxiđọc được nó, và chỉ thay đổi được quaDrive. - Không được kế thừa: hàm khởi tạo.
Taxiphải tự khai báo hàm khởi tạo và nối tới một hàm khởi tạo củaVehiclebằng: base(plate). Trang về hàm khởi tạo trình bày thứ tự chạy của hai hàm này.
Mọi lớp rốt cuộc đều kế thừa từ object, đó là lý do đối tượng nào cũng có ToString(), Equals() và GetHashCode().
virtual và override
Lớp cơ sở đánh dấu một phương thức là virtual để nói rằng "lớp dẫn xuất có thể cung cấp phiên bản riêng". Lớp dẫn xuất thay thế nó bằng override. Bên trong phương thức override, base.Method() gọi phiên bản của lớp cơ sở.
Đầu ra:
[email] to lea@example.com: Your order 1042 has shipped today
(unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today
Biến vòng lặp có kiểu Notification, vậy mà mỗi đối tượng lại hiển thị theo cách riêng. Đó chính là đa hình: lời gọi n.Render(...) được xác định lúc chạy dựa trên kiểu thực tế của đối tượng. Để ý thêm rằng base.Render trong Email dùng Channel, bản thân thuộc tính này cũng là virtual, nên phương thức cơ sở in ra email chứ không phải generic. Một lời gọi virtual bên trong lớp cơ sở vẫn đi tới phiên bản override.
Thuộc tính cũng có thể là virtual, như Channel cho thấy. Trường thì không.
Trình biên dịch buộc bạn dùng đúng cả hai từ khóa. Viết override trên một phương thức không phải virtual là lỗi CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). Quên override khi phương thức cơ sở là virtual chỉ là một cảnh báo, nhưng nó thay đổi hoàn toàn ý nghĩa, như phần tiếp theo cho thấy.
new và override: che giấu thay vì ghi đè
Nếu lớp dẫn xuất khai báo một phương thức có cùng chữ ký với phương thức cơ sở mà không viết override, nó sẽ che giấu (hide) phương thức cơ sở. Trình biên dịch cảnh báo (CS0114 với phương thức cơ sở là virtual, CS0108 trong các trường hợp khác) và gợi ý từ khóa new, từ khóa này tắt cảnh báo nhưng vẫn giữ hành vi che giấu:
Đầu ra:
Sales report
Report
Draft report
b và c là cùng một loại đối tượng, vậy mà chúng in ra tiêu đề khác nhau. Với new, phương thức được chọn phụ thuộc vào kiểu của biến, được quyết định lúc biên dịch. Code xử lý báo cáo dưới dạng Report (một list, một tham số phương thức, một callback của framework) không bao giờ thấy DraftReport.Title. Đó gần như không bao giờ là điều bạn muốn. Hãy dùng override cho đa hình; new tồn tại chủ yếu cho trường hợp một lớp cơ sở mà bạn không kiểm soát thêm một thành viên có tên trùng với thành viên bạn đã có.
sealed
sealed trên một lớp cấm kế thừa từ lớp đó:
sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'
string là lớp sealed, nhiều kiểu của framework cũng vậy. Trên một phương thức override, sealed chặn chuỗi ghi đè tại tầng đó:
class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed
Thiết kế một lớp để kế thừa đòi hỏi công sức: quyết định cái gì là virtual, lớp dẫn xuất được dựa vào điều gì, mọi thứ diễn ra theo thứ tự nào. Một lớp không được thiết kế theo cách đó thì để sealed sẽ an toàn hơn, và sealed có thể gỡ bỏ sau này mà không làm hỏng code của ai, còn bỏ sealed thì không thể rút lại khi người khác đã kế thừa từ lớp của bạn. Lời gọi tới thành viên của lớp sealed cũng có thể nhanh hơn một chút, vì runtime biết chắc không có override nào.
Một lớp cơ sở, nhiều interface
Một lớp C# có đúng một lớp cơ sở. class Admin : User, Employee là lỗi CS1721 ("cannot have multiple base classes"). Tuy nhiên, một lớp có thể triển khai bao nhiêu interface cũng được bên cạnh lớp cơ sở của nó:
class Admin : User, IAuditable, IComparable<Admin>
{
// base class first, then interfaces, in any order
}
Hãy dùng lớp cơ sở cho quan hệ "là một loại của, và dùng chung phần triển khai với", còn interface cho quan hệ "làm được". Khi lớp cơ sở chỉ tồn tại để buộc các lớp dẫn xuất điền vào một số phương thức, lớp trừu tượng (abstract class) là công cụ phù hợp.
Ép kiểu lên và xuống trong cây kế thừa
Một đối tượng dẫn xuất luôn có thể dùng ở nơi cần kiểu cơ sở của nó. Phép upcast đó là ngầm định và không thể thất bại. Đi theo chiều ngược lại, phép downcast cần ép kiểu tường minh và thất bại lúc chạy nếu đối tượng không thuộc kiểu đó:
Đầu ra:
Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball
Viết Dog d = pet; mà không ép kiểu là lỗi biên dịch (CS0266: "An explicit conversion exists (are you missing a cast?)"), vì trình biên dịch chỉ biết pet là một Animal nào đó. Hãy ưu tiên is kèm biến khi đối tượng có thể thuộc kiểu khác, và chỉ ép kiểu trực tiếp khi mọi trường hợp khác đều là bug. Downcast thường xuyên là dấu hiệu thiết kế có vấn đề: hành vi đó thường nên nằm trong một phương thức virtual của lớp cơ sở. Trang pattern matching trình bày đầy đủ các dạng của is.
Kế thừa và collection
Đa hình hoạt động trên từng phần tử, nhưng collection generic của kiểu dẫn xuất không phải là collection của kiểu cơ sở: List<Animal> animals = new List<Dog>(); không biên dịch được, vì khi đó list sẽ nhận cả một Cat. Các dạng xem chỉ đọc có tính hiệp biến, nên IEnumerable<Animal> animals = new List<Dog>(); thì ổn. Trang generics giải thích lý do.
Lỗi thường gặp
- Quên
override. Phương thức vẫn biên dịch kèm cảnh báo và âm thầm che giấu thay vì ghi đè. Hãy coi CS0114 là lỗi. - Đánh dấu mọi thứ là
virtual. Mỗi thành viên virtual là một lời hứa với lớp dẫn xuất về thời điểm nó được gọi và những gì nó được giả định. Chỉ đánh dấu những điểm mở rộng bạn thực sự muốn. - Cây kế thừa quá sâu. Ba hoặc bốn tầng kế thừa khiến khó biết phiên bản nào của một phương thức sẽ chạy. Composition, tức một lớp giữ một đối tượng khác và gọi nó, thường đơn giản hơn thêm một tầng mới.
- Gọi phương thức virtual từ hàm khởi tạo. Phiên bản override chạy trước phần thân hàm khởi tạo của lớp dẫn xuất, nên mọi trường được gán trong phần thân đó vẫn giữ giá trị mặc định.
- Dùng kế thừa chỉ để tái sử dụng code. Nếu
Stackkế thừa từList, nơi gọi có thểInsertvào giữa stack của bạn. Hãy giữ mộtListtrong một trường private thay vào đó.
Câu hỏi thường gặp
Kế thừa hoạt động thế nào trong C#?
Một lớp ghi tên một lớp cơ sở sau dấu hai chấm: class Dog : Animal. Lớp dẫn xuất nhận mọi thành viên của lớp cơ sở (trường, thuộc tính, phương thức, sự kiện), có thể thêm thành viên riêng, và có thể ghi đè những thành viên mà lớp cơ sở đánh dấu virtual. Hàm khởi tạo không được kế thừa, còn các thành viên private, dù có mặt trong đối tượng, thì lớp dẫn xuất không truy cập được.
virtual và override trong C# khác nhau thế nào?
virtual đặt trên phương thức của lớp cơ sở và nói rằng "lớp dẫn xuất có thể thay thế phương thức này". override đặt trên phương thức của lớp dẫn xuất và thực hiện việc thay thế. Cần cả hai: ghi đè một phương thức không phải virtual, abstract hoặc đã override là lỗi CS0506. Khi một phương thức virtual được gọi, runtime chạy phiên bản ứng với kiểu thực tế của đối tượng, không phải kiểu của biến.
new và override trong C# khác nhau thế nào?
override thay thế phương thức cơ sở với mọi nơi gọi, kể cả code giữ đối tượng qua một biến kiểu lớp cơ sở. new chỉ che giấu nó: code nhìn đối tượng theo kiểu dẫn xuất gọi phương thức mới, còn code nhìn nó theo kiểu cơ sở vẫn gọi phương thức cơ sở. Vì vậy Base b = new Derived(); b.M(); chạy Derived.M khi dùng override và Base.M khi dùng new.
C# có hỗ trợ đa kế thừa không?
Không với lớp: một lớp có đúng một lớp cơ sở, và liệt kê hai lớp là lỗi CS1721. Một lớp có thể triển khai bao nhiêu interface cũng được, đó là cách C# mô hình hóa "kiểu này làm được nhiều việc". Từ C# 8, interface còn có thể chứa phần triển khai mặc định cho phương thức.
sealed nghĩa là gì trong C#?
Một lớp sealed không thể làm lớp cơ sở; kế thừa từ nó là lỗi CS0509. Ví dụ, string là lớp sealed. Trên một phương thức, sealed override ngăn các lớp ở tầng dưới trong cây kế thừa ghi đè lại lần nữa (CS0239). Sealed các lớp không được thiết kế để kế thừa là một lựa chọn mặc định hợp lý.
Làm sao gọi phương thức của lớp cơ sở trong C#?
Dùng base.MethodName(...) bên trong lớp dẫn xuất, thường là bên trong phương thức override: public override string Describe() => base.Describe() + " with GPS";. Với hàm khởi tạo, dùng : base(...) sau danh sách tham số để chọn hàm khởi tạo nào của lớp cơ sở sẽ chạy.