Thứ Hai, 25 tháng 6, 2012

Sử dụng IEnumerable và IEnumerator


Mục đích của interface IEnumerable là cho phép chúng ta có thể sử dụng từ khóa foreach trên đối tượng của class cài đặt interface này. Một ví dụ về class chúng ta thường dùng có cài đặt IEnumerable là List. Trong trường hợp chúng ta sử dụng Generics List<> thì interface được cài đặt là IEnumerable<>.Giả sử chúng ta có khai báo về class Person như sau:
class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
}
Trong hàm Main, chúng ta tạo ra một danh sách các đối tượng Person và thực hiện duyệt qua các đối tượng đó bằng câu lệnh foreach như sau
List personList = new List();
personList.Add(new Person { Age = 10, Name = "John" });
personList.Add(new Person { Age = 5, Name = "Anna" });
personList.Add(new Person { Age = 8, Name = "Kevin" });

foreach (var item in personList)
{
    Console.WriteLine(item.Name + ":" + item.Age);
}
Giả sử chúng ta không muốn sử dụng List<> làm danh sách chứa các đối tượng Person mà chúng ta muốn tự mình tạo ra một lớp riêng gọi là PersonCollection (có thể bạn thắc mắc tại sao phải tạo ra lớp riêng? Đơn giản bởi vì có thể chúng ta muốn cài đặt một phương thức đặt biệt nào đó mà bản thân List<> không cung cấp). Nội dung của class PersonCollection là như sau:
class PersonCollection
{
    private List personList;
    public void Add(Person person)
    {
        personList.Add(person);
    }
}
Trong class PersonCollection, chúng ta có một dữ liệu private kiểu List dùng để lưu trữ danh sách các đối tượng Person. Trong thực tế, bạn có thể chọn các lưu trữ khác do bạn tự định nghĩa như Stack, Queue, LinkedList, Tree… miễn sao bạn có thể lưu lại các đối tượng mà bạn sẽ thao tác trong PersonCollection. Ngoài ra chúng ta cũng có 1 phương thức là Add dùng để thêm một đối tượng vào danh sách private của class.
Khi đã có class PersonCollection, chúng ta thử thay thế đoạn code trong hàm Main bằng đoạn code sử dụng class vừa mới tạo thử xem:
PersonCollection personList = new PersonCollection();
personList.Add(new Person { Age = 10, Name = "John" });
personList.Add(new Person { Age = 5, Name = "Anna" });
personList.Add(new Person { Age = 8, Name = "Kevin" });

foreach (var item in personList)
{
    Console.WriteLine(item.Name + ":" + item.Age);
}
Console.ReadKey(true);
Tuy nhiên, khi bạn biên dịch đoạn code này thì bạn sẽ nhận được thông báo lỗi: foreach statement cannot operate on variables of type ‘Example.PersonCollection’ because ‘Example.PersonCollection’ does not contain a public definition for ‘GetEnumerator’ 
Nguyên nhân lỗi này xuất hiện là vì PersonCollection không có phương thức GetEnumerator, hay nói cách khác là PersonCollection không cài đặt interface IEnumerable để lệnh foreach sử dụng. Hãy thử cài đặt interface này cho class PersonCollection:
class PersonCollection : IEnumerable
{
    private List personList;
    public PersonCollection()
    {
        personList = new List();
    }
    public void Add(Person person)
    {
        personList.Add(person);
    }

