C#: Hiểu Sâu Về Tính Tổng Quát (Generics) Để Tối Ưu Hóa Mã Nguồn

Giới thiệu về Generics trong C#

Tính tổng quát (Generics) là một thành phần cốt lõi của .NET Framework và .NET Core, đóng vai trò cung cấp cơ chế để thiết kế các cấu trúc dữ liệu và phương thức linh hoạt mà không bị ràng buộc bởi kiểu dữ liệu cụ thể tại thời điểm khai báo. Thay vì phải viết lại logic xử lý cho từng kiểu như int, string, hay DateTime, chúng ta sử dụng một placeholder duy nhất để biểu diễn kiểu. Điều này không chỉ giảm thiểu độ dài mã nguồn (DRY principle) mà còn nâng cao hiệu năng nhờ việc loại bỏ quá trình đóng gói (boxing) và giải đóng gói (unboxing) khi làm việc với giá trị (value types).

Mục đích chính của Generics là đảm bảo tính an toàn kiểu dữ liệu (Type Safety) ngay từ lúc biên dịch, đồng thời tối ưu hóa trải nghiệm lập trình bằng cách cho phép tham chiếu đối tượng trừu tượng hơn.


Cấu Trúc Thực Hiện

1. Lớp Tổng Quát (Generic Class)

Lớp tổng quát cho phép định nghĩa một lớp với một hoặc nhiều biến kiểu dữ liệu. Khi khai báo, hệ thống biết trước rằng lớp này cần một loại dữ liệu cụ thể nhưng không biết đó là loại gì, cho đến khi nó được khởi tạo.

// Khai báo lớp chứa dữ liệu theo kiểu T bất kỳ
public class TypedContainer<T>
{
    // Thuộc tính lưu trữ giá trị chung
    private T _data;

    public void SetData(T value)
    {
        _data = value;
    }

    public T GetData()
    {
        return _data;
    }
}

Khi sử dụng, ta truyền vào kiểu dữ liệu mong muốn:

static void Main(string[] args)
{
    // Khởi tạo với kiểu Integer
    var containerInt = new TypedContainer<int>();
    containerInt.SetData(100);
    
    // Khởi tạo với kiểu String
    var containerStr = new TypedContainer<string>();
    containerStr.SetData("Dữ liệu văn bản");
    
    Console.WriteLine(containerInt.GetData());
    Console.WriteLine(containerStr.GetData());
}

Về mặt kế thừa, một lớp tổng quát có thể mở rộng một lớp tổng quát khác hoặc lớp thường, nhưng phải tuân thủ quy tắc về số lượng tham số kiểu:

// Lớp con kế thừa lớp cha với cùng tham số kiểu T
public class UserAccount<T> : TypedContainer<T>
{
}

// Lớp con đóng (Closed Type) tham số kiểu là cụ thể
public class UserProfile : TypedContainer<string>
{
}

2. Phương Thức Tổng Quát (Generic Method)

Không chỉ lớp, phương thức cũng có thể tổng quát. Điều này hữu ích khi bạn muốn xây dựng các hàm tiện ích hoạt động trên nhiều kiểu dữ liệu mà không cần ghi đè nhiều lần.

public class ActionExecutor
{
    // Phương thức riêng biệt cho Int (Lúc cũ, giờ dùng Generic thay thế)
    public void ExecuteInteger(int iValue)
    {
        Console.WriteLine($"Số nguyên: {iValue}");
    }

    // Phương thức riêng biệt cho String
    public void ExecuteText(string sValue)
    {
        Console.WriteLine($"Chuỗi: {sValue}");
    }

    // Phương thức tổng quát - Cách hiện đại
    public void ExecuteGeneric<T>(T genericValue)
    {
        Console.WriteLine($"Kiểu nhập vào: {typeof(T)}, Giá trị: {genericValue}");
    }
}

Trình biên dịch sẽ tự động suy luận kiểu dữ liệu (Type Inference) dựa trên đối số được truyền vào khi gọi hàm:

ActionExecutor exe = new ActionExecutor();

// Gọi phương thức thông thường
exe.ExecuteInteger(50);
exe.ExecuteText("Văn bản mẫu");

// Gọi phương thức tổng quát
exe.ExecuteGeneric<double>(3.14);
exe.ExecuteGeneric<bool>(true);

3. Giao Diện Tổng Quát (Generic Interface)

Việc định nghĩa giao diện sử dụng tham số kiểu giúp chuẩn hóa các hợp đồng API giữa các thư viện. Ví dụ phổ biến nhất là IEnumerable<T> trong thư viện System.Collections.Generic.

// Định nghĩa giao diện tùy chỉnh
public interface ITypedHandler<T>
{
    void Process(T item);
    T Fetch();
}

// Lớp thực hiện giao diện với kiểu cố định
public class Logger : ITypedHandler<string>
{
    public void Process(string item) { /* Xử lý log */ }
    public string Fetch() { return string.Empty; }
}

// Lớp thực hiện giao diện với kiểu động
public class Buffer<T> : ITypedHandler<T>
{
    public void Process(T item) { /* Lưu trữ bộ đệm */ }
    public T Fetch() { throw new NotImplementedException(); }
}

4. Delegate Tổng Quát (Generic Delegate)

Delegate cho phép định nghĩa hành vi (logic) đi kèm với kiểu dữ liệu đầu vào hoặc trả về cụ thể nhưng linh hoạt.

// Định nghĩa delegate nhận tham số và trả về void
public delegate void CommandHandler<T>(T argument);

