If you look through the list of issues tagged for System.Collections in .NET 3.0, you’ll find both proposals that were accepted and rejected. In this report we’ll cover some of the highlights.
IList<T>.RemoveAll and Default Interface Methods
IList<T> has a way to remove items by identity or index, it doesn’t provide an efficient way of removing items according to a predicate. Theoretically this missing method could be added in new versions of .NET without introducing a breaking change. This can be done through the use of a default interface method.
The first step towards this is support for default interface methods in .NET Standard 2.1. This feature, however, will not be offered for .NET Framework 4.8, which is thought to be the last major version of the original .NET Framework.
As a result, if
IList<T> were modified then .NET Standard and .NET Framework would have different versions forever. In turn this means anything that implements
IList<T> explicitly would have to differentiated using
#if blocks. An implicit implementation of the interface, or just allowing for the default implementation, should be ok in this scenario.
For now, there are no immediate plans to actually use default interface methods even if they are offered in .NET Standard 2.1. Other issues, such as support in languages other than C#, still need to be considered. Immo Landwerth, a program manager at Microsoft, says, “when VB and F# offer full support for them, we’ll leverage them in the BCL.”
SSE2, also known as Streaming SIMD Extensions 2, is an old CPU technology dating back to the year 2000. Though it can offer significant performance when working with large sets of data, adoption has been slow. For example, SSE2 support wasn’t enabled by default in Visual C++ until 2012. And in the .NET world, Mono gained support for SIMD code in 2008, long before Microsoft’s .NET implementation offered it.
BitArray, using SSE2 can see a significant performance boost for large arrays. When testing a binary OR operation over 1000 integers, the average time dropped from 654.6 ns to 169.8 ns. These gains are far less impressive for small arrays and using SSE2 can be slower when working with less than 8 integers (256 bits). You can see the special handling used for small arrays as well as the SSE2 code itself in the pull request.
Span<T> is also being used to optimize
BitArray. Unlike SSE2, the changes needed to use
Span<T> often simplify the code as well.
Immutable Collection Builders and the Pit of Success
A core concept in .NET API design is what’s known as the “pit of success”. The basic concept is that an API should make it inconvenient or even hard to use a class incorrectly. Conversely, the easiest thing to do is most often the correct thing to do. You can see an example of this in proposal #21055.
When working with immutable collections, it is often advantageous to start by populating a mutable “builder” object. When you are done with your ImmutableArray.Builder, ImmutableDictionary.Builder, etc. then you are supposed to call
ToImmutable() to freeze it into the corresponding immutable collection type.
However, if you are using any other kind of collection as a source then you need to call
IEnumerable<T>.ToImmutableDictionary(), etc. These are generic extension methods and thus are slower than using the builder’s native
ToImmutable() method. But from habit and consistency, most developers use the extension method and may not even know there is a better option.
In order to create a pit of success,
ImmutableArray.Builder and friends are adding corresponding
ToImmutableXxx methods that simply redirect the call to the correct
ToImmutable() method. Since they are being added as instance methods, each supersedes the extension method with the same name.
Removing Items While Enumerating a Dictionary
Removing items from a collection is often frustrating because collections cannot normally be modified while they are being enumerated. If you modify the collection in any way, the enumerator is marked as invalid and must be discarded.
The work around is to create a copy of the collection. This copy is enumerated, allowing the original to be changed. However, this can be expensive, especially in the case of a
Dictionary<K,V>where hash codes would need be calculated twice.
The aforementioned enumerator invalidation is handled by an internal field called “version”. Simply by not incrementing the version number when an item is removed, the need to make a preemptive copy is eliminated.
Meanwhile, a version increment was added to the
EnsureCapacity() methods, as these “could modify the backing storage in such a way that they could break enumeration.” This may result in a breaking change if either method is called inside a for-each loop.
ObservableCollection, AddRange, and WPF
A common complaint about the
ObservableCollection<T> classes is the lack of an
AddRange method. Found on the
List<T>class, this method would allow for a much more efficient combining of collections than you would see when adding one item at a time in a loop. Other methods in the group include InsertRange, RemoveRange, and ReplaceRange.
At first glance it seems like it would be simple enough to add the missing methods. In the case of
CollectionChangedevent already allows you to indicate multiple items were added or removed at one time.
The problem with the “easy fix” is WPF. Due to a design flaw dating back over a decade, most of the built-in controls dealing with collections are incapable of processing collection changed events that refer to more than one item at a time. Rather than programming to the interface, WPF was designed only to work with the specific implementation of
ObservableCollection<T>. Which means changes to it such as adding the
AddRangemethod is akin to a breaking change.
The request to fix WPF so that it correctly handles collection changed events is tagged as a future enhancement. Meanwhile the design details for adding range methods to
ObservableCollection<T> continue to be debated.