Covariance, Contravariance và Invariance là gì?
Trong lập trình hướng đối tượng và kiểu dữ liệu, ba khái niệm này định nghĩa cách các kiểu dữ liệu liên quan đến nhau (ví dụ, lớp cha - lớp con) có thể được thay thế cho nhau trong các ngữ cảnh khác nhau, như mảng, kế thừa, và đặc biệt là generic.
- Covariance (Hiệp biến): Cho phép bạn sử dụng một kiểu con (subtype) ở nơi mà kiểu cha (supertype) được mong đợi. Ví dụ, nếu bạn có một hàm nhận đầu vào là
Shape, bạn có thể truyền vào một đối tượngCircle(nếuCirclelà con củaShape). - Contravariance (Phản biến): Cho phép bạn sử dụng một kiểu cha ở nơi mà kiểu con được mong đợi. Ví dụ, nếu bạn có một hàm cần một
Comparison<Circle>, bạn có thể dùngComparison<Shape>. - Invariance (Bất biến): Không cho phép sự thay thế nào ở trên. Kiểu phải khớp chính xác.
Covariance và Contravariance với Generics (C#, Java)
Trong C#, các tham số generic mặc định là bất biến. Tuy nhiên, bạn có thể đánh dấu chúng là hiệp biến (out) hoặc phản biến (in) khi khai báo interface hoặc delegate.
- Covariance (out): Bạn có thể dùng
IEnumerable<string>ở nơi cầnIEnumerable<object>. Tham số generic được đánh dấuoutvì nó thường ở vị trí "đầu ra" (output) – chỉ đọc (read-only). - Contravariance (in): Bạn có thể dùng
IComparable<object>ở nơi cầnIComparable<string>. Tham số generic được đánh dấuinvì nó thường ở vị trí "đầu vào" (input) – chỉ ghi (write).
// Ví dụ Covariance trong C#
public interface IEnumerator<out T>
{
T Current { get; } // Vị trí đầu ra (output)
bool MoveNext();
}
IEnumerator<string> strEnum = new Enumerator<string>();
IEnumerator<object> objEnum = strEnum; // OK: Covariance
// Ví dụ Contravariance trong C#
public interface IComparer<in T>
{
int Compare(T a, T b); // Vị trí đầu vào (input)
}
IComparer<object> objComp = new Comparer<object>();
IComparer<string> strComp = objComp; // OK: Contravariance
Trong Java, tham số generic cũng bất biến. Bạn có thể sử dụng ký tự đại diện (wildcard) với extends và super để đạt được hiệu ứng tương tự tại điểm sử dụng.
List<? extends T>là covariant (dùng cho Producer – chỉ đọc).Tlà giới hạn trên.List<? super T>là contravariant (dùng cho Consumer – chỉ ghi).Tlà giới hạn dưới.
Tính An Toàn Kiểu Dữ Liệu
- Covariance an toàn: Khi bạn đọc dữ liệu từ một vị trí
out, bạn đảm bảo lấy ra được kiểu con hoặc kiểu cha. Việc thay thế kiểu con bằng kiểu cha là an toàn vì kiểu con chắc chắn có thể được xem như kiểu cha. - Contravariance an toàn: Khi bạn ghi dữ liệu (thêm vào) một vị trí
in, bạn đảm bảo kiểu dữ liệu bạn đưa vào có thể được xử lý. Việc thay thế kiểu cha bằng kiểu con là an toàn vì kiểu cha có thể nhận kiểu con.
// Ví dụ áp dụng cả hai trong Java
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // src là Producer (read), dest là Consumer (write)
}
}
}
Covariance với Mảng (C#, Java)
Trong C# và Java, mảng là covariant. Điều này có nghĩa là string[] là một kiểu con của object[].
- Ví dụ:
string[]có thể được gán cho biếnobject[]. - Không an toàn: Tính năng này có thể gây ra lỗi runtime (ví dụ: gán một
Integervào một mảngString[]). Lý do lịch sử là Java và C# không có generics từ đầu, và covariance giúp tận dụng được tính đa hình (polymorphism) với mảng.
Covariance, Contravariance và Kế Thừa (C++, Java)
Một số ngôn ngữ như C++ và Java hỗ trợ covariant return type. Điều này cho phép một phương thức ở lớp con ghi đè (override) phương thức ở lớp cha với kiểu trả về là kiểu con của kiểu trả về ban đầu. C# không hỗ trợ tính năng này.
class VehicleFactory {
public:
virtual Vehicle* create() const { return new Vehicle(); }
virtual ~VehicleFactory() {}
};
class CarFactory : public VehicleFactory {
public:
// Covariant return type: Car* là kiểu con của Vehicle*
virtual Car* create() const override { return new Car(); }
};
Ngược lại, hầu hết các ngôn ngữ chính thống (C++, C#, Java) không hỗ trợ contravariant hoặc covariant argument types trong kế thừa để đảm bảo an toàn kiểu.
Mối Quan Hệ và Quy Tắc Tổng Quát
Nếu S là kiểu con của T (ký hiệu S <: T):
- Covariant trong ngữ cảnh
F: Quan hệ con-cha được giữ nguyên.F(S) <: F(T) - Contravariant trong ngữ cảnh
F: Quan hệ bị đảo ngược.F(T) <: F(S) - Invariant: Không có quan hệ nào.
Một cách dễ nhớ cho kiểu hàm (A -> B):
- Tham số (Input) là Contravariant: Nếu bạn có hàm nhận
Shape, bạn có thể dùng nó ở nơi cần hàm nhậnCircle. - Giá trị trả về (Output) là Covariant: Nếu bạn có hàm trả về
Circle, bạn có thể dùng nó ở nơi cần hàm trả vềShape.
Covariance, Contravariance trong C++
Trong C++, con trỏ và tham chiếu tự nhiên là covariant (con trỏ lớp con có thể được dùng thay cho con trỏ lớp cha).
Đối với templates (khuôn mẫu):
- Bất biến theo mặc định.
std::shared_ptrvàstd::unique_ptrhỗ trợ covariance (giống con trỏ thô).std::functionhỗ trợ cả covariance (cho kiểu trả về) và contravariance (cho kiểu tham số).
#include <memory>
#include <functional>
struct Base {};
struct Derived : Base {};
int main() {
std::shared_ptr<Derived> p;
std::shared_ptr<Base> p2 = p; // Covariance của shared_ptr
std::function<Derived*(Base*)> f;
std::function<Base*(Derived*)> f2 = f; // Covariance (trả về), Contravariance (tham số)
}