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: Adding a scoped index parameter to foreach #8130
Comments
I like this idea, we just need to make sure this won't conflict with the destructuring assignment syntax. |
If tuples are added as first-class citizens to the language, this could be replaced by an extension method on |
@orthoxerox Probably not, the existing consensus in so far in #6400 seems to lean towards using |
@SolalPirelli Good point. I assume you mean something like this: static void ForEach(this IEnumerable<T> source, Action<T, int> action)
{
int count = 0;
foreach (var item in source) action(item, count++);
} I agree that would be less work than implementing a new language feature. It would be kinda strange to newcomers though, since you would have some people writing ninja edit Eric Lippert has a blog post on this: https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/ |
What's wrong with |
@jamesqo No, I meant an |
@alrz Because items.Select((item, i) =>
{
// work
return 0;
}); edit Also not to mention Select is lazy so you'd have to iterate through it to see any side effects. |
@SolalPirelli Hmm, seems interesting. Would it be possible to omit the parentheses in the tuple declaration? |
@jamesqo @SolalPirelli That was proposed in #6067 but it seemingly dismissed. Then you have to do this: foreach(var tuple in list.Indexed()) {
let (var item, var index) = tuple;
// ...
} |
You could try more meaningful variable names than 'index'. In your example, 'indexItems1' and 'indexItems2' would be a better name. Further, if you're not using the value of 'indexItems1' after the loop you could just use index over and over. Or you could do this.
|
@Blecki See my updated post. Your solution would definitely work, I just don't think it's readable as it could be.
Well, I guess it's a matter of opinion. But let's be honest, would you rather have code that looks like this: int indexItems1 = 0;
foreach (var item in items1)
{
// do something...
indexItems1++;
}
int indexItems2 = 0;
foreach (var item in items2)
{
// do something...
indexItems2++;
} or this: foreach (var item, index in items1)
// do something...
foreach (var item, index in items2)
// do something...
Yeah, but if you accidentally forget to reset int i = 0;
foreach (var item in items1) { /* */ i++; }
// Forgot to reset i = 0...
foreach (var item in items2)
{
// Whoops! Now i starts at items1.Count() and continues from there.
i++;
} |
If you are going to do this there needs to be a "Finally" so I can access the index. |
@jamesqo, I can't argue with the readability. I avoid tuples because the names 'item1' and 'item2' are meaningless. |
How about a mashup of both for and foreach:
This gives you the freedom to change the behavior of the index if you wanted to for some reason:
In fact, you wouldn't even need "foreachindex", just overload the foreach function to accept the additional arguments:
|
public struct IndexedElement<T>
{
public T Value { get; }
public int Index { get; }
public IndexedElement(T value, int index)
{
Value = value;
Index = index;
}
public static IndexedElement<T> Map(T value, int index)
{
return new IndexedElement<T>(value, index);
}
}
void Main()
{
var items = new List<string> { "Foo", "Bar" };
foreach (var item in items.Select(IndexedElement<string>.Map))
{
Console.WriteLine(item.Index);
}
} |
@techman2553 I'm not convinced, the whole point of this proposal was to eliminate the boilerplate and the need for a state variable. Your suggestion seems to just move it to a different place (inside the foreach loop). @GeirGrusom Your solution is good, but there's still a need to write out |
The general consensus so far seems to be "we don't need this." If we choose not to go ahead with this proposal, I think we should reconsider the earlier suggestion about using the new destructuring syntax in loops. That would allow us to do something like this: foreach (let (thing, i) in things.Index())
{
// do something with thing and i
} Although it's definitely a bit more verbose than my proposal above, it would support the scenario with no new syntax additions to the language (at least, after tuples/destructuring assignments are added in C# 7). |
Correct me if I'm wrong, but wouldn't it be: foreach ((var item, var index) in things.Index())
{
// do something with item and index
} |
@RichiCoder1 You can't use patterns in |
For reference, 'de-structuring foreach' did end up being allowed, eg. this is ok:
Neither linq or morelinq have the "
|
I realize that there are already several other issues (#1651, #1752, and #5194) covering this, but they didn't address exactly what I had in mind so I'm making a new one here.
Background
A common pattern when working with
IEnumerable<T>
is to capture the index during each iteration of the loop. For example, LINQ has many overloads that take aFunc<T, int>
as well as aFunc<T>
, such as Select and Where. Languages like Python solve this problem with a method calledenumerate
, which returns the index and value of the current item in the collection:Problem
At the moment, there's no way to capture the index in a
foreach
loop without 1) introducing a local variable in the outer scope and 2) incrementing it manually after each iteration. For example:Aside from the boilerplate, this can be problematic in functions that have multiple
foreach
loops in the same scope: a new index variable with a new name has to be initialized for each loop.Other Solutions
For-loops
A traditional for-loop works and scopes correctly, but
I'm sure you've all seen plenty of examples of for-loops already, so I'll leave it to you to weigh their pros and cons.
foreach + Select / other LINQ-based variations
This also works well and scopes properly:
But let's be honest, this is not as readable as it could be. It's not immediately clear what
Item1
andItem2
mean, and theSelect
expression makes the code look like an overstretched diving board (see the similarity?). Language support for tuples may improve the problem somewhat, but it'd still have to look like this.Proposal: Add a scoped index parameter to foreach
With the new syntax, the code example above now looks like this:
The compiler-generated code is equivalent to
Benefits
index
variable after every loop has been eliminated.IEnumerable
,ICollection
, orIReadOnlyCollection
can be used with the new syntax.index
is limited to inside the loop, which circumvents the problem above with multiple loops in the same scope.index
is readonly, preventing accidental assignments and making the code easier to read.Type Inference
index
is inferred to beint
.index
may also be prefixed withvar
orint
, e.g.for those who prefer to spell their types out explicitly.
int.MaxValue
(viaLongCount
), the type ofindex
may also be explicitly specified aslong
:This ensures that we keep performant code for the vast majority of cases, but support using larger numbers when they are needed.
The text was updated successfully, but these errors were encountered: