Skip to content

Proposal: implement generic IList<T> on collection types #2644

Open

Description

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 calling Cast<T>() or OfType<T>() first but this hides the type of the collection so LINQ cannot make use of e.g. Count or CopyTo interface methods.
  • foreach on an untyped list does implicit typecasts on every element - implementing the generic IList<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>() or OfType<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() and ToList() 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    InvestigateItems we want to investigate before making a decision on an issueapi-needs-work(3) API needs work before it is approved, it is NOT ready for implementation; applied by repo ownersdesign-discussionOngoing discussion about design without consensus

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions