IEnumerable Và IEnumerator Trong C#

IEnumerable Và IEnumerator Trong C#

Là 2 interface được định nghĩa sẵn trong namespace System.Collections của .NET. Vậy IEnumerable và IEnumerator là gì và có tác dụng như nào? Chúng ta sẽ cùng tìm hiểu kỹ hơn trong bài viết này nhé.

Trong bài viết này mình sẽ sử dụng danh sách các số nguyên để làm ví dụ như sau:

List<int> listInt = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

IEnumerable là gì và dùng như thế nào?

Bất cứ ai khi sử dụng C# để lập trình thường xuyên phải thực hiện duyệt một danh sách từ phần tử đầu tới phần tử cuối như đoạn code dưới đây:

var count = listInt.Count;
            for (var i = 0; i < count; i++)
            {
                var item = listInt[i];

                Console.WriteLine("{0}", item);
            }

Hoặc

foreach (var item in listInt)
{
     Console.WriteLine("{0}", item);
}

Với cách đầu tiên, đây là cách thức duyệt danh sách kinh điển mà bất cứ ngôn ngữ lập trình nào cũng sử dụng. Chúng ta sẽ không bàn tới cách thức này.

Với cách thứ 2, rất nhiều ngôn ngữ lập trình hiện đại đều hỗ trợ. Và C# cũng không phải là một ngoại lệ. Vậy làm thế nào để C# có thể thực hiện duyệt danh sách sử dụng foreach? Câu trả lời là đối tượng đó implement interface IEnumerable hoặc IEnumerable<T> (hỗ trợ kiểu generic). Chúng ta hãy xem định nghĩa của 2 interface trên như sau:

public interface IEnumerable
{
	IEnumerator GetEnumerator();
}

Kiểu generic sẽ là:

public interface IEnumerable<out T> : IEnumerable
{
	IEnumerator<T> GetEnumerator();
}

Trong C# có rất nhiều kiểu danh sách như Dictionary, List, ArrayList, HashSet.. và ngay kiểu dữ liệu string đều implement từ IEnumerable hoặc IEnumerable<T>.

public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IDictionary, ICollection, IReadOnlyDictionary<TKey, TValue>, IReadOnlyCollection<KeyValuePair<TKey, TValue>>, ISerializable, IDeserializationCallback
{
	//...
}

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{
	//...
}

public class ArrayList : IList, ICollection, IEnumerable, ICloneable
{
	//...
}

public class HashSet<T> : ICollection<T>, IEnumerable<T>, IEnumerable, ISerializable, IDeserializationCallback, ISet<T>, IReadOnlyCollection<T>
{
	//...
}

public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable, IComparable<String>, IEnumerable<char>, IEquatable<String>
{
	//...
}


Vậy IEnumerable giúp một đối tượng có thể thực hiện duyệt các phần tử bằng foreach. Chúng ta hãy xem ví dụ dưới đây:

public class MyIntergerCollection : IEnumerable
    {
        private readonly int[] _listInt;

        public MyIntergerCollection(int[] listInt)
        {
            _listInt = listInt;
        }

        public IEnumerator GetEnumerator()
        {
            return _listInt.GetEnumerator();
        }
    }


Và khi thực hiện duyệt các phần tử từ đầu đến cuối như sau:

var customMyCollection = new MyIntergerCollection(listInt.ToArray());
            foreach (var item in customMyCollection)
            {
                Console.WriteLine(item);
            }


Kết quả bạn sẽ thấy như bạn thực hiện for hoặc foreach bình thường:

1
2
3
4
5
6
7
8
9
10


Vậy IEnumerator là gì và có tác dụng như nào?

Chúng ta hãy chú ý đến định nghĩa của IEnumerable có hàm GetEnumerator() trả về là một đối tượng được implement từ interface IEnumerator và chúng ta hãy xem định nghĩa của interface này:

public interface IEnumerator
{
	object Current { get; }
	bool MoveNext();
	void Reset();
}

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
	T Current { get; }
}

Trong đó:

Thuộc tính Current: trả về phần tử hiện tại của danh sách đang duyệt.
Hàm MoveNext(): trả về true nếu có thể duyệt đến phần tử tiếp theo, ngược lại trả về false.
Hàm Reset(): thực hiện thiết lập lại vị trí duyệt ban đầu.

Với định nghĩa trên, chúng ta có thể khẳng định rằng IEnumerator giúp chúng ta duyệt phần tử như thế nào? Mặc định .NET giúp chúng ta duyệt từ phần tử đầu tới phần tử cuối của danh sách. Ở ví dụ trên chúng ta sử dụng hàm _listInt.GetEnumerator() (vì mảng cũng implement interface IEnumerable) để trả về đối tượng tương ứng. Giờ chúng ta hãy tự tạo ra một cách thức duyệt nhưng có cùng kết quả như trên.

public class MyIntergerEnumerator : IEnumerator
    {
        private readonly int[] _listInt;
        private int _currentIndex = -1;

        public MyIntergerEnumerator(int[] listInt)
        {
            _listInt = listInt;
        }

        public object Current
        {
            get
            {
                return _listInt[_currentIndex];
            }
        }

        public bool MoveNext()
        {
            _currentIndex++;

            return _currentIndex < _listInt.Length;
        }

        public void Reset()
        {
            _currentIndex = -1;
        }
    }

Và đoạn code ví dụ trên sẽ được thay thành

public class MyIntergerCollection : IEnumerable
    {
        private readonly int[] _listInt;

        public MyIntergerCollection(int[] listInt)
        {
            _listInt = listInt;
        }

        public IEnumerator GetEnumerator()
        {
            return new MyIntergerEnumerator(_listInt);
        }
    }

Đọc đến đây chắc các bạn sẽ nghĩ rằng tại sao phải làm như vậy (viết 2 class mới là MyIntergerCollection, MyIntergerEnumerator) trong khi .NET đã hỗ trợ? Thực tế với cách viết như này, chúng ta có thể:

- Tạo ra một kiểu collection mới có thể thực hiện duyệt bằng foreach
- Tùy biến quá trình duyệt

Như ví dụ phía trên, chúng ta thấy rằng quá trình foreach bắt đầu từ phần tử đầu tiên đến phần tử cuối cùng. Giờ chúng ta hãy thử với trường hợp foreach từ phần tử cuối đến phần tử đầu tiên, chúng ta chỉ cần chỉnh lại class MyIntergerEnumerator như sau:

public class MyIntergerEnumerator : IEnumerator
    {
        private readonly int[] _listInt;
        private int _currentIndex;

        public MyIntergerEnumerator(int[] listInt)
        {
            _listInt = listInt;
            _currentIndex = _listInt.Length;
        }

        public object Current
        {
            get
            {
                return _listInt[_currentIndex];
            }
        }

        public bool MoveNext()
        {
            _currentIndex--;

            return _currentIndex >= 0;
        }

        public void Reset()
        {
            _currentIndex = _listInt.Length;
        }
    }


Và kết quả của chương trình là:

10
9
8
7
6
5
4
3
2
1

Ohh, thật thú vị phải không các bạn.

Kết luận:

Bài viết trên đây, mình đã giới thiệu với các bạn về IEnumerable và IEnumerator, rất mong những điều này sẽ giúp ích cho các bạn trong công việc cũng như học tập của mình.