public class Runner
{
    // Biến instance của delegate
    private CommandHandler<string> _logger;

    public void Initialize()
    {
        // Gán hàm Sing cho handler
        _logger = Sing;
        // Trỏ sang kiểu chuỗi
        _logger?.Invoke("Chạy lệnh với chuỗi");
    }

    // Phương thức thực thi logic tương ứng
    public void Sing<T>(T t) 
    {
        // Logic xử lý
    }
}

5. Ràng Buộc Kiểu Dữ Liệu (Constraints)

Khi sử dụng Generics, đôi khi chúng ta cần giới hạn kiểu T phải thỏa mãn điều kiện nào đó (như bắt buộc có constructor không tham số, hoặc phải kế thừa từ một class cụ thể). Cụm từ khóa where được dùng để khai báo điều này.

// Chỉ cho phép T là Struct (Giá trị)
public class SafeRepository<T> where T : struct
{
}

// Chỉ cho phép T là Class (Tham chiếu) hoặc null
public class DynamicStore<T> where T : class
{
}

// Kết hợp nhiều ràng buộc
public class MultiConstraint<T, K>
    where T : struct
    where K : class, new() // Bắt buộc T có ctor không args
{
}

Nếu truyền kiểu dữ liệu không thỏa mãn ràng buộc, chương trình sẽ lỗi ngay tại bước biên dịch.


Mở Rộng và Ứng Dụng Nâng Cao

1. Biến Đổi Kiểu (Covariance & Contravariance)

Hệ thống Generics hỗ trợ hai khái niệm quan trọng giúp tối ưu việc gán kiểu dữ liệu con/trưởng một cách an toàn mà không cần ép kiểu tường minh.

  • Contravariance (Biến nghịch - in): Cho phép truyền tham số kiểu cha vào vị trí cần kiểu con hơn. Thường dùng cho đầu vào (argument).
  • Covariance (Biến thuận - out): Cho phép trả về kiểu con khi định nghĩa là kiểu cha. Thường dùng cho đầu ra (return).

Chúng chỉ áp dụng được cho các loại tham chiếu (Reference Types), không áp dụng cho Value Types.

// Minh họa Contravariance với giao diện (Giao diện chấp nhận loại đối tượng rộng hơn)
interface IConsumer<in T>
{
    void Consume(T input);
}

public class Animal { }
public class Dog : Animal { }

// Implement logic chấp nhận Animal, nhưng có thể coi là Dog Consumer
public class AnimalHandler : IConsumer<Animal>
{
    public void Consume(Animal input)
    {
        Console.WriteLine("Tiêu thụ sinh vật");
    }
}

// Sử dụng
static void TestContravariance()
{
    IConsumer<Dog> dogConsumer = new AnimalHandler();
    // Hợp lệ vì IConsumer<Animal> có thể gán cho IConsumer<Dog>
    dogConsumer.Consume(new Dog());
}
// Minh họa Covariance với giao diện (Giao diện trả về loại đối tượng hẹp hơn)
interface IProducer<out T>
{
    T Produce();
}

public class Product
{
    public string GetName() => "";
}

public class SuperProduct : Product
{
    public string GetName() => "Super";
}

public class Factory : IProducer<SuperProduct>
{
    public SuperProduct Produce() => new SuperProduct();
}

// Sử dụng
static void TestCovariance()
{
    IProducer<Product> productProducer = new Factory();
    // Hợp lệ vì SuperProduct là con của Product
    productProducer.Produce(); 
}

2. Ứng Dụng Thực Tế: Bộ Nhớ đệm Tĩnh (Static Cache)

Một ứng dụng thực tiễn của Generics là tạo ra các bộ nhớ đệm (cache) chuyên biệt cho từng kiểu dữ liệu mà không cần sao chép code. Vì mỗi kiểu dữ liệu khác nhau sẽ tạo ra một phiên bản riêng của lớp tổng quát, ta có thể tận dụng đặc tính này.

// Class quản lý dữ liệu tĩnh cho từng kiểu T riêng biệt
public class MemoizationTable<T> where T : class
{
    // Danh sách lưu trữ cho kiểu T cụ thể này
    private static readonly Dictionary<object, T> _storage = new Dictionary<object, T>();

    public static void Store(object key, T value)
    {
        if (!_storage.ContainsKey(key))
        {
            _storage[key] = value;
        }
    }

    public static T Retrieve(object key)
    {
        _storage.TryGetValue(key, out T val);
        return val;
    }
    
    public static bool ClearAll()
    {
        _storage.Clear();
        return true;
    }
}

Ví dụ về Model sử dụng:

public class ItemModel
{
    public int Id { get; set; }
    public string Label { get; set; }
}

Sử dụng bộ đệm:

static void Main(string[] args)
{
    // Lưu chuỗi vào cache cho kiểu string
    MemoizationTable<string>.Store("key1", "Nội dung A");
    
    // Lưu Model vào cache cho kiểu ItemModel
    MemoizationTable<ItemModel>.Store("key2", new ItemModel { Label = "Sản phẩm B" });

    // Truy xuất
    var strVal = MemoizationTable<string>.Retrieve("key1");
    var modelVal = MemoizationTable<ItemModel>.Retrieve("key2");

    Console.WriteLine(strVal);
    Console.WriteLine(modelVal.Label);
}

Tài Nguyên Tham Khảo

Thẻ: csharp Generics oop type-constraints variance

Đăng vào ngày 25 tháng 8 lúc 05:43