    public IEnumerator GetEnumerator()
    {
    }
 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
 {
      throw new NotImplementedException();
 }
}
Interface IEnumerable có 2 phương thức cùng có tên là GetEnumerator, một cái trả về kiểu IEnumerator<> còn kiểu kia trả về IEnumerator. Phương thức đầu tiên là có trong phiên bản mới hơn để hỗ trợ cho các lớp generics, còn phương thức thứ hai chỉ có mục đích tương thích với các phiên bản .NET cũ không có hỗ trợ generics.
Nhưng hãy để ý kiểu trả về của phương thức GetEnumerator, kiểu trả về là IEnumerator. Có nghĩa là nó đòi hỏi kiểu trả về phải là một đối tượng của lớp có cài đặt interface IEnumerator. Có vẻ như mọi thứ đang trở nên phức tạp hơn nhỉ. Tuy nhiên điều này là không có gì phức tạp nếu như bạn đã từng làm việc với Iterator trong C++ hoặc Java. Enumerator có ý nghĩa tương tự. Nó là một đối tượng mà sẽ nói cho chúng ta biết được rằng cách duyệt qua từng phần tử là như thế nào, phần tử hiện tại là gì…
Thế nhưng lấy đâu ra một đối tượng thuộc kiểu IEnumerator bây giờ? Rất đơn giản, chúng ta chỉ cần tạo ra một lớp cài đặt phương thức trong interface IEnumerator hoặc là cài đặt interface đó trực tiếp trên lớp Person. Hãy thử làm cách đầu tiên trước, đó là tạo ra một lớp riêng cài đặt interface IEnumerator
class PersonEnumerator : IEnumerator
    {
        private List list;
        private int currentIndex = -1;
        private Person currentPerson;
        public PersonEnumerator(List _list)
        {
            list = _list;
        }
        public Person Current
        {
            get { return currentPerson; }
        }

        public void Dispose() { }

        object System.Collections.IEnumerator.Current
        {
            get { throw new NotImplementedException(); }
        }

        public bool MoveNext()
        {
            if (++currentIndex >= list.Count)
            {
                return false;
            }
            else
            {
                currentPerson = list[currentIndex];
            }
            return true;
        }

        public void Reset()
        {
            currentIndex = -1;
        }
    }
Interface IEnumerator bao gồm 2 phương thức quan trọng: MoveNextReset và một Property là Current.
- Property Current trả về phần tử hiện tại đang được duyệt tới trong danh sách.
MoveNext dùng để đi đến phần tử tiếp theo trong danh sách (hay nói cách khác thay đổi giá trị của Property Current). Phương thức này trả về giá trị true nếu như việc di chuyển đến đối tượng tiếp theo thành công, trả về false nếu thất bại (trong trường hợp đã đến cuối danh sách). Khi MoveNext trả về false thì Current sẽ có giá trị không xác định.
Reset dùng để đưa con trỏ hiện tại về vị trí ban đầu. Vị trí ban đầu này là vị trí nằm ngày trước phần tử đầu tiên trong danh sách. Phương thức này có thể không cần cài đặt, nó chỉ được dùng để tương thích với các ứng dụng COM.
Phương thức Dispose có mục đích là hủy các tài nguyên sau khi sử dụng, tuy nhiên ta không có gì để hủy cả nên chỉ cần để trống phương thức này.
Ở trên, chúng ta có truyền vào cho constructor của Enumerator một danh sách các phần tử để duyệt. Tham số truyền vào có thể là mảng, danh sách, đối tượng dạng tập hợp… miễn sao phương thức cài đặt tương ứng trong lớp thỏa mãn việc duyệt qua danh sách đó (Bạn hãy thử làm một Enumerator cho phép duyệt từ cuối danh sách đến đầu danh sách trong câu lệnh foreach thử xem ^_^).
Sau khi đã có lớp PersonEnumerator như trên, ta sẽ quay trở lại phương thức GetEnumerator trong lớp Person. Cài đặt phương thức này rất đơn giản như sau
public IEnumerator GetEnumerator()
{
    return new PersonEnumerator(personList);
}
Chắc bạn cũng để ý thấy rằng việc tạo lớp Enumerator như trên là hơi dư thừa vì dữ liệu duy nhất bạn truyền vào là một List, trong khi List này đã có sẵn trong lớp PersonCollection rồi. Do đó, ta có thể cài đặt interface IEnumerator trực tiếp ngay trên lớp PersonCollection luôn. Các phương thức thì cài đặt cũng tương tự như bên Enumerator, chỉ có điều phương thức GetEnumerator() sẽ được sửa lại tương ứng như sau:
public IEnumerator GetEnumerator()
{
    return this;
}
Ở đây chúng ta return chính đối tượng hiện tại vì đối tượng này có cài đặt interface dùng để duyệt qua danh sách. Cách này giúp bạn giảm thiểu việc viết thêm một class mới vào ứng dụng. Tuy nhiên có thể sẽ làm phức tạp thêm ứng dụng của bạn khi muốn thay đổi, mở rộng hoặc chỉnh sửa sau này (Giả sử bạn có 2 cách duyệt danh sách khác nhau và muốn thay đổi nó thì sao?)
Sau khi đã hoàn thành IEnumerable và IEnumerator, bạn sẽ có thể sử dụng được câu lệnh foreach trên đối tượng thuộc lớp PersonCollection.

Không có nhận xét nào: