Прежде всего, да, это создает "итератор" и фактически не выполняет итерацию, пока вы не материализовали запрос в foreach
или не вызвали ToList
для него. Когда вы делаете это, количество итераций, которые происходят, основано на базовом типе. Reverse
создаст буферный массив для любого источника, который вы ему дадите, и переберите его в обратном направлении. Если источником является ICollection<T>
, то он будет использовать свой метод CopyTo
для заполнения массива, что обычно должно приводить к единой массовой копии непрерывных данных в постоянное время. Если это не ICollection<T>
, он будет перебирать источник в буфере, а затем перебирать его в обратном направлении. Имея это в виду, вот что происходит с вашим конкретным c запросом при итерации.
Сначала последний Reverse
начнет итерировать свой источник (который не является ICollection<T>
).
Затем Skip
начнет итерацию своего источника
Тогда первый обратный будет либо делать CopyTo
, если его источником является ICollection<T>
, либо он будет перебирать источник в буферный массив, размер которого изменяется как Нужен.
Затем первый реверс будет перебирать свой буферный массив в обратном направлении
Затем Пропуск получит результаты, пропустив первые два и получив остальные
Затем последний реверс возьмет результат и добавит их в свой буферный массив и при необходимости изменит его размер.
Наконец, последний метод Reverse будет повторять буферный массив в обратном направлении.
Так что, если вы имеете дело с ICollecion<T>
это одна CopyTo
, а затем 1 итерация всех значений, а затем 1 итерация всех значений, кроме 2. Если это не ICollection<T>
, это в основном 3 итерации значений (на самом деле последняя итерация всего, кроме 2). И так или иначе, в процессе также используются два промежуточных массива.
И чтобы доказать, что запрос не выполняет итераций, пока вы его не материализуете, вы можете проверить этот пример
void Main()
{
var query = MyValues().Reverse().Skip(2).Reverse();
Console.WriteLine($"After query before materialization");
var results = query.ToList();
Console.WriteLine(string.Join(",", results));
}
public IEnumerable<int> MyValues()
{
for(int i = 0; i < 10; i ++)
{
Console.WriteLine($"yielding {i}");
yield return i;
}
}
, который производит output
After query before materialization
yielding 0
yielding 1
yielding 2
yielding 3
yielding 4
yielding 5
yielding 6
yielding 7
yielding 8
yielding 9
0,1,2,3,4,5,6,7
По сравнению с другим примером, который у вас был x.Take(x.Count() - 2)
, он будет выполнять итерацию источника до того, как вы материализуете его один раз для Count
(если только это не ICollection
или ICollection<T>
, в этом случае он просто будет использовать свойство Count
), затем он будет повторять его снова, когда вы его материализуете.
Вот тот же пример с другим кодом и полученным результатом.
void Main()
{
var x = MyValues();
var query = x.Take(x.Count() - 2);
Console.WriteLine($"After query before materialization");
var results = query.ToList();
Console.WriteLine(string.Join(",", results));
}
public IEnumerable<int> MyValues()
{
for(int i = 0; i < 10; i ++)
{
Console.WriteLine($"yielding {i}");
yield return i;
}
}
С вывод
yielding 0
yielding 1
yielding 2
yielding 3
yielding 4
yielding 5
yielding 6
yielding 7
yielding 8
yielding 9
After query before materialization
yielding 0
yielding 1
yielding 2
yielding 3
yielding 4
yielding 5
yielding 6
yielding 7
0,1,2,3,4,5,6,7
То, какой из них лучше, полностью зависит от источника. Для ICollection<T>
или ICollection
предпочтительнее были бы Take
и Count
(если источник не может измениться между моментом создания запроса и его материализацией), но если он не является ни тем, ни другим, вы можете предпочесть двойное Reverse
чтобы избежать повторения источника дважды (особенно, если источник может меняться между созданием запроса и его материализацией, поскольку размер также может измениться), но это должно быть взвешено с увеличением общего числа выполненных итераций и объема памяти использование.