Trong lập trình C#, Generics là một tính năng mạnh mẽ cho phép bạn tạo ra các lớp, phương thức, giao diện và đại biểu có thể hoạt động với nhiều kiểu dữ liệu khác nhau mà không cần viết lại mã cho từng kiểu cụ thể. Điều này giúp tăng tính tái sử dụng, an toàn kiểu và hiệu suất của mã nguồn.
Contents
1. Tổng quan về Generics
Generics cho phép trì hoãn việc xác định kiểu dữ liệu cụ thể cho đến khi sử dụng, giúp bạn viết mã tổng quát hơn. Thay vì xác định kiểu dữ liệu cụ thể trong định nghĩa lớp hoặc phương thức, bạn sử dụng các tham số kiểu (thường được ký hiệu bằng T
, U
, V
,…) để đại diện cho các kiểu dữ liệu sẽ được cung cấp khi sử dụng.
Ví dụ bạn cần chuyển đổi giá trị lưu trong hai biến kiểu int
, thì bạn có thể xây dựng phương thức theo cách thông thường như sau:
// phương thức này tráo đổi giá trị giữa hai biến kiểu int
public static void Swap(ref int a, ref int b) {
int c = a;
a = b;
b = c;
}
Tuy nhiên, nếu cần tráo đổi giá trị giữa hai biến kiểu string
thì phương thức trên không dùng được, bạn lại cần xây dựng một phương thức khác dành cho tham số kiểu string
// phương thức này tráo đổi giá trị giữa hai biến kiểu int
public static void Swap(ref string a, ref string b) {
string c = a;
a = b;
b = c;
}
Bạn nhận thấy, giải thuật giải quyết bài toán giống nhau nhưng tham số khác nhau dẫn đến bạn phải viết hai đoạn code. Với những trường hợp như trên, logic giống nhau trên những kiểu dữ liệu khác nhau thì sẽ dùng đến Generic, thay vì viết code trên kiểu dữ liệu cụ thể thì code trên những kiểu chung chung.
2. Phương thức Generic
Phương thức Generic cho phép bạn định nghĩa các hàm có thể hoạt động với nhiều kiểu dữ liệu khác nhau mà không cần viết lại mã cho từng kiểu cụ thể.
Ví dụ về phương thức Generic:
public void Swap<T>(ref T a, ref T b)
{
T temp = a; // Lưu giá trị của 'a' vào biến tạm 'temp'.
a = b; // Gán giá trị của 'b' vào 'a'.
b = temp; // Gán giá trị của 'temp' (giá trị ban đầu của 'a') vào 'b'.
}
Giải thích:
<T>
: Đây là tham số kiểu Generic.T
có thể là bất kỳ kiểu dữ liệu nào (int, string, object,…).ref
: Từ khóaref
cho phép thay đổi giá trị của biến được truyền vào.- Hoán đổi giá trị: Phương thức này không quan tâm
a
vàb
thuộc kiểu nào, miễn là chúng cùng kiểu.
Sử dụng phương thức Generic:
// Sử dụng với kiểu int
int x = 5, y = 10;
Console.WriteLine($"Before Swap: x = {x}, y = {y}");
Swap<int>(ref x, ref y); // Gọi phương thức với kiểu int
Console.WriteLine($"After Swap: x = {x}, y = {y}");
// Sử dụng với kiểu string
string firstName = "Alice", lastName = "Smith";
Console.WriteLine($"Before Swap: firstName = {firstName}, lastName = {lastName}");
Swap<string>(ref firstName, ref lastName); // Gọi phương thức với kiểu string
Console.WriteLine($"After Swap: firstName = {firstName}, lastName = {lastName}");
3. Lớp Generic
Lớp Generic cho phép bạn định nghĩa các lớp có thể làm việc với bất kỳ kiểu dữ liệu nào, giúp tạo ra các cấu trúc dữ liệu và thuật toán tổng quát.
Ví dụ về lớp Generic:
public class GenericList<T>
{
private T[] elements;
private int count;
public GenericList(int capacity)
{
elements = new T[capacity];
count = 0;
}
public void Add(T item)
{
if (count < elements.Length)
{
elements[count] = item;
count++;
}
else
{
throw new InvalidOperationException("List is full");
}
}
public T Get(int index)
{
if (index >= 0 && index < count)
{
return elements[index];
}
else
{
throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
Giải thích:
T
: Biến tham số kiểu, đại diện cho kiểu dữ liệu sẽ được cung cấp khi sử dụng lớp.- Khởi tạo danh sách: Sử dụng mảng để lưu các phần tử, kích thước mảng được xác định khi khởi tạo.
- Phương thức
Add
: Thêm một phần tử vào danh sách. - Phương thức
Get
: Lấy phần tử tại chỉ mục
Lớp GenericList
trong ví dụ trên có thể lưu trữ các phần tử của bất kỳ kiểu dữ liệu nào, được xác định khi tạo instance của lớp.
Sử dụng lớp Generic:
var intList = new GenericList<int>(10);
intList.Add(1);
intList.Add(2);
Console.WriteLine(intList.Get(0)); // Output: 1
var stringList = new GenericList<string>(10);
stringList.Add("Hello");
stringList.Add("World");
Console.WriteLine(stringList.Get(1)); // Output: World
4. Giới hạn kiểu trong Generics
Bạn có thể đặt ra các giới hạn cho tham số kiểu bằng cách sử dụng từ khóa where
, giúp đảm bảo rằng kiểu dữ liệu được sử dụng đáp ứng các yêu cầu nhất định.
Ví dụ về giới hạn kiểu:
public class Repository<T> where T : class
{
public void Save(T entity)
{
Console.WriteLine($"{entity} saved.");
}
}
Giải thích:
where T : class
: Giới hạnT
phải là kiểu tham chiếu (class).- Phương thức
Save
chỉ làm việc với các kiểu dữ liệu là lớp.
Sử dụng:
var repo = new Repository<string>();
repo.Save("Data"); // Output: Data saved.
5. Lợi ích của việc sử dụng Generics
- Tăng tính tái sử dụng mã nguồn: Generics cho phép bạn viết mã tổng quát, có thể áp dụng cho nhiều kiểu dữ liệu khác nhau mà không cần lặp lại mã.
- An toàn kiểu dữ liệu: Generics giúp phát hiện lỗi kiểu dữ liệu tại thời điểm biên dịch, giảm thiểu lỗi runtime.
- Hiệu suất cao: Sử dụng Generics tránh được việc ép kiểu và boxing/unboxing không cần thiết, cải thiện hiệu suất của ứng dụng.
6. Các cấu trúc dữ liệu Generic trong .NET
.NET Framework cung cấp nhiều cấu trúc dữ liệu Generic trong không gian tên System.Collections.Generic
, như:
- List<T>: Danh sách các phần tử có thể thay đổi kích thước.
- Dictionary<TKey, TValue>: Tập hợp các cặp khóa-giá trị.
- Queue<T>: Hàng đợi theo cơ chế FIFO (First-In-First-Out).
- Stack<T>: Ngăn xếp theo cơ chế LIFO (Last-In-First-Out).
Sử dụng các cấu trúc dữ liệu Generic này giúp bạn quản lý và thao tác với dữ liệu một cách hiệu quả và an toàn.
7. Kết luận
Generics là một tính năng quan trọng trong C#, giúp bạn viết mã tổng quát, tái sử dụng và an toàn kiểu. Việc hiểu và áp dụng đúng Generics sẽ nâng cao chất lượng và hiệu quả của mã nguồn trong các ứng dụng C#.