1. Một chút lịch sử
Ngày xưa ở C#, làm việc với collection chỉ có hai niềm vui: dùng mảng hoặc mấy collection kiểu "không chặt chẽ về kiểu" — như ArrayList, nơi bạn có thể nhét bất cứ object nào vào. Nghe thì tự do đấy! Nhưng thử cho vào đó vừa string vừa số nguyên — là bắt đầu loạn: không thể lấy phần tử ra một cách an toàn nếu không kiểm tra kiểu (và kiểm tra cũng chưa chắc ăn). Thêm nữa — lỗi ngớ ngẩn xuất hiện bất ngờ và phải nhảy múa với việc ép kiểu (type casting).
Ví dụ về collection không-generic (ArrayList):
using System.Collections;
ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("Hello C#");
int number = (int)stuff[0]; // OK
string text = (string)stuff[1]; // OK
int fail = (int)stuff[1]; // BÙM! InvalidCastException (runtime error)
Đúng là compiler không báo lỗi — lỗi chỉ xuất hiện khi chạy chương trình! Kiểu như mở tủ lạnh mà thấy nồi đang nóng — bất ngờ chưa!
Bản chất của generic là gì?
Generic collections xuất hiện để bạn có thể tạo collection mà kiểu dữ liệu bên trong được đảm bảo. Điều này mang lại ba lợi ích chính:
- An toàn kiểu dữ liệu ngay khi compile. Compiler sẽ không cho bạn nhét nhầm thứ gì vào collection.
- Tiện lợi: không cần phải ép kiểu thủ công mỗi lần lấy ra.
- Hiệu năng: không tốn thời gian cho "boxing/unboxing" các kiểu giá trị (boxing/unboxing).
2. Generics (obobsheniya) là gì?
Generics — là cách thần kỳ để mô tả cấu trúc dữ liệu và method sao cho chúng hoạt động với bất kỳ kiểu nào, nhưng vẫn đảm bảo chặt chẽ về kiểu.
Hãy tưởng tượng một cái hộp đa năng, có thể dùng cho sách hoặc tất, nhưng chỉ cho một loại tại một thời điểm. Nếu hộp khai báo là "chỉ cho sách" — không ai nhét được tất vào. Collection generic cũng vậy: nếu bạn tạo collection kiểu int, không thể nhét nhầm string vào.
Ví dụ khai báo class generic
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Hello Generics!" };
T — là "tham số kiểu", chỉ định cái hộp này sẽ chứa gì. C# sẽ bắt bạn luôn phải chỉ rõ kiểu khi dùng.
Generic collections trong .NET
Trong .NET Framework, hầu hết các collection hiện đại đều có phiên bản generic. Ví dụ:
- List<T> — danh sách động các phần tử kiểu T.
- Dictionary<TKey, TValue> — mảng kết hợp (dictionary) với key và value.
- Queue<T>, Stack<T> — queue và stack.
- và nhiều loại khác nữa.
3. Cách hoạt động: cấu trúc của Generics
Tham số kiểu
Khi bạn khai báo collection như List<int>, compiler C# sẽ tạo một phiên bản riêng (specialization) của class này cho kiểu int. Khi bạn khai báo List<string>, compiler lại tạo một phiên bản khác cho string, v.v.
Còn với bạn — lập trình viên — nó hoạt động như phép màu:
List<int> numbers = new List<int>();
numbers.Add(1); // Chỉ thêm được int
List<string> words = new List<string>();
words.Add("hello"); // Chỉ thêm được string
Nếu thử thêm phần tử kiểu khác, compiler sẽ phản đối ngay:
numbers.Add("fail"); // LỖI khi compile!
An toàn kiểu dữ liệu: Compile-time vs. Run-time
Tức là, lỗi như trên sẽ không bao giờ đến được giai đoạn chạy chương trình (run-time). Collection của bạn được bảo vệ cực kỳ chắc chắn — compiler canh gác như chó ba đầu Cerberus trước cửa.
Bên trong
- Với kiểu tham chiếu (ví dụ string, object) không còn phải ép kiểu lung tung nữa.
- Với kiểu giá trị (int, double) không còn vấn đề "boxing/unboxing".
- Dùng generics trong .NET không làm code bị phình ra — CLR tối ưu hóa khi JIT-compile.
4. Generics giúp tăng hiệu năng như thế nào
Nhấn mạnh lại mấy điểm thần kỳ mà Generics mang lại cho code của bạn:
An toàn kiểu dữ liệu (Type Safety):
Đây là quan trọng nhất. Nhờ Generics, compiler như vệ sĩ riêng của bạn, không cho "người lạ" vào collection. Bạn hoàn toàn yên tâm List<Product> chỉ chứa object Product, không bao giờ có string hay số lạ. Loại bỏ hẳn một lớp lỗi mà trước đây chỉ xuất hiện khi chạy chương trình và gây ra mấy vụ "nổ tung" bất ngờ (InvalidCastException). Code của bạn sẽ ổn định và dễ đoán hơn nhiều.
Hiệu năng (Performance):
Như đã nói, với kiểu giá trị (int, double hay DateTime), Generics giúp quản lý bộ nhớ và CPU hiệu quả hơn. Với collection chứa hàng triệu phần tử hoặc thay đổi liên tục, điều này cực kỳ quan trọng. Thay vì phải chuyển cam từ thùng này sang bao nọ rồi lại chuyển về, bạn chỉ cần bỏ thẳng vào thùng đúng loại.
Tái sử dụng code (Code Reusability):
Generics cho phép bạn viết một đoạn code dùng được cho nhiều kiểu dữ liệu khác nhau mà không phải copy-paste. Ví dụ, bạn cần hàm đổi chỗ hai biến. Không có Generics, bạn phải viết SwapInt(ref int a, ref int b), SwapString(ref string a, ref string b), SwapProduct(ref Product a, ref Product b) v.v. Với Generics, chỉ cần một hàm: Swap<T>(ref T a, ref T b). Collection cũng vậy: không cần IntList, StringList, ProductList – chỉ cần List<T>. Code của bạn sẽ gọn, dễ bảo trì và mở rộng.
5. Generic method và tự tạo generic-class
Generic method
Generics không chỉ dành cho collection! Bạn có thể tự viết generic method, hoạt động với bất kỳ kiểu nào. Code sẽ linh hoạt hơn nhiều.
//Khai báo tham số kiểu T ngay sau tên method
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// Sử dụng:
int a = 10, b = 20;
Swap(ref a, ref b); // Giờ a == 20, b == 10
string one = "một", two = "hai";
Swap(ref one, ref two); // Dùng cho string cũng được!
Lưu ý, compiler C# có thể tự xác định tham số kiểu method dựa vào kiểu biến truyền vào. Nên trong ví dụ trên, khi gọi Swap không cần chỉ rõ kiểu.
Tự tạo generic-class
Bạn còn có thể tự tạo "hộp" của riêng mình (generic-class). Đơn giản lắm:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// Sử dụng:
var pair = new Pair<int, string> { First = 42, Second = "câu trả lời" };
6. Ví dụ dùng generic-collections
Quay lại với "danh sách việc cần làm" (To-Do List) mà ta từng xây trong các bài trước. Trước đây ta lưu task trong mảng hoặc chỉ in ra màn hình. Giờ — lưu động trong List<string>.
Ví dụ: thêm task động vào danh sách
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string>();
Console.WriteLine("Nhập task (hoặc để trống để kết thúc):");
string input;
while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
{
tasks.Add(input);
Console.WriteLine("Đã thêm task! Nhập tiếp (hoặc để trống để kết thúc):");
}
Console.WriteLine("\nDanh sách task hôm nay của bạn:");
foreach (string task in tasks)
{
Console.WriteLine("- " + task);
}
}
}
Có gì hay ở đây?
- Collection tự động mở rộng khi thêm task mới.
- Không thể nhét nhầm thứ gì ngoài string vào danh sách.
- Dễ dàng duyệt và in ra nội dung danh sách.
7. Lỗi thường gặp của newbie, lưu ý và mẹo
Chuyển từ collection không-generic sang generic đôi khi gây bối rối. Đây là vài tình huống newbie hay gặp:
- Thử thêm phần tử sai kiểu (List<int> numbers = new List<int>(); numbers.Add("hi"); // Lỗi).
- Muốn trộn nhiều kiểu trong một collection — lúc này phải chọn kiểu cha (ví dụ List<object>) — và sau đó luôn phải ép kiểu lại.
- Quên mất constraints, viết generic quá "rộng" và gặp lỗi lạ khi compile.
GO TO FULL VERSION