question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal: implement generic IList<T> on collection types

See original GitHub issue

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.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:23
  • Comments:35 (32 by maintainers)

github_iconTop GitHub Comments

6reactions
RussKiecommented, Mar 31, 2020

I’ve written some simple benchmarks to check any perceived benefit of going from IEnumerable to IEnumerable<T>:

    [MemoryDiagnoser]
    public class IEnumerableBenchmarks
    {
        private UntypedCollection<int> _untypedCollection;
        private TypedCollection<int> _typedCollection;

        [Params(10, 100, 1000, 100_000)]
        public int N;

        [GlobalSetup]
        public void Setup()
        {
            var list = Enumerable.Range(1, N).ToArray();

            _untypedCollection = new UntypedCollection<int>(list);
            _typedCollection = new TypedCollection<int>(list);
        }

        [Benchmark(Baseline = true)]
        public int untyped_GetEnumerator_var()
        {
            var sum = 0;
            foreach (var item in _untypedCollection)
            {
                sum += (int)item;
            }

            return sum;
        }

        [Benchmark]
        public int untyped_GetEnumerator_explicit()
        {
            var sum = 0;
            foreach (int item in _untypedCollection)
            {
                sum += item;
            }

            return sum;
        }

        [Benchmark]
        public int typed_GetEnumerator_var()
        {
            var sum = 0;
            foreach (var item in _typedCollection)
            {
                sum += item;
            }

            return sum;
        }

        [Benchmark]
        public int typed_GetEnumeratorOfT_explicit()
        {
            var sum = 0;
            foreach (int item in _typedCollection)
            {
                sum += item;
            }

            return sum;
        }

        private class UntypedCollection<T> : IEnumerable
        {
            public UntypedCollection(IList list)
            {
                List = list;
            }

            private IList List { get; }

            public int Property { get; set; }
            IEnumerator IEnumerable.GetEnumerator() => List?.GetEnumerator();
        }

        private class TypedCollection<T> : IEnumerable<T>
        {
            public TypedCollection(IList<T> list)
            {
                List = list;
            }

            private IList<T> List { get; }

            IEnumerator IEnumerable.GetEnumerator() => throw new NotSupportedException(); // ((IEnumerable)List)?.GetEnumerator();

            IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)List)?.GetEnumerator();
        }
    }

The gains appear substantial, time is a in a order of magnitude, memory off the scales:

Method Toolchain N Mean Error StdDev Ratio RatioSD Rank Gen 0 Gen 1 Gen 2 Allocated
untyped_GetEnumerator_var .NET Core 3.1 10 622.00 ns 4.259 ns 3.983 ns 1.00 0.00 2 0.0429 - - 272 B
untyped_GetEnumerator_explicit .NET Core 3.1 10 627.46 ns 5.096 ns 4.255 ns 1.01 0.01 2 0.0429 - - 272 B
typed_GetEnumerator_var .NET Core 3.1 10 62.74 ns 0.529 ns 0.469 ns 0.10 0.00 1 0.0050 - - 32 B
typed_GetEnumeratorOfT_explicit .NET Core 3.1 10 62.65 ns 0.495 ns 0.463 ns 0.10 0.00 1 0.0050 - - 32 B
untyped_GetEnumerator_var .NET Core 5.0 10 638.19 ns 3.148 ns 2.945 ns 1.00 0.00 3 0.0429 - - 272 B
untyped_GetEnumerator_explicit .NET Core 5.0 10 649.95 ns 3.419 ns 3.031 ns 1.02 0.01 4 0.0429 - - 272 B
typed_GetEnumerator_var .NET Core 5.0 10 65.46 ns 1.315 ns 1.292 ns 0.10 0.00 2 0.0050 - - 32 B
typed_GetEnumeratorOfT_explicit .NET Core 5.0 10 62.27 ns 0.618 ns 0.578 ns 0.10 0.00 1 0.0050 - - 32 B
untyped_GetEnumerator_var .NET Core 3.1 100 5,962.18 ns 25.594 ns 22.689 ns 1.00 0.00 2 0.3815 - - 2432 B
untyped_GetEnumerator_explicit .NET Core 3.1 100 6,335.70 ns 81.597 ns 72.334 ns 1.06 0.01 3 0.3815 - - 2432 B
typed_GetEnumerator_var .NET Core 3.1 100 532.20 ns 10.319 ns 9.652 ns 0.09 0.00 1 0.0048 - - 32 B
typed_GetEnumeratorOfT_explicit .NET Core 3.1 100 528.51 ns 7.519 ns 7.033 ns 0.09 0.00 1 0.0048 - - 32 B
untyped_GetEnumerator_var .NET Core 5.0 100 6,179.92 ns 61.956 ns 54.922 ns 1.00 0.00 3 0.3815 - - 2432 B
untyped_GetEnumerator_explicit .NET Core 5.0 100 6,508.18 ns 36.783 ns 30.715 ns 1.05 0.01 4 0.3815 - - 2432 B
typed_GetEnumerator_var .NET Core 5.0 100 549.16 ns 6.002 ns 5.615 ns 0.09 0.00 2 0.0048 - - 32 B
typed_GetEnumeratorOfT_explicit .NET Core 5.0 100 519.67 ns 3.655 ns 3.240 ns 0.08 0.00 1 0.0048 - - 32 B

I trimmed the results to 10 and 100 items, that likely be our major customer use-cases, e.g. tens or hundreds of controls on a Form, rather than thousands.

The full results are here: Benchmarks.IEnumerableBenchmarks-report-github.md.txt

3reactions
RussKiecommented, Nov 8, 2022

I believe, such change is quite welcome 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

Creating a custom IList class that uses CollectionEditor
Implement the non generic IList as usual but make the implemented methods private. Then add an another public method for each one of...
Read more >
IList<T> Interface (System.Collections.Generic)
Represents a collection of objects that can be individually accessed by index. ... The IList<T> generic interface is a descendant of the ICollection<T> ......
Read more >
When To Use IEnumerable, ICollection, IList And List
I think that the question when to use IEnumerable, ICollection, IList or List is a common one that hasn't often being answered in...
Read more >
Solved: .NET Generics and TestStand - NI Community
Collections.Generic.IList<T>and use an internal concrete instance of List<T> to implement the interface. The C# editor can help with a lot ...
Read more >
Know your collections: from IEnumerable to List and beyond
The non generic interface implements both IEnumerable and ICollection , and will add the following properties: IsFixedSize , IsReadOnly ,and ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found