-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[API Proposal]: Generic Enumerable.Range method #97689
Comments
Tagging subscribers to this area: @dotnet/area-system-linq Issue DetailsBackground and motivationSo that Enumerable.Range can be used with anything that implements API Proposalpublic static class Enumerable
{
public static IEnumerable<T> Range<T>(T start, T end) where T : IBinaryInteger<T>
} API Usagepublic record struct MyInterval<T> where T : IBinaryInteger<T>
{
public T Start { get; }
public T End { get; }
// ...
public IEnumerable<T> GetAll() => Enumerable.Range(Start, End); Alternative DesignsNo response RisksNo response
|
@eiriktsarpalis this seems reasonable overall to me. There's potentially a desire to support more than just It may even be desirable to allow users to specify a custom step (that is, give a range from |
This could also be a source-breaking change, right? e.g. today if I have: IEnumerable<int> e = Enumerable.Range((byte)0, (byte)5); and then we add the proposed API, that will fail to compile because it won't be able to cast the resulting |
Yes, but that is also a simple fix and the compiler is (by default) already raising a diagnostic saying the cast is redundant. |
Not if it were instead: byte start = 0, count = 5;
IEnumerable<int> e = Enumerable.Range(start, count); Not saying it's a deal-breaker, simply pointing out that this has more source-breaking consequences than come with typical static API additions. |
.... that case is likely too simple, anyhow, since the |
Let me update the proposal with the suggestions: Including all numbers (I think for floating point numbers, it would make sense to provide the increment) as @tannergooding suggest: public static class Enumerable
{
public static IEnumerable<T> Range<T>(T start, T count, T increment) where T : INumber<T>
} I believe, this API would not be a breaking change anymore. I will also capture the concern of breaking change by @stephentoub in the proposal. |
Seems reasonable, we can try reviewing it. |
On second thought, I find it kind of odd that we have a generic
My personal preference would be for option (2) since it's consistent with similar APIs available in Matlab, Python, or F#: public static class Enumerable
{
public static IEnumerable<T> Range<T>(T start, T end, T step) where T : INumber<T>
{
// TODO determine if end is exclusive or inclusive upper bound
for (T current = start; current <= end; current += step)
{
yield return current;
current += step;
}
}
} I suspect this also implies that we should adopt a different name for the method, since it changes the semantics of the second parameter when compared to the existing |
Yes, I changed from |
To share additional information from community implementations, there is: |
.... given that Note that we should make it safe to use negative |
I would expect end to be Exclusive end, overall, makes the code simpler and more intuitive for a large number of scenarios and makes it work more naturally with arrays, ranges, indexing, and other expressions that are typical for .NET developers. It also typically makes the code cheaper to actually execute and to do other common computations such as determining the length (which is simply It could also simply be |
Should the constraint be |
That seems a bit counterintuitive though, in most cases I'd have to run a division in my head to determine the appropriate
We could, but it in my view complicates the API signature without a clear use case. Can you define intervals in types that do not conform with the |
It may be counter-intuitive to what's desired, but I think the alternative is counter-intuitive to the current API. For example, if I look at the following code: var seq1 = Enumerable.Range(2, 3);
var seq2 = Enumerable.Range(2, 3, 1); What reason do I have to expect that Perhaps a different name would be helpful if [start..end) is the desired sequence of values? |
Correct, which is why using a different name might be necessary. |
You're likely going to end up doing some mental arithmetic no matter what you do and I imagine that many usages will include some attached comment like The |
Proposalmaybe this? public static class Enumerable
{
public static IEnumerable<T> Range<T>(T start, int count) where T : INumber<T>;
public static IEnumerable<T> Range<T>(T start, int count, T step) where T : INumber<T>;
public static IEnumerable<T> Sequence<T>(T start, T end, T step) where T : INumber<T>;
} |
If you have an |
I do agree, maybe |
The only thing I don't like about the two methods approach, is that intuitively I would expect that |
public static class Enumerable
{
public static IEnumerable<T> Range<T>(T start, int count) where T : IBinaryInteger<T>
} |
This isn't going to make it to .NET9, right? |
That's right, this now falls into the .NET 10 bucket. |
Any chance this could be considered alongside #64031? |
I think they could be kept separate. |
For consistency it's sometimes better to lump things together in order to ensure we produce a solution that makes sense across the board. For cases like this, where we can add things piecemeal without complexity explosion, keeping them separate helps to get easy stuff done without being held hostage by harder work. |
I'm late to the party but technically |
Yes! I can rewrite the implementation to such a public API. It must first be changed and approved. |
It is not necessarily always ideal to make it the least constrained possible. It is largely intentional that the approved shape is only: public static IEnumerable<T> Range<T>(T start, int count) where T : IBinaryInteger<T> This is in part because what this does can be non-sensible for other types, such as API review discussed exposing additional less constrained APIs in the future, such as some |
If one is multiplied by the number of steps and added to the start, then the implementation will count correctly for any numbers.
The only additional thing the current implementation does is check for no range overflow. |
This will not do the right thing in all cases and it may be unexpected in others. It may be unexpected for a user to see It was very explicitly intentional that the API was approved in the shape it was. We can relax this in the future if we decide that is the best case scenario. But that would only occur after fully considering the rest of the scenarios, what it means for arbitrary types, etc. |
I fully support an extension to more general types and a step argument. This will allow the method to be used for |
Likewise you can have The only real difference between |
Background and motivation - UPDATED 2024/01/31
So that Enumerable.Range can be used with anything that implements
IBinaryInteger<T>
.API Proposal
API Usage
Alternative Designs
An alternative could be to handle all
INumber<T>
, in this case having a 3rd increment parameter could be reasonableRisks
The original proposal with 2 parameters overload for the
Range
method could be source-breaking, as the following case returns anIEnumerable<int>
and notIEnumerablem<byte>
today:The text was updated successfully, but these errors were encountered: