Language Integrated Query построен на базе функций языка, многие из которых являются новыми для VB.NET и C#. Давайте же рассмотрим их.
Деревья выражений и λ-выражения
Примечание
Поскольку λ-выражения для VB.NET ещё не реализованы в текущем Community Technology Preview, примеры кода для него не даны. Статья будет обновлена с примерами кода, как только это станет возможным.
Как было сказано раннее, λ-выражения являются ещё более мощным способом сократить объём кода, чем анонимные методы. Например, λ-выражение
Код C# 3.0
forum => forum.Length == 7
эквивалентно анонимному методу
Код C# 3.0
delegate (string forum) {return forum.Length == 7;}
или обычному
Код C# 3.0
bool MyLambda (string forum)
{
return forum.Length == 7;
}
Код VB.NET 9.0
Function MyLambda (forum As String)
Return forum.Length = 7
End Function
Но это ещё не всё. λ-выражения приводятся к делегату System.Query.Func<>, который может быть представлен в следующем виде:
· public delegate T Func<T> ()
· public delegate T Func<A0, T> (A0 arg0)
· public delegate T Func<A0, A1, T> (A0 arg0, A1 arg1)
· public delegate T Func<A0, A1, A2, T> (A0 arg0, A1 arg1, A2 arg2)
· public delegate T Func<A0, A1, A2, A3, T> (A0 arg0, A1 arg1, A2 arg2, A3 arg3)
Таким образом, код, который мы использовали для запроса, можно переписать так:
Код C# 3.0
Func<string, bool> filter = (forum => forum.Length == 7);
Func<string, string> extract = (forum => forum);
Func<string, string> project = (forum => forum.ToUpper());
IEnumerable<string> expr = names.Where(filter)
.OrderBy(extract)
.Select(project);
Без использования λ-выражений этот код выглядел бы даже так:
Код C# 3.0
Func<string, bool> filter = delegate (string forum) {
return forum.Length == 7;
};
Func<string, string> extraction = delegate (string forum) {
return forum;
};
Func<string, string> projection = delegate (string forum) {
return forum.ToUpper();
};
IEnumerable<string> expr = names.Where(filter)
.OrderBy(extraction)
.Select(projection);
На самом деле компилятору абсолютно всё равно, какой синтаксис вы используете. Более того, неужели вы думаете, что я смог удержаться и не глянуть Reflector’ом на то, что генерирует компилятор, когда встречает λ-выражения;-)
|
Смотрите сами.
В нашем классе появились два метода, названные соответственно _Lambda_Main_9_13 и _Lambda_Main_B_12. Как нетрудно догадаться, это и есть наши λ-выражения, которые компилятор превратил в private-методы. Однако, это не единственный способ использовать λ-выражения.
С приходом LINQ появилось новое пространство имён System.Expressions. В нём определён тип Expression<T>, который позволяет строить деревья выражений (expression trees). Вся суть в том, что при присвоении λ-выражения переменной типа Expression<T> метод не генерируется: λ-выражение сохраняется в бинарном виде. Вот что сказано по этому поводу в MSDN: Expression trees are efficient in-memory data representations of lambda expressions and make the structure of the expression transparent and explicit. Возникает здравый вопрос: зачем же это нужно? Всё очень просто. При проектировании DLinq было необходимо преобразовывать λ-выражение, переданное оператору Where, в SQL-запрос. Разумеется, тело метода трудно обратно декомпилировать и транслировать в SQL, поэтому и ввели деревья выражений.
Допустим, у нас есть два выражения:
Код C# 3.0
Func<string, bool> isVingradFunc = (s => s == "Vingrad");
Expression<Func<string,bool>> isVingradExpr = (s => s == "Vingrad");
В таком случае, этот код скомпилируется:
Код C# 3.0
bool isVingrad = isVingradFunc("NotVingrad");
А этот вызовет ошибку, т.к. выражения являются всего лишь данными и не могут выполняться:
Код C# 3.0
bool isVingrad = isVingradExpr("NotVingrad");
Лично меня очень впечатлила возможность вот такого «разбора» λ-выражения:
Код C# 3.0
Expression<Func<string,bool>> isVingrad = (s => s == "Vingrad");
|
Console.WriteLine("Number of parameters: {0}", isVingrad.Parameters.Count);
foreach (ParameterExpression p in isVingrad.Parameters)
Console.WriteLine(" Parameter {0} of type {1};", p.Name, p.Type);
Console.WriteLine("Return type: {0}", isVingrad.Body.Type);
MethodCallExpression body = (MethodCallExpression)isVingrad.Body;
Console.WriteLine("Expression calls {0} method", body.Method.Name);
ConstantExpression secondPart = (ConstantExpression)body.Parameters[1];
Console.WriteLine("Second part of expression: {0}", secondPart.Value);
Console.WriteLine("Expression.ToString method: {0}", isVingrad);
Этот код выводит
Number of parameters: 1
Parameter s of type System.String;
Return type: System.Boolean
Expression calls op_Equality method
Second part of expression: Vingrad
Expression.ToString method: |s| op_Equality(s, "Vingrad")
Методы-расширения
Для начала нам необходимо познакомиться с методами-расширениями (extension methods). Метод-расширение позволяет нам добавить свой метод ко всем классам. Выглядит примерно так:
Код C# 3.0
namespace MyExtensions
{
using System.Runtime.CompilerServices;
public static class MyExtension
{
[Extension]
public static string GetTypeFullName(this object source)
{
return source.GetType().FullName;
}
[Extension]
public static int MakeDouble(this int num)
{
return num * 2;
}
}
}
Использование:
Код C# 3.0
namespace ExtensionsTest
{
using MyExtensions;
class Program
{
static void Main(string[] args)
{
object o = new object();
int i = 5;
Console.WriteLine(o.GetTypeFullName());
//Console.WriteLine(MyExtension.GetTypeFullName(o));
Console.WriteLine(i.MakeDouble());
//Console.WriteLine(MyExtension.MakeDouble(i));
Console.Read();
}
}
}
Короче, весьма удобная штука. Но у них ещё одна функция – именно они позволяют нам использовать операторы LINQ. По сути, стандартные операторы Select, Where, OrderBy являются методами-расширениями, определёнными в классе System.Query.OrderedSequence (где-то мы его видели?). Давайте взглянем на методы-расширения более детально. Вот несколько их особенностей:
· Классы, определяющие методы-расширения, должны быть статическими.
|
· Методы-расширения должны быть объявлены с атрибутом [System.Runtime.CompilerServices.Extension].
· В метод-расширение всегда передаётся параметр, помеченный ключевым словом this – ссылка на экземпляр класса, к которому это расширение применяется. Кстати, тип объекта, к которому расширение можно применить, можно ограничить, просто определив тип этого параметра, отличный от object, как показано в примере с MakeDouble().
· Для импорта методов-расширений, определённых в каком-то классе, необходимо объявить пространство имён, в котором находится этот класс, в директиве using.
После того, как мы ознакомились с концепцией методов-расширений в общих чертах, давайте взглянем на конкретный пример реализации запроса:
Код C# 3.0
namespace System.Query
{
using System;
using System.Collections.Generic;
public static class Sequence {
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source, //Источник – перечисление
Func<T, bool> predicate) { //Функция-условие
foreach (T item in source) //Для каждого объекта в источнике
if (predicate(item)) //Если условие выполняется
yield return item; //Добавить в перечисление-результат
}
}
}
Как видно, тип объекта, к которому применяется расширение, ограничен типом IEnumerable<T> (перечислением). Теперь мы можем написать такой код:
Код C# 3.0
IEnumerable<string> coolForum = forums.Where(f => f.Length == 7);
Он будет эквивалентным следующему коду:
Код C# 3.0
IEnumerable<string> coolForum = Sequence.Where(forums,
f => f.Length == 7);
Поскольку, чтобы компилятор «заметил» присутствие статических типов с методами-расширениями, необходимо объявить их в директиве using (Imports в VB), для использования стандартных операторов запросов, как нетрудно догадаться, необходимо написать
Код C# 3.0
using System.Query; // делает видимыми операторы запросов
Код VB.NET 9.0
Imports System.Query // делает видимыми операторы запросов
Все операторы запросов работают с перечислениями IEnumerable<T>, кроме оператора OfType, который работает с перечилениями в стиле 1.0/1.1 фреймворка, «приводя их в порядок», то есть к виду IEnumerable<T>. Разумеется, он также может применяться и к перечислениям вида IEnumerable<T>, позволяя выбирать из перечислений только объекты определённого типа.
Вот пример приведения коллекции из вида 1.0 к виду IEnumerable<T>:
Код C# 3.0
// коллекция в «классическом» виде не может использоваться с запросами...
IEnumerable classic = new OlderCollectionType();
//... поэтому мы представим её в виде IEnumerable<object>
IEnumerable<object> modern = classic.OfType<object>();
Вот пример другого использования оператора OfType:
Код C# 3.0
IEnumerable<object> stuff = new object[]{"Vingrad", 1, true, "RSDN"};
IEnumerable<string> forums = stuff.OfType<string>(); //отбираем только строки
Собственно говоря, вот стандартная реализация оператора OfType.
Код C# 3.0
public static IEnumerable<T> OfType<T>(this IEnumerable source) {
foreach (object item in source)
if (item is T)
yield return (T)item;
}