Search code examples
c#arrayscollectionsparametersreadonly

Collection<T> is read-only?! What's the matter with using the params keyword to instantiate a collection?


I'm using Unity version 2023.1.6. I have a custom collection that inherits from Collection<T>. If I instantiate a collection and then try to use the .Add() method I receive the following error :

System.NotSupportedException: Collection is read-only.
  at System.Collections.ObjectModel.Collection`1[T].Add (T item) [0x0000d] in <46a5f68c38604d68a85231bbf55f6b8f>:0 

I noticed that this error occurs only if I instantiate the collection using this constructor:

public Selection(params T[] selectionElements) : base(selectionElements) { }

I fixed the error by changing the constructor to this:

public Selection(params T[] selectionElements) : base(selectionElements.ToList()) { }

I don't know what LINQ does behind the scenes and even why this works (I just tried...), so I remain with a few more questions:

  • What's the problem with using the params keyword?

  • Does params create an array? If yes, why it's not ok to pass in an array as an IList<T>? If it's not ok why there is no compile time error?

  • Why the LINQ method fixed the error?


Solution

  • There's no problem using the params keyword, the problem is passing an array as an IList and expecting to be able to add to it. This is an example of the C# base class library not doing the best job with the Liskov Substitution Principle (the 'L' in SOLID).

    IList<T> contains an 'IsReadOnly' property. An Array<T> implements IList<T>, but of course you can't add to an array - you would need to copy all the contents into a larger one first, which is exactly the job that List<T> does. The IsReadOnly property is true, and if you try to call Add() it will throw an exception.

    A separate IReadOnlyList<T> was added later on to make it more obvious what's going on, but Array<T> still implements the original IList<T> interface for the sake of backwards compatibility.

    Why does this work when you call selectionElements.ToList()? ToList() does the following:

    • Creates a new List<T>
    • Enumerates the array
    • Adds each item from the array to the new list
    • Returns the list

    This means that you now have a List<T> with all the same values in it, which implements IList<T> and importantly is not read-only