Не смешивайте out-параметры и LINQ

Я вернулся из ежегодного отпуска, проведенного в прекрасном месте на юго-западе Онтарио; прежде чем переходить к теме сегодняшнего поста, посмотрите на снимок, сделанный с помощью Windows Phone при возвращении домой. Мы находимся на высоте 37000 футов сразу за городом Биллингс, штат Монтана, за несколько минут до захода солнца:

clip_image001

Все небо было заполнено молниями, которые, к сожалению, я не смог запечатлеть на фотографии. Это была самая масштабная гроза, которую я когда либо видел. Обратите внимание на форму облака в виде наковальни; как я уже писал ранее, перед нами находится огромный тепловой двигатель, выделяющий скрытое тепло из воды и использующий его для засасывания более теплого воздуха для движения тучи вперед. Очень красиво.

Итак, достаточно о погоде. Сегодня:что не так с этим кодом?

var seq = new List<string> { "1", "blah", "3" };
int tmp;
var nums =
from item in seq
let success = int.TryParse(item, out tmp)
select success ? tmp : 0;

Идея довольно простая: взять последовательность строк и превратить ее в последовательность целых чисел, путем преобразования строки в целое число, если это возможно, или вернуть нуль в противном случае.

При попытке скомпилировать этот код компилятор языка C# выдаст ошибку определенного присваивания (definite assignment error), что кажется весьма странным. Почему? Подумайте о том, во что данный код будет преобразован компилятором:

var nums =
seq.Select(item=>new {item, success = int.TryParse(item, out tmp)})
.Select(transparent => transparent.success ? tmp : 0);

У нас два вызова метода и два лямбда-выражения. Очевидно, что первое лямбда-выражение устанавливает значение tmp, а второе – его читает, но у нас нет никаких гарантий того, что первый вызов метода Select приведет к вызову первого лямбда-выражения! Вполне возможно, что по какой-то странной причине он никогда его не вызовет. А поскольку компилятор не может доказать, что переменная tmp будетгарантированно проинициализирована (definitely assigned) до ее чтения, то компиляция завершается с ошибкой.

Решит ли это проблему инициализация переменной tmp в месте объявления? Конечно, нет! Программа начнет компилироваться, но это «плохая практика» изменять подобные переменные. Помните, что одна переменная будет использоваться совместно всеми исполняемыми делегатами! В таком простом примере, использующем LINQ-to-Objects, делегаты вызываются в разумном порядке, но даже небольшое изменение кода приведет к нарушению этого допущения:

int tmp = 0;
var nums =
from item in seq
let success = int.TryParse(item, out tmp)
orderby item
select success ? tmp : 0;
foreach(var num in nums) { Console.WriteLine(num); }

А что будет теперь?

Мы создаем объект, представляющий собой запрос. Объект-запроса содержит три шага: выполнить проекцию “let”, выполнить сортировку и выполнить конечную проекцию. Помните, что запрос не исполняется до запроса на получение первого результата; присваивание переменной “nums” всего лишь создает объект, представляющий запрос, но не приводит к его исполнению.

Затем мы исполняем запрос в теле цикла. Это приводит к целой цепочке событий, но в этом случае нам явно нужно выполнить проекцию “let” целиком, чтобы затем отсортировать результирующую последовательность выражением “orderby”. Выполнение лямбда-выражения проекции “let” трижды приведет к тому, что переменная tmp будет изменена три раза. Только после сортировки будет выполнена последняя проекция, в которойбудет использовано текущее значение переменной tmp, а не значение из далекого прошлого!

Так как же правильно поступить в этом случае? Решение заключается в написании собственного метода расширения TryParse так, как он должен был сразу же выглядеть при появлении значимых типов, допускающих null:

static int? MyTryParse(this string item)
{
int tmp;
bool success = int.TryParse(item, out tmp);
return success ? (int?)tmp : (int?)null;
}

И теперь можно переписать наш пример так:

var nums =
from item in seq
select item.MyTryParse() ?? 0;

Изменение переменной теперь ограничено телом метода, а не побочным эффектом, который используется в запросе.Старайтесь всегда избегать побочных эффектов в запросах.

Спасибо Биллу Вагнеру (Bill Wagner) за вопрос, который инициировал это сообщение.