1. Indexer với nhiều tham số
Indexer đơn giản (ví dụ, this[int index]) rất tiện khi bạn cần truy cập phần tử qua chỉ số nguyên — gần giống như mảng. Nhưng C# cho phép làm nhiều hơn: bạn có thể dùng nhiều tham số, tham số với kiểu khác nhau, đặt modifier truy cập khác nhau cho get và set, implement nhiều indexer trong một class (nếu signature khác nhau).
Ngoài ra, các version mới của C# còn có thêm tính năng giúp code với indexer gọn hơn, dễ dùng hơn, kiểu như cú pháp sugar.
Indexer không nhất thiết chỉ nhận một index và chỉ kiểu int. Tham số là do bạn định nghĩa — có thể có nhiều tham số với kiểu khác nhau, miễn là phù hợp với bài toán của bạn.
public class ChessBoard
{
private string[,] board = new string[8, 8];
// Indexer với hai tham số!
public string this[int row, int col]
{
get { return board[row, col]; }
set { board[row, col] = value; }
}
}
Giờ bạn có thể viết code như này:
ChessBoard chess = new ChessBoard();
chess[0, 0] = "Xe";
chess[7, 7] = "Vua";
string piece = chess[0, 0]; // "Xe"
Kiểu này rất hợp cho ma trận, bản đồ hai chiều, board game, collection phức tạp.
2. Indexer với tham số kiểu khác nhau
Indexer của bạn không chỉ nhận tham số kiểu int, mà còn bất kỳ kiểu nào hợp lý. Quan trọng là logic phù hợp với bài toán.
using System.Collections.Generic;
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Position { get; set; }
}
public class EmployeeCollection
{
private List<Employee> employees = new List<Employee>();
// Indexer theo tên nhân viên (string)
public Employee this[string name]
{
get
{
foreach (var employee in employees)
{
if (employee.Name == name)
return employee;
}
return null; // Hoặc có thể throw exception
}
set
{
for (int i = 0; i < employees.Count; i++)
{
if (employees[i].Name == name)
{
employees[i] = value;
return;
}
}
// Nếu chưa có nhân viên tên này — thêm mới
employees.Add(value);
}
}
// Cho tương thích — indexer theo chỉ số số nguyên
public Employee this[int index]
{
get { return employees[index]; }
set { employees[index] = value; }
}
}
var company = new EmployeeCollection();
company[0] = new Employee { Name = "Ivan", Age = 30, Position = "Lap trinh vien" };
company[1] = new Employee { Name = "Maria", Age = 25, Position = "Designer" };
Employee employee = company["Ivan"];
company["Petr"] = new Employee { Name = "Petr", Age = 28, Position = "Tester" };
Lưu ý: nếu bạn có nhiều indexer — signature của chúng phải khác nhau về số lượng và kiểu tham số.
3. Modifier truy cập khác nhau cho get và set
Đôi khi bạn chỉ muốn cho phép đọc qua index, còn ghi thì cấm (hoặc ngược lại). Trong C# bạn có thể đặt modifier truy cập khác nhau cho accessor get và set của indexer.
public class SecureEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
internal set { employees[index] = value; }
}
}
Cách này thường dùng để bảo vệ collection khỏi bị sửa ngoài ý muốn, giúp class kiểm soát tốt hơn.
4. Indexer chỉ đọc (read-only) và chỉ ghi (write-only)
Đôi khi bạn chỉ muốn cho phép đọc hoặc chỉ ghi qua indexer.
public class ReadOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
// không có set — không thể sửa!
}
}
public class WriteOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
set
{
employees.Insert(index, value);
}
// không có get — không thể đọc!
}
}
Thực tế "write-only" gần như không dùng: thường chỉ cần "read-only", ví dụ khi class chỉ cho phép xem dữ liệu qua indexer mà không cho sửa.
5. Indexer với kiểm tra giới hạn và logic
Trong ví dụ chất lượng, không chỉ truy cập qua index mà còn phải xử lý đúng khi vượt giới hạn, lỗi tìm kiếm và các tình huống ngoại lệ khác.
public class SafeEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Nhan vien voi chi so nay khong ton tai!");
return employees[index];
}
set
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Khong the thay the nhan vien khong ton tai!");
employees[index] = value;
}
}
}
Bạn có thể trả về null hoặc dùng pattern hiện đại để xử lý giá trị thiếu — tuỳ logic app của bạn.
6. Indexer với tham số lạ
Bạn có thể gặp collection mà indexer nhận kiểu không chuẩn: enum (enum), struct tự định nghĩa, thậm chí nhiều tham số kiểu khác nhau.
public enum Department { IT, HR, Finance, Marketing }
public class DepartmentEmployeeCollection
{
private Dictionary<Department , Employee> departmentLeads = new Dictionary<Department , Employee>();
public Employee this[Department department]
{
get { return departmentLeads.TryGetValue(department, out var employee) ? employee : null; }
set { departmentLeads[department] = value; }
}
}
var company = new DepartmentEmployeeCollection();
company[Department.IT] = new Employee { Name = "Anna", Age = 35, Position = "Truong phong IT" };
Employee itLead = company[Department.IT];
Kiểu này dùng khi bạn có identifier duy nhất hoặc mapping rõ ràng giữa kiểu và giá trị — tiện, dễ hiểu và type-safe.
7. Tính năng hiện đại: Range và Index
Với sự xuất hiện của Range và Index trong C# 8, indexer có thêm khả năng làm việc với khoảng và index từ cuối:
public class SmartArray
{
private int[] numbers = Enumerable.Range(0, 100).ToArray();
public int[] this[Range range] => numbers[range];
public int this[Index index] => numbers[index];
}
// Sử dụng:
var smart = new SmartArray();
int[] middle = smart[20..30]; // từ phần tử 20 đến 29
int last = smart[^1]; // phần tử cuối cùng
Nếu bạn tự implement collection, hỗ trợ Range và Index sẽ làm nó "native" và cực kỳ tiện cho dev.
8. So sánh Property và Indexer
Để hiểu rõ khi nào dùng property, khi nào dùng indexer, bạn nên so sánh đặc điểm của chúng.
- Property tiện để truy cập thuộc tính riêng lẻ của object qua tên: person.Name, car.Speed.
- Indexer hợp để truy cập phần tử collection hoặc cấu trúc qua key: employees[0], phoneBook["Ivan"].
- Property luôn có tên cụ thể và trong class chỉ có một property với tên đó.
- Indexer dùng từ khoá this và có thể có nhiều version nếu signature khác nhau.
- Property có thể là static, indexer thì không, vì this luôn trỏ đến instance cụ thể.
9. Lỗi thường gặp khi dùng indexer
Lỗi #1: không kiểm tra giới hạn.
Nếu không kiểm tra index vượt giới hạn mảng, bạn sẽ gặp IndexOutOfRangeException bất ngờ.
Lỗi #2: không xử lý null trả về từ indexer.
Nếu indexer theo string trả về null khi không có phần tử, mà code gọi lại dùng luôn kết quả, sẽ bị NullReferenceException.
Lỗi #3: trùng signature indexer.
C# không cho phép tạo hai indexer với cùng tập tham số.
Lỗi #4: logic khó hiểu trong set-accessor.
Nếu trong set lại dùng logic "thêm mới nếu chưa có" thay vì thay thế, sẽ gây khó hiểu. Nên làm rõ và document kỹ nếu dùng kiểu này.
10. Ứng dụng thực tế và kết luận
Trong project thực tế, indexer thường dùng để tạo collection chuyên biệt, cache, dictionary có logic riêng, ma trận và cấu trúc dữ liệu đa chiều. Nó làm code dễ đọc, dễ hiểu hơn — thay vì gọi method kiểu GetElementByIndex(5) bạn chỉ cần viết collection[5].
Nhớ nhé: indexer nên phù hợp với bản chất class của bạn. Nếu class không phải collection hay cấu trúc dữ liệu, có lẽ không cần indexer. Nhưng nếu class quản lý tập hợp phần tử, indexer sẽ làm việc với nó tự nhiên và tiện hơn rất nhiều.
GO TO FULL VERSION