dotnet add package Linq.IndexRange
Proposal: dotnet/runtime#28776
C# 8.0 introduces index and range features for array and countable types. It is natural and convenient to generally support index and range for all IEnumerable types and LINQ. I searched the API review notes, didn't find such APIs, so propose them here.
var element = source1.ElementAt(index: ^5);
var elements = source2.Slice(range: 10..^10); // or ElementIn(range: 10..^10)
var query1 = Enumerable.Range(10..20).Select().Where();
var query2 = (10..20).AsEnumerable().Select().Where();
Index/range are language level features. Currently, they
- work with array (compiled to
RuntimeHelpers.GetSubArray
, etc.) - work with countable type
- do not work with "uncountable"
IEnumerable<T>
types, or LINQ query.
The goals of these LINQ APIs are:
- Use index to locate an element in sequence.
- Use a range to slice a sequence. The usage should be consistent with array, but with deferred execution.
- Use a range to start fluent LINQ query.
This enables index and range language features to work with any type that implements IEnumerable<T>
, and LINQ queries.
LINQ already has ElementAt(int)
and ElementAtOrDefault(int)
operators. It would be natural to have a System.Index
overload: ElementAt(Index)
and ElementAtOrDefault(Index)
, and a new method ElementsIn(Range)
(or Slice(Range)
), so that LINQ can seamlessly work with C# 8.0:
Index index = ...;
var element1 = source1.ElementAt(index);
var element2 = source2.ElementAtOrDefault(^5);
Range range = ...;
var slice1 = source3.ElementsIn(range); // or Slice(range)
var slice2 = source4.ElementsIn(2..^2); // or Slice(2..^2)
var slice2 = source5.ElementsIn(^10..); // or Slice(^10..)
The following Range
overload and AsEnumerable
overload work the same, they convert System.Range
to a sequence, so that LINQ query can be started fluently from there:
var query1 = Enumerable.Range(10..).Select(...);
Range range = ...;
var query2 = range.AsEnumerable().Select(...);
var query3 = (10..20).AsEnumerable().Where(...);
With these APIs, the C# countable[Index]
and countable[Range]
syntax are enabled for sequences & LINQ queries as enumerable.ElementAt(Index)
and enumerable.Slice(Range)
.
For LINQ to Objects:
namespace System.Linq
{
public static partial class Enumerable
{
public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, Index index);
public static TSource ElementAtOrDefault<TSource>(this IEnumerable<TSource> source, Index index);
public static IEnumerable<TSource> ElementsIn<TSource>(this IEnumerable<TSource> source, Range range);
public static IEnumerable<TSource> Slice<TSource>(this IEnumerable<TSource> source, Range range);
public static IEnumerable<TSource> Range<TSource>(Range range);
public static IEnumerable<TSource> AsEnumerable<TSource>(this Range source);
}
}
For remote LINQ:
namespace System.Linq
{
public static partial class Queryable
{
public static TSource ElementAt<TSource>(this IQueryable<TSource> source, Index index);
public static TSource ElementAtOrDefault<TSource>(this IQueryable<TSource> source, Index index);
public static IQueryable<TSource> ElementsIn<TSource>(this IQueryable<TSource> source, Range range);
public static IQueryable<TSource> Slice<TSource>(this IQueryable<TSource> source, Range range);
}
}
The API review process says PR should not be submitted before the API proposal is approved. So currently I implemented these APIs separately Dixin/Linq.IndexRange:
Enumerable
:Queryable
:
Please see the unit tests about how they work.
These proposed APIs can be used by adding NuGet package Linq.IndexRange
:
dotnet add package Linq.IndexRange
If this proposal is doable, I can submit a PR quickly.
Should ElementsIn(Range)
be called Slice
? Currently I implemented both.
ElementsIn(Range)
keeps the naming consistency with originalElementAt(index)
. Might it be natural for existing LINQ users?Slice
is consistent with the countable types, which requires aSlice
method to support range. I preferSlice
for this reason.
If Index is out of the boundaries, for ElementAt(Index)
, there are 2 options:
- Array behavior: throw
IndexOutOfRangeException
- LINQ behavior: throw
ArgumentOutOfRangeException
. My current implementation goes this way, to keepElementAt(Index)
consistent with orginalElementAt(int)
.
If Range goes off the boundaries of source sequence, for Slice(Range)
or ElementsIn(Range)
, there are 2 options:
- Array behavior: Follow the behavior of
array[Range]
, throwArgumentOutOfRangeException
. I implementedElementsIn(Range)
following this way. See unit tests ofElementsIn
. - LINQ behavior: Follow the behavior of current partitioning LINQ operators like
Skip
/Take
/SkipLast
/TakeLast
, do not throw any exception. I implementedSlice
following this way. See unit test ofSlice
.
As @bartdesmet mentioned in the comments, LINQ providers may have issues when they see ElementAt
having an Index
argument, etc. Should we have a new name for the operator instead of overload? For example, At(Index)
or Index(Index)
?
For Range(Range)
and AsEnumerable(Range)
, the question is: what does range's start index and end index mean, when the index is from the end? For example, 10..20
can be easily converted to a sequence of 10, 11,12, ... 19, but how about ^20...^10
?
There are 2 options:
-
The easiest way is to disallow, and throw exception.
-
My current implementation attempts to make it flexible. Regarding
Index
'sValue
can be from0
toint.MaxValue
, I assume a virtual "full range"0..2147483648
, and anyRange
instance is a slice of that "full range". So:- Ranges
..
and0..
and..^0
and0..^0
are converted to "full sequence" 0, 1, .. 2147483647 - Range
100..^47
is converted to sequence 100, 101, .. 2147483600 - Range
^48..^40
is converted to sequence 2147483600, 2147483601 .. 2147483607 - Range
10..10
is converted to empty sequence, etc.
- Ranges
Should this be provided to bridge range to LINQ? For me, (10..20).AsEnumerable().Select().Where()
is intuitive and natural.