Description
openedon Jan 6, 2020
Historically WinForms existed before generic collections, and when generics were introduced the collection types in WinForms never were updated to implement the generic IList<T>
interface. I think its time to revisit all collection types and implement IList<T>
for an appropriate type T
.
For maximum compatibility the T
in IList<T>
should match the already existing indexer on the collection type, because binding logic will currently orient itself on the indexer to determine the type of the list, but if you start implementing IList<T>
it will prefer that over the indexer. To avoid changing how collection types behave during binding you should use the indexer type as T
.
Some advantages of implementing IList<T>
in addition to IList
:
- using LINQ expressions can make use of additional optimizations - you could use LINQ on the existing
IList
collections by callingCast<T>()
orOfType<T>()
first but this hides the type of the collection so LINQ cannot make use of e.g.Count
orCopyTo
interface methods. foreach
on an untyped list does implicit typecasts on every element - implementing the genericIList<T>
avoids these- better integration with modern APIs and Analyzers which are more likely to be based on the generic
IList<T>
- better integration with nullability annotations
Since it turned out there are many trade-offs when implementing generic collections here is a breakdown of breaking changes vs. their benefits:
Breaking changes grouped by benefit/scenario
Convenience
(1) var
in foreach loops is not supported: foreach (var child in parent.Controls)
defaults to child being object
(same for every other collection type, not just the control collection). You have to write out the collection content type manually in every loop.
Fixing this requires implementing generic IEnumerable<T>
and changing the return type of the public GetEnumerator
from non-generic IEnumerator
to generic IEnumerator<T>
because foreach picks up the public method in favor of interfaces. I assume this breaks binary compatibility.
(2) You can use LINQ without prefixing the collection with a Cast<T>()
or OfType<T>()
LINQ call. Currently you have to write panel.Controls.Cast<Control>()
to access the control collection, repeating the content type same as in the foreach loop.
Fixing this does not require a breaking change, you can implement IEnumerable<T>
explicitely without breaking binary compatibility (if I understood correctly adding an interface is not breaking compatibility).
Performance
(3) LINQ expressions such as Last()
or ToList()
will iterate over the whole collection since they don't see the non-generic IList. Implementing generic IList will allow more efficient access.
Fixing this requires implementing IList<T>
but it can be implemented explicitely, so theoretically it can be done without breaking binary compatibility.
Practically there is a problem where some collection types use virtual methods, theoretically allowing 3rd party subclasses. Explicitely implementing IList<T>
without requiring an update of subclasses leads to very weird and inefficient implementations of bool ICollection<T>.Remove(T item)
because the current virtual methods won't tell whether an item was removed.
Analyzers
(4) Analyzers are usually only written with generic collections in mind. This actually came up during writing the PR, xunit only has support for generic collections, its analyzers won't pick up on antipatterns if you are using non-generic collections.
For adding analyzer support it probably should be enough implementing IList<T>
explicitely so it probably has no extra cost and is just a benefit if you decide to implement generic collections at all.
Nullability
(5) Nullability checks on foreach
loops will look at the public GetEnumerator
, nullability checks for LINQ and other extension methods will look at the interface.
Most collections can't contain null, but you have no way to annotate the nullability on non-generic IList. Adding support for nullability to foreach loops requires a breaking change because you need to change the return type of public GetEnumerator
methods to the generic version.
(6) some collection classes expose non-generic backing collections to subclasses. These are holding back the WinForms code base from modernizing itself (including proper use of nullability annotations), since you can't change backing collections to a generic type if they are exposed the way they are currently.
Benefits grouped by amount of breaking change
Benefits are incremental in the order I list them.
explicit IEnumerable<T>
Implement only IEnumerable<T>
as explicit interface implementation on collection classes.
- allows using LINQ expressions without prefixing them with a
Cast<T>()
orOfType<T>()
- enables some analyzers to assist the user (analyzers are often only written for generic collections)
- nullability annotations for LINQ expressions (but not foreach loops)
explicit IList<T>
Implement IList<T>
but make the implementation not public unless the method already exists with the exact signature. All other methods are implemented as explicit interface implementations.
- allows fast access via LINQ e.g. for
Last()
andToList()
extension methods - enables more analyzers to assist the user
changing GetEnumerator signature to a generic type
This is the minimum breaking change you have to take for additional benefits.
- foreach loops can start using
var
- foreach loops see nullability annotations
changing void Remove()
signature to return boolean
For non-virtual Remove() methods this is optional, you can always do a private or internal Remove() implementation which returns bool, but for virtual Remove() methods this is a major breaking change.
- simplifies the explicit implementation of
IList<T>.Remove
massively, including better performance - without this change you have to double-check the contents for removal
change backing collection types
Some collections expose their (non-generic) backing collection to subclasses. Changing this is obviously a major breaking change.
- the WinForms codebase itself would probably the major receiver of any benefits you get from this, as without this changes you are forced to use non-generic collections internally (at least in parts of the codebase which expose backing collections). This means nullability annotations within WinForms itself will be missing around those parts if you can't change backing collections to generic types.
❗️ This may impact VS Designer, and this impact will have to be assessed.