Implementing GetEnumerator() for a collection inherited from List<string>

26,481

Solution 1

new public IEnumerator GetEnumerator()
{
  using(IEnumerator ie = base.GetEnumerator())
    while (ie.MoveNext()) {
      yield return Path.Combine(baseDirectory, ie.Current);
  }
}

Solution 2

If you have C# 3, you don't need to write a special class to do this. Supposing you have a sequence of strings, such as a List<string> or string[], anything that supports IEnumerable<string>, called filePathCollection, you can just use:

var prefixedPaths = filePathCollection.Select(path => baseDirectory + path);

Hey presto - you now have an IEnumerable<string> of paths prefixed with baseDirectory, so you can use foreach on it, etc. We're done.


The rest of this answer is more general explanation to help you (and others) spot where this can be applied in other cases.

Select essentially means: take this sequence of items, do something with each item and give me back a new sequence containing all the results. The "something" is specified by providing a method that accepts one parameter of the same type as stored in the old sequence, and returns any type you like, which will be the item type of the new sequence. In this case, we're not changing the item type. Also we're defining the method "in place" using a lambda:

path => baseDirectory + path

The compiler figures out that the element type of the source collection is string, so path is a string - you can think of path as playing the same role as a "loop variable" if you had to write the whole thing out yourself. And on path we use concatenation, so the result is another string, so the new sequence must also be IEnumerable<string>. This is "type inference" and is an important part of how this stuff reduces the amount of code you have to write.

The other important thing happening is "closure", which is the technical name for how we make our lambda depend not only on its "open" parameter path but also on a "closed" parameter baseDirectory which isn't even being explicitly passed into it as a parameter. A lambda can just reach outside itself and get to the variables visible the method it is defined within. This is specifically what frees you from the need to write a constructor that takes baseDirectory as a parameter and stores it in a _baseDirectory field so you can use it later repeatedly in some other method.

Note that the new sequence will always be the same length as the incoming sequence. If you want to filter items out, use Where. If you want to make a sequence longer, use SelectMany.

Solution 3

using the new keyword can cause you polymorphism problems: in case some does:

List<string> files = new FilePathCollection();

calling foreach (var files in files) will cause to call the not overridden enumerator.

I think the best is inherit from IEnumerable<string> and hold a private field with your List.

For example this could be a way to do it: inheriting from IList<string> which it already inherit from IEnumerable<T>

 public class FilePathCollection :  IList<string>
    {
        string baseDirectory;
        private List<string> internalList;

        public FilePathCollection(string baseDirectory)
        {
            this.baseDirectory = baseDirectory;
        }

        #region IList<string> Members

        public int IndexOf(string item)
        {
            return GetFileNameOnly(internalList.IndexOf(item));
        }
        private string GetFileNameOnly(string p)
        {
            //your implementation.......
            throw new NotImplementedException();
        }

        private int GetFileNameOnly(int p)
        {
           //your implementation.......
            throw new NotImplementedException();
        }

        public void Insert(int index, string item)
        {
            internalList.Insert(index, item);
        }

        public void RemoveAt(int index)
        {
            internalList.RemoveAt(index);
        }

        public string this[int index]
        {
            get
            {
                return GetFileNameOnly(internalList[index]);
            }
            set
            {
                this[index] = value;
            }
        }



        #endregion

        #region ICollection<string> Members

        public void Add(string item)
        {
            internalList.Add(item);
        }

        public void Clear()
        {
            internalList.Clear();
        }

        public bool Contains(string item)
        {
            return internalList.Contains(item);
        }

        public void CopyTo(string[] array, int arrayIndex)
        {
            internalList.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get { return internalList.Count; }
        }

        public bool IsReadOnly
        {
            get { return false; }
        }

        public bool Remove(string item)
        {
            return internalList.Remove(item);
        }

        #endregion

        #region IEnumerable<string> Members

        public IEnumerator<string> GetEnumerator()
        {
            foreach(string value in internalList)
                yield return baseDirectory + value;
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            foreach(string value in internalList) 
                yield return baseDirectory + value;
        }

        #endregion
    }

Solution 4

You could probably use base.GetEnumerator() and manually iterate over that.

However, I think you've going to run in to all kinds of problems designing the class the way you're trying to do it. You should get the same values out of a list that you put in to them. For example, with the code you show you'd get different values enumerating the list than when using the indexer. Also, would it be clear what other methods of List do such as Add() or Contains()?

Is there a reason you couldn't just create a static method that takes in a file name list and a base directory, then generates a new result list where each element is dir+file?

Solution 5

You could just cast to base type.

new public System.Collections.IEnumerator GetEnumerator()
{
    foreach (string value in (List<string>)this) //<-- note the cast
        yield return baseDirectory + value;
}

Ideally I would just use the Select extension method. And give it a generic overload please...

public new IEnumerator<string> GetEnumerator()
{
    return ((List<string>)this).Select(x => baseDirectory + x).GetEnumerator();
}
Share:
26,481
Admin
Author by

Admin

Updated on July 22, 2022

Comments

  • Admin
    Admin almost 2 years

    I am trying to implement FilePathCollection. Its items would be simple file names (without a path - such as "image.jpg"). Once the collection is used via foreach cycle, it should return the full path created by concatenating with baseDirectory. How can I do that?

    public class FilePathCollection : List<string>
    {
        string baseDirectory;
    
        public FilePathCollection(string baseDirectory)
        {
            this.baseDirectory = baseDirectory;
        }
    
        new public System.Collections.IEnumerator GetEnumerator()
        {
            foreach (string value in this._items) //this does not work because _list is private
                yield return baseDirectory + value;
        }
    }
    
  • Dykam
    Dykam about 14 years
    Indeed, composition instead of inheritance.
  • Daniel Earwicker
    Daniel Earwicker about 14 years
    This is quite a long-winded way to rewrite Enumerable.Select.
  • Daniel Earwicker
    Daniel Earwicker about 14 years
    This looks very much like a specialised rewrite of Enumerable.Select.
  • Matthew Flaschen
    Matthew Flaschen about 14 years
    Daniel, I showed how to take the approach he chose. You have a good argument for not using the class at all.