Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a System.Array object have an Add() method?

I fully understand that a System.Array is immutable.

Given that, why does it have an Add() method?

It does not appear in the output of Get-Member.

$a = @('now', 'then')
$a.Add('whatever')

Yes, I know this fails and I know why it fails. I am not asking for suggestions to use [System.Collections.ArrayList] or [System.Collections.Generic.List[object]].

like image 591
lit Avatar asked Dec 11 '22 05:12

lit


1 Answers

[System.Array] implements [System.Collections.IList], and the latter has an .Add() method.

That Array implements IList, which is an interface that also covers resizable collections, may be surprising - it sounds like there are historical reasons for it[1] .

In C#, this surprise is hard to stumble upon, because you need to explicitly cast to IList or use an IList-typed variable in order to even access the .Add() method.

By contrast, since version 3, PowerShell surfaces even a type's explicit interface implementations as direct members of a given type's instance. (Explicit interface implementations are those referencing the interface explicitly in their implementation, such as IList.Add() rather than just .Add(); explicit interface implementations are not a direct part of the implementing type's public interface, which is why C# requires a cast / interface-typed variable to access them).

As a byproduct of this design, in PowerShell the .Add() method can be called directly on System.Array instances, which makes it easier to stumble upon the problem, because you may not realize that you're invoking an interface method. In the case of an array, the IList.Add() implementation (rightfully) throws an exception stating that Collection was of a fixed size; the latter is an exception of type NotSupportedException, which is how types implementing an interface are expected to report non-support for parts of an interface.

What helps is that the Get-Member cmdlet and even just referencing a method without invoking it - simply by omitting () - allow you to inspect a method to determine whether it is native to the type or an interface implementation:

PS> (1, 2).Add  # Inspect the definition of a sample array's .Add() method

OverloadDefinitions
-------------------
int IList.Add(System.Object value)

As you can see, the output reveals that the .Add() method belongs to the Ilist interface.


[1]Optional reading: Collection-related interfaces in .NET with respect to mutability

Disclaimer: This is not my area of expertise. If my explanation is incorrect / can stand improvement, do tell us.

The root of the hierarchy of collection-related interfaces is ICollection (non-generic, since v1) and ICollection<T> (generic, since v2).

(They in turn implement IEnumerable / IEnumerable<T>, whose only member is the .GetEnumerator() method.)

While the non-generic ICollection interface commendably makes no assumptions about a collection's mutability, its generic counterpart (ICollection<T>) unfortunately does - it includes methods for modifying the collection (the docs even state the interface's purpose as "to manipulate generic collections" (emphasis added)). In the non-generic v1 world, the same had happened, just one level below: the non-generic IList includes collection-modifying methods.

By including mutation methods in these interfaces, even read-only/fixed-size lists/collections (those whose number and sequence of elements cannot be changed, but their element values may) and fully immutable lists/collections (those that additionally don't allow changing their elements' values) were forced to implement the mutating methods, while indicating non-support for them with NotSupportedException exceptions.

While read-only collection implementations have existed since v1.1 (e.g, ReadOnlyCollectionBase), in terms of interfaces it wasn't until .NET v4.5 that IReadOnlyCollection<T> and IImmutableList<T> were introduced (with the latter, along with all types in the System.Collections.Immutable namespace, only available as a downloadable NuGet package).

However, since interfaces that derive from (implement) other interfaces can never exclude members, neither IReadOnlyCollection<T> nor IImmutableCollection<T> can derive from ICollection<T> and must therefore derive directly from the shared root of enumerables, IEnumerable<T>. Similarly, more specialized interfaces such as IReadOnlyList<T> that implement IReadOnlyCollection<T> can therefore not implement IList<T> and ICollection<T>.

More fundamentally, starting with a clean slate would offer the following solution, which reverses the current logic:

  • Make the major collection interfaces mutation-agnostic, which means:

    • They should neither offer mutation methods,
    • nor should they make any guarantees with respect to immutability.
  • Create sub-interfaces that:

    • add members depending on the specific level of mutability.
    • make immutability guarantees, if needed.

Using the example of ICollection and IList, we'd get the following interface hierarchy:

IEnumerable<T> # has only a .GetEnumerator() method
  ICollection<T>  # adds a .Count property (only)
   IResizableCollection<T> # adds .Add/Clear/Remove() methods
   IList<T> # adds read-only indexed access
    IListMutableElements<T> # adds writeable indexed access
    IResizableList<T> # must also implement IResizableCollection<T>
      IResizableListMutableElements<T> # adds writeable indexed access
    IImmutableList<T> # guarantees immutability

Note: Only the salient methods/properties are mentioned in the comments above.

Note that these new ICollection<T> and IList<T> interfaces would offer no mutation methods (no .Add() methods, ..., no assignable indexing).

IImmutableList<T> would differ from IList<T> by guaranteeing full immutability (and, as currently, offer mutation-of-a-copy-only methods).

System.Array could then safely and fully implement IList<T>, without consumers of the interface having to worry about NotSupportedExceptions.

like image 192
mklement0 Avatar answered Dec 21 '22 22:12

mklement0