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

[Proposal]: Relax Add requirement for collection expression conversions to types implementing IEnumerable #8034

Open
cston opened this issue Apr 4, 2024 · 1 comment

Comments

@cston
Copy link
Member

cston commented Apr 4, 2024

Relax Add requirement for collection expression conversions to types implementing IEnumerable

Summary

The conversion rules for collection expressions were tightened at LDM-2024-01-10 to require target types that implement IEnumerable, and that do not have create method, to have:

An accessible Add instance or extension method that can be invoked with value of iteration type as the argument.

That is a breaking change for collection types where the Add method has a parameter type that is implicitly convertible but not identical to the iteration type. Those types are no longer valid targets for collection expressions. We should relax the recent Add method requirement to address the breaking change.

Additionally, there are collection types where there is no conversion between the Add method parameter type and the iteration type. Those types are valid targets for classic collection initializers but have never been valid for collection expressions. We could consider supporting those types for collection expressions as well.

Examples

There are several categories of collection types that are supported by classic collection initializers that are not supported in collection expressions.

The first two categories were supported before the recent requirement for Add, and these two categories represent the breaking change. The last category has not been supported previously with collection expressions but could be considered.

Category 1: Types that implement IEnumerable but not IEnumerable<T> and have a strongly-typed Add(T).

Example: System.Windows.Input.InputGestureCollection implements IEnumerable but not IEnumerable<T>, and has an Add(InputGesture) but no Add(object).

namespace System.Windows.Input
{
    public sealed class InputGestureCollection : System.Collections.IList
    {
        public int Add(InputGesture inputGesture);
        // ...
    }
}

InputGestureCollection c = [new KeyGesture(default)]; // error: breaking change

Category 2: Types that implement IEnumerable<T> and have a strongly-typed Add(U) where U is implicitly convertible to T.
This is a generic form of category 1.

Example: System.CommandLine.Command implements IEnumerable<Symbol> and has Add() methods for derived types of Symbol but not for Symbol.

namespace System.CommandLine
{
    public class Symbol { /*...*/ }

    public class Argument : Symbol { /*...*/ }

    public class Option : Symbol { /*...*/ }

    public class Command : Symbol, IEnumerable<Symbol>
    {
        public IEnumerator<Symbol> GetEnumerator();
        public void Add(Argument argument);
        public void Add(Option option);
        public void Add(Command command);
        // ...
    }

    public class RootCommand : Command { /*...*/ }
}

RootCommand c = [new Argument()]; // error: breaking change

Category 3: Types that implement IEnumerable<T> and have a strongly-typed Add(U) where U and T are unrelated.

This category is distinctly different from the previous two categories since it has not been supported previously with collection expressions.

Example: Xunit.TheoryData<T> implements IEnumerable<object[]>, and has an Add(T) but no Add(object[]).

namespace Xunit
{
    public abstract class TheoryData : IEnumerable<object[]>, IEnumerable
    {
        // ...
    }

    public class TheoryData<T> : TheoryData
    {
        public void Add(T p);
    }
}

TheoryData<string> d;
d = ["a", "b", "c"]; // error
d = [default];       // ok?

Background

This issue affects target types that implement IEnumerable and do not have create methods. The conversion requirements for such types were modified in LDM-2024-01-10, and those changes were implemented in 17.10p1.

In 17.8, conversion simply requires the target type to implement non-generic IEnumerable, and does not require an Add method:

An implicit collection expression conversion exists from a collection expression to the following types:

  • ...
  • A struct or class type that implements System.Collections.IEnumerable.

The implicit conversion exists if the type has an iteration type U where for each element Eᵢ in the collection expression:

  • If Eᵢ is an expression element, there is an implicit conversion from Eᵢ to U.
  • If Eᵢ is an spread element Sᵢ, there is an implicit conversion from the iteration type of Sᵢ to U.

Even though an Add method is not required for conversion, an Add method is required for construction of the collection instance. That requirement is checked after determining convertibility, and the requirement is that for each element there is an applicable Add method that can be called with that expression:

  • The constructor that is applicable with no arguments is invoked.

  • For each element in order:

    • If the element is an expression element, the applicable Add instance or extension method is invoked with the element expression as the argument. (Unlike classic collection initializer behavior, element evaluation and Add calls are not necessarily interleaved.)
    • If the element is a spread element then ...

In 17.10p1, conversion was updated to require the constructor and also to require an Add method callable with an argument of the iteration type. For construction, the requirements were unchanged from 17.8. The updated conversion requirement:

An implicit collection expression conversion exists from a collection expression to the following types:

  • ...
  • A struct or class type that implements System.Collections.IEnumerable where:
    • The type has an applicable constructor that can be invoked with no arguments, and the constructor is accessible at the location of the collection expression.
    • If the collection expression has any elements, the type has an applicable instance or extension method Add that can be invoked with a single argument of the iteration type, and the method is accessible at the location of the collection expression.

