Ensure the selector gets run during Count. #14435
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -155,9 +155,41 @@ public TResult[] ToArray() | |
return builder.ToArray(); | ||
} | ||
|
||
public List<TResult> ToList() => new List<TResult>(this); | ||
public List<TResult> ToList() | ||
{ | ||
var list = new List<TResult>(); | ||
|
||
foreach (TSource item in _source) | ||
{ | ||
list.Add(_selector(item)); | ||
} | ||
|
||
return list; | ||
} | ||
|
||
public int GetCount(bool onlyIfCheap) | ||
{ | ||
// In case someone uses Count() to force evaluation of | ||
// the selector, run it provided `onlyIfCheap` is false. | ||
|
||
if (onlyIfCheap) | ||
{ | ||
return -1; | ||
} | ||
|
||
public int GetCount(bool onlyIfCheap) => onlyIfCheap ? -1 : _source.Count(); | ||
int count = 0; | ||
|
||
foreach (TSource item in _source) | ||
{ | ||
_selector(item); | ||
checked | ||
{ | ||
count++; | ||
} | ||
} | ||
|
||
return count; | ||
} | ||
} | ||
|
||
internal sealed class SelectArrayIterator<TSource, TResult> : Iterator<TResult>, IPartition<TResult> | ||
|
@@ -226,6 +258,17 @@ public List<TResult> ToList() | |
|
||
public int GetCount(bool onlyIfCheap) | ||
{ | ||
// In case someone uses Count() to force evaluation of | ||
// the selector, run it provided `onlyIfCheap` is false. | ||
|
||
if (!onlyIfCheap) | ||
{ | ||
foreach (TSource item in _source) | ||
{ | ||
_selector(item); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, if we just returned There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JonHanna During #12703 when I had optimized // Leave it to Count to iterate through us
public int GetCount(bool onlyIfCheap) => onlyIfCheap ? -1 : EnumerableHelpers.Count(this); However, @stephentoub argued against this. See here for context: #12703 (comment) I ended up writing everything inline for
Virtual method calls are pretty expensive; going from 2 -> 3 virtual method calls ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. |
||
|
||
return _source.Length; | ||
} | ||
|
||
|
@@ -351,7 +394,20 @@ public List<TResult> ToList() | |
|
||
public int GetCount(bool onlyIfCheap) | ||
{ | ||
return _source.Count; | ||
// In case someone uses Count() to force evaluation of | ||
// the selector, run it provided `onlyIfCheap` is false. | ||
|
||
int count = _source.Count; | ||
|
||
if (!onlyIfCheap) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise, your reason for doing this isn't obvious from the code alone, so should be commented on. And likely elsewhere, so I won't call out other cases. |
||
{ | ||
for (int i = 0; i < count; i++) | ||
{ | ||
_selector(_source[i]); | ||
} | ||
} | ||
|
||
return count; | ||
} | ||
|
||
public IPartition<TResult> Skip(int count) | ||
|
@@ -491,7 +547,20 @@ public List<TResult> ToList() | |
|
||
public int GetCount(bool onlyIfCheap) | ||
{ | ||
return _source.Count; | ||
// In case someone uses Count() to force evaluation of | ||
// the selector, run it provided `onlyIfCheap` is false. | ||
|
||
int count = _source.Count; | ||
|
||
if (!onlyIfCheap) | ||
{ | ||
for (int i = 0; i < count; i++) | ||
{ | ||
_selector(_source[i]); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above, could we just return |
||
} | ||
|
||
return count; | ||
} | ||
|
||
public IPartition<TResult> Skip(int count) | ||
|
@@ -703,6 +772,17 @@ public List<TResult> ToList() | |
|
||
public int GetCount(bool onlyIfCheap) | ||
{ | ||
// In case someone uses Count() to force evaluation of | ||
// the selector, run it provided `onlyIfCheap` is false. | ||
|
||
if (!onlyIfCheap) | ||
{ | ||
foreach (TSource item in _source) | ||
{ | ||
_selector(item); | ||
} | ||
} | ||
|
||
return _source.GetCount(onlyIfCheap); | ||
} | ||
} | ||
|
@@ -852,7 +932,21 @@ public List<TResult> ToList() | |
|
||
public int GetCount(bool onlyIfCheap) | ||
{ | ||
return Count; | ||
// In case someone uses Count() to force evaluation of | ||
// the selector, run it provided `onlyIfCheap` is false. | ||
|
||
int count = Count; | ||
|
||
if (!onlyIfCheap) | ||
{ | ||
int end = _minIndexInclusive + count; | ||
for (int i = _minIndexInclusive; i != end; ++i) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An issue came up here that I wasn't sure best how to approach:
One way to fix this would be to start from Ideally, we would somehow have a way to differentiate if cc @JonHanna There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm inclined to think that we don't care. A scenario that was called out as important is someone calling Count() on a Select result specifically to trigger side effects in selectors. (Not a sound practice IMO, but that's another matter). Such a use would be stymied by optimisations that skipped the selectors, and so we avoid such optimisation. A user who skips something has indicated indifference to that thing. As such I'm inclined to think it doesn't matter whether we run n or n-1 selectors. Indeed, I'm happy running 0 in this case and just calculating what the result of Count() would be. Others may not be as willing to go with quite so observable a difference to .Net4.6 Framework behaviour though. TBH if this was my PR I'd be taking the fastest route but prepared to back down if I failed to convince on that point. |
||
{ | ||
_selector(_source[i]); | ||
} | ||
} | ||
|
||
return count; | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1168,5 +1168,65 @@ public static IEnumerable<object[]> MoveNextAfterDisposeData() | |
yield return new object[] { new int[1] }; | ||
yield return new object[] { Enumerable.Range(1, 30) }; | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(RunSelectorDuringCountData))] | ||
public void RunSelectorDuringCount(IEnumerable<int> source) | ||
{ | ||
int timesRun = 0; | ||
var selected = source.Select(i => timesRun++); | ||
selected.Count(); | ||
|
||
Assert.Equal(source.Count(), timesRun); | ||
} | ||
|
||
// [Theory] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disabled currently because the first assert is giving inconsistent results. See comment above |
||
[MemberData(nameof(RunSelectorDuringCountData))] | ||
public void RunSelectorDuringPartitionCount(IEnumerable<int> source) | ||
{ | ||
int timesRun = 0; | ||
|
||
var selected = source.Select(i => timesRun++); | ||
|
||
if (source.Any()) | ||
{ | ||
selected.Skip(1).Count(); | ||
Assert.Equal(source.Count() - 1, timesRun); | ||
|
||
selected.Take(source.Count() - 1).Count(); | ||
Assert.Equal(source.Count() * 2 - 2, timesRun); | ||
} | ||
} | ||
|
||
public static IEnumerable<object[]> RunSelectorDuringCountData() | ||
{ | ||
var transforms = new Func<IEnumerable<int>, IEnumerable<int>>[] | ||
{ | ||
e => e, | ||
e => ForceNotCollection(e), | ||
e => ForceNotCollection(e).Skip(1), | ||
e => ForceNotCollection(e).Where(i => true), | ||
e => e.ToArray().Where(i => true), | ||
e => e.ToList().Where(i => true), | ||
e => new LinkedList<int>(e).Where(i => true), | ||
e => e.Select(i => i), | ||
e => e.Take(e.Count()), | ||
e => e.ToArray(), | ||
e => e.ToList(), | ||
e => new LinkedList<int>(e) // Implements IList<T>. | ||
}; | ||
|
||
var r = new Random(unchecked((int)0x984bf1a3)); | ||
|
||
for (int i = 0; i <= 5; i++) | ||
{ | ||
var enumerable = Enumerable.Range(1, i).Select(_ => r.Next()); | ||
|
||
foreach (var transform in transforms) | ||
{ | ||
yield return new object[] { transform(enumerable) }; | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for this is obscure and would likely benefit from being commented on. Without context this looks like pointless busy work that should be deleted to improve efficiency.