Начнем с такого примера. Допустим, у нас есть сущность «тип действия». У типа действия есть human-readable имя и системное имя — некий уникальный идентификатор, по которому с объектами этой сущности мы сможем работать из кода. Вот такая структура в виде объектов в коде:
class ActionType { public int id; public string systemname; public string name; }
var ActionTypes = new ActionType[] { new ActionType { id = 1, systemname = "Registration", name = "Регистрация" }, new ActionType { id = 2, systemname = "LogOn", name = "Вход на сайт" }, new ActionType { id = 3, systemname = null, name = "Некоторый тип действия без системного имени" } };
Для такой же структуры с аналогичными данными создана таблица в БД и вспомогательные объекты для использования LINQ to SQL. Допустим, нам необходимо выяснить, существует ли у нас тип действия с системным именем NotExistingActionType. Вопрос в том, что будет выведено на экран после выполнения этих инструкций:
var resultForObjects = ActionTypes.All(actionType => actionType.systemname != "NotExistingActionType"); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.ActionTypes.All(actionType => actionType.SystemName != "NotExistingActionType"); Console.WriteLine("Result for objects: " + resultForObjects + "\nResult for Linq to sql: " + resultForLTS); Console.ReadLine();
Ответ в данном случае на первый взгляд странный. Результатом работы приложения будет:
Result for objects: True
Result for LINQ to sql: False
Почему «один и тот же метод» возвращает разные значения для одних и тех же данных? Всё дело в том, что это совсем не один и тот же метод, а разные методы с одинаковыми названиями. Первый итерирует по объектам в памяти, второй же преобразуется в SQL запрос, который будет выполнен на сервере и вернет нам другой результат. Результаты отличаются из-за наличия среди наших типов действий одного с неопределенным системным именем. И в данном моменте проявляются специфические различия двух сред выполнения: для .NET выражение null != objRef — истина (если конечно objRef не null), а следовательно и значение выражения «системные имена всех типов действий не равны NotExistingActionType» в нашей ситуации будет истинным.
Но LINQ to SQL выражения преобразуются в SQL и выполняются на сервере, и в SQL сравнение с NULL работает по-другому. Значения выражений NULL == Something, NULL != Something и даже NULL == NULL всегда будут ложными, таков стандарт, поэтому выражение «системные имена всех типов действий не равны NotExistingActionType» и не будет истинной, так как NULL != ‘NotExistingActionType’ — ложь.
Теперь рассмотрим другой пример. Допустим, у нас есть пользователи с их текущим балансом:
class User { public int id; public int balance; public string name; }
var users = new User[] { new User { id = 1, name = "Василий", balance = 0 }, new User { id = 2, name = "Георгий", balance = 0 } };
Вопрос в том, что должна возвращать сумма по пустому набору элементов. Для меня, например, очевидным значением является 0, но тут тоже не всё так просто. Выполним что-то типа такого:
var resultForObjects = users.Where(user => user.id < 0).Sum(user => user.balance); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.Users.Where(user => user.Id < 0).Sum(user => user.Balance); Console.WriteLine("Result for objects: " + resultForObjects + "\nResult for Linq to sql: " + resultForLTS); Console.WriteLine(context.ActionTypes.First().Name); Console.ReadLine();
Результат снова будет необычным. Вообще говоря, выполнить эти инструкции в таком виде мы не сможем, так как при выполнении возникнет исключение:
System.InvalidOperationException: «Значение NULL не может быть присвоено члену, который является типом System.Int32, не допускающим значения NULL.»
Причины происходящего снова в трансляции наших вызовов в SQL. Для обычного IEnumerable экстеншн Sum возвращает 0 для пустого набора, в чем легко убедиться, не вычисляя resultForLTS (ну или в конце концов прочитав это вот тут msdn.microsoft.com/ru-ru/library/bb549046). Однако СУБД вычисляет сумму пустого набора как NULL (правильно это или нет — вопрос довольно холиварный, но сейчас это просто факт), и LINQ, пытаясь вернуть null вместо целого числа, немедленно терпит фиаско. Починить это место крайне просто, но необходимо держать ухо востро:
var resultForLTS = context.Users.Where(user => user.Id < 0).Sum(user => (int?)user.Balance) ?? 0;
Тут возвращаемое значение функции Sum становится не int, а nullable int (этого можно добиться и явным указанием типа generic’а), что дает возможность LINQ вернуть null, а оператор ?? превратит этот null в 0.
Ну и последний пример. Удивительно, но трансляция в SQL дает нам немного синтаксического сахара. Рассмотрим вот какой пример. Добавим объект Location, и у пользователей теперь будет ссылка на их город:
class User { public int id; public int balance; public string name; public Location location; } class Location { public int id; public string Name; }
Не будем создавать никаких объектов Location и изменять пользователей, интерес представляет вот такой код:
var resultForObjects = users.Select(user => user.location == null ? "Локация не указана" : user.location.Name == null ? "Локация не указана" : user.location.Name) .First(); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.Users.Select(user => user.Location == null ? "Локация не указана" : user.Location.name == null ? "Локация не указана" : user.Location.name) .First();
В обоих случаях результатом будет строка «Локация не указана», так как она действительно не указана, но что будет, если написать вот так:
var resultForLTS = context.Users.Select(user => user.Location.name ?? "Локация не указана");
Вы можете подумать, что так это не будет работать, так как тут присутствует явный NullReferenceException (ни один пользователь не имеет объекта Location априори, мы их не создавали, в базу не записывали), но не забываем, что этот код не будет запущен в окружении .NET, а будет транслирован в SQL и запущен СУБД. На самом деле, запрос, который получится из этого кода, будет выглядеть так (LINQPad в помощь):
SELECT COALESCE([t1].[name],@p0) AS [value] FROM [Users] AS [t0] LEFT OUTER JOIN [Locations] AS [t1] ON [t1].[Id] = [t0].[LocationId]
Этот «трюк» позволяет нам не писать дикое количество тернарных операторов в запросах на LINQ.
Вывод:
Когда мы пишем код, мы постоянно полагаемся на функции более низкого уровня и считаем, что эти функции работают верно. Прекрасно, что есть такой способ сокращения сложности, но нужно всегда отдавать себе отчет в том, достаточно хорошо ли мы понимаем, что сделает та или иная функция, которую мы используем. А для этого — RTFM!
ссылка на оригинал статьи http://habrahabr.ru/post/203836/
Добавить комментарий