The implicit conversion exists if the type has an iteration type U where for each element Eᵢ in the collection expression:

  • If Eᵢ is an expression element, there is an implicit conversion from Eᵢ to U.
  • If Eᵢ is an spread element Sᵢ, there is an implicit conversion from the iteration type of Sᵢ to U.

There were two motivations for the additional Add method requirement:

  1. Reduce the number of IEnumerable implementations that are considered valid target types for collection expressions but which fail to bind successfully. This is important for overload resolution to avoid unnecessary ambiguities.
  2. Align with the params collections preview feature which has the additional requirement to allow validating the params type at the method declaration rather than only at call sites.

For params collections, the requirements for the preview feature are:

The type of a parameter collection shall be one of the following valid target types for a collection expression:

  • ...
  • A struct or class type that implements System.Collections.IEnumerable where:
    • The type has a constructor that can be invoked with no arguments, and the constructor is at least as accessible as the declaring member.
    • The type has an instance (not an extension) method Add that can be invoked with a single argument of
      the iteration type,
      and the method is at least as accessible as the declaring member.

Proposal

Relax the requirement for conversion to require an instance or extension Add method that can be invoked with a single argument, but without requirements on the method parameter type.
For construction, there are no changes.

For collection expression conversions, the proposed change:

An implicit collection expression conversion exists from a collection expression to the following types:

  • ...
  • A struct or class type that implements System.Collections.IEnumerable where:
    • The type has an applicable constructor that can be invoked with no arguments, and the constructor is accessible at the location of the collection expression.
    • If the collection expression has any elements, the type has an instance or extension method Add where:
      • The method can be invoked with a single argument.
      • If the method is generic, the type arguments can be inferred from the collection and argument.
      • The method is accessible at the location of the collection expression.

The implicit conversion exists if the type has an iteration type U where for each element Eᵢ in the collection expression:

  • If Eᵢ is an expression element, there is an implicit conversion from Eᵢ to U.
  • If Eᵢ is an spread element Sᵢ, there is an implicit conversion from the iteration type of Sᵢ to U.

The proposed change would resolve the breaking change in 17.10p1 and allow types in category 1 and 2 to be used as collection expression target types.
Types in category 3 would still not be valid target types, unchanged from 17.8.

For params collections, there is a corresponding proposed change:

The type of a parameter collection shall be one of the following valid target types for a collection expression:

  • ...
  • A struct or class type that implements System.Collections.IEnumerable where:
    • The type has a constructor that can be invoked with no arguments, and the constructor is at least as accessible as the declaring member.
    • The type has an instance (not an extension) method Add where:
      • The method can be invoked with a single argument.
      • If the method is generic, the type arguments can be inferred from the argument.
      • The method is at least as accessible as the declaring member.

Alternate (extended) proposal

We could extend the proposal above to remove the requirement that each element in the collection expression is implicitly convertible to the iteration type for these types. (We would only remove this requirement for types that implement IEnumerable and do not use a create method.)

For collection expression conversions, the alternate proposed change:

An implicit collection expression conversion exists from a collection expression to the following types:

  • ...
  • A struct or class type that implements System.Collections.IEnumerable where:
    • The type has an applicable constructor that can be invoked with no arguments, and the constructor is accessible at the location of the collection expression.
    • If the collection expression has any elements, the type has an instance or extension method Add where:
      • The method can be invoked with a single argument.
      • If the method is generic, the type arguments can be inferred from the collection and argument.
      • The method is accessible at the location of the collection expression.

The implicit conversion exists if the type has an iteration type U where for each element Eᵢ in the collection expression:

  • If Eᵢ is an expression element, there is an implicit conversion from Eᵢ to U.
  • If Eᵢ is an spread element Sᵢ, there is an implicit conversion from the iteration type of Sᵢ to U.

The alternate proposal would allow types in category 1, 2, and 3 to be used as collection expression target types.

The alternate proposal would however have implications that would need to be understood and addressed. For example, the following would now become ambiguous without further changes.

F([1, 2, 3]); // error: ambiguous

void F(List<int> list) { }
void F(List<string> list) { }

Absent further work, this would be a breaking change. As this is a scenario we want to keep working, this would need be carefully considered and designed, and we are not proposing making this change in 17.10.

Meetings

Issues

cston added a commit that referenced this issue Apr 5, 2024
Add #8034 to meeting schedule.
@cston
Copy link
Member Author

cston commented Apr 16, 2024

I've re-labeled the "Extended proposal" section to "Alternate (extended) proposal" to make it clearer it is an alternative and not the main proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant