A Range ..
operator is proposed for C# 7.3. This tracks compiler work for that feature.
Proposals:
See also some discussion elsewhere
..
Range operator ..(int, int)
LongRange operator ..(long, long)
``` c#
namespace System
{
public struct Range
{
public Range(int left, int right) ...
}
public struct LongRange
{
public LongRange(long left, long right) ...
}
}
- [ ] Supported in `Span<T>` and `ReadonlySpan<T>`?
``` c#
Span<T> x = ...;
Span<T> y = x.Slice([1..0]);
ForEachToFor
refactoring should recognize range syntax1..
..10
..
..
for half-open and and ...
for closed..<
for half-open and ...
for closed a[1..10]
?Would a type be able to accept a Range
as an indexer argument, e.g. foo[1..10]
? Personally, from an API/syntax point of view I think that's what I'd prefer for slicing arrays, strings or spans.
Would also be nice for the compiler to know how to optimize away enumerating over a range, so that:
foreach (var number in 0..<10) { ... }
would be translated to:
for (var number = 0; number < 10; number++) { ... }
Would a type be able to accept a Range as an indexer argument, e.g.
foo[1..10]
?
Yes - to me, this is 95% of the purpose of this feature.
We had two options to support slicing:
x..y
syntax (..
would sort of become something like the comma in a method call - very built-in to the calling of the operator itself).x..y
produces an ordinary struct (perhaps called Range
), then perform plain-old API changes to add a Range
overload to indexers on relevant types.The second is what we went with: it's clear to see that the Range
type can be used outside of pure slicing scenarios, and it's useful to make it be a distinct type (e.g. supporting more complex slicing scenarios involving manipulation of Range
objects - for example, an array of Range
, for slicing an n-dimensional array). So, of course it will be supported as an indexer overload - it's the primary use case driving this feature.
Would also be nice for the compiler to know how to optimize
That would indeed be cool! Before looking into doing this, though, I would want to look into how the JIT performs on a plain Range
struct (with a pattern-based struct enumerator), and see how much worse it is than a plain for loop. That performance test can actually be done right now, though - it'd be super useful information to have!
@HaloFour
Would also be nice for the compiler to know how to optimize
I would expect Range
to both implement IEnumerable<int>
and also implement the IEnumerable
pattern using a value type. The latter is generally what permits the runtime to optimize it; the C# compiler then doesn't need to do anything.
minor nitpick: Rust is changing the syntax for closed intervals from ...
to ..=
@khyperia @gafter
I whipped up a quick "benchmark" and I'm seeing that a struct enumerator over a range struct being about 4x slower in 64-bit release mode outside of any debuggers. I'm sure that someone could write a better benchmark but I doubt that you could ever actually approach a raw loop short of the JIT completely converting the range enumeration to one.
I look at a Range
type as being more to support a language feature of ranges and I'd rather it serve more as a "dumb" container that the language could optimize away where it could. This would be similar to how C# treats tuples.
Also, this has come up before in some other proposals but I don't see it mentioned in the ones linked, but it would be nice to support the range syntax as a pattern, too:
int value = GetValue();
if (value is 0..10) { }
var person = GetPerson();
switch (person) {
case Student { Gpa is 3.0..4.0 }:
...
break;
}
3.0..4.0
I don't know what would it take to support that (in literals). From the links, only int and long are considered.
@alrz
I don't know what would it take to support that. From the links, only int and long are considered.
True, and if it's limited to just those types that is fine, but it would be nice if the operator support could be extended to any comparable type. I'm more making the case to allow the range syntax to be used in patterns. The supported types should be a separate conversation altogether.
It's easily practical in patterns -- you won't be able to pass it around so it doesn't need an underlying type. But for range literals there is obviously a need for a data structure.
@alrz
But for range literals there is obviously a need for a data structure.
Definitely. Could be as simple as:
public struct Range<T> : IEnumerable<T>
where T : IComparable<T>
{
public readonly T Start;
public readonly T End;
public Range(T start, T end) => (Start, End) = (start, end); // look ma, no tuples!
public RangeEnumerator GetEnumerator() => new RangeEnumerator(this);
// other members of IEnumerable<T> and IEnumerable here
public struct RangeEnumerator : IEnumerator<T> {
// implementation of IEnumerator<T>, IEnumerator and IDisposable here
}
}
Consider it just spaghetti against the wall, and I wouldn't want it to slow/derail the conversation about integral ranges, but I think it's worth having the conversation.
@HaloFour IComparable
isn't really enough here, for this to be generic, we'll need generic numeric operations. I think this is another use case that could be fed into the traits proposal.
@alrz
That's true, you'd need that support to handle any enumeration. But the structure could still be used for comparing values. Anyway, that's a conversation that can be shelved until later.
it would be nice to support the range syntax as a pattern, too
c#
object o = 3..10;
Console.WriteLine(o is 3..10); // prints True or False?
o = 5;
Console.WriteLine(o is 3..10); // prints True or False?
I'd say a range shouldn't be pattern compatible with object
, only numeric types. so an error in both cases is the least harmful result and removes a lot of these edge cases.
I think the Range operator should produce a structure that is just to values. So that the checking, if value is within (inclusive upper and lower bounds) is then lowered to a simple(ch >= 'A') & (ch <='Z')
, rather as an enumerable this enumerated to test is a value is present.
```c#
var ch = 'X';
var IsInAtoZ = ch == 'A'..'Z';
It can implement IEnumerable, secondarily.
```c#
struct Range<T : IComparable<T>> : IEnumerable<T>
{
public readonly T Limit0;
public readonly T Limit1;
public static bool ==( Range<T> x, T y ) => (x.Limit0 <= y) & (y <=x.Limit1);
public static bool !=( Range<T> x, T y ) => !(x == y);
public static bool ==( T x, Range<T> y ) => (y == x);
public static bool !=( T x, Range<T> y ) => (y != x);
public RangeEnumerator<T> GetEnumerator()
{
}
}
@gafter wrote:
, or start and count?
To me, that is a span not a range.
New token
..
I'd make it ...
(ellipses), also could be contracted using typographic ligatures in the ide / font.
Example of an open range (via IEnumerable) to also supply the index.
Eg Zip( xs, 0... , (x,idx)=>(x,idx))
I know it could be done with Select( xs, (x, idx)=>(x,idx) );
.
Decide on some underlying special name
What at OpDotDotDot
?
the checking, if value is within (inclusive upper and lower bounds) is then lowered to a simple(ch >= 'A') & (ch <='Z'), rather as an enumerable this enumerated to test is a value is present
I don't think anyone had considered the possibility that we would test for inclusion in a range by enumerating the elements and testing against each one. Thank you for that perspective.
Re range literals:
There is a high chance that one wants a range beyond int
and long
. Like char
. To not require a separate Range
type for each, I propose we use the generalized foreach
support (https://github.com/dotnet/csharplang/issues/1085).
struct Range<T> { }
struct InclusiveRange<T> { }
struct RangeWithStep<T> { }
static LongRangeEnumerator GetEnumerator(this Range<long> range) { ... }
static CharRangeEnumerator GetEnumerator(this Range<char> range) { ... }
static Int32RangeEnumerator GetEnumerator(this Range<int> range) { ... }
// etc
var x = 3..10;
// ->
var x = new Range<int>(3, 10);
This decouples the enumeration logic so that it can be extended with arbitrary types.
Traits are probably the best way to model this. For example, you can't have a DateTime range here because it needs a different Step
type. but I think no one really wants this post-8.0 :)
Re range patterns:
We could permit matching against any IComparable<T>
where T
is the type of bounds.
bool Match(object o) => o is 1...3;
bool Match(object o) => o is IComparable<int> t && t.CompareTo(1) >= 0 && t.CompareTo(3) <= 0;
bool Match(IComparable<int> o) => o is 1...3;
bool Match(IComparable<int> o) => o.CompareTo(1) >= 0 && o.CompareTo(3) <= 0;
Trivial cases like int
, long
, char
could be optimized though.
o is 3..10
Thinking more about this, half-open ranges probably shouldn't be permitted in patterns, otherwise it could be easily mistaken with closed ranges.
@alrz Re: extension methods, that seems like a great idea! It also meshes really nicely with this great way to declare Range creation overloads for custom types (right now it seems kind of clunky to me):
https://twitter.com/lambdageek/status/931562022159781888
Is this only for builtin number types or any type with appropriate overload ? (My preference is that it should be syn sugar for
lhs.EnumUpTo(rhs)
which can be an extension method for Int32, Double, MyInfinitePrecisionInt, etc)
My only worry would be compiler performance - I forget how fast extension method lookup is.
What if we supported this syntax for open/closed:
````
foreach(var x in 0..3) // Default. 0,1,2
foreach(var x in [0..3)) // Same as above
foreach(var x in [0..3]) // 0,1,2,3
foreach(var x in (0..3]) // 1,2,3
foreach(var x in (0..3)) // 1,2
````
@TonyValenti
I don't like the idea that x
and (x)
would mean two different things, nor that brackets would have a significant meaning outside of an indexer where ranges are most likely to be found.
What if we supported this syntax for open/closed:
That could be significantly difficult for editors to match these up.
@TonyValenti @CyrusNajmabadi
It would be possible if we had different operators [[
[(
. )]
, ]]
. note these are single operators not operator pairs.
```
[[0..9]] // inclusive, inclusive
[[0..9)] // inclusive, exclusive
[(0..9]] // exclusive, inclusive
[(0..9)] // exclusive, exclusive
````
We can't have those. Because [( is already two tokens today. Necessary for things like this[(x + y)]
var IsInAtoZ = ch == 'A'..'Z';
I'd much rather have an 'in' operator than using == to relate a scaler to a range of values.
@CyrusNajmabadi I'm hoping that operator will be is
, e.g. var IsInAtoZ = ch is 'A'..'Z';
Hrmm... personally, not a fan (maybe it would grow on me...). "in" seems so perfect. it's already a keyword. And it expresses so well the concept here. Any reason to prefer 'is' over 'in'?
@CyrusNajmabadi Yes, because I'd like it to work everywhere a pattern is permitted, not just in a simple test in an expression, e.g. I'd like to permit case 'A'..'Z':
, and in recursive patterns.
I guess i'm not seeing why you would not be able to do that if you used 'in' as the keyword for the specific binary op test. i.e. you could have x in y..z
as well as: switch (x) { ... case y..z:
. They're complimentary afaict.
i.e. we can say "in the context of a switch pattern, or a recursive pattern, matches of a scaler against a range will be done with 'in' semantics".
I think that's because case
and is
both accept patterns so if we have a range pattern it's expected to work in both. Excluding it just in is
would complicates the grammar.
Enh. :)
It can be fine gramatically, but disallowed semantically. Like i said above, i have no problem with it in a 'case clause', or in a recursive pattern. We could totally just disallow "is a..z", and just tell the user "use 'in'". I agree it's not the most consistent from a language perspective. But it does fit the actual terminology of 'is' and 'in'. i.e. 'is' feels like the way to test a scaler against a scaler. 'in' does not. I don't think one has to reuse the exact same syntax everywhere just because its grammatically convenient and because it can be given meaning. I'd far prefer less reuse here, and better matching from what is typed and the intuition about what it means.
BTW, my original question has been answered:
Any reason to prefer 'is' over 'in'?
Specifically, the reason is so that this construct can just be a pattern that immediately slots in to all the other places we have patterns today. IMO, while this is convenient, it's not the best choice for the language or end user. I think it would be fine to allow it to be a pattern, but go for a special syntax that fits more cleanly with the semantics of what is going on here.
Anyways, that's just my 2c. Take it as you will :)
I'd say we should have used e as var x
instead of e is var x
for the exact same reason, plus, it doesn't have to return a boolean, as an alternative for https://github.com/dotnet/csharplang/issues/973, but it's a consequence of var x
being a pattern and nobody bothered to disallow it in an is
expression.
That's fair. But at least there it was scaler->scaler so i didn't mind much. For e is {} x
it makes sense to me. Effective "is the computed value of 'e' null or not? if not, then put that value into x". Since there is no actual check with e is var x
i agree that 'is' is a bit wonky. But i personally think 'is' for ranges is wayyyy wonkier :)
Shrug, I personally think that if (x is 0...10)
reads pretty nicely, and it fits in so easily with the rest of the pattern matching syntax without any special accommodations. It would be immediately applicable with recursive patterns, e.g. if (pt is Point(0...10, var x))
or if (person is Student { Gpa is 3.0...4.0 })
.
It's likely a personal preference issue.
if (x is 0...10)
reads pretty nicely
To me, that reads "if 'x' is the range 0-10", not, "if x is in the range of 0 to 10".
It would be immediately applicable with recursive patterns, e.g. if (pt is Point(0...10, var x)) or if (person is Student { Gpa is 3.0...4.0 })
I would be fine with 0..10 being a pattern and working fine for the recursive case of "Point(0...10, var x)", and i think it would read fine to say if (person is Student { Gpa in 3.0...4.0 })
--
Note, i'm warning to "is" being used. i think i need to change from reading it as "if 'x' is the range 0 through 10" to "if 'x' is between 0 through 10". In that sense there's an implicit "between" word. But i could grow to read it as such without too much effort i think.
if (x is 0...10)
It does also read to "x is the range 0 to 10".
Using in
to mean is in the following range
, as it used in Linq Query Expressions.
c#
var xs = from x in [0...10] select x;
I think for βin the rangeβ we should use IN and not IS.
It would be really nice if there was an IN keyword that would basically do
a βContainsβ. If you have an array or collection it would operate as
normal or If you have the range, it could do the math comparison.
On Tue, Nov 21, 2017 at 9:14 PM Adam Speight notifications@github.com
wrote:
if (x is 0...10)
It does also read to "x is the range 0 to 10".
β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/dotnet/roslyn/issues/23205#issuecomment-346231013,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AM-qVkS_R-eZO8H2oTNv7fd3rrWfF5vwks5s45GkgaJpZM4QfyEc
.>
Tony Valenti
Since is
has generally been used to type-check I'm really used to it being used that way.
var x = i in 1..10
should check if i
is inside the range
and
var x = i is 1..10
should check if i
is a range that goes from 1 to 10
Everything else would be pretty confusing. Am I missing something?
Hey all, chiming in from the reddit thread here with a few questions:
[x..y]
, can there ever be x > y?This would be useful when iterating, since you could have something like:
``` c#
foreach(int i in [5..1]) {
...
}
and have the `foreach` loop iterate from 5 down to 1. However, this makes very little sense for specifying a slice for a Span, but I'm assuming the slice method would simply throw an `InvalidOperationException` if x > y.
2. Open ended spans:
Open ended spans would be really useful for avoiding the entire `var slice = arr[5..arr.Length-1]` issue with `var slice = arr[5..]`. It would also make `Substring()` really nice: `"the quick brown fox".Substring([..5])`, and generally avoid needing to do `.Length -1` a lot.
It could map to another struct, something like `UnboundRange`, so as to not increase the implementation complexity of things that only require a bound range:
``` c#
struct UnboundRange: IEnumerable<int>
{
public UnboundRange(int? start, int? end);
public int? Start { get; }
public int? End { get; }
public static implicit operator UnboundRange(Range range) => new UnboundRange(range.Start, range.End);
...
}
Extra LINQ extensions could then be added to handle .Contains()
, .Intersect()
, etc for UnboundRange
.
in
, this will be an extension to the current C# Linq Query Expressions that "under the hood" translates into the relevant Contains()
extension?Just want to throw in my 2 cents regarding ..
vs. ...
vs. other proposals.
The ...
operator is the first thing that occurred to me for the inclusive version. And I immediately disliked it, because of similarity to ==
and ===
in JS, which causes no end of headaches. Yes, it seems easy enough, but just look at how often it's done wrong in practice.
IMO, 0..10
without any prior knowledge looks like it's supposed to include 10. So I would prefer the ..<
operator for the exclusive case, to make it more obvious. Whether the inclusive case is ..
. or ...
doesn't matter, so long as the distinction is obvious.
@gafter Please link https://github.com/dotnet/corefx/issues/25220 too. This is the Range<T>
API proposal.
..
and ..<
are nice to me. They cover both important use cases, and have a reasonably intuitive syntax. I know i def want 'inclusive' in some cases i.e:
c@
switch (c)
case 'a'..'z': // i def don't want to write: case 'a'..'{':
but there's also a strong need for exclusive give how many things are goign to go from 0...len(somethingElse)
.
I've already posted an API proposal about Range
implementation, but unfortunately it wasn't linked to the issue until yesterday.
At first I think that would be better to have a generic one instead Range
, LongRange
, etc. Why so? Because ranges can be used not only for spans and integers, but also for math computations. So there are two new types of ranges should be DoubleRange
and FloatRange
. Already four types. But wait! We also need a range of char
and probably Utf8Char
which will come later from the CoreFxLab repository. Six types! Also do not forget about DateTime
, Date
and Time
(yep, they will come to CoreFx soon). For strings ranges could be applied too (for example when you are using sorted sets of strings).
Having a new range type for each value type is a bad idea. It would be better to have a common generic type. And it doesn't matter what is T
. The only one restriction is that it should be comparable.
Please read conversations in https://github.com/dotnet/corefx/issues/25220 and in https://github.com/dotnet/corefxlab/pull/1859. Thanks.
I think that range should be half-open because in .NET API the end means the next value after the last. Also it's possible to compute length for that kind of ranges if its values is floating points or something else which can not have length. See https://github.com/dotnet/corefx/issues/25220#issuecomment-344253048 and https://github.com/dotnet/corefxlab/pull/1859#issuecomment-343304390.
If closed intervals are needed too, then
From the perspective of Range<T>
not all ranges can support enumerations. Probably that thing should be done by the compiler for now because the compiler can not use GetEnumerator
extension methods unfortunately. See https://github.com/dotnet/roslyn/issues/23205#issuecomment-344797249.
foreach (var i in 1..10) { }
// should be translated to
for (var i = 1; i < 10; i++) { }
Absolutely yes! The idea is to have a syntax sugar for slices in that way (https://github.com/dotnet/corefxlab/issues/1306):
Span<byte> buffer = ...
Span<byte> view1 = buffer.Slice(3, 2);
Span<byte> view2 = buffer[3..4];
// view1 == view2
Why not Slice([3..4])
? Because it's longer than Slice(3, 2)
.
That thing perfectly fits the following scenario:
// There is no start value, only the end one.
Range<int?> range = ..10;
range.Contains(-20); // Yes
range.Contains(0); // Yes
range.Conatins(20); // No
But that should work only if values are nullable types.
@CyrusNajmabadi what if in
were part of the pattern?
if (ch is in βaβ..βzβ)
case in βaβ..βzβ:
case Point(in 0..10, var y)
ch switch (in βaβ..βzβ => Letter, in β0β..β9β => Digit, _ => Other)
I've added support for null
values (https://github.com/dotnet/corefxlab/pull/1859/commits/02a8d039db3cab90b03a40fd96e791af58441c24). So now Range<T>
works even for Nullable<T>
, but it treats null
as an special value. Therefore Contains(null)
will return false
always.
It's possible to support any kind of ranges (close, open, half-open with inclusive start or end), but I'm not sure that we need them all because of performance reasons. If that would be implemented then factories should be used instead of constructors:
public readonly struct Range<T>
{
private const int InclusiveFromFlag = 0x01;
private const int InclusiveToFlag = 0x02;
private readonly int _flags;
private readonly T _from;
private readonly T _to;
public T From => _from;
public T To => _to;
public bool InclusiveFrom => _flags & InclusiveFromFlag != 0;
public bool InclusiveTo => _flags & InclusiveFromTo != 0;
public static Range<T> Inclusive(T from, T, to) { throw null; } // [from, to]
public static Range<T> InclusiveFrom(T from, T, to) { throw null; } // [from, to)
public static Range<T> InclusiveTo(T from, T, to) { throw null; } // (from; to]
// etc.
}
Mostly it's possible to convert ranges of a different kind to each other by adding or subtracting epsilon to/from the start or the end of the range. For example:
// for integers
(-2, 2) == [-1, 1] == (-2, 1] == [-1, 2)
As I said before, it's better to have half-open ranges.
Will the C# syntax support an expression as the to/from?
var staff = GetStaff();
var range = 0..(staff.Count - 1);
@JamesNK I imagine that would get translated by the compiler to var range = new Range<int>(0, staff.Count - 1);
@gafter Why simply not if (ch in βaβ..βzβ)
?
@YohDeadfall asked and answered.
@gafter Yep, my mistake. Rereading the thread helped me.
I thought a bit more about ranges. Making them inclusive makes sense for most types because it's intuitive as @CyrusNajmabadi said. So it isn't a very good idea to use null
s to include maximum values because of castings from T?
to T
, performance and syntax. Also it would allow to use IComparable<T>
constraint on T
and would allow to include null
as an regular value which is important when T
is a reference type like string
.
The only one thing which isn't fit well is time (https://github.com/dotnet/corefxlab/pull/1859#issuecomment-343304390).
Just to chime in on the subject of the exclusive upper bound, that's really important if they are to be used generically, such as
Range<DateTime>
. Consider two meetings back-to-back. The first goes from 1:00 to 2:00. The second from 2:00 to 3:00. Which meeting is going on exactly at 2:00? Most people would generally say the second one, because the first meeting has ended. In other words,[1:00, 2:00) [2:00, 3:00)
is the only rational way to think about ranges of time.
For floating-point types the following length computation is probably acceptable:
var length = range.To - range.From + double.Epsilon;
Perhaps instead of a base class it should be an interface with a few
implementations. I agree with you on Time and it would also be Nice if it
was possible to combine ranges. For example:
Var ranges = Range.combine(0..9, 11..21,30..37);
And still use βis inβ for pattern matching and such. Also, as an interface,
we can allow the implementor to handle what operations like βcontainsβ and
βget enumeratorβ means.
On Fri, Nov 24, 2017 at 10:50 AM Yoh Deadfall notifications@github.com
wrote:
I thought a bit more about ranges. Making them inclusive makes sense for
most types because it's intuitive as @CyrusNajmabadi
https://github.com/cyrusnajmabadi said. So it isn't a very good idea to
use nulls to include maximum values because of castings from T? to T,
performance and syntax. Also it would allow to use IComparable
constraint on T and would allow to include null as an regular value which
is important when T is a reference type like string.The only one thing which isn't fit well is time (dotnet/corefxlab#1859
(comment)
https://github.com/dotnet/corefxlab/pull/1859#issuecomment-343304390).Just to chime in on the subject of the exclusive upper bound, that's
really important if they are to be used generically, such as
Range. Consider two meetings back-to-back. The first goes from
1:00 to 2:00. The second from 2:00 to 3:00. Which meeting is going on
exactly at 2:00? Most people would generally say the second one, because
the first meeting has ended. In other words, [1:00, 2:00) [2:00, 3:00) is
the only rational way to think about ranges of time.For floating-point types the following length computation is probably
acceptable:var length = range.To - range.From + double.Epsilon;
β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/dotnet/roslyn/issues/23205#issuecomment-346867738,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AM-qVog5okOgKmkxQcOuHGNewN6-Vwcgks5s5vPDgaJpZM4QfyEc
.>
Tony Valenti
case Point(in 0..10, var y)
Now that looks like a ref readonly
argument π
If we are looking at extending this feature to VB also my I suggest xx To yy
Dim range0 As Range<Int> = 0 To 10 ' Inclusive, Inclusive
Dim range1 As IEnumerable<Int>= From 0 To 10 ' Inclusive,Inclusive
As it aligns to existing usages, For Loop
, and Case 0 To 10
.
@TonyValenti Classes and interfaces would lead to heap allocations, so struct should be used. But combining of ranges is possible.
public readonly struct Range<T>
{
private readonly T _from;
private readonly T _to;
private readonly Continuation _rest;
public T From => _from;
public T To => _to;
public Range<T>? Rest => _rest == null ? default : new Range<T>(_rest._from, _rest._to, _rest._rest);
private sealed class Continuation
{
internal readonly T _from;
internal readonly T _to;
internal readonly Continuation _rest;
}
}
From https://github.com/dotnet/corefxlab/pull/1859#issuecomment-347247889:
The language design at this time does not intend to support a generic Range type. Rather we are focusing on concrete types and operator extensibility to support additional ranges.
Do not think we should take this change as it's going the opposite direction as the language / compiler.
Well that's disappointing. What's direction is the language / compiler going towards for ranges?
@alrz
The types used in the range expression should use the type compatibility rules that the same one as used for addition and subtraction. (If a _"number type"_ ). Which could be done at the binding stage.
As using the same type for step
as that is for the start
and finish
can cause problems. eg
c#
uint x = 10;
uint y = 0;
int s = -1;
range r = [ x .. y : s ] ; // 10 To 0 Step -1
Also enumerating the range of values, should never throw an exception. As the value approaches the MinValue
and MaxValue
of a type, if shouldn't underflow or overflow, or throw an exception.
This then would allow the full range of values of a type to expressed.
eg [Byte.Min .. Byte.MaxValue]
has 256 values not 255.
An solution for the Clusivity problem of the value is give the programmer the option of prefixing it with an attribute, with the assumed default being (In, Ex)
c#
var r = [range(Clusivity.In, Clusivity.In)] 0 .. 10 // 0, 1,2,3,4,5,6,7,8,9,10
Another solution, is subtle change to the operator.
| LB | UB | Op | Use | Range Values |
| -- | -- |---- |------ | ------------------------------ |
| EX | EX |::
|0::9
| 1, 2, 3, 4, 5, 6, 7, 8
|
| EX | IN |:.
|0:.9
| 1, 2, 3, 4, 5, 6, 7, 8, 9
|
| IN | EX |.:
|0.:9
| 0, 1, 2, 3, 4, 5, 6, 7, 8
|
| IN | IN |..
|0.:9
| 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
|
@AdamSpeight2008
It's too similar to conditional operators, isn't it?
double f(bool cond) => cond ? 1:.2; // cond ? 1 : 0.2
@ufcpp
If [ ]
are also part of the syntax then should be okay, or if parenthesis were required then there is no conflict. It is though worth looking into, even it the syntax is different.
I don't know if this has been discussed before but we could simply use (
/[
to denote inclusive/exclusive respectively, then we can write
(3..6) # 4, 5
(3..6] # 4, 5, 6
[3..6) # 3, 4, 5
[3..6] # 3, 4, 5, 6
@gafter @HaloFour @khyperia @portal-chan @alrz
I wish I could write the range in the form:
(0 to 10)
besides the form:
(0..10)
Will it be possible to iterate over a range? So can I do:
foreach (var i in 0..10)
{
...
}
I've tested this in the current preview, and it currently doesn't work. I know that indices can also be relative (read: negative), which one of course also has to consider. But the general iteration use case could be really useful I feel.
@ErikSchierboom hmm, that's curious. Despite being in the proposal, this isn't working in the preview. I don't know if it's something that's going to be implemented in the future or if the feature was dropped. I'm not aware of the spec, but I think it's achievable for well defined ranges (closed and not "from end" - perhaps open start could mean "0", idk). I think it's ok for 0..10
being start-inclusive and end-exclusive for slicing, but it sounds weird for loops.
@Logerfo Let's hope it will be added! The current source code does indeed not implement IEnumerable
.
Is this the right place to discuss the Range feature in C# 8.0 Preview?
Correct me if I'm wrong, but isn't the proposed Range design missing something important and very common: Most programming uses extents instead of ranges! Therefore isn't it a mistake to design this new feature for ranges when actually extents are what we all normally use? It would be OK if it has good support for _both_ extents and ranges, but if it only supports ranges, then it's a mistake, isn't it?
Hundreds of examples exist to demonstrate that we all use extents rather than ranges, usually. One of the major examples is System.Span<T>
. Span is designed as an extent not a range. Span is an extent because it has a Length property instead of "StartIndex" and "EndIndex" properties.
Span is intended to replace the older System.ArraySegment<T>
, which was also designed as an extent not a range, as you can see by the fact that ArraySegment contains "Offset" and "Count" properties, not "StartOffset" and "EndOffset" properties.
Hundreds or thousands of examples exist. Here are a few more:
System.String.Substring(int startIndex, int length)
System.Array.Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length)
System.Array.Clear(Array array, int index, int length)
System.Array.IndexOf<T>(T[ ] array, T value, int startIndex, int count)
System.Array.BinarySearch<T>(T[ ] array, int index, int length, T value)
.....and many more.....
Occasionally in special cases, ranges are needed, but otherwise the normal pattern is to use extents.
Why do we all use extents instead of ranges? Because extents are better than ranges in most cases! Ranges are generally awkward to use, therefore we all use extents usually (except in special cases where ranges are justified).
Therefore why is this new feature in C# 8.0 being designed as ranges when in fact extents are better and the standard practice? The proposal makes no sense to me. If I've misunderstood the proposal, then please tell me.
@verelpode
The primary motivation for adding ranges to the language (at least in the form proposed for C# 8.0) is to facilitate slicing. The point is to make it easier to do so without having to manually do the math for calculating the length from two known offsets.
I'm not sure why you think it has to be either ranges or extents? And what exactly are you proposing to facilitate extents?
I'm not sure why you think it has to be either ranges or extents?
Actually I wrote: "It would be OK if it has good support for _both_ extents and ranges"
And what exactly are you proposing to facilitate extents?
Good question. I'll have to think about that before I can answer. I'd like this feature much better if it supports both extents and ranges. An API can publicly accept both extents and ranges and internally it may convert either an extent or a range to the desired internal representation of an extent or range (noting that an extent is easily converted to a range and vice-versa).
Maybe dotnet/csharplang #185 is a better place to discuss this.
Has the precedence been decided? (I recall seeing BNF somewhere that put the precedence below the addition operator.)
There are two obvious choices. One is a high precedence (above * /
), so that you can add two ranges with a..b + x..y
((a..b) + (x..y)
). The other is a low precedence, so that x+1..y
means (x+1)..y
. For reference, Swift made the latter choice.
The second choice creates a tension involving spaces. Normally, only the high-precedence operators don't have spaces around them: A.B
, A::B
, A(B)
, A++
. But ranges look most natural without spaces around the "..
". And Visual Studio typically puts spaces around * / + -
. So if you write A+B..C
, a naive IDE would want to change it to A + B..C
, which users can be expected to mentally parse as A + (B..C)
. That's bad if (A + B)..C
is the correct interpretation.
@qwertie Precedence was decided in LDM yesterday. Notes should be published shortly.
@gafter @agocke Can we close this issue at this point?
@jcouv Was anything published? I hoped to find it here but didn't.
@agocke The 7-17 notes don't mention the question of precedence and range. Did we have any more notes on that?
Range precedence was decided and implemented as just below the existing unary precedence here: https://github.com/dotnet/roslyn/pull/36543
Closing this issue since it's obsolete.
Most helpful comment
@CyrusNajmabadi what if
in
were part of the pattern?if (ch is in βaβ..βzβ)
case in βaβ..βzβ:
case Point(in 0..10, var y)
ch switch (in βaβ..βzβ => Letter, in β0β..β9β => Digit, _ => Other)