Вы не совсем правильно понимаете, как работает LINQ.
Дело в том, что в LINQ реализованы ленивые вычисления. Реальный пробег Where производится не тогда, когда вы определяете res, а в тот момент, когда вы производите перечисление.
Последовательность, которая «приходит» в Where, может быть слишком большой, или браться из файла, или генерироваться автоматически, и тем самым быть бесконечной.
Например, такой код:
IEnumerable<double> RandomSequence()
{
var r = new Random();
while (true)
yield return r.NextDouble();
}
производит бесконечную последовательность действительных чисел.
Поэтому все LINQ-операции лишь запоминают как должны быть произведены вычисления, но не производят их до тех пор, пока это реально не потребуется.
В вашем первом случае, в tmp записан лишь «рецепт», как получить последовательность. чисел, больших 0. Если вы хотите реально применить этот рецепт и получить List<int> (это называется «материализовать последовательность»), вы можете применить .ToList(), или пробежаться циклом foreach:
var list = new byte[] { 0, 0, 1, 0, 1 };
int LinqCnt = 0;
var tmp = list.Where(x =>
{
LinqCnt++;
Console.WriteLine("In where: " + LinqCnt);
return x > 0;
});
Console.WriteLine("After where: " + LinqCnt);
foreach (var n in tmp)
Console.WriteLine("Enumerating: " + n);
Console.WriteLine("After enumeration: " + LinqCnt);
Вы увидите, что вычисление очередного члена последовательности происходит параллельно с выдачей результатов: числа, выводимые в Where-клаузе, «перемешиваются» в выводимым в цикле текстом:
After where: 0
In where: 1
In where: 2
In where: 3
Enumerating: 1
In where: 4
In where: 5
Enumerating: 1
After enumeration: 5
Что происходит во втором случае? Опять-таки, в tmp записан лишь «рецепт», как получать один за одним члены последовательности. Поскольку вы используете First, то вся последовательность не нужна: будет вычислен лишь первый член. Для этого исходная последовательность будет проматываться до тех пор, пока Where не выдаст первый член результата, и на этом обработка закончится. Поскольку внутрь if'а мы не попадаем, больше ничего не происходит.
И наконец, третий случай. У вас вызывается .First и .Last, для одного и того же «рецепта» tmp. Это значит, что рецепт применяется два раза: один раз последовательность проматывается, чтобы получить первый элемент (до момента, когда этот самый первый элемент будет выдан), а затем — чтобы получить последний элемент. Давайте немного модифицируем код:
var list = new byte[] { 0, 0, 2, 0, 1 };
int LinqCnt = 0;
var tmp = list.Where(x =>
{
LinqCnt++;
Console.WriteLine("In where: " + LinqCnt + ", x = " + x);
return x > 0;
});
Console.WriteLine("After where: " + LinqCnt);
if (tmp.First() == tmp.Last())
Console.WriteLine(LinqCnt + " ");
Console.WriteLine("After enumeration: " + LinqCnt);
Получаем результат:
After where: 0
In where: 1, x = 0
In where: 2, x = 0
In where: 3, x = 2
In where: 4, x = 0
In where: 5, x = 0
In where: 6, x = 2
In where: 7, x = 0
In where: 8, x = 1
After enumeration: 8
Для вычисления First понадобилось пройти три члена исходной последовательности: 0, 0 и 2. Для получения Last — все пять.
Кстати, отсутствие «ненужных» вычислений — это не фича оптимизатора. Это документированное поведение ленивых LINQ-последовательностей, которое не зависит от того, включен у вас режим оптимизации в компиляторе или нет.
Почему же результаты с LINQ выглядят настолько странно? Дело в том, что LINQ пришло к нам из функционального программирования. Там предпочитаются pure-функции, то есть, функции без побочных эффектов. Для таких функций всё равно, сколько раз и когда вызывать их — результат будет один и тот же. Поэтому с «чистыми» функциями ленивые вычисления не приводят к неожиданностям, т. к. всё равно, выполняются ли вычисления (например, фильтрация в Where) «ленивым» или «энергичным» образом.
Ваша функция фильтрации, однако, содержит побочные эффекты: она изменяет внешнюю переменную и выводит текст на консоль. Поэтому поведение Where и кажется вам неожиданным.
Рекомендуется использовать в LINQ-выражениях чистые функции, тогда вам не придётся следить за состоянием, и держать в голове, какие из LINQ-функций ленивые, а какие нет. Если же вы пользуетесь функциями с побочными эффектами, то вам придётся запомнить, что функции, возвращающие последовательность (например, Where, Select, Distinct) обычно ленивые, а вот функции, возвращающие конкретный элемент (First, Sum, Max) — энергичные.