CodeGym /Các khóa học /C# SELF /Extension Members: indexers mở rộng

Extension Members: indexers mở rộng

C# SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Hãy tưởng tượng, tụi mình có class DogShelter yêu thích, nó lưu một collection các chú chó. Ở các bài trước, tụi mình đã thêm indexer để lấy chó theo số thứ tự trong shelter: Dog firstDog = myShelter[0];. Quá ngon!

Nhưng nếu user muốn lấy chó không phải theo số, mà ví dụ theo tên? Hay theo giống? Hoặc thậm chí là theo tổ hợp thuộc tính? Dĩ nhiên, tụi mình có thể thêm các method kiểu GetDogByName("Buddy") hoặc GetDogByBreedAndAge("Labrador", 5). Cách này cũng ổn thôi.

Nhưng đôi khi, mình lại muốn truy cập kiểu "giống mảng" và trực quan hơn. Để có thể viết: Dog buddy = myShelter["Buddy"]; hoặc Dog oldLab = myShelter["Labrador", 8];.

Nếu DogShelter là class của mình, thì chỉ cần thêm indexer mới vào trong đó là xong. Nhưng nếu DogShelter là class từ thư viện ngoài, mình không sửa được? Hoặc mình muốn thêm cách truy cập rất riêng mà không muốn "làm bẩn" class gốc?

Đó chính là lúc extension indexers (Extension Indexers) xuất hiện!

2. "Dấu ngoặc vuông" từ bên ngoài

Nhớ không, bài trước tụi mình đã thêm extension property DisplayName cho Dog? Với indexer cũng gần giống vậy luôn!

Extension indexer là một indexer static, được định nghĩa trong static class, cho phép bạn dùng cú pháp obj[индекс] cho các object của type đã có sẵn, dù type đó chưa từng có indexer, hoặc bạn muốn thêm indexer với kiểu tham số khác.

Kiểu như bạn mua cái tủ lạnh, rồi nghĩ ra cách gõ vào chỗ nào đó là nó tự nhả ra chai coca. Tủ lạnh vẫn như cũ, nhưng chức năng mới được "gắn ngoài"!

Cú pháp extension indexer


public static class MyExtensionClass
{
    extension(ObjectType экземпляр)
    {    
        public static ReturnType this[ТипИндекса index ]
        {
            get
            {
                // Logic đọc, dùng экземпляр và index
                return ...;
            }
            set
            {
                // Dùng экземпляр, index và từ khóa 'value'
                // 'value' là giá trị mới
            }
        }
    }        
}
Cú pháp extension indexer (Extension Indexer)

Chú ý this ТипРасширяемогоОбъекта экземпляр. Cú pháp này giống hệt như extension method và property. экземпляр là tên object mà bạn mở rộng, dùng trong accessor get và set.

3. Khai báo Extension Indexer (không bị rối não)?

Cú pháp giống Extension Properties mà tụi mình nói ở bài trước, chỉ khác là có thêm tham số index. Đây là ví dụ tối giản:


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {      
        public static Dog this[string name]
        {
            get
            {
                foreach (var dog in shelter)
                {
                    if (dog.Name == name)
                        return dog;
                }
                return null;
            }
        }
    }        
}
Extension Indexer theo tên cho DogShelter

Các thành phần quen thuộc:

  • this trước tham số đầu tiên – bắt buộc với Extension Members (object mở rộng).
  • Sau tên class là danh sách tham số, sẽ dùng trong dấu ngoặc vuông.
Nó hoạt động gần như indexer bình thường, chỉ khác là bạn không sửa class gốc!

Thực hành: Mở rộng DogShelter bằng indexer theo tên

Cùng sửa project học tập nhé. Giả sử tụi mình có shelter cho chó, mỗi chú chó là duy nhất theo tên:

Class DogShelter (thư viện/mã ngoài)

public class Dog
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class DogShelter : IEnumerable<Dog>
{
    private List<Dog> dogs = new List<Dog>();

    public void AddDog(Dog dog) => dogs.Add(dog);

    // Indexer cũ theo số thứ tự
    public Dog this[int index]
    {
        get => dogs[index];
        set => dogs[index] = value;
    }

    public IEnumerator<Dog> GetEnumerator() => dogs.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Muốn: shelter["Буся"]

Trước đây – chỉ qua method:

// Trước C# 14:
public static Dog? FindByName(this DogShelter shelter, string name) { ... }

Bây giờ – qua Extension Indexer:


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {    
        public static Dog? this[string name]
        {
            get
            {
                foreach (var dog in shelter)
                    if (dog.Name == name)
                        return dog;
                return null; 
            }
            set
            {
                for (int i = 0; i < shelter.Count; i++)
                {
                    if (shelter[i].Name == name)
                    {
                        shelter[i] = value!;
                        return;
                    }
                }
                throw new ArgumentException("Không tìm thấy chó");
            }
        }
    }        
}

