none
A good IEnumerable<T> implementation practice. RRS feed

  • General discussion

  • The IEnumerable<T> interface represents the base of every generic sequence in the Microsoft .NET Framework class library, and extensionally the base of every generic collection implemented and integrated to this environment. Its implementation on a custom collection is as easier as define how you want your collection iterates through the contained elements.

    This interface has only one member: GetEnumerator() which returns an IEnumerator<T> object whose methods provides the iteration. For more information about Iterators in the Microsoft .NET Framework see: Iterators (C# and Visual Basic).

    The C# and Visual Basic languages allows you to automatically generate an enumerator for your IEnumerable<T> implementation. Let’s see an example of a simple IEnumerable<T> collection implementation using this practice:

    /// <summary>
    /// Represents a strongly typed collection of objects that stores a static number of elements.
    /// </summary>
    /// <typeparam name="T">The type of elements in the collection.</typeparam>
    public class StaticCapacityCollection<T> : IEnumerable<T>
    {
        /// <summary>
        /// The default collection capacity if specified value is wrong or not present.
        /// </summary>
        private const int DefaultCapacity = 4;
        /// <summary>
        /// The array to store values.
        /// </summary>
        private T[] s_elements;
        /// <summary>
        /// The actual number of values in the collection
        /// </summary>
        private int s_count;
        /// <summary>
        /// The default constructor for the collection. Initializes the collection with default capacity (4).
        /// </summary>
        public StaticCapacityCollection()
            : this(DefaultCapacity)
        {
        }
        /// <summary>
        /// An overloaded constructor. Initializes the collection with specified size, if the size value is invalid, default capacity is used.
        /// </summary>
        /// <param name="capacity">The capacity of the collection.</param>
        public StaticCapacityCollection(int capacity)
        {
            if (capacity < 0)
            {
                capacity = DefaultCapacity;
            }
            this.s_elements = new T[capacity];
            this.s_count = 0;
        }
        /// <summary>
        /// The actual number of values in the collection.
        /// </summary>
        public int Count
        {
            get
            {
                return s_count;
            }
        }
        /// <summary>
        /// Adds the specified value to the collection. If the collection is full returns false; otherwise true.
        /// </summary>
        /// <param name="value">The value to add to the collection.</param>
        public bool Add(T value)
        {
            if (this.s_count == this.s_elements.Length)
            {
                return false;
            }
            this.s_elements[this.s_count] = value;
            this.s_count++;
            return true;
        }
        /// <summary>
        /// Determines whether the collection contains the specified value. Null value is considered.
        /// </summary>
        /// <param name="value">The value to find in the collection.</param>
        public bool Contains(T value)
        {
            if (value == null)
            {
                for (int i = 0; i < this.s_count; i++)
                {
                    if (this.s_elements[i] == null)
                    {
                        return true;
                    }
                }
            }
            else
            {
                IEqualityComparer<T> comparer = EqualityComparer<T>.Default;
                for (int i = 0; i < this.s_count; i++)
                {
                    if (comparer.Equals(this.s_elements[i], value))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        /// <summary>
        /// Removes the specified value from the collection. If object is present and removed returns true; otherwise false.
        /// </summary>
        /// <param name="value">The value to remove from the collection.</param>
        public bool Remove(T value)
        {
            int index = Array.IndexOf(s_elements, value, 0, this.s_count);
            if (index >= 0)
            {
                this.s_count--;
                if (index < this.s_count)
                {
                    Array.Copy(this.s_elements, index + 1, this.s_elements, index, this.s_count - index);
                }
                this.s_elements[this.s_count] = default(T);
            }
            return false;
        }
        /// <summary>
        /// Returns an enumerator that iterates through the elements of the collection.
        /// </summary>
        public IEnumerator<T> GetEnumerator()
        {
            for (int i = 0; i < this.s_count; i++)
            {
                yield return s_elements[i];
            }
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    }

    This way you can simply store elements, perform basic collection operations (Add, Remove, and Find) and iterate through the contained elements for any state of the collection. Next example shows some code using previous implemented collection.

    static void Main(string[] args)
    {
        StaticCapacityCollection<int> myCollection = new StaticCapacityCollection<int>(10);
        myCollection.Add(1);
        myCollection.Add(2);
        myCollection.Add(7);
        myCollection.Add(16);
        Console.WriteLine("myCollection after adding:");
        ShowSequence<int>(myCollection);
        Console.WriteLine();
        Console.WriteLine("myCollection after removing:");
        myCollection.Remove(1);
        myCollection.Remove(7);
        ShowSequence<int>(myCollection);
        Console.WriteLine();
        Console.WriteLine("Contains value 7: {0}", myCollection.Contains(7));
        Console.WriteLine("Contains value 2: {0}", myCollection.Contains(2));
        Console.ReadLine();
        /*Output:
            * 	myCollection after adding:
            *  1, 2, 7, 16, 
            *
            *  myCollection after removing:
            *  2, 16
            *
            *  Contains value 7: False
            *  Contains value 2: True
            */
    }
    static void ShowSequence<T>(IEnumerable<T> sequence)
    {
        foreach (T value in sequence)
        {
            Console.Write(value + ", ");
        }
        Console.WriteLine();
    }

    As ShowSequence method shows, the collection can be used in a foreach statement, this is because it implements the IEnumerable<T> interface. This is a comfortable way to implement a collection and iterate through the contained elements, but it ignores some bad issues that can provoke wrong behaviors or not guarantee appropriate performances.

    The first issue to treat is the auto-generated enumerator. This is a fantastic practice in some scenarios but collections have particular behaviors to respect. Any collection object supposes to manage stored elements so, the enumerator should be resettable, auto-generated enumerators are not (this is because some sequences can disappear after the enumeration or have a non-linear conduct). The IEnumerator.Reset member throws an exception of type System.NotSupportedException in auto-generated enumerator. Besides in some complex data structures auto-generated enumerators not guarantees best performance or a proper exception handling. This is why in good collections implementations enumerator should be manually implemented and, preferably, public exposed.

    The other common issue is the uncontrolled states of the collection when iterating. When an iterator is performing the collection can easily being modified, this can bring disastrous results to your code or the end-user of the implemented collection. To prevent such calamities update a stored value on each modification to the collection can be a solution, this way you can verify the collection isn’t modified on each iteration step since the iterator was created. The value used to the verification should be a value type object instead of a reference type object in order to have good results (recommended types are numeric values <int>, <long>, <double>, etc.).

    Previous code should be updated in order to use this patterns by declaring another field and update it on each modification and implementing and exposing an IEnumerator<T> object. Updated code should look like this:

    /// <summary>
    /// Represents a strongly typed collection of objects that stores a static number of elements.
    /// </summary>
    /// <typeparam name="T">The type of elements in the collection.</typeparam>
    public class StaticCapacityCollection<T> : IEnumerable<T>
    {
        /// <summary>
        /// The default collection capacity if specified value is wrong or not present.
        /// </summary>
        private const int DefaultCapacity = 4;
        /// <summary>
        /// The array to store values.
        /// </summary>
        private T[] s_elements;
        /// <summary>
        /// The actual number of values in the collection
        /// </summary>
        private int s_count;
        /// <summary>
        /// A value updated on each modification to control collection changes.
        /// </summary>
        private long s_modifications;
        /// <summary>
        /// The default constructor for the collection. Initializes the collection with default capacity (4).
        /// </summary>
        public StaticCapacityCollection()
            : this(DefaultCapacity)
        {
        }
        /// <summary>
        /// An overloaded constructor. Initializes the collection with specified size, if the size value is invalid, default capacity is used.
        /// </summary>
        /// <param name="capacity">The capacity of the collection.</param>
        public StaticCapacityCollection(int capacity)
        {
            if (capacity < 0)
            {
                capacity = DefaultCapacity;
            }
            this.s_elements = new T[capacity];
            this.s_count = 0;
            this.s_modifications = 0;
        }
        /// <summary>
        /// The actual number of values in the collection.
        /// </summary>
        public int Count
        {
            get
            {
                return s_count;
            }
        }
        /// <summary>
        /// Adds the specified value to the collection. If the collection is full returns false; otherwise true.
        /// </summary>
        /// <param name="value">The value to add to the collection.</param>
        public bool Add(T value)
        {
            if (this.s_count == this.s_elements.Length)
            {
                return false;
            }
            this.s_elements[this.s_count] = value;
            this.s_count++;
            this.s_modifications++;
            return true;
        }
        /// <summary>
        /// Determines whether the collection contains the specified value. Null value is considered.
        /// </summary>
        /// <param name="value">The value to find in the collection.</param>
        public bool Contains(T value)
        {
            if (value == null)
            {
                for (int i = 0; i < this.s_count; i++)
                {
                    if (this.s_elements[i] == null)
                    {
                        return true;
                    }
                }
            }
            else
            {
                IEqualityComparer<T> comparer = EqualityComparer<T>.Default;
                for (int i = 0; i < this.s_count; i++)
                {
                    if (comparer.Equals(this.s_elements[i], value))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        /// <summary>
        /// Removes the specified value from the collection. If object is present and removed returns true; otherwise false.
        /// </summary>
        /// <param name="value">The value to remove from the collection.</param>
        public bool Remove(T value)
        {
            int index = Array.IndexOf(s_elements, value, 0, this.s_count);
            if (index >= 0)
            {
                this.s_count--;
                if (index < this.s_count)
                {
                    Array.Copy(this.s_elements, index + 1, this.s_elements, index, this.s_count - index);
                }
                this.s_elements[this.s_count] = default(T);
                this.s_modifications++;
            }
            return false;
        }
        /// <summary>
        /// Returns an enumerator that iterates through the elements of the collection.
        /// </summary>
        public IEnumerator<T> GetEnumerator()
        {
            for (int i = 0; i < this.s_count; i++)
            {
                yield return s_elements[i];
            }
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
        /// <summary>
        /// Represents an Enumerator that iterates the elements of a <see cref="EnumerableExample.StaticCapacityCollection`1"/>
        /// </summary>
        public struct Enumerator : IEnumerator<T>
        {
            /// <summary>
            /// The collection to iterate through.
            /// </summary>
            private StaticCapacityCollection<T> s_collection;
            /// <summary>
            /// The current index of the enumeration.
            /// </summary>
            private int s_index;
            /// <summary>
            /// The modification value of the collection when the enumerator was created.
            /// </summary>
            private readonly long s_modification;
            /// <summary>
            /// The current value of the enumeration.
            /// </summary>
            private T s_current;
            /// <summary>
            /// The constructor.
            /// </summary>
            /// <param name="s_collection">The collection to iterate through.</param>
            internal Enumerator(StaticCapacityCollection<T> s_collection)
            {
                this.s_collection = s_collection;
                this.s_index = 0;
                this.s_current = default(T);
                this.s_modification = s_collection.s_modifications;
            }
            /// <summary>
            /// Gets the current value of the iteration. Throws an exception if enumerator has whether not started or finished.
            /// </summary>
            public T Current
            {
                get
                {
                    if (this.s_index == 0)
                    {
                        throw new InvalidOperationException("Enumeration has not started.");
                    }
                    if (this.s_index > s_collection.s_count)
                    {
                        throw new InvalidOperationException("Enumeration has finished.");
                    }
                    return this.s_current;
                }
            }
            /// <summary>
            /// The method corresponding to the IDisposable interface.
            /// </summary>
            public void Dispose()
            {
            }
            /// <summary>
            /// Advances the enumerator to the next element in the collection. Returns true if the enumerator has succefully advanced; otherwise false.
            /// </summary>
            public bool MoveNext()
            {
                if (this.s_modification != this.s_collection.s_modifications)
                {
                    throw new InvalidOperationException("Enumeration canceled. Collection was modified.");
                }
                if (this.s_index < this.s_collection.s_count)
                {
                    this.s_current = this.s_collection.s_elements[this.s_index];
                    this.s_index++;
                    return true;
                }
                this.s_index = this.s_collection.s_count + 1;
                this.s_current = default(T);
                return false;
            }
            /// <summary>
            /// Sets the enumerator to its initial position, which is before the first element in the collection.
            /// </summary>
            public void Reset()
            {
                if (this.s_modification != this.s_collection.s_modifications)
                {
                    throw new InvalidOperationException("Enumeration canceled. Collection was modified.");
                }
                this.s_index = 0;
                this.s_current = default(T);
            }
            object System.Collections.IEnumerator.Current
            {
                get
                {
                    return Current;
                }
            }
        }
    }

    Next code will throw some exception for bad issue of the collection and its enumerator:

    static void Main(string[] args)
    {
        StaticCapacityCollection<int> myCollection2 = new StaticCapacityCollection<int>(10);
        myCollection2.Add(1);
        myCollection2.Add(2);
        myCollection2.Add(7);
        myCollection2.Add(16);
        myCollection2.Add(21);
        IEnumerator<int> enumerator = myCollection2.GetEnumerator();
        //Exception 1 (Next Line). Current value is not accesible neither before start the enumeration or after the enumeration has finished.
        //Exception Text: "Enumeration has not started."
        int current = enumerator.Current;
        while (enumerator.MoveNext())
        {
            Console.WriteLine(enumerator.Current);
            //Exception 2 (Next Line). Collection must not be modified during the enumeration process.
            //Exception Text: "Enumeration canceled. Collection was modified."
            myCollection2.Add(3);
        }
    }

    Using this practices you will give a lot of stability to your collections. Try it and enjoy the results.



    Friday, March 8, 2013 8:06 AM

All replies