Этим постом я хочу начать серию публикаций о LINQ. Я планирую сделать серию из трех статей, в которых будет раскрыто: новые возможности и кострукции в .NET Framework и в C# в частности, также особое внимание будет уделено query methods и query expressions, я попытаюсь расказать как писать LINQ-запросы для in-memory коллекций (LINQ to Entities), также вкратце сделаю обзор expression trees, и на последок – LINQ to SQL и LINQ to XML.
Но все по-порядку.
В этом посте я хочу сделать обзор новых конструций .NET Framework 3.5 (все примеры, которые будут использованы ниже и далее приводятся на C#). Итак, мы рассмотрим:
Начнем, пожалуй...
Прежде чем приступить к рассмотрению анотнимных типов, давайте познакомимся с таким новым ключевым словом как var. Многим, особенно знакомым с JavaScript, это ключевое слово будет знакомо, но в отличии от JavaScript, где оно описывает late-bound objects, в .NET 3.0 это строго типизированная переменная.
Например:
Как видно из примера, ключевое слово var можно использовать для любого типа данных, но в тоже время получать строгую типизацию данных. Помните, что var – это не boxing, и никакого приведения к object не происходит «за кулисами». Тем более, что для поддержки var в IL не было добавлено никаких новых инструкций, и если посмотреть Reflector'ом на код, то пример, приведенный выше будет выглядеть привычным образом:
CLR никогда «не узнает», что вы использовали var для обьявления локальной переменной. Это всего лишь «синтаксический сахар». Но в то же время без введения var было бы сложно работать с LINQ.
Использование var в свою очередь накладывает ряд ограничений. А именно:
Но без использования var нельзя бы было использовать и анонимные типы. У нас просто нет выбора, т. к. при использовании анонимных типов мы не знаем имя класса (оно генерируется компилятором автоматически). Но об этом далее.
Итак, мы уже узнали что такое var, какие ограничения он накладывает при использовании. Сейчас давайте рассмотрим еще одну новую возможность .NET Framework 3.0 – анонимные типы (anonymous types).
Пример:
Анонимные типы – это удобная возможность C# (VB.NET), которая позволяет программистам кратко описывать inline CLR типы, без необходимости явного определения классов. Анонимные типы очень важны при выполнении и преобразовани запросов в LINQ.
Т. к. при создании объекта, который приведен выше, мы явно не указывали тип, то компилятору «не отстается выбора» как создать этот тип за нас.
Что же происходит при создании анонимного типа. Открыв код Reflector'ом мы не увидим истинной картины, потому что Reflector настолько «умный», что понимает анонимные типы, и отображает их так как они описаны в коде (приведенном выше).
Но переключившись в режим отображения IL кода можно увидеть, что создается новый объект типа <>f__AnonymousType0`3<string, string, int32>.
Компилятор создал для нас строго типизированный объект. Но предугадать имя класса, которое будет сгененированно для анонимного типа практически невозможно, потому что оно зависит от компилятора. Для CLR нет никакой разницы между использованием анонимных типов и явно определенных именованых типов. Анонимные типы, так же как и var, просто синтаксический сахар, позволяющие сократить время на написание кода.
Важный момент, который стоит отметить: если мы обявим два одинаковых анонимных типа, с одинаковым набором свойств и их порядком, компилятор будет использовать один и тот же сгенерированный объект:
Но стоит нам поменять местами свойства, или добавить новое свойство – сразу будет создан новый объект.
Анонимные типы работают для случаев, когда:
Одним из важных моментов, на мой взгляд, является то что поддерживается data binding для анонимных типов. Следовательно, нет ограничения на использование анонимных типов в ASP.NET или Windows Forms. Но об этом я раскажу в следующем посте.
По поводу использования var и анонимных типов было много споров и дискуссий. Наблюдая и делая для себя выводы, хочу сказать: не стоит бояться использовать var или анонимные типы, но в тоже время, хотел бы заметить что наиболее целесообразно использовать их в связке с LINQ. Не стоит терять читаемость кода и тем самым усложнять code review и его поддержу (что может произойти при использовании var, например).
Наример:
Когда переменная var x инициализируется с какой-то функции, и мне, как reviewer'у не известно, что эта функция возвращает, и имя переменной x тоже ни о чем мне не говорит, то сразу же возникает много вопросов.
Наверное все, кто пишет сейчас на C#, привыкли описывать классы следующим образом:
Как видно из примера, никакой дополнительной логики в get/set нет. Возникает вопрос: а почему бы просто не использовать поля класса, и не использовать свойства. Есть много недостатков в использовании публичных полей класса, вместо свойств, основными из которых являются:
В C# появилась новая возможность определять автоматические свойства, без необходимости создавать приватные поля для свойств. Компилятор их создаст за нас. Используя автоматические свойства, приведенный выше пример можно переписать следующим образом:
Когда компилятор встречает пустое get/set свойство, то он автоматически создаст приватное поле и реализацию для свойства. Давайте посмотрим как это происходит. При генерации приватных полей компилятор добавляет CompilerGeneratedAttribute к их обьявлению и к get/set операторам свойств.
Вот как выглядит наш класс Company с автоматическими свойствами в Reflector'е:
Исходя и этого, CLR не видит разницы между привычным для нас способом описания классов и классов c использованием автоматических свойств.
Важным момент: для обявления атоматического свойства всегда должны присутствовать как get так и set. Нельзя создавать read-only или write-only свойства (упуская get или set).
Из личного опыта, отмечу, что если вам необходимо свойство только на чтение или на запись, то можно определить private уровень доступа к get (или к set) оператору свойства (эта возможность работает для автоматических свойств).
При использовании автоматических свойств есть ограничения: если в будущем вам понадобится добавить логику валидации в get (или set), то прийдется явно реализовать свойство (потому что нет возможности обратится к автоматически сгенерированному полю, относящемуся к этому свойству).
Когда мы рассматривали анонимные типы, мы уже бегло расмотрели инициализаторы объектов. Давайте познакомимся с ними поближе и также рассмотрим инициализаторы коллекций.
Программирование с использованием .NET сильно зависит от использования свойств объектов, с которыми мы работаем. При создании объекта мы практически всегда инициализируем свойства этого объекта. И наверное вам всегда хотелось сделать это быстрее и короче (всю процедуру инициализации). С появлением инициализаторов объектов это стало возможно. Рассмотрим «класический» пример:
Этот же код можно записать, используя инициализаторы объектов, следующим образом:
Можно заметить, что то же самое можно сделать, используя конструктор класса. Но у нас не всегда есть такой уровень контроля (и не всегда мы можем менять объекты, с которыми мы работаем). В то же время, ничто не мешает нам использовать классы с параметризированными конструкторами вместе с инициализаторами объектов. Например, предположим, что использованный нами для примера класс Company содержит конструктор, принимающий имя компании. Мы можем записать наш пример следующим образом:
Кроме того, инициализаторы объектов позволяют инициализировать вложенные объекты. Расширим наш класс-пример Company, добавив в него свойство Address (типа Address). Тогда,используя инициализаторы объектов, можно инициализировать как свойства Company, так и свойства Address, который является вложенным объектом Company.
Важный момент: запись, приведенная в первом примере, где мы создавали объект а потом инициализоровали его свойства, и второй пример, с использованием инициализатора объекта – они НЕ равны.
Отличие заключается в том, как объект создается компилятором. В первом случае сразу создается объект company, во стором же случае объект сначала создается во временную переменную, а затем присваивается newCompany. Это называется атомарное присвоение (atomic assignment). Присвоение должно читатся справа налево: выполнить правую часть и присвоить левой. Это важно при использовании многопоточности.
Чтобы подробней узнать, о том как работают инициализаторы объектов, советую к прочтению пост Bart De Smet : C# 3.0 Object Initializers Revisited.
Инициализаторы коллекций работают подобно инициализаторам объектов. Рассмотрим пример. Привычным для нас способом добавления объектов в коллекцию (в .NET 2.0), является следующий способ: сначала создается объект коллекции, а потом добавляются элементы коллекции, используя метод Add.
Используя инициализаторы коллекций, этот пример можно записать следующим образом:
Когда компилятор встречает такой синтаксис, он преобразует это в вызовы метода Add коллекции. Никаких новых IL инструкций не было добавлено для поддержки инициализаторов объектов и коллекций. Для CLR оба вызова идентичны.
В предыдущих версиях "Orcas" можно было упускать имя типа (Company в нашем случае) для элементов при инициализации коллекции, но в релизной версии компилятор требует явного указания типа объекта. Это было сделано по довольно простым причинам: в случае, если вы создаете типизированную коллекцию, и указываете интерфейс или абстрактный класс, какое дожно быть правильное поведение компилятора в этом случае?
На этом пожалуй и все об инициализаторах объектов и коллекций. Далее рассмотрим методы-расширения (extension methods).
Методы-расширения (назовем их так), или extension methods – это еще одна новая возможность .NET Framework 3.5 для расширения уже существующих публичных контрактов существующих CLR типов.
Порой возникают ситуации, когда нам очень нехватает какого-нибудь метода в существующем типе, и нам приходиться писать своего рода helper'ы. Например, мне всегда нехватало TrimIsNullOrEmpty у String. Сейчас добавить этот метод, используя extension method'ы, не составляет особой сложности.
Итак, рассмотрим пример простого extension method'а.
Обратите внимание, что статический метод первым параметром указывает на объект, который необходимо «расширить». Происходит это с помощью ключевого слова this перед параметром метода. Это ключевое слово указывает компилятору на то, что этот статический метод должен быть добавлен к объекту типа String.
Использовать этот extension method очень просто:
Отличить extension method'ы в IntelliSence можно по значку: .
Методы-расширения могут быть применены как к классам, так и к интерфейсам (interface) или перечислениям (enum). Эта возможнось позволяет расширять IEnumerable<T> интерфейс, обеспечивая тем самым поддержку LINQ.
У extension method'ов есть свои ограничения на использование:
Большой набор методов-расширений поставляются вместе с LINQ, и являются частью LINQ. Сейчас мы их рассматривать не будем, приведу только пример использования extension method'ов в LINQ:
Этот LINQ запрос будет транслирован компилятором в вызовы extension method'ов следующим образом:
Новый синтаксис, который вы вероятно заметили: c => c.NumberOfEmployees > 100 называется лямбда-выражением (lambda expression), о котором я раскажу немного позже.
А сейчас немного о extension method resolution, или о том, как компилятор определяет, какой из методов вызвать – метод-расшерение или метод класса, если оба совпадают по имени, возвращаемому и принимаемому значениям. Рассмотрим пример:
Далее, для объектов ClassA, ClassB и ClassC вызовем метод Do с разными параметрами:
В итоге можно получить следующие результаты выполнения этого кода:
Extension method Do(this object element, int id) called. Extension method Do(this object element, string name) called. ClassB.Do called Extension method Do(this object element, string name) called. ClassC.Do calledClassC.Do called
Итак, как же работает extension method resolution. Ответ довольно «простой», компилятор выберет метод, который является наиболее «близким». А точнее:
- Сначала ищется строго типизированный метод класса, и если он найден, то используется он;- Затем комплятор ищет строготипизированный метод-расширение и если он найден, то использует его;- Далее, компилятор ищет нетипизированный метод класса, и если находит, то он будет вызван, иначе – ищется нетипизированный метод-расширение. - Если же ни один из вариантов не найден – то произойдет ошибка компиляции.
Отмечу, что если использовать эффективно методы-расширения, то это может значительно улучшить как читаемость кода (особенно при code review) так и уменьшить количество строк кода и багов. НО: если у вас есть возможность изменить код или пронаследовать класс, чтоб добавить новый метод – я бы советовал поступить именно так. Прибегайте к использованию extension method'ов только по крайней необходимости.
Еще одним нововведением в .NET Framework 3.5 являются «частичные методы» (или partial methods). Всем уже знакома концепция частичных классов, которая была добавлена в .NET 2.0. Используя partial классы в Windows Forms и ASP.NET разделяется код, который был сгенерирован, например дизайнером, и пользовательский код. Также удобно выносить какие-нибудь низкоуровневые задачи бизнес-логики в отдельные файлы (объедененные логически одним partial классом).
Примечание: Используя тег <DependentUpon> (а файле проекта) можно низкоуровневые реализации класса визуально сгруппировать под классом с основной реализацией.
В .NET Framework 3.5 существует возможность создавать не только partial классы, но и partial методы. В основном partial методы используются при автоматической генерации кода (например, при использовании дизайнера в LINQ to SQL). Но у них есть свои практические преимущества (при использовании их при разработке библиотек классов).
Начнем с примера:
В примере я определил обе части partial класса в одном файле, но в «реальной жизни» они вероятней всего будут разделены по разным файлам. В первой части я определил метод DoSubtask как partial и упустил реализацию метода (как это обычно делаеться для абстрактных классов или интерфейсов). И использовал метод DoSubtask в методе DoTask(). Используем наш класс Worker и вызовем метод DoTask (а затем посмотрим что же происходит «за кулисами»).
Что же происходит с методом DoSubtask, когда он дожен вызватся, ведь он не реализован. Ответ – ничего. Да, ничего, если посмотреть «внутрь» с помощью Reflector'а или ILDASM'а, то можно увидеть что этого метода в сборке просто не существует. Компилятор, встречая partial метод без реализации не только исключает его из сборки, но и исключает все его вызовы.
Когда нам понадобится реализовать метод DoSubtask достаточно написать partial в классе, и IntelliSence подскажет какие из partial методов не реализованы.
Покажу еще один пример использования partial методов в LINQ (т. к. это не является целью этого поста, то не буду объяснять КАК это сделать, раскажу лишь, как это работает). Итак, когда мы используем LINQ to SQL Classes компонент для создания модели базы, то для добавленных в дизайнере объектов будут созданы дополнительные partial методы, которые можно реализовывать, тем самым расширяя функциональность того или иного объекта или контекста.
Использование partial методов имеет свои ограничения.
Во-первых, partial метод должен всегда быть приватным, если вы попытаетесь добавить модификатор доступа, отличный от приватного, то вы получите ошибку компиляции:
error CS0750: A partial method cannot have access modifiers or the virtual, abstract, override, new, sealed or extern modifiers
В случае, если бы было можно создавать public partial методы, то для внешних контрактов, использующих ваш класс небыло бы никакой гарантии, что partial метод реализован. По этой же причине нельзя создать делегат на partial метод.
Еще одним ограничением является то что, partial методы должны возвращать void. Если попытатся скомпилировать следующий код:
То получим следующую ошибку компиляции:
error CS0766: Partial methods must have a void return type
По той же причине partial методы не могут принимать out параметры. Т.к. out параметр должен быть инициализирован при выходе из метода, и если partial метод не будет реализован, то и out параметр не будет инициализирован. Но, не смотря на ограничение с out, параметры передающиеся с помощью ref разрешены (хотя, на самом деле, out и ref – одно и то же, просто компилятор выполняет разные проверки).
error CS0752: A partial method cannot have out parameters
На этом пожалуй и все, что я хотел расказать о partial методах. И у нас осталась одна не раскрытая тема – лямбда-выражения (lambda expressions).
С выходом .NET 2.0 уразработчиков появилась возможность создавать inline-методы, в тех случаях где ожидались делегаты (например в List<T>.Find() можно указать предикат поиска не создавая предикат отдельно).
Лямбда-выражения – это более сжатый функциональный синтаксис для определения анонимных методов. Лямбда-выражения повсеместно используются в LINQ, и предоставляют возможность очень компактного и типобезопасного способа записи методов, для последующей передачи и в качестве аргументов методов. Где-то так..
Но лучше разобратся на примере.
В нашем примере c => c.NumberOfEmployees < 100 – это и есть лямбда-выражение: c – имя параметра, => - лямбда оператор, c.NumberOfEmployess < 100 – это «тело» лямбды.
Самый простой способ понять лямбда-выражения – думайте о них как об анонимных методах. Но у лямбда-выражений есть свои преимущества перед анонимными методами:
Оператор => всегда следует засписком параметров не стоит путать его с операторами сравнения: >= или <=.
Примеры лямбда-выражений:
В примере f1 и f2 – с неявным определением типа (с выводом типа из тела выражения), в то время как f3 и f4 – с явным указанием типа аргумента; f1, f3, f5, f6, f8-f10 – это лямбда-выражения с выражением (expression) в качестве тела, все остальные – с блоком операторов (statement block) в теле лямбда-выражения; f5 и f10 – лямбды с несколькими параметрами.
Приведу еще один пример лямбда-выражения:
На этом пожалуй и все. В следующий раз я раскажу о expression trees и о том как строить запросы к коллекциям в памяти используя LINQ.
Happy coding.
P.S.: Вы можете также скачать примеры и презентацию к этой статье.
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.