Проверяет ли foreach массив на каждой итерации?
Я хочу создать foreach
, который пропускает первый элемент. Я видел в другом месте, что самый простой способ сделать это - использовать myCollection.Skip(1)
, но у меня есть вопрос:
Документация MSDN на .Skip()
описывает, что она "обходит определенное количество элементов в последовательности и затем возвращает остальные элементы". Означает ли это, что вызов
foreach(object i in myCollection.Skip(1))
{ ... }
Будет ли программа выполнять .Skip(1)
каждый раз, когда итерация foreach
? Или foreach
(несколько как a switch
) не требует нескольких оценок массива?
Было бы более эффективным создание манекена var _dummy = myCollection.Skip(1)
и вместо этого итерация?
Ответы
Ответ 1
Я просто высмеял ваш код с помощью этого
foreach(var v in Enumerable.Range(1,10).Skip(1))
v.Dump();
И здесь генерируется IL.
IL_0001: nop
IL_0002: ldc.i4.1
IL_0003: ldc.i4.s 0A
IL_0005: call System.Linq.Enumerable.Range
IL_000A: ldc.i4.1
IL_000B: call System.Linq.Enumerable.Skip//Call to Skip
IL_0010: callvirt System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator
IL_0015: stloc.1 // CS$5$0000
IL_0016: br.s IL_0026
IL_0018: ldloc.1 // CS$5$0000
IL_0019: callvirt System.Collections.Generic.IEnumerator<System.Int32>.get_Current
IL_001E: stloc.0 // v
IL_001F: ldloc.0 // v
IL_0020: call LINQPad.Extensions.Dump
IL_0025: pop
IL_0026: ldloc.1 // CS$5$0000
IL_0027: callvirt System.Collections.IEnumerator.MoveNext
IL_002C: stloc.2 // CS$4$0001
IL_002D: ldloc.2 // CS$4$0001
IL_002E: brtrue.s IL_0018
IL_0030: leave.s IL_0042
IL_0032: ldloc.1 // CS$5$0000
IL_0033: ldnull
IL_0034: ceq
IL_0036: stloc.2 // CS$4$0001
IL_0037: ldloc.2 // CS$4$0001
IL_0038: brtrue.s IL_0041
IL_003A: ldloc.1 // CS$5$0000
IL_003B: callvirt System.IDisposable.Dispose
IL_0040: nop
IL_0041: endfinally
Как вы можете видеть, Skip
вызывается только один раз.
Эквивалентный код С# будет выглядеть примерно так:
IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();//Get the enumerator
try
{
int m;//This variable is here prior to c#5.0
while(e.MoveNext())
{//int m; is declared here starting from c#5.0
m = (int)(int)e.Current;
//Your code here
}
}
finally
{
if (e != null) ((IDisposable)e).Dispose();
}
Рассмотрим приведенный ниже код. Если foreach вызывает VeryLongRunningMethodThatReturnsEnumerable
на каждой итерации, то это будет кошмар. Огромная ошибка в дизайне языка. К счастью, этого не делает.
foreach(var obj in VeryLongRunningMethodThatReturnsEnumerable())
{
//Do something with that obj
}
Ответ 2
Вы должны понимать, как работает foreach
. Этот цикл foreach:
foreach(T t in GetSomeEnumerable())
DoSomethingWithT(t);
эквивалентен этому коду:
var e = GetSomeEnumerable().GetEnumerator();
try{
while(e.MoveNext()){
T t = (T)e.Current; // unless e is the generic IEnumerator<T>,
// in which case, there is no cast
DoSomethingWithT(t);
}
}finally{
if(e is IDisposable)
e.Dispose();
}
Ответ 3
Вытяните его, и он, вероятно, станет более ясным.
var myCollection = new List<object>();
var skipped = myCollection.Skip(1);
foreach (var i in skipped) {
Console.WriteLine(i.ToString());
}
Таким образом, пропущен только IEnumerable
, который теперь перечисляется foreach
.
Вот что выглядит IL в этом случае:
IL_0000: newobj System.Collections.Generic.List<System.Object>..ctor
IL_0005: stloc.0 // myCollection
IL_0006: ldloc.0 // myCollection
IL_0007: ldc.i4.1
IL_0008: call System.Linq.Enumerable.Skip
IL_000D: stloc.1 // skipped
IL_000E: ldloc.1 // skipped
IL_000F: callvirt System.Collections.Generic.IEnumerable<System.Object>.GetEnumerator
IL_0014: stloc.3 // CS$5$0000
IL_0015: br.s IL_0029
IL_0017: ldloc.3 // CS$5$0000
IL_0018: callvirt System.Collections.Generic.IEnumerator<System.Object>.get_Current
IL_001D: stloc.2 // i
IL_001E: ldloc.2 // i
IL_001F: callvirt System.Object.ToString
IL_0024: call System.Console.WriteLine
IL_0029: ldloc.3 // CS$5$0000
IL_002A: callvirt System.Collections.IEnumerator.MoveNext
IL_002F: brtrue.s IL_0017
IL_0031: leave.s IL_003D
IL_0033: ldloc.3 // CS$5$0000
IL_0034: brfalse.s IL_003C
IL_0036: ldloc.3 // CS$5$0000
IL_0037: callvirt System.IDisposable.Dispose
IL_003C: endfinally
IL для вашего кода выглядит аналогично:
var myCollection = new List<object>();
foreach (var i in myCollection.Skip(1)) {
Console.WriteLine(i.ToString());
}
IL_0000: newobj System.Collections.Generic.List<System.Object>..ctor
IL_0005: stloc.0 // myCollection
IL_0006: ldloc.0 // myCollection
IL_0007: ldc.i4.1
IL_0008: call System.Linq.Enumerable.Skip <-- 1 Call to .Skip() outside the loop.
IL_000D: callvirt System.Collections.Generic.IEnumerable<System.Object>.GetEnumerator
IL_0012: stloc.2 // CS$5$0000
IL_0013: br.s IL_0027
IL_0015: ldloc.2 // CS$5$0000
IL_0016: callvirt System.Collections.Generic.IEnumerator<System.Object>.get_Current
IL_001B: stloc.1 // i
IL_001C: ldloc.1 // i
IL_001D: callvirt System.Object.ToString
IL_0022: call System.Console.WriteLine
IL_0027: ldloc.2 // CS$5$0000
IL_0028: callvirt System.Collections.IEnumerator.MoveNext
IL_002D: brtrue.s IL_0015
IL_002F: leave.s IL_003B
IL_0031: ldloc.2 // CS$5$0000
IL_0032: brfalse.s IL_003A
IL_0034: ldloc.2 // CS$5$0000
IL_0035: callvirt System.IDisposable.Dispose
IL_003A: endfinally
У этого по-прежнему есть только один вызов .Skip().
Ответ 4
Все выражение с Skip
будет вызвано только один раз. Skip
использует отложенное выполнение так, чтобы оно выполнялось после того, как есть действия, которые не используют отложенное выполнение. В этот момент дерево выражений построено на фоне, а ссылка на экземпляр IEnumerable
возвращается обратно вызывающему, который использует его, если ничего не изменяется.
Ответ 5
То, что вы повторяете, является результатом команды:
myCollection.Skip(1)
Это эффективно возвращает IEnumerable
типа myCollection
, который опустил первый элемент. Таким образом, ваш foreach тогда против нового IEnumerable
, которому не хватает первого элемента. foreach
принудительно выполняет фактическую оценку полученного метода Skip(int)
посредством перечисления (его выполнение откладывается до перечисления, как и другие методы LINQ, такие как Where
и т.д.). Это будет то же самое, что:
var mySkippedCollection = myCollection.Skip(1);
foreach (object i in mySkippedCollection)
...
Здесь код, который Skip(int)
действительно заканчивает выполнение:
private static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
while (count > 0 && enumerator.MoveNext())
{
count--;
}
if (count <= 0)
{
while (enumerator.MoveNext())
{
yield return enumerator.Current; // <-- here your lazy eval
}
}
}
yield break;
}