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.