19

When I have a variable of ICollection<T> in C#, I cannot pass it to a function that expects an IReadOnlyCollection<T>:

public void Foo()
{
  ICollection<int> data = new List<int>();
  // Bar(data); // Not allowed: Cannot implicitly cast ICollection<int> to IReadOnlyCollection<int>
  Bar(data.ToList()); // Works, since List<T> implements IReadOnlyCollection<T>
}

public void Bar(IReadOnlyCollection<int> data)
{
  if (data.Count == 1) { /* ... */ }
  // ...
}

Apparently the problem is that ICollection<T> does not inherit from IReadOnlyCollection<T> - but why? ICollection<T> should be the full functional set of IReadOnlyCollection<T> plus the functions that modify the collection.

And what is the best solution to pass the arguments?

On the one hand, since I don't want to alter the collection in Bar and just need the count and iterate over the collection, I'd like to require an IReadOnlyCollection.

On the other hand, I don't want to create a new list object every time I call that function.

LWChris
  • 3,320
  • 1
  • 22
  • 39
  • What exactly are you doing with the collection in that method? Just checking the count? There's probably a better way to deal with that situation. – Jeff Mercado Dec 18 '15 at 19:13
  • You really only use a ReadOnlyCollection when you don't want anything outside of your class modifying your collection. If it's not a public property to begin with, the concern becomes trivial. – Xcalibur37 Dec 18 '15 at 19:17
  • @M.kazemAkhgary That's just break at runtime if someone passes an `ICollection` that's not an `IReadOnlyCollection`. – Servy Dec 18 '15 at 19:20
  • @JeffMercado The `ICollection` is the `Children` (readonly) property from a hierarchical data structure, so allowing the modification of the collection is intentional. However I don't need to modify them in context where I am evaluating the tree, so I'd like to limit my function to the most restrictive interface. The `Count == 1` case is a special treatment for when there is just one child node, where the whole hierarchical level is skipped. – LWChris Dec 18 '15 at 19:33
  • 1
    @Xcalibur37 that's a little short-sighted I think. A method accepting a `IReadOnlyCollection` indicates that the method will not modify the collection. – user247702 Oct 26 '17 at 12:30

2 Answers2

19

There is no standard solution AFAIK, but it's not hard to make your own like this

public static class MyExtensions
{
    public static IReadOnlyCollection<T> AsReadOnly<T>(this ICollection<T> source)
    {
        if (source == null) throw new ArgumentNullException("source");
        return source as IReadOnlyCollection<T> ?? new ReadOnlyCollectionAdapter<T>(source);
    }

    sealed class ReadOnlyCollectionAdapter<T> : IReadOnlyCollection<T>
    {
        readonly ICollection<T> source;
        public ReadOnlyCollectionAdapter(ICollection<T> source) => this.source = source;
        public int Count => source.Count;
        public IEnumerator<T> GetEnumerator() => source.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

And then use it as follows

Bar(data.AsReadOnly());
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
7

You can pretty trivially create a class that composes an ICollection<T> while implementing IReadOnlyCollection<T>. You can also create an extension method to do the wrapping (and thus allow for generic type inference):

public class ReadOnlyCollectionWrapper<T> : IReadOnlyCollection<T>
{
    private ICollection<T> collection;
    public ReadOnlyCollectionWrapper(ICollection<T> collection)
    {
        this.collection = collection;
    }

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

    public IEnumerator<T> GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }
}

public static class ReadOnlyCollectionWrapper
{
    public static IReadOnlyCollection<T> AsReadOnly<T>(this ICollection<T> collection)
    {
        return new ReadOnlyCollectionWrapper<T>(collection);
    }
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • Thanks! Ivan Stoev suggested exactly the same below. I accepted his answer since he was 2 minutes faster. I hope you understand. – LWChris Dec 18 '15 at 19:41