1. Vài lời về collection
Đến giờ tụi mình mới chỉ làm việc với mảng — đây là cách đơn giản nhất để lưu nhiều phần tử cùng kiểu. Nhưng trong C# có rất nhiều collection tiện lợi và mạnh mẽ hơn, giải quyết nhiều bài toán khác nhau.
Collection — là một object có thể chứa một nhóm object khác. Khác với mảng, collection thường có thể thay đổi kích thước động, cung cấp các method tiện lợi để thao tác dữ liệu và được tối ưu cho nhiều tình huống sử dụng khác nhau.
Các loại collection chính (tóm tắt):
List<T> — mảng động, có thể tăng giảm kích thước:
List<string> names = new List<string>();
names.Add("Alekx");
names.Add("Maria");
Console.WriteLine(names[0]); // Alekx
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> — collection kiểu "key-value", mỗi key ứng với một value:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alekx"] = 25;
ages["Maria"] = 30;
Console.WriteLine(ages["Alekx"]); // 25
Lưu ý: trong ví dụ trên tụi mình dùng dấu ngoặc vuông [] để truy cập phần tử trong collection — y hệt như với mảng! Đó là nhờ có indexer, và hôm nay tụi mình sẽ nói về nó.
Đây mới chỉ là làm quen với collection — chi tiết về khả năng, sự khác biệt và ứng dụng tụi mình sẽ học ở các bài sau. Giờ quan trọng là hiểu collection cho phép truy cập phần tử qua dấu ngoặc vuông, và đó không phải phép màu, mà là một cơ chế đặc biệt của ngôn ngữ.
2. Indexer
Object C# bình thường hoạt động qua property và method. Nhưng nếu object của bạn là một kiểu mini-collection thì sao? Ví dụ, tưởng tượng nhé:
- Bạn viết class Week, cần trả về tên ngày trong tuần theo số: week[0] → "Thứ hai".
- Hoặc class Library, nơi có thể truy cập sách theo số: library[3] → "Ve phia Svan".
Nghe tiện đúng không? Sẽ hơi kỳ nếu mỗi lần lại phải viết library.GetBookByIndex(3) — ai cũng muốn truy cập object như mảng ấy!
Đó chính là lúc indexer xuất hiện.
Indexer — là một thành viên đặc biệt của class, cho phép dùng object của class đó với cú pháp dấu ngoặc vuông như mảng: obj[0], obj["key"], v.v.Indexer nhìn ngoài giống property, nhưng thay vì tên thì nhận tham số trong dấu ngoặc vuông. Nó giống như bạn viết .Name, nhưng thay vào đó là [i].
3. Collection đơn giản với indexer
Làm thử một mini-class lưu màu yêu thích nhé. Dùng mảng string để lưu. Nếu không có indexer thì phải làm method kiểu GetColor(int i). Nhưng với indexer:
using System;
public class FavoriteColors
{
// Trường private để lưu màu
private string[] colors = new string[5];
// Indexer:
public string this[int index]
{
get
{
// Kiểm tra giới hạn mảng (encapsulation thực chiến!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Chi so mau khong hop le!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Chi so mau khong hop le!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "Xanh la";
favorites[1] = "Xanh duong";
favorites[2] = "Do";
favorites[10] = "Tim"; // Nem ra exception!
Console.WriteLine(favorites[1]); // Xanh duong
}
}
Ở đây có gì?
- Tụi mình tạo mảng private để không ai bên ngoài nghịch trực tiếp.
- Indexer được định nghĩa là public string this[int index], trong đó this là từ khóa chỉ indexer thuộc về object.
- Bên trong get và set tụi mình kiểm tra giới hạn, không cho vượt biên/ghi null.
- Kết quả là có thể làm favorites[0] như với mảng bình thường.
4. Cú pháp indexer: chi tiết
Cú pháp giống property, nhưng thay vì tên (ví dụ Age) thì dùng từ khóa this với tham số:
// Signature indexer (mẫu tổng quát)
[modifier] KieuKetQua this[KieuIndex tenIndex]
{
get { ... }
set { ... }
}
Ví dụ: Cổ điển
public class MyCollection
{
private int[] data = new int[10];
// Indexer cho đọc và ghi
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Indexer không chỉ nhận int
Điểm hay: indexer không bắt buộc chỉ nhận int. Bạn có thể dùng bất kỳ kiểu nào (miễn là truy cập bằng key hợp lý):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
Ví dụ, trong class danh bạ điện thoại thì tìm theo tên là hợp lý:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
Về collection và cách Dictionary<string, string> hoạt động mình sẽ kể ở bài sau :P
5. Ví dụ thực tế: Đếm từ trong văn bản
Tiếp tục phát triển app của tụi mình. Giả sử giờ có class đếm số lần mỗi từ xuất hiện trong text. Sẽ rất tiện nếu user có thể truy cập object qua dấu ngoặc vuông để lấy số lượng theo từ:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// Indexer theo string (từ)
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // Nếu không có từ đó thì trả về 0.
}
set
{
counter[word] = value;
}
}
// Method để đếm từ từ string
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// Trong Main:
var wc = new WordCounter();
wc.AddWords("mama myla ramu myla mama papa");
Console.WriteLine($"'mama' xuat hien {wc["mama"]} lan");
Console.WriteLine($"'ramu' xuat hien {wc["ramu"]} lan");
Console.WriteLine($"'kot' xuat hien {wc["kot"]} lan"); // 0
Thực tế dùng để làm gì? Cách này thường dùng để tự làm collection riêng, thư viện lưu trữ, mapping (dictionary và index) và thậm chí cả DSL (ngôn ngữ chuyên biệt trong C#).
6. Giới hạn và lưu ý
Indexer rất mạnh, nhưng có vài quy tắc và bẫy (không thể thiếu nhỉ).
Indexer không có tên
Khác với property, indexer không có tên, chỉ có signature kiểu this[loai tham so]. Nếu bạn viết public int MyIndexer[int i] — compiler sẽ ngạc nhiên đó. Chỉ dùng this thôi nhé.
Không có indexer static
Indexer luôn được tạo cho instance của class, không cho static member. Nghĩa là không thể khai báo static int this[int i] — vì this luôn chỉ một instance cụ thể.
Có thể overload theo kiểu/số tham số
Bạn có thể tạo nhiều indexer trong một class, nếu tham số khác kiểu hoặc số lượng. Ví dụ:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
Hợp lệ, compiler không nhầm đâu — và sẽ nhắc bạn nếu tham số bị trùng.
Chỉ hỗ trợ qua property
Không thể khai báo indexer mà không có accessor get hoặc set. Nếu chỉ muốn đọc — bỏ set, chỉ muốn ghi — bỏ get. Thường thì dùng cả hai.
7. Lợi ích thực tế và tại sao nên biết
- Indexer được dùng nhiều trong collection dữ liệu. Nhiều class .NET có sẵn indexer: ví dụ List<T>, Dictionary<TKey,TValue>. Khi bạn viết list[2], chính là đang dùng indexer đó!
- Indexer giúp ẩn cài đặt bên trong (encapsulation!), nhưng vẫn cung cấp interface quen thuộc, tiện lợi. Người dùng class của bạn không cần biết bạn lưu data thế nào, chỉ cần dùng [index] như thường.
- Code của bạn sẽ ngắn gọn, dễ hiểu — điều này rất được đánh giá cao khi phỏng vấn (và cả đồng nghiệp tương lai của bạn nữa).
So sánh Property và Indexer
| Property | Indexer | |
|---|---|---|
| Tên | Có (ví dụ Name) | Không (thay tên bằng this[tham so]) |
| Truy cập | Theo tên | Theo index (hoặc key khác) |
| Static | Có thể static | Chỉ cho instance |
| Nhiều trong class | Có, bao nhiêu cũng được | Có, nhưng khác signature tham số |
| Ứng dụng | Lưu/truy cập dữ liệu | Mini-collection, dữ liệu liên kết |
8. Lỗi thường gặp và mẹo
Lỗi #1: cố làm indexer static.
Không được đâu — this[...] chỉ dùng với object thôi.
Lỗi #2: quên kiểm tra index.
Nếu không kiểm tra giới hạn mảng trong get hoặc set, chương trình có thể crash.
Lỗi #3: nhầm kiểu tham số.
Nếu tạo hai indexer cùng tham số, compiler sẽ báo lỗi.
Lỗi #4: quên implement get hoặc set.
Nếu cần truy cập cả đọc và ghi — phải có cả hai.
Mẹo: nếu class của bạn bọc mảng — chỉ cần chuyển truy cập mảng qua indexer. Vừa nhanh vừa dễ hiểu code.
9. Tại sao nên biết mấy thứ này
Indexer làm cho interface của object đơn giản, dễ hiểu và tiện lợi. Chúng giúp ẩn cài đặt bên trong, nhưng vẫn cho dev cách truy cập data cực kỳ tiện.
Chính các collection built-in như string, List<T>, Dictionary<TKey, TValue>, Span<T> v.v. đều dùng indexer. Khi bạn viết array[2] hoặc text[0], bạn đã dùng indexer rồi đó.
Và quan trọng nhất — giờ bạn có thể tự viết class của mình hoạt động linh hoạt, gọn gàng như vậy. Nghĩa là — bạn đã tiến gần hơn tới việc viết code chuyên nghiệp, dễ đọc rồi nhé.
GO TO FULL VERSION