Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: implement generic IList interface #2749

Closed
wants to merge 1 commit into from

Conversation

weltkante
Copy link
Contributor

@weltkante weltkante commented Jan 21, 2020

Contributes to #2644
Alternative implementation to PR #2999

This is a WIP PR for exploring the impact #2644 may have and whether its worth taking the breaking change.

This PR contains adding IList<T> interface implementation to types currently implementing IList. I thought it'd be a good way to start the work. If you want me to split down further in either multiple commits or separate PRs I don't mind, I fully expect having to go through the full changes several times anyways. On that note, if you have nits don't bother reviewing/annotating all of them, just pick one representative for discussion.

Notes:

  • As far as placement of new code goes I tried to orient myself on the corresponding IList methods (where available), this makes for weird placement since not all IList<T> methods are together, but considering I don't really want to reorder the existing methods (unless asked to) I figured it'd be the best solution to minimize diffs for now.
  • There are plenty of odd design decisions in the original collections subclassing hierarchy which makes updating them harder.
  • Some subclass hierarchies could profit from adding generics to their signatures. Of course this is a highly breaking change.
  • Some classes expose a backing collection (usually of type ArrayList) to their subclasses. Should we break subclasses by changing the type of the backing list to a generic collection? Nullability annotations would profit from such a change.
  • Existing Remove implementations don't tell the caller whether they removed something, but IList<T> requires this in its API. I've updated them where it looked sensible but some Remove implementations are virtual and changing return type to bool would require updating subclasses. I opted to not do this in this PR before discussing this point, instead for now I did implement IList<T>.Remove as explicit interface with a weird workaround. That is just placeholder until it is clear what is desired.
    • Should we require updating subclasses and change the return type?
    • If not, how much do we want to assume about how the subclass behaves? (This decides how weird/complicated the explicit interface implementation has to be.)
  • There are bugs in some existing collection classes where they don't check in Remove implementations whether the item to be removed is actually part of the collection. They run logic which severs the item and partially disconnects it from its true owner, but without notifying that owner. I did not fix these bugs yet (some are marked with comments though), I'm going to create a separate issue for this problem (so it doesn't have to wait for this issue/PR, which probably will take a while to dicscuss and resolve all points).
  • xunit apparently doesn't support non-generic lists, so when you start implementing the generic list interface all kinds of collection-tests stop compiling because some analyzer wants you to use Assert.Contains or Assert.DoesNotContain. Minor inconvenience which is easily fixed.
Microsoft Reviewers: Open in CodeFlow

@codecov
Copy link

codecov bot commented Jan 21, 2020

Codecov Report

Merging #2749 into master will decrease coverage by 3.09285%.
The diff coverage is 27.4%.

@@                Coverage Diff                 @@
##              master      #2749         +/-   ##
==================================================
- Coverage   61.94595%   58.8531%   -3.09285%     
==================================================
  Files           1256       1240         -16     
  Lines         447545     428861      -18684     
  Branches       39209      38934        -275     
==================================================
- Hits          277236     252398      -24838     
- Misses        164844     171122       +6278     
+ Partials        5465       5341        -124
Flag Coverage Δ
#Debug 58.8531% <27.4%> (-3.09286%) ⬇️
#production 31.8017% <20.74236%> (-1.45615%) ⬇️
#test 98.97457% <100%> (+0.00131%) ⬆️

using System.Diagnostics;

namespace System.Windows.Forms
{
public class ArraySubsetEnumerator : IEnumerator
public class ArraySubsetEnumerator<T> : IEnumerator<T>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this may look like a major breaking change it isn't, the class previously was internal and mistakenly was made public in a recent refactoring.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed it as well:

System.Windows.Forms.ArraySubsetEnumerator
System.Windows.Forms.ArraySubsetEnumerator.ArraySubsetEnumerator(object[] array, int count) -> void
System.Windows.Forms.ArraySubsetEnumerator.Current.get -> object
System.Windows.Forms.ArraySubsetEnumerator.MoveNext() -> bool
System.Windows.Forms.ArraySubsetEnumerator.Reset() -> void

We need to make it internal again.

Copy link
Contributor Author

@weltkante weltkante left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This iteration tries to limit signature changes to GetEnumerator methods to see whats required to enable foreach picking up the generic signature. Turns out a few classes have problems with that because their GetEnumerator is virtual.

Comment on lines +180 to +186
public void Remove(string value) => TryRemove(value);

bool ICollection<string>.Remove(string value) => TryRemove(value);

private bool TryRemove(string value)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR iteration instead of changing return type of public Remove methods I instead add a private TryRemove method to avoid code duplication so that interfaces can use the same call.

{
int[] indices = new int[Count];
CopyTo(indices, 0);
return indices.GetEnumerator();
return WindowsFormsUtils.GetArrayEnumerator(indices);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the generic IEnumerator<> from an array requires casting the array, going through a helper function to avoid getting the cast wrong. If you prefer inlined casts I can of course remove this helper function.

Comment on lines +277 to +287
public new virtual IEnumerator<Control> GetEnumerator()
{
return new ControlCollectionEnumerator(this);
}

protected override IEnumerator GetEnumeratorCore()
{
return GetEnumerator();
}
Copy link
Contributor Author

@weltkante weltkante Jan 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an attempt to enable foreach picking up the generic enumerator, if you have better suggestions I'm open to try them out.

This particular attempt is adding a second virtual mehtod GetEnumeratorCore on the base class so subclasses can both override and shadow the GetEnumerator method at the same time. Alternatives which do less contortions (e.g. adding a generic parameter to the base class or changing how GetEnumerator is exposed on the base class) would probably be even more breaking to the subclass hierarchy. Since for now we want to explore solutions with minimum breaking changes while still getting the advantages of foreach signatures I chose to explore above workaround.

@@ -536,6 +536,7 @@ public void ColumnHeaderCollection_Clear_Empty_Success()
}

[Fact]
[Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2017:Do not use Contains() to check if a value exists in a collection", Justification = "We are testing the Contains method itself")]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you prefer #pragma warning disable over attributes? Can update if desired.

@@ -15,7 +15,7 @@ public class BindingsCollectionTests : IClassFixture<ThreadExceptionFixture>
public void Ctor_Default()
{
var collection = new SubBindingsCollection();
Assert.Equal(0, collection.Count);
Assert.Empty(collection);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not testing the Count property explicitely so I followed the xunit analyzer and updated the call

@weltkante
Copy link
Contributor Author

Closing PR, changing GetEnumerator signatures is not acceptable due to binary compatibility requirements - it is expected to run 3.x binaries on 5.0 (see issue discussion for alternative approaches)

@weltkante weltkante closed this Jun 2, 2020
@ghost ghost added the draft draft PR label Sep 11, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Feb 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
draft draft PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants