3
\$\begingroup\$

I wanted something more flexible than System.Collections.ObjectModel.ReadOnlyCollection. The goal was: consuming classes should only see what I want to show them, not whole underlying collection.

For example: I have an array of 10 elements. If I want to show consumer only first 5 elements, I don't want to create new List and rewrite that data. Another example: I want to show consumer reordered collection, why create new List and rewrite all ?

Here is interface (.NET version defined in 4.5):

public interface IReadOnlyCollection<out T> : IEnumerable<T>
    {
        T this[int index] { get; }
        int Count { get; }
    }

and implementation (not tested yet):

public delegate T ProxiedIndexer<out T>(int index);

public class ProxiedReadOnlyCollection<T> : IReadOnlyCollection<T>, IEnumerable<T>
{
    public ProxiedReadOnlyCollection(ProxiedIndexer<T> indexer, Func<int> countGetter)
    {
        this.indexer = indexer;
        this.countDelegate = countGetter;
    }

    #region IReadOnlyCollection

    /// <summary>
    /// Override to decorate in fly.
    /// </summary>
    public virtual T this[int index]
    {
        get
        {
            if (index < 0 || index >= countDelegate())
                throw new IndexOutOfRangeException();

            return indexer(index);
        }
    }

    public int Count { get { return countDelegate(); } }

    private readonly ProxiedIndexer<T> indexer;
    private readonly Func<int> countDelegate;
    #endregion //IReadOnlyCollection

    #region IEnumerable
    public IEnumerator<T> GetEnumerator()
    {
        return this.Enumerate().GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    private IEnumerable<T> Enumerate()
    {
        for (int i = 0; i < Count; i++)
            yield return this[i];
    }
    #endregion //IEnumerable

Useful extensions:

public static IReadOnlyCollection<T> AsReadOnly<T>(this IList<T> list)
{
    return new ProxiedReadOnlyCollection<T>(i => list[i], () => list.Count);
}

public static IReadOnlyCollection<T> AsReadOnly<T>(this T[] array)
{
    return new ProxiedReadOnlyCollection<T>(i => array[i], () => array.Length);
}

public static IReadOnlyCollection<T> AsReadOnly<T>(this IEnumerable<T> enumerable)
{
    return enumerable.ToArray().AsReadOnly();
}

public static IReadOnlyCollection<T> Reverse<T>(this IReadOnlyCollection<T> readOnlyCollection)
{
    return new ProxiedReadOnlyCollection<T>(i => readOnlyCollection[readOnlyCollection.Count - 1 - i], () => readOnlyCollection.Count);
}

What do you think about my implementations and other possible class use cases ?

EDIT

To understand how this 'collection' works lets look at constructor. It needs two delegates:

  • indexer - for given index it returns T (this is exposed to consumer as T this[int index])
  • countGetter - just returns element count

So the whole point is that consumer sees it as indexed collection, but there is no real collection at all. It is similar to IEnumerable's yield (generating items on demand, not storing them).

Performance (on i7-2760, Win7):

Iterating all elements:

    var enumerable = Enumerable.Range(0, 100000000);
    var list = enumerable.ToList();
    int x;

    var frameworkReadOnly = new ReadOnlyCollection<int>(list);
    var start = DateTime.Now;
    for (int i = 0; i < list.Count; i++)
        x = frameworkReadOnly[i];
    Console.WriteLine((DateTime.Now - start).TotalMilliseconds);//632 ms

    var proxiedReadOnly = list.AsReadOnly();
    start = DateTime.Now;
    for (int i = 0; i < proxiedReadOnly.Count; i++)
        x = proxiedReadOnly[i];
    Console.WriteLine((DateTime.Now - start).TotalMilliseconds);//935 ms

ProxiedReadOnlyCollection is ~ 50% slower. It's even worse when we change initialization to:

var proxiedReadOnly = new ProxiedReadOnlyCollection<int>(i => i, () => 100000000);

1633 ms => ~160% slower

It's slower significantly. But have in mind what we were measuring. These were access times. Lets measure creation times:

var enumerable = Enumerable.Range(0, 100000000);
var list = enumerable.ToList();
var frameworkReadOnly = new ReadOnlyCollection<int>(list);

1468 ms

var proxiedReadOnly = new ProxiedReadOnlyCollection<int>(i => i, () => 100000000);

1 ms

Total times:

  • framework collection: 2100 ms
  • proxied collection: 1634 ms

Conclusions:

Although access time for proxied collection is bigger, it has no creation overhead and could be faster when iterating all elements once. That's the key - one time. If consumer would iterate all elements several times, then framework's ReadOnlyCollection is faster.

Of course there is also memory benefit (proxied collection does not remember elements, it only serves them on demand).

Serving elements on demand could be implemented as lazy object initialization, which has its own benefits.

Cons: Slower performance when it comes to accessing elements many times. In some cases we cannot predict element at specified index, so creating them on demand is not an option. Other time we just want to create items in advance.

For me it's just a tool, that can do some tasks better than .NET ReadOnlyCollection and other worse.

\$\endgroup\$
  • \$\begingroup\$ I think it's a nice abstraction, but could you please add some usage cases? \$\endgroup\$ – Ron Klein Sep 30 '12 at 4:35
  • \$\begingroup\$ Edited original post. Added performance measurements + pros/cons, which should help to answer when to use it. Originally I created this class for some script algorithm simulations, but I don't want to show this use case here (lots of code and niche). \$\endgroup\$ – Kuba Sep 30 '12 at 12:41
  • 1
    \$\begingroup\$ The only problem I have here is that you haven't yet stated the problem. I say that because without knowing the problem it's not possible for us to know if this is the right solution. Can you state the actual problem (even if it's a fabrication to protect your niche). \$\endgroup\$ – Mike Perrenoud Oct 3 '12 at 12:29
  • \$\begingroup\$ This is supposed to be lazy indexing collection proxy. Actually I don't have a problem with implementation. Main reason for posting this was: am I doing it right ? Were you doing something similar ? What do you think about my approach ? This is how I understand "code review". \$\endgroup\$ – Kuba Oct 3 '12 at 19:06
1
\$\begingroup\$

I don't see how would this be useful.

If you want to return the first 5 items from a collection, use LINQ: Take(5). This will return IEnumerable, but that's most likely okay.

If you want to return reordered collection and don't want to modify the original list, you do need to create the sorted list, so your code won't be useful in that case either.

\$\endgroup\$
  • \$\begingroup\$ "I don't see how would this be useful..." I think you are missing the point that I am working with indexed collections. Take(5) will return IEnumerable, as you said. Then we have to make list from it and only then we are able to create .NET's ReadOnlyCollection. Ordering IEnumerable collection has huge overhead. Reversing IEnumerable is also unnecessary if we already have indexed collection. \$\endgroup\$ – Kuba Sep 30 '12 at 7:29
1
\$\begingroup\$

That seems like way to much work to test and maintain for an improvement that needs to be measured in milliseconds (and that's only under the circumstances of your test case).

(Developer cycles are more valuable than CPU cycles!)

You'd be better off just using the BCL ReadOnlyCollection.

\$\endgroup\$
  • 1
    \$\begingroup\$ It depends. In performance measurement example I used 100 million item collection. ReadOnlyCollection needed > 400 mb memory. What if collection is to big to fit in the memory ? Time performance improvement is connected to concrete scenario. Think about binary search. There would be huge gain. \$\endgroup\$ – Kuba Sep 30 '12 at 17:54
  • \$\begingroup\$ Yes, but if you are working on a project that actually requires you to process a 100 million item collection, then you can probably afford to buy more RAM and CPU power (which would actually cost less in the long-run than paying you to create/maintain a more efficient ReadOnlyCollection class). \$\endgroup\$ – Jeremy Bell Sep 30 '12 at 18:19
  • \$\begingroup\$ Maybe I wrote wrong examples. Think of my implementation like indexed brother of IEnumerable's yield. Collection is providing elements on demand and does not store them - creates them in fly (we could decorate it to do caching but that is different story). \$\endgroup\$ – Kuba Sep 30 '12 at 18:44
0
\$\begingroup\$

The ReadOnlyCollection has all the IEnumerable extension methods so on the other side anyone can call on that a ToList() so you will loose the read-only-thing. You are reinventing the wheel for no real reason.

\$\endgroup\$
  • \$\begingroup\$ Am I ? You are expecting second WriteLine in following code to print '5' ? var collection = new ReadOnlyCollection<int>(new List<int>(new int[] { 0 })); Console.WriteLine(collection[0]); collection.ToList()[0] = 5; Console.WriteLine(collection[0]); \$\endgroup\$ – Kuba Sep 30 '12 at 8:28
  • \$\begingroup\$ It will write out 0 but this is becouse integers are value types try this with a simple POCO class (one int type auto property). \$\endgroup\$ – Peter Kiss Sep 30 '12 at 9:06
  • \$\begingroup\$ I was afraid that you would write that this is value type case :| That's not the case. If you would write: readOnlyCollection.ToList()[0] = new FooClass();then readOnlyCollection[0] still returns old reference. If collection is hosting reference types that support state change (through properties, methods, whatever), then they could be changed. But that's different context. You cannot blame ReadOnlyCollection for this. In this case it doesn't matter if objects are hosted in ReadOnlyCollection, List, Array or other IEnumerable implementation. \$\endgroup\$ – Kuba Sep 30 '12 at 10:51
  • \$\begingroup\$ Now i know that you are familiar with all these stuff i understand what you are like to tell us and the idea is not bad but i still think is unnecessary. With your solution the other side will enumerate on the collection through your collection and maybe not only once which will be slover then creating a brand new list (IEnumerable.ToList<T> is watching after ICollection implementation!). \$\endgroup\$ – Peter Kiss Sep 30 '12 at 11:35
  • \$\begingroup\$ I have edited original post to add performance measurements and some conclusions. \$\endgroup\$ – Kuba Sep 30 '12 at 12:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.