Giờ code chính của tụi mình nhìn xịn hơn hẳn:

var shelter = new DogShelter();
shelter.AddDog(new Dog { Name = "Буся", Age = 3 });
shelter.AddDog(new Dog { Name = "Тузик", Age = 5 });

// Dùng extension-indexer luôn!
Dog busya = shelter["Буся"]!;
Console.WriteLine(busya.Age);

shelter["Буся"] = new Dog { Name = "Буся", Age = 4 };

Hình dung: chuyện gì xảy ra?

Thao tác Cách cũ Với Extension Indexer
Tìm theo tên shelter.FindByName("X") shelter["X"]
Sửa chó theo tên shelter.UpdateName("X", ..) shelter["X"] = ...

4. Những điểm cần chú ý với Extension Indexers

Compiler và phạm vi nhìn thấy

  • Extension Indexer phải được khai báo trong static public class (giống extension methods).
  • Nhớ thêm using đúng namespace. Quên là compiler im re, code không chạy đâu.
  • Nếu class gốc đã có indexer cùng signature – không mở rộng được (phải khác signature).

Triển khai set-accessor

Bạn có thể chỉ khai báo get (indexer chỉ đọc). Hoặc thêm cả set (như ví dụ trên) – lúc đó vừa đọc vừa ghi qua indexer được luôn.

Truyền theo giá trị và tham chiếu

Extension Indexer làm việc với instance object mà bạn gắn extension (this trước tham số đầu). Nếu object là reference type, bạn thay đổi trạng thái của nó luôn.

Nhiều indexer trong một class

Không vấn đề gì – bạn có thể khai báo nhiều extension-indexer với các bộ tham số khác nhau! Ví dụ, tìm theo tuổi: shelter[5] (cũ), shelter["Буся"] (mới), shelter[age: 3] (thêm nữa nếu thích).

Ví dụ: thêm hai indexer cho DogShelter


public static class DogShelterExtensions
{
    extension(DogShelter shelter)
    {       
        // Theo tên
        public static Dog? this[string name]
        {
            get => shelter.FirstOrDefault(d => d.Name == name);
            set
            {
                for (int i = 0; i < shelter.Count; i++)
                    if (shelter[i].Name == name)
                        shelter[i] = value!;
            }
        }

        // Theo tuổi – trả về chú chó đầu tiên có tuổi đó
        public static Dog? this[int age]
        {
            get => shelter.FirstOrDefault(d => d.Age == age);
        }
    }        
}

Giờ bạn có thể viết:

var youngDog = shelter[1];           // Theo tuổi
var tony = shelter["Тони"];          // Theo tên
shelter["Тузик"] = new Dog { Name = "Тузик", Age = 9 };

Tình huống thực tế

  • Thư viện ngoài: Bạn muốn thêm các cách index mới cho class ngoài mà không đụng vào source. Ví dụ, làm việc với collection đơn hàng, tìm theo số, ngày, trạng thái... mà không phải viết method wrapper lặp lại.
  • "Pattern adapter": Bạn biến collection cũ với API "cùi" thành hiện đại, gọn, chuẩn C#, không phá vỡ tương thích ngược.
  • Migrate code cũ: Thêm tính năng mới cho type đã viết mà không đụng vào code/test cũ.
  • Tiện cho test: Bạn có thể gắn indexer tạm cho nhu cầu test (ví dụ tìm theo thuộc tính đặc biệt cho test), không sợ làm bẩn class chính.

5. Lỗi phổ biến và bẫy khi dùng Extension Indexers

Nếu class gốc đã có indexer với signature y hệt, extension-indexer sẽ không được gọi – indexer gốc ưu tiên hơn.

Extension-indexer cũng là extension member, nếu thiếu using (namespace), extension sẽ không thấy đâu.

Lỗi hay gặp nữa là trả về null mà không báo trước. Nếu ai đó lỡ truy cập phần tử không tồn tại, extension-indexer trả về null, có thể gây NullReferenceException ở chỗ khác. Nên cân nhắc: ném exception, trả về object dummy, hay cứ trả null.

Nếu bạn có nhiều extension-indexer, nhớ đảm bảo chúng khác nhau về kiểu và số lượng tham số. Không thể tạo hai indexer cùng signature – compiler sẽ báo lỗi.

Extension-indexer chỉ hoạt động với instance object, không dùng cho static type.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Làm quen với indexer
Indexer và Extension Members
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION