Chuyển tới nội dung chính

Generics

Định nghĩa

Generics cho phép bạn định nghĩa các lớp, giao diện, phương thức và ủy quyền có tham số kiểu (Type Parameters). Bằng cách giới thiệu các tham số kiểu (ví dụ <T>), bạn viết mã hoạt động với bất kỳ kiểu dữ liệu nào trong khi vẫn giữ an toàn kiểu tại thời điểm biên dịch (Compile-Time Type Safety) và tránh chi phí boxing/unboxing.

Tại sao Generics tồn tại

Vấn đề không có GenericsGiải pháp với Generics
ArrayList lưu trữ object — boxing kiểu giá trị gây chi phíList<T> lưu trữ giá trị trực tiếp — không boxing
Ép kiểu từ object có thể thất bại tại thời điểm chạyLỗi kiểu được phát hiện tại thời điểm biên dịch
Mã trùng lặp cho mỗi kiểu dữ liệuMột triển khai cho mọi kiểu
// Without generics — runtime risk, boxing overhead
ArrayList list = new ArrayList();
list.Add(42); // boxes int to object
int value = (int)list[0]; // unboxes, runtime cast

// With generics — compile-time safety, no boxing
List<int> list = new List<int>();
list.Add(42); // no boxing
int value = list[0]; // no cast needed
Lợi ích chính
  • An toàn kiểu (Type Safety) tại thời điểm biên dịch
  • Tái sử dụng mã (Code Reuse) mà không trùng lặp
  • Hiệu suất tốt hơn — không boxing cho kiểu giá trị
  • Mã tự tài liệu thông qua các tham số kiểu rõ ràng

Khái niệm cốt lõi

Lớp Generic (Generic Classes)

Các lớp generic sử dụng một hoặc nhiều tham số kiểu trong định nghĩa của chúng. Thư viện .NET Base Class Library đi kèm nhiều bộ sưu tập generic.

// Built-in generic classes
List<string> names = new List<string> { "Alice", "Bob" };
Dictionary<string, int> ages = new Dictionary<string, int>
{
["Alice"] = 30,
["Bob"] = 25
};

// Custom generic class
public class Repository<T>
{
private readonly List<T> _items = new();

public void Add(T item) => _items.Add(item);

public T? FindById(Func<T, bool> predicate) =>
_items.FirstOrDefault(predicate);

public IReadOnlyList<T> GetAll() => _items.AsReadOnly();
}

Phương thức Generic (Generic Methods)

Các phương thức có thể giới thiệu tham số kiểu riêng độc lập với lớp chứa. Trình biên dịch thường suy luận các đối số kiểu từ cách sử dụng.

public class Utilities
{
// Generic method with type inference
public T GetDefault<T>() => default(T);

// Explicit type arguments when inference is not possible
public T ConvertTo<T>(object value) => (T)value;

// Generic method with constraint
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
}

// Usage
Utilities util = new Utilities();
int max = util.Max(10, 20); // type inferred as int
string def = util.GetDefault<string>(); // explicit — returns null

Giao diện Generic (Generic Interfaces)

// Built-in
public interface IComparable<T>
{
int CompareTo(T? other);
}

// Custom generic interface
public interface IRepository<T> where T : class
{
T GetById(int id);
void Add(T entity);
void Delete(T entity);
IEnumerable<T> GetAll();
}

public class InMemoryRepository<T> : IRepository<T> where T : class
{
private readonly List<T> _store = new();
public T GetById(int id) => throw new NotImplementedException();
public void Add(T entity) => _store.Add(entity);
public void Delete(T entity) => _store.Remove(entity);
public IEnumerable<T> GetAll() => _store;
}

Ủy quyền Generic (Generic Delegates)

// Built-in generic delegates
Func<int, int, int> add = (a, b) => a + b; // returns int
Action<string> log = msg => Console.WriteLine(msg); // returns void
Predicate<int> isEven = n => n % 2 == 0; // returns bool

// Custom generic delegate
public delegate TResult Transformer<TInput, TResult>(TInput input);

Ràng buộc (Constraints)

Ràng buộc giới hạn các kiểu có thể được sử dụng làm đối số kiểu, cho phép trình biên dịch biết những thao tác nào có sẵn.

Ràng buộcMô tả
where T : structT phải là kiểu giá trị (Value Type)
where T : classT phải là kiểu tham chiếu (Reference Type)
where T : new()T phải có hàm tạo không tham số
where T : BaseClassT phải kế thừa từ BaseClass
where T : IInterfaceT phải triển khai IInterface
where T : unmanagedT phải là kiểu không quản lý (không có trường tham chiếu)
where T : notnullT phải là kiểu không nullable
where T : EnumT phải là enum
// Multiple constraints
public class Factory<T> where T : class, new()
{
public T Create() => new T();
}

// Constraint on multiple type parameters
public class Pair<TFirst, TSecond>
where TFirst : struct
where TSecond : class
{
public TFirst First { get; }
public TSecond Second { get; }
}

Biểu thức giá trị mặc định (Default Value Expression)

T value = default(T); // null for reference types, zero-initialized for value types

// In generic context
public T? FindOrDefault<T>(IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
if (predicate(item)) return item;
return default; // default(T) — shorthand
}

Đồng biến và Nghịch biến (Covariance and Contravariance)

Các chú thích phương sai (Variance Annotations) cho phép tham số kiểu generic được sử dụng đa hình.

// Covariance (out) — type can only appear as output
IEnumerable<Derived> derived = new List<Derived>();
IEnumerable<Base> bases = derived; // valid because IEnumerable<out T>

// Contravariance (in) — type can only appear as input
IComparer<Base> baseComparer = new BaseComparer();
IComparer<Derived> derivedComparer = baseComparer; // valid because IComparer<in T>

// Custom covariant interface
public interface IProducer<out T>
{
T Produce();
}

// Custom contravariant interface
public interface IConsumer<in T>
{
void Consume(T item);
}
Giới hạn của phương sai (Variance)
  • Phương sai chỉ áp dụng cho kiểu giao diệnủy quyền, không phải lớp hoặc struct.
  • Tham số out chỉ dùng cho đầu ra; tham số in chỉ dùng cho đầu vào.
  • Kiểu giá trị không hỗ trợ phương sai — IEnumerable<int> không thể gán cho IEnumerable<object>.

Suy luận kiểu Generic (Generic Type Inference)

Trình biên dịch có thể suy luận đối số kiểu từ các đối số phương thức, làm cho các lệnh gọi gọn gàng hơn.

// Compiler infers T from arguments
var result = Max(3, 5); // T inferred as int
var names = new[] { "a", "b" };
var first = names.FirstOrDefault(); // T inferred as string

Khi nào sử dụng

  • Bộ sưu tập và cấu trúc dữ liệu — bất kỳ khi nào bạn cần một container hoạt động với nhiều kiểu.
  • Repository và service — mẫu IRepository<T> cho tầng truy cập dữ liệu.
  • Phương thức tiện ích — hoán đổi (swap), max, min, chuyển đổi (convert) và các thao tác không phụ thuộc kiểu.
  • Xử lý sự kiện và callbackEventHandler<TEventArgs>, Func<T>, Action<T>.
  • Tránh khi chỉ có một hoặc hai kiểu cụ thể và không có lợi ích tái sử dụng — sự đơn giản thắng.

Lỗi thường gặp

Ràng buộc quá mức (Over-Constraining)

// Bad — too restrictive, limits reusability
public class Cache<T> where T : class, IEntity, IComparable, new()

// Better — only constrain what you actually need
public class Cache<T> where T : class

Ép kiểu Generic (Generic Type Casting)

// This does NOT compile — cannot cast to type parameter
T Convert(object value) => (T)value;

// Correct approach
T Convert<T>(object value) => (T)(dynamic)value;
// Or use Convert.ChangeType
T Convert<T>(object value) => (T)System.Convert.ChangeType(value, typeof(T));

Reflection với Generics

// Getting the open generic type
Type openType = typeof(List<>);
Type closedType = openType.MakeGenericType(typeof(int));

// Checking generic type at runtime
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
Type elementType = type.GetGenericArguments()[0];
}

Giới hạn của ràng buộc new()

// new() only works with public parameterless constructors
public T Create<T>() where T : new() => new T();

// Cannot use new() with types that have required parameters or internal constructors
Ràng buộc new()

Ràng buộc new() yêu cầu hàm tạo không tham số công khai. Nó không thể được thỏa mãn bởi các kiểu chỉ có hàm tạo internal/private hoặc các kiểu có tham số hàm tạo bắt buộc (Required Constructor Parameters).

Điểm chính cần nhớ

  • Generics cung cấp an toàn kiểu, tái sử dụng mã và hiệu suất bằng cách loại bỏ boxing và ép kiểu tại thời điểm chạy.
  • Sử dụng ràng buộc (Constraints) để giới hạn tham số kiểu và truy cập các thành viên cụ thể.
  • Đồng biến (Covariance) (out) và nghịch biến (Contravariance) (in) cho phép sử dụng đa hình của giao diện generic.
  • Trình biên dịch thường có thể suy luận đối số kiểu — ưu tiên để trình biên dịch làm điều đó cho mã gọn gàng hơn.
  • Tránh ràng buộc quá mức; chỉ áp dụng các ràng buộc mà triển khai của bạn thực sự cần.

Câu hỏi phỏng vấn

H: Generics là gì và tại sao sử dụng chúng? Generics cho phép bạn định nghĩa các kiểu và phương thức có tham số kiểu. Chúng cung cấp an toàn kiểu tại thời điểm biên dịch, loại bỏ boxing/unboxing cho kiểu giá trị, và cho phép mã có thể tái sử dụng mà không trùng lặp.

H: Ràng buộc generic (Generic Constraints) là gì? Ràng buộc (where T : ...) giới hạn các kiểu có thể được sử dụng làm đối số, cho phép trình biên dịch xác minh rằng các thành viên cụ thể (phương thức, hàm tạo, thuộc tính) có sẵn trên T.

H: Đồng biến (Covariance) và nghịch biến (Contravariance) là gì? Đồng biến (out) cho phép giao diện generic chấp nhận các kiểu phái sinh hơn (Derived Types) làm đầu ra. Nghịch biến (in) cho phép chấp nhận các kiểu cơ sở hơn (Base Types) làm đầu vào. Cả hai đều cho phép gán đa hình các kiểu generic.

H: Sự khác biệt giữa List<T>ArrayList là gì? ArrayList lưu trữ object, gây boxing cho kiểu giá trị và yêu cầu ép kiểu tại thời điểm chạy. List<T> là generic — nó lưu trữ giá trị trực tiếp mà không boxing và cung cấp an toàn kiểu tại thời điểm biên dịch.

H: Bạn có thể kế thừa từ kiểu generic không? Có. Bạn có thể kế thừa từ kiểu generic đóng (Closed Generic Type) (class MyList : List<int>), hoặc giữ tham số mở (class MyList<T> : List<T>). Bạn cũng có thể ràng buộc tham số kiểu trong lớp kế thừa.

Tài liệu tham khảo