Книга: Microsoft Visual C#. Подробное руководство. 8-е издание
Назад: 20. Отделение логики приложения и обработка событий
Дальше: 22. Перегрузка операторов

21. Запрос данных, находящихся в памяти, с помощью выражений в виде запросов

Прочитав эту главу, вы научитесь:

определять запросы на встроенном в C# расширении (Language-Integrated Query (LINQ)) для исследования содержимого перечисляемых коллекций;

использовать методы расширения LINQ и операторы запросов;

объяснять, как LINQ откладывает вычисление запроса и как можно принудить систему к немедленному выполнению LINQ-запроса и кэшированию его результатов.

Большинство функциональных особенностей языка C# уже рассмотрено. Но до сих пор я обходил молчанием один важный аспект, который был бы наверняка полезен во многих приложениях: имеется в виду предоставляемая C# поддержка составления запросов данных. Вы уже поняли, что для моделирования данных можно определять структуры и классы и что для временного хранения данных в памяти можно использовать коллекции и массивы. Но как выполнить такую широко востребованную задачу, как поиск в коллекции элементов, отвечающих конкретному набору критериев? Например, если есть коллекция объектов Customer, как в ней найти всех клиентов, проживающих в Лондоне, или как определить, в каких городах находится большинство клиентов, пользующихся вашими услугами? Можно написать собственный код для сквозного обхода элементов коллекции и исследования полей каждого объекта, но подобного рода задачи настолько широко востребованы, что разработчики C# решили включить в язык функциональные средства для минимизации обязательного для написания объема кода. В этой главе вы научитесь использовать усовершенствования языка C#, созданные с целью запроса данных и работы с ними.

Что такое LINQ?

Обрабатывать данные приходится всем приложениям, за исключением, может быть, самых примитивных. Исторически сложилось так, что большинство приложений предоставляло для выполнения связанных с этим операций свою собственную логику. Но такая стратегия может привести к тому, что код в приложении станет тесно связанным со структурой обрабатываемых данных. Если структура данных изменится, может потребоваться внесение большого объема изменений в код, обрабатывающий данные. Разработчики среды Microsoft .NET Framework долго и упорно обдумывали пути решения этих проблем и решили облегчить жизнь разработчикам приложений, предоставив функциональные возможности, отделяющие механизм, используемый приложением для запроса данных, от самого кода приложения. Эти возможности были названы Language-Integrated Query (LINQ).

Создатели LINQ непредвзято присмотрелись к способу, используемому реляционными системами управления базами данных, такими как Microsoft SQL Server, отделив язык, используемый для запросов к базе данных, от внутреннего формата данных в базе данных. Разработчики обращались к базе данных SQL Server, выдавая инструкции на языке структурных запросов (Structured Query Language (SQL)). SQL предоставляет высокоуровневое описание данных, которые разработчику нужно извлечь, но не дает точных указаний, как именно система управления базами данных должна извлекать эти данные. Эти тонкости являются прерогативой самой системы управления базами данных. Следовательно, приложение, выдающее SQL-инструкции, не интересуется тем, как система управления базами данных физически сохраняет или извлекает данные. Формат, используемый системой управления базами данных, может изменяться (например, при выпуске новой версии), не требуя от разработчиков приложений внесения изменений в SQL-инструкции, используемые приложениями.

LINQ предоставляет синтаксис и семантику, которые напоминают SQL и во многом обеспечивают такие же преимущества. Основную структуру запрашиваемых данных можно изменять, не испытывая при этом необходимости в изменении кода, фактически выполняющего запросы. Следует иметь в виду: несмотря на то что LINQ-запрос похож на SQL, он намного гибче и способен управлять широким диапазоном логических структур данных. LINQ может обрабатывать данные с иерархической организацией, к примеру, данные, которые могут храниться в XML-документе. Но в этой главе основное внимание уделяется использованию LINQ в реляционной форме.

Использование LINQ в приложении на C#

Возможно, проще всего объяснить использование функциональных средств C#, поддерживающих LINQ, путем проработки ряда простых примеров, основанных на следующих наборах информации о клиентах и адресах (табл. 21.1 и 21.2).

Таблица 21.1

CustomerID (Идентификатор клиента)

FirstName (Имя)

LastName (Фамилия)

CompanyName (Название компании)

1

Kim

Abercrombie

Alpine Ski House

2

Jeff

Hay

Coho Winery

3

Charlie

Herb

Alpine Ski House

4

Chris

Preston

Trey Research

5

Dave

Barnett

Wingtip Toys

6

Ann

Beebe

Coho Winery

7

John

Kane

Wingtip Toys

8

David

Simpson

Trey Research

9

Greg

Chapman

Wingtip Toys

10

Tim

Litton

Wide World Importers

Таблица 21.2

CompanyName (Название компании)

City (Город)

Country (Страна)

Alpine Ski House

Berne

Switzerland

Coho Winery

San Francisco

United States

Trey Research

New York

United States

Wingtip Toys

London

United Kingdom

Wide World Importers

Tetbury

United Kingdom

LINQ-расширению требуется, чтобы данные хранились в структуре данных, реализующей интерфейс IEnumerable или IEnumerable<T> в соответствии с описанием, которое дано в главе 19 «Перечисляемые коллекции». При этом абсолютно неважно, какая структура используется (массив, HashSet<T>, Queue<T> или любые другие типы коллекций или даже тип коллекции, определенный вами), при условии, что она будет перечисляемой. Но чтобы ничего не усложнять, примеры в этой главе предполагают, что информация о клиентах и об адресах хранится в массивах customers и addresses, показанных в следующем примере кода.

174225.png

ПРИМЕЧАНИЕ В реальном приложении содержимое этих массивов будет наполняться путем считывания данных из файла или из базы данных.

var customers = new[] {

    new { CustomerID = 1, FirstName = "Kim", LastName = "Abercrombie",

          CompanyName = "Alpine Ski House" },

    new { CustomerID = 2, FirstName = "Jeff", LastName = "Hay",

          CompanyName = "Coho Winery" },

    new { CustomerID = 3, FirstName = "Charlie", LastName = "Herb",

          CompanyName = "Alpine Ski House" },

    new { CustomerID = 4, FirstName = "Chris", LastName = "Preston",

          CompanyName = "Trey Research" },

    new { CustomerID = 5, FirstName = "Dave", LastName = "Barnett",

          CompanyName = "Wingtip Toys" },

    new { CustomerID = 6, FirstName = "Ann", LastName = "Beebe",

          CompanyName = "Coho Winery" },

    new { CustomerID = 7, FirstName = "John", LastName = "Kane",

          CompanyName = "Wingtip Toys" },

    new { CustomerID = 8, FirstName = "David", LastName = "Simpson",

          CompanyName = "Trey Research" },

    new { CustomerID = 9, FirstName = "Greg", LastName = "Chapman",

          CompanyName = "Wingtip Toys" },

    new { CustomerID = 10, FirstName = "Tim", LastName = "Litton",

          CompanyName = "Wide World Importers" }

};

 

var addresses = new[] {

    new { CompanyName = "Alpine Ski House", City = "Berne",

          Country = "Switzerland"},

    new { CompanyName = "Coho Winery", City = "San Francisco",

          Country = "United States"},

    new { CompanyName = "Trey Research", City = "New York",

          Country = "United States"},

    new { CompanyName = "Wingtip Toys", City = "London",

          Country = "United Kingdom"},

    new { CompanyName = "Wide World Importers", City = "Tetbury",

          Country = "United Kingdom"}

};

174232.png

ПРИМЕЧАНИЕ В разделах «Выбор данных», «Фильтрация данных», «Упорядочение, группировка и статистическая обработка данных» и «Объединение данных» будут показаны основные возможности и синтаксис, применяемый для запроса данных с помощью методов LINQ. Временами синтаксис будет немного усложняться, но когда вы дойдете до раздела «Использование операторов запросов», то поймете, что запоминать, как весь этот синтаксис работает, совершенно ни к чему. Однако вам будет полезно по крайней мере просмотреть все эти разделы, чтобы получить более полное представление о том, как операторы запросов, предоставляемые C#, справляются со своими задачами.

Выбор данных

Предположим, вам нужно вывести на экран список, состоящий из имен всех клиентов, присутствующих в массиве customers. Эту задачу можно выполнить с помощью следующего кода:

IEnumerable<string> customerFirstNames =

    customers.Select(cust => cust.FirstName);

 

foreach (string name in customerFirstNames)

{

    Console.WriteLine(name);

}

Несмотря на весьма скромный размер блока кода, он проделывает большую работу и требует некоторых разъяснений, начинающихся с использования метода Select массива customers.

С помощью метода Select можно извлечь из массива конкретные данные, в нашем случае это всего лишь значение поля FirstName каждого элемента массива. Как это работает? Параметром метода Select фактически является еще один метод, получающий строку из массива customers и возвращающий из этой строки выбранные данные. Для выполнения этой задачи можно определить свой собственный специализированный метод, но, как показано в предыдущем примере, проще всего воспользоваться лямбда-выражением и определить безымянный метод. А теперь следует усвоить три важных обстоятельства:

• Переменная cust является параметром, переданным методу. Параметр cust можно воспринимать как псевдоним для каждой строки массива customers. Компилятор выводит это из того факта, что метод Select вызывается в отношении массива customers. Вместо cust можно использовать любой допустимый идентификатор C#.

• На этой стадии метод Select не извлекает данные, он просто возвращает перечисляемый объект, который будет получать данные, идентифицированные методом Select при последующем проведении итерации. К этому аспекту LINQ мы еще вернемся в разделе «LINQ и отложенное вычисление».

• Метод Select фактически не является методом типа Array. Это метод расширения класса Enumerable, который находится в пространстве имен System.Linq и предоставляет изрядный набор статических методов для запросов объектов, реализующих интерфейс-обобщение IEnumerable<T>.

В предыдущем примере метод Select массива customers используется для создания IEnumerable<string>-объекта по имени customerFirstNames. (Он имеет тип IEnumerable<string>, поскольку метод Select возвращает перечисляемую коллекцию имен клиентов, являющихся строками.) Инструкция foreach выполняет сквозной обход этой коллекции строк, выводя на экран имена всех клиентов в следующей последовательности:

Kim

Jeff

Charlie

Chris

Dave

Ann

John

David

Greg

Tim

Теперь можно вывести на экран имя каждого клиента. А как получить имя и фамилию каждого клиента? Эта задача решается немного сложнее. Если изучить определение метода Enumerable.Select в пространстве имен System.Linq по документации, предоставленной Microsoft Visual Studio 2015, то можно увидеть его таким:

public static IEnumerable<TResult> Select<TSource, TResult> (

         this IEnumerable<TSource> source,

         Func<TSource, TResult> selector

)

По сути, это свидетельствует о том, что Select является методом-обобщением, получающим два параметра с именами TSource и TResult, а также два обычных параметра с именами source и selector. Параметр TSource является типом коллекции, для которой создается перечисляемый набор результатов (в данном случае объектов customer), а TResult является типом данных в перечисляемом наборе результатов (в данном случае строковых объектов). Следует напомнить, что Select является методом расширения, поэтому параметр source фактически ссылается на тип, подвергшийся расширению (обобщенную коллекцию объектов customer, реализующих в данном примере интерфейс IEnumerable). Параметр selector указывает на метод-обобщение, идентифицирующий извлекаемые поля. (Следует напомнить, что Func — это название типа делегата-обобщения в .NET Framework, которым можно воспользоваться для инкапсуляции метода-обобщения, возвращающего результат.) Метод, на который ссылается параметр selector, получает параметр типа TSource (в данном случае customer) и выдает объект типа TResult (в данном случае string). Значение, возвращаемое методом Select, является перечисляемой коллекцией объектов типа TResult (и опять это string).

174240.png

ПРИМЕЧАНИЕ Как работают методы расширения и какова роль первого параметра для метода расширения, объясняется в главе 12 «Работа с наследованием».

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

• Можно в методе Select объединить имя и фамилию в одну строку:

    IEnumerable<string> customerNames =

        customers.Select(cust => $"{cust.FirstName} {cust.LastName}");

• Можно определить новый тип, заключающий в себе имя и фамилию, и воспользоваться методом Select для создания экземпляров этого типа:

    class FullName

    {

        public string FirstName{ get; set; }

        public string LastName{ get; set; }

    }

    ...

    IEnumerable<FullName> customerNames =

        customers.Select(cust => new FullName

        {

            FirstName = cust.FirstName,

            LastName = cust.LastName

        } );

Возможно, второй вариант предпочтительнее, но если это единственный случай использования в вашем приложении типа FullName, то лучше будет, наверное, воспользоваться безымянным типом, особенно для единственной операции:

var customerNames =

    customers.Select(cust => new { FirstName = cust.FirstName,

                                   LastName = cust.LastName } );

Обратите внимание на использование здесь для определения типа перечис­ляемой коллекции ключевого слова var. Типы объектов в коллекции являются безымянными, поэтому конкретный тип объектов в коллекции вам неизвестен.

Фильтрация данных

Используя метод Select, можно указать или выдать поля, которые требуется включить в перечисляемую коллекцию. Но кроме этого может потребоваться наложить ограничения на строки, которые будут содержаться в этой перечисляемой коллекции. Предположим, к примеру, что нужно составить список, состоящий только из тех называний компаний в массиве addresses, которые находятся на территории США. Для этого можно воспользоваться методом Where:

IEnumerable<string> usCompanies =

    addresses.Where(addr => String.Equals(addr.Country, "United States"))

        .Select(usComp => usComp.CompanyName);

 

foreach (string name in usCompanies)

{

    Console.WriteLine(name);

}

Синтаксически метод Where похож на метод Select. Он ожидает получения параметров, определяющих метод, фильтрующий данные в соответствии с указанным критерием. В этом примере используется еще одно лямбда-выражение. Переменная addr является псевдонимом для строки в массиве addresses, а лямбда-выражение возвращает все строки, в которых поле Country соответствует строковому значению «United States». Метод Where возвращает перечисляемую коллекцию строк, содержащих каждое поле из исходной коллекции. Затем к строкам применяется метод Select, чтобы передать из этой перечисляемой коллекции только одно поле CompanyName и вернуть еще одну перечисляемую коллекцию строковых объектов. (Переменная usComp является псевдонимом для типа каждой строки в перечисляемой коллекции, возвращенной методом Where.) Следовательно, типом результата всего этого выражения будет IEnumerable<string>. Полезно усвоить такую последовательность операций: сначала для фильтрации строк применяется метод Where, затем для указания полей применяется метод Select. Инструкция foreach, выполняющая сквозной обход элементов этой коллекции, выводит на экран названия следующих компаний:

Coho Winery

Trey Research

Упорядочение, группировка и статистическая обработка данных

Тем, кто уже знаком с SQL, известно, что этот язык кроме простых выделения и фильтрации позволяет выполнять широкий спектр реляционных операций. Например, можно указать, что данные должны возвращаться в определенном порядке, можно сгруппировать возвращаемые строки по одному или нескольким ключевым полям, а можно подсчитать суммарные значения на основе строк в каждой группе. LINQ предоставляет такие же функциональные возможности.

Чтобы извлечь данные в определенном порядке, можно воспользоваться методом OrderBy. Так же, как и методы Select и Where, метод OrderBy в качестве своего аргумента ожидает предоставления метода. Этот метод определяет выражения, которые нужно использовать для сортировки данных. Например, можно вывести на экран название каждой компании из массива addresses в возрастающем порядке:

IEnumerable<string> companyNames =

    addresses.OrderBy(addr => addr.CompanyName).Select(comp => comp.CompanyName);

 

foreach (string name in companyNames)

{

    Console.WriteLine(name);

}

Этот блок кода выведет на экран компании, показанные в таблице адресов, в алфавитном порядке:

Alpine Ski House

Coho Winery

Trey Research

Wide World Importers

Wingtip Toys

Если нужно перечислить данные в убывающем порядке, можно воспользоваться методом OrderByDescending. Если данные нужно упорядочить по нескольким ключевым значениям, можно после OrderBy или OrderByDescending воспользоваться методом ThenBy или ThenByDescending.

Чтобы сгруппировать данные в соответствии с общими значениями в одном или нескольких полях, можно воспользоваться методом GroupBy. В следующем примере показано, как сгруппировать компании в массиве addresses по странам:

var companiesGroupedByCountry =

    addresses.GroupBy(addrs => addrs.Country);

 

foreach (var companiesPerCountry in companiesGroupedByCountry)

{

    Console.WriteLine(

        $"Country: {companiesPerCountry.Key}\t{companiesPerCountry.Count()}

                                               companies");

    foreach (var companies in companiesPerCountry)

    {

        Console.WriteLine($"\t{companies.CompanyName}");

    }

}

Теперь общая схема уже должна быть вам ясна. Метод GroupBy ожидает передачи ему метода, указывающего поля, по которым следует группировать данные. Но метод GroupBy немного отличается от других методов, рассмотренных ранее.

Интереснее всего отсутствие необходимости использования метода Select для выделения полей, попадающих в результат. Перечисляемый набор, возвращаемый GroupBy, содержит все поля исходной коллекции, но строки выстроены в набор перечисляемых коллекций на основе поля, определяемого методом, указываемым методу GroupBy. Иными словами, результатом работы метода GroupBy является перечисляемый набор групп, каждая из которых представляет собой перечисляемый набор строк. В только что показанном примере перечисляемый набор companiesGroupedByCountry является набором стран. Элементы этого набора сами по себе являются перечисляемыми коллекциями, содержащими компании для каждой из стран, выстроенные в очередь. Код, выводящий компании в каждой стране, использует для сквозного обхода набора companiesGroupedByCountry цикл foreach, а для сквозного обхода набора компаний каждой страны — вложенный цикл foreach. Обратите внимание на то, что во внешнем цикле foreach доступ к группируемому значению можно получить путем использования поля Key каждого элемента, а суммарные данные для каждой группы можно подсчитать, воспользовавшись для этого такими методами, как Count, Max, Min и многими другими. Выводимая кодом примера информация имеет следующий вид:

Country: Switzerland 1 companies

       Alpine Ski House

Country: United States 2 companies

       Coho Winery

       Trey Research

Country: United Kingdom 2 companies

       Wingtip Toys

       Wide World Importers

Многими из методов, выдающих статистическую информацию, например Count, Max и Min, можно воспользоваться, применяя их непосредственно к результатам работы метода Select. Если нужно узнать, сколько компаний находится в массиве addresses, можно использовать следующий блок кода:

int numberOfCompanies = addresses.Select(addr => addr.CompanyName).Count();

Console.WriteLine($"Number of companies: {numberOfCompanies}");

Обратите внимание на то, что результатом работы этих методов является одно скалярное значение, а не перечисляемая коллекция. Предыдущий блок кода выведет на экран следующую информацию:

Number of companies: 5

Но здесь следует сделать некоторые предостережения. Методы, выдающие статистические итоги, не различают строки в обрабатываемом наборе, содержащие продублированные значения в выделяемых полях. Строго говоря, это означает, что в предыдущем примере показывается только количество строк в массиве addresses, содержащих значение в поле CompanyName. Если нужно определить, сколько различных стран упомянуто в данной таблице, может возникнуть соблазн воспользоваться следующим кодом:

int numberOfCountries = addresses.Select(addr => addr.Country).Count();

Console.WriteLine($"Number of countries: {numberOfCountries}");

Он выведет на экран следующую информацию:

Number of countries: 5

Но на самом деле в массиве addresses содержатся только три страны — так уж вышло, что и United States, и United Kingdom фигурируют в нем дважды. Убрать дубликаты из вычисления можно с помощью метода Distinct:

int numberOfCountries =

    addresses.Select(addr => addr.Country).Distinct().Count();

Console.WriteLine($"Number of countries: {numberOfCountries}");

Вот теперь инструкция Console.WriteLine выведет на экран вполне ожидаемый результат:

Number of countries: 3

Объединение данных

Точно так же, как и язык SQL, расширение LINQ позволяет объединять несколько наборов данных по одному или нескольким общим ключевым полям. В следующем примере показано, как вывести на экран имя и фамилию каждого клиента вместе с названием страны его проживания:

var companiesAndCustomers = customers

    .Select(c => new { c.FirstName, c.LastName, c.CompanyName })

    .Join(addresses, custs => custs.CompanyName, addrs => addrs.CompanyName,

    (custs, addrs) => new {custs.FirstName, custs.LastName, addrs.Country });

 

foreach (var row in companiesAndCustomers)

{

    Console.WriteLine(row);

}

Имена и фамилии клиентов доступны в массиве customers, но страна для каждой компании, в которой работают клиенты, хранится в массиве addresses. Общим ключом для массивов customers и addresses является название страны. В методе Select указываются интересующие вас поля в массиве customers (FirstName и LastName), а также поле, содержащее общий ключ (CompanyName). Для объединения данных, которые были определены методом Select, c другой перечисляемой коллекцией используется метод Join. В этом методе используются следующие параметры:

• перечисляемая коллекция, с которой происходит объединение;

• метод, определяющий общие ключевые поля из данных, которые были определены методом Select;

• метод, определяющий общие ключевые поля, по которым объединяются выбранные данные;

• метод, указывающий столбцы, требующиеся в перечисляемом результирующем наборе, возвращаемом методом Join.

В данном примере метод Join объединяет перечисляемую коллекцию, содержащую поля FirstName, LastName и CompanyName из массива customers, со строками в массиве addresses. Два набора данных объединяются там, где значение в поле CompanyName в массиве customers соответствует значению в поле CompanyName в массиве addresses. В получающийся в результате этого набор включаются строки, содержащие поля FirstName и LastName из массива customers, а также поле Country из массива addresses. Код, выводящий на экран данные из коллекции companiesAndCustomers, показывает следующую информацию:

{ FirstName = Kim, LastName = Abercrombie, Country = Switzerland }

{ FirstName = Jeff, LastName = Hay, Country = United States }

{ FirstName = Charlie, LastName = Herb, Country = Switzerland }

{ FirstName = Chris, LastName = Preston, Country = United States }

{ FirstName = Dave, LastName = Barnett, Country = United Kingdom }

{ FirstName = Ann, LastName = Beebe, Country = United States }

{ FirstName = John, LastName = Kane, Country = United Kingdom }

{ FirstName = David, LastName = Simpson, Country = United States }

{ FirstName = Greg, LastName = Chapman, Country = United Kingdom }

{ FirstName = Tim, LastName = Litton, Country = United Kingdom }

174250.png

ПРИМЕЧАНИЕ Следует помнить, что коллекции в памяти не являются эквивалентом таблиц в реляционной базе данных и содержащиеся в них данные не подпадают под действие тех же ограничений целостности данных. Вполне допустимо предполагать, что в реляционной базе данных у каждого клиента имеется соответствующая компания и у каждой компании есть собственный уникальный адрес. В коллекциях отсутствует принудительная целостность данных на таком же уровне, значит, в них запросто может оказаться клиент, ссылающийся на компанию, отсутствующую в массиве addresses, и в этом же массиве какая-то компания может встречаться несколько раз. В подобных ситуациях получаемые результаты могут быть точными, но неожиданными. Операции объединения работают лучше в том случае, когда понятны взаимоотношения между объединяемыми данными.

Использование операторов запросов

В предыдущих разделах были рассмотрены многие функциональные возможности, доступные для создания запросов к находящимся в памяти данным путем использования методов расширения для класса Enumerable, определенного в пространстве имен System.Linq. В синтаксисе использовался ряд расширенных свойств языка C#, и получающийся в результате код порой мог быть трудным для понимания и сопровождения. Чтобы избавить вас от этих осложнений, разработчики C# добавили к языку операторы запросов, позволяющие использовать функциональные возможности расширения LINQ, применяя синтаксис, больше похожий на синтаксис языка SQL.

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

IEnumerable<string> customerFirstNames =

    customers.Select(cust => cust.FirstName);

Эту инструкцию можно переделать, используя операторы запросов from и select:

var customerFirstNames = from cust in customers

                         select cust.FirstName;

В ходе своей работы компилятор C# преобразует это выражение в соответствующий вызов метода Select. Оператор from определяет псевдоним для коллекции-источника, а оператор select указывает поля, извлекаемые с использованием этого псевдонима. В результате получается перечисляемая коллекция имен клиентов. Тем, кто знаком с SQL, следует обратить внимание на то, что оператор from ставится перед оператором select.

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

var customerNames = from cust in customers

                    select new { cust.FirstName, cust.LastName };

Для фильтрации данных используется оператор where. В следующем примере показано, как из массива addresses можно получить названия компаний, находящихся в США:

var usCompanies = from a in addresses

                  where String.Equals(a.Country, "United States")

                  select a.CompanyName;

Для упорядочения данных используется оператор orderby:

var companyNames = from a in addresses

                   orderby a.CompanyName

                   select a.CompanyName;

Группировка данных осуществляется с помощью оператора group:

var companiesGroupedByCountry = from a in addresses

                                group a by a.Country;

Заметьте, что здесь по аналогии с рассмотренным ранее примером группировки данных оператор select не предоставляется и сквозной обход результатов можно выполнить с помощью такого же кода, как и там:

foreach (var companiesPerCountry in companiesGroupedByCountry)

{

    Console.WriteLine(

        $"Country: {companiesPerCountry.Key}\t{companiesPerCountry.Count()}

                                               companies");

    foreach (var companies in companiesPerCountry)

    {

        Console.WriteLine($"\t{companies.CompanyName}");

    }

}

Функции статистической обработки, например Count, можно вызвать в отношении коллекции, возвращенной перечисляемой коллекцией, с помощью следующего кода:

int numberOfCompanies = (from a in addresses

                         select a.CompanyName).Count();

Заметьте, что выражение заключено в круглые скобки. Если требуется проигнорировать продублированные значения, воспользуйтесь методом Distinct:

int numberOfCountries = (from a in addresses

                         select a.Country).Distinct().Count();

175243.png

СОВЕТ Зачастую требуется просто подсчитать количество строк в коллекции, а не сумму чисел в полях во всех строках этой коллекции. В таком случае можно метод Count вызвать непосредственно в отношении исходной коллекции:

int numberOfCompanies = addresses.Count();

Оператор join можно использовать для объединения двух коллекций по общему ключу. В следующем примере показан запрос, возвращающий данные из массивов customers и addresses на основе столбца CompanyName каждой коллекции, переделанный в этот раз под использование оператора join. Для указания характера связи двух коллекций с оператором equals используется условие on.

174259.png

ПРИМЕЧАНИЕ В настоящее время в LINQ поддерживается только объединение equi-joins (объединение на основе равенства). Тому, кто занимался разработкой баз данных и привык к использованию SQL, могут быть знакомы объединения на основе других операторов, таких как > и <, но в LINQ подобные функциональные возможности не предоставляются.

var countriesAndCustomers = from a in addresses

                                 join c in customers

                                 on a.CompanyName equals c.CompanyName

                                 select new { c.FirstName, c.LastName, a.Country };

174266.png

ПРИМЕЧАНИЕ В отличие от SQL, порядок выражений в условии on LINQ-выражения играет важную роль. Объединяемый элемент из другой коллекции (ссылающийся на данные в коллекции в условии from) должен находиться слева от оператора equals, а элемент, с которым происходит объединение (ссылающийся на данные в коллекции в условии join), — справа от него.

Для получения сводной информации и объединения, а также для группировки и ведения поиска в данных расширением LINQ предоставляется большое количество других методов. В этом разделе были рассмотрены лишь наиболее востребованные из них. К примеру, в LINQ предоставляются методы Intersect и Union, которые могут использоваться для выполнения широкого спектра операций. Также в этом расширении предоставляются такие методы, как Any и All, которые можно использовать для определения наличия хотя бы одного элемента в коллекции или соответствия всех элементов коллекции определенному предикату. Значения в перечисляемой коллекции можно разбить на части, используя методы Take и Skip. Дополнительную информацию обо всех этих методах можно найти в материалах в разделе «LINQ» документации, предоставляемой средой Visual Studio 2015.

Запрос данных в объектах Tree<TItem>

В рассмотренных ранее примерах главы было показано, как данные запрашиваются из массива. Точно такую же технологию можно применять для любого класса коллекции, в котором реализован интерфейс-обобщение IEnumerable<T>. В следующем упражнении будет определен новый класс для моделирования работников компании. Вами будет создан объект BinaryTree, содержащий коллекцию объектов типа Employee, а затем для запроса нужной информации будет использовано расширение LINQ. Сначала методы расширения LINQ станут вызываться напрямую, а затем код будет изменен под использование операторов запросов.

Извлечение данных из BinaryTree с помощью методов расширения

Откройте в среде Visual Studio 2015 решение QueryBinaryTree, которое находится в папке \Microsoft Press\VCSBS\Chapter 21\QueryBinaryTree вашей папки документов. В проекте имеется файл Program.cs, в котором определяется класс Program с методами Main и doWork, которые уже встречались в предыдущих упражнениях.

В обозревателе решений щелкните правой кнопкой мыши на проекте QueryBinaryTree, укажите на пункт Добавить и щелкните на пункте Класс. Наберите в поле Имя диалогового окна Добавить новый элемент — Query BinaryTree строку Employee.cs и щелкните на кнопке Добавить.

Добавьте к классу Employee автоматически создаваемые свойства, выделенные в следующем фрагменте кода жирным шрифтом:

class Employee

{

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string Department { get; set; }

    public int Id { get; set; }

}

Добавьте к классу Employee выделенный в следующем примере кода жирным шрифтом метод ToString. Типы в среде .NET Framework используют этот метод при преобразовании объекта в строковое представление точно так же, как это делается при его выводе на экран с помощью инструкции Console.WriteLine:

class Employee

{

    ...

    public override string ToString()

    {

        return

          $"Id: {this.Id}, Name: {this.FirstName} {this.LastName}, Dept: {this.

                                  Department}";

    }

}

Измените определение класса Employee так, чтобы в нем реализовывался интерфейс IComparable<Employee>:

class Employee : IComparable<Employee>

{

}

Необходимость этого объясняется тем, что класс BinaryTree указывает на то, что его элементы должны быть сравниваемыми.

Наведите указатель мыши на интерфейс IComparable<Employee> в определении класса, щелкните на появившемся значке с горящей лампочкой, а затем щелкните в контекстном меню на пункте Реализовать интерфейс явно. В результате будет создана исходная реализация метода CompareTo. Следует напомнить, что класс BinaryTree вызывает этот метод, когда ему нужно сравнить элементы при вставке их в дерево.

Замените тело метода CompareTo следующим кодом, выделенным жирным шрифтом. Эта реализация метода CompareTo сравнивает объекты типа Employee на основе значения поля Id:

int IComparable<Employee>.CompareTo(Employee other)

{

    if (other == null)

    {

        return 1;

    }

 

    if (this.Id > other.Id)

    {

        return 1;

    }

 

    if (this.Id < other.Id)

    {

        return -1;

    }

    return 0;

}

174272.png

ПРИМЕЧАНИЕ Описание интерфейса IComparable<T> дается в главе 19.

В обозревателе решений щелкните правой кнопкой мыши на решении QueryBinaryTree, укажите на пункт Добавить и щелкните на пункте Существующий проект. В диалоговом окне Добавить существующий проект перейдите в папку Microsoft Press\VCSBS\Chapter 21\BinaryTree вашей папки документов, щелкните на проекте BinaryTree, а затем на кнопке Открыть. В проекте BinaryTree содержится копия перечисляемого класса BinaryTree, который был реализован в главе 19.

В обозревателе решений щелкните правой кнопкой мыши на проекте QueryBinaryTree, укажите на пункт Добавить и щелкните на пункте Ссылка. Щелкните в левой панели диалогового окна Менеджер ссылок — QueryBinaryTree на пункте Решение. Установите в средней панели флажок напротив проекта BinaryTree, а затем щелкните на кнопке OK.

Выведите в окно редактора файл Program.cs проекта QueryBinaryTree и убедитесь в том, что в список директив using в начале файла включена следующая строка кода:

using System.Linq;

Добавьте в список в начале файла Program.cs директиву using, чтобы поместить в область видимости пространство имен BinaryTree:

using BinaryTree;

Удалите из метода doWork в классе Program комментарий // TODO: и добавьте инструкции, выделенные в следующем примере кода жирным шрифтом, чтобы создать и заполнить экземпляр класса BinaryTree:

static void doWork()

{

  Tree<Employee> empTree = new Tree<Employee>(

    new Employee { Id = 1, FirstName = "Kim", LastName = "Abercrombie",

                        Department = "IT"});

  empTree.Insert(

    new Employee { Id = 2, FirstName = "Jeff", LastName = "Hay",

                        Department =     "Marketing"});

  empTree.Insert(

    new Employee { Id = 4, FirstName = "Charlie", LastName = "Herb",

                        Department = "IT"});

  empTree.Insert(

    new Employee { Id = 6, FirstName = "Chris", LastName = "Preston",

                        Department = "Sales"});

  empTree.Insert(

    new Employee { Id = 3, FirstName = "Dave", LastName = "Barnett",

                        Department = "Sales"});

  empTree.Insert(

    new Employee { Id = 5, FirstName = "Tim", LastName = "Litton",

                        Department="Marketing"

});

}

Добавьте к концу метода doWork следующие выделенные жирным шрифтом инструкции. Этот код вызывает метод Select для вывода списка подразделений, найденных в двоичном дереве.

static void doWork()

{

    ...

    Console.WriteLine("List of departments");

    var depts = empTree.Select(d => d.Department);

 

    foreach (var dept in depts)

    {

        Console.WriteLine($"Department: {dept}");

    }

}

Щелкните в меню Отладка на пункте Запуск без отладки.

Приложение должно вывести на экран следующий список подразделений:

List of departments

Department: IT

Department: Marketing

Department: Sales

Department: IT

Department: Marketing

Department: Sales

Каждое подразделение фигурирует дважды, поскольку в каждом подразделении по два работника. Порядок следования подразделений определяется методом CompareTo класса Employee, который использует для сортировки данных свойство Id каждого работника. Первое подразделение показано для работника с Id, имеющим значение 1, второе подразделение показано для работника с Id, имеющим значение 2, и т.д.

Нажмите клавишу Ввод, чтобы вернуться в среду Visual Studio 2015.

Измените в методе doWork класса Program инструкцию, создающую перечисляемую коллекцию подразделений, выделенную в данном примере жирным шрифтом:

var depts = empTree.Select(d => d.Department).Distinct();

Метод Distinct удаляет из перечисляемой коллекции дубликаты строк.

Щелкните в меню Отладка на пункте Запуск без отладки.

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

List of departments

Department: IT

Department: Marketing

Department: Sales

Нажмите клавишу Ввод, чтобы вернуться в среду Visual Studio 2015.

Добавьте к концу метода doWork следующие инструкции, показанные далее жирным шрифтом. В этом блоке кода для фильтрации работников и возвращения только тех из них, кто работает в IT-подразделении, используется метод Where. Метод Select возвращает всю строку, не выделяя конкретные столбцы:

static void doWork()

{

    ...

    Console.WriteLine("\nEmployees in the IT department");

    var ITEmployees =

        empTree.Where(e => String.Equals(e.Department, "IT"))

        .Select(emp => emp);

 

    foreach (var emp in ITEmployees)

    {

        Console.WriteLine(emp);

    }

}

После показанного выше кода добавьте к концу метода doWork следующий код, выделенный здесь жирным шрифтом. В этом коде для группировки работников, найденных в двоичном дереве по подразделению, используется метод GroupBy. Внешняя инструкция foreach выполняет сквозной обход каждой группы, выводя на экран название подразделения. Внутренняя инструкция foreach выводит на экран имена работников каждого подразделения:

static void doWork()

{

    ...

    Console.WriteLine("\nAll employees grouped by department");

    var employeesByDept = empTree.GroupBy(e => e.Department);

 

    foreach (var dept in employeesByDept)

    {

        Console.WriteLine($"Department: {dept.Key}");

        foreach (var emp in dept)

        {

            Console.WriteLine($"\t{emp.FirstName} {emp.LastName}");

        }

    }

}

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что приложение выводит на экран следующие данные:

List of departments

Department: IT

Department: Marketing

Department: Sales

 

Employees in the IT department

Id: 1, Name: Kim Abercrombie, Dept: IT

Id: 4, Name: Charlie Herb, Dept: IT

 

All employees grouped by department

Department: IT

        Kim Abercrombie

        Charlie Herb

Department: Marketing

        Jeff Hay

        Tim Litton

Department: Sales

        Dave Barnett

        Chris Preston

Нажмите Ввод, чтобы вернуться в среду Visual Studio 2015.

Извлечение данных из BinaryTree с помощью операторов запросов

Закомментируйте в методе doWork инструкцию, создающую коллекцию подразделений, и вставьте вместо нее эквивалентную инструкцию, выделенную жирным шрифтом, в которой используются операторы запросов from и select:

// var depts = empTree.Select(d => d.Department).Distinct();

var depts = (from d in empTree

             select d.Department).Distinct();

Закомментируйте инструкцию, создающую коллекцию работников IT-подразделения, и вставьте следующий код, выделенный жирным шрифтом:

// var ITEmployees =

//    empTree.Where(e => String.Equals(e.Department, "IT"))

//    .Select(emp => emp);

var ITEmployees = from e in empTree

                  where String.Equals(e.Department, "IT")

                  select e;

Закомментируйте инструкцию, создающую коллекцию, группирующую работников по подразделениям, и вставьте вместо нее инструкции, выделенные в следующем коде жирным шрифтом:

// var employeesByDept = empTree.GroupBy(e => e.Department);

var employeesByDept = from e in empTree

                      group e by e.Department;

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что приложение выводит на экран тот же результат, что и прежде:

List of departments

Department: IT

Department: Marketing

Department: Sales

 

Employees in the IT department

Id: 1, Name: Kim Abercrombie, Dept: IT

Id: 4, Name: Charlie Herb, Dept: IT

 

All employees grouped by department

Department: IT

        Kim Abercrombie

        Charlie Herb

Department: Marketing

        Jeff Hay

        Tim Litton

Department: Sales

        Dave Barnett

        Chris Preston

Нажмите Ввод, чтобы вернуться в среду Visual Studio 2015.

LINQ и отложенное вычисление

Когда расширение LINQ применяется для определения перечисляемой коллекции путем использования либо методов расширения LINQ, либо операторов запросов, следует помнить, что приложение фактически не создает коллекцию в ходе выполнения метода расширения LINQ — коллекция перечисляется, только когда выполняется ее обход. Это означает, что за время между выполнением LINQ-запроса и извлечением данных, определяемых запросом, данные в исходной коллекции могут измениться: извлекаться неизменно будут наиболее свежие данные. Например, следующий запрос (который уже был показан ранее) определяет перечисляемую коллекцию компаний, находящихся в США:

var usCompanies = from a in addresses

                  where String.Equals(a.Country, "United States")

                  select a.CompanyName;

Данные из массива addresses не извлекаются, и никакие условия, указанные в фильтре Where, не вычисляются, пока не будет выполняться сквозной обход элементов коллекции usCompanies:

foreach (string name in usCompanies)

{

    Console.WriteLine(name);

}

Если изменить данные в массиве addresses в период между определением коллекции usCompanies и сквозным обходом коллекции (например, если добавить новую компанию, находящуюся в США), эти новые данные станут видны. Такая стратегия называется отложенным вычислением.

Если LINQ-запрос определяет и создает статическую кэшируемую коллекцию, вычисление можно сделать вынужденным. Получаемая в результате коллекция является копией исходных данных и не будет меняться при изменении данных в исходной коллекции. Для создания статического List-объекта расширение LINQ предоставляет метод ToList, содержащий кэшированную копию данных. Она используется следующим образом:

var usCompanies = from a in addresses.ToList()

                  where String.Equals(a.Country, "United States")

                  select a.CompanyName;

На этот раз список компаний фиксируется при создании запроса. Если вы добавляете к массиву address еще больше компаний из США, то их при сквозном обходе элементов коллекции usCompanies видно не будет. Расширение LINQ также предоставляет метод ToArray, сохраняющий кэшированную коллекцию в массиве.

В заключительном упражнении этой главы будет выполнено сравнение эффектов использования отложенного вычисления LINQ-запроса с эффектами создания кэшированной коллекции.

Изучение эффектов отложенного и кэшированного вычисления LINQ-запроса

Выведите в окно редактора Visual Studio 2015 файл Program.cs проекта QueryBinaryTree. Закомментируйте часть содержимого метода doWork, кроме инструкций, создающих двоичное дерево empTree:

static void doWork()

{

  Tree<Employee> empTree = new Tree<Employee>(

      new Employee { Id = 1, FirstName = "Kim", LastName = "Abercrombie",

                   Department = "IT"});

  empTree.Insert(

    new Employee { Id = 2, FirstName = "Jeff", LastName = "Hay",

                 Department = "Marketing"});

  empTree.Insert(

    new Employee { Id = 4, FirstName = "Charlie", LastName = "Herb",

                 Department = "IT"});

  empTree.Insert(

    new Employee { Id = 6, FirstName = "Chris", LastName = "Preston",

                 Department = "Sales"});

  empTree.Insert(

    new Employee { Id = 3, FirstName = "Dave", LastName = "Barnett",

                 Department = "Sales"});

  empTree.Insert(

    new Employee { Id = 5, FirstName = "Tim", LastName = "Litton",

                 Department="Marketing"});

    // Закомментируйте всю остальную часть метода

    ...

}

175248.png

СОВЕТ Закомментировать блок кода можно, выбрав весь блок в окне редактора и щелкнув на кнопке панели инструментов. Закомментировать выделенные строки можно, нажав клавишу Ctrl и, удерживая ее, последовательно нажав клавиши K и C.

Добавьте к методу doWork сразу после кода, создающего и заполняющего двоичное дерево empTree, следующий код, выделенный жирным шрифтом:

static void doWork()

{

    ...

    Console.WriteLine("All employees");

    var allEmployees = from e in empTree

                       select e;

 

    foreach (var emp in allEmployees)

    {

        Console.WriteLine(emp);

    }

    ...

}

Этот код создает перечисляемую коллекцию работников allEmployees, а затем выполняет сквозной обход элементов этой коллекции, выводя на экран сведения, касающиеся каждого работника.

Добавьте сразу же после только что набранных инструкций следующий код:

static void doWork()

{

    ...

    empTree.Insert(new Employee

    {

        Id = 7,

        FirstName = "David",

        LastName = "Simpson",

        Department = "IT"

    });

    Console.WriteLine("\nEmployee added");

 

    Console.WriteLine("All employees");

    foreach (var emp in allEmployees)

    {

        Console.WriteLine(emp);

    }

    ...

}

Инструкции этого кода добавляют к дереву empTree нового работника, а затем еще раз выполняют сквозной обход элементов коллекции allEmployees.

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что приложение выводит следующие данные:

All employees

Id: 1, Name: Kim Abercrombie, Dept: IT

Id: 2, Name: Jeff Hay, Dept: Marketing

Id: 3, Name: Dave Barnett, Dept: Sales

Id: 4, Name: Charlie Herb, Dept: IT

Id: 5, Name: Tim Litton, Dept: Marketing

Id: 6, Name: Chris Preston, Dept: Sales

 

Employee added

All employees

Id: 1, Name: Kim Abercrombie, Dept: IT

Id: 2, Name: Jeff Hay, Dept: Marketing

Id: 3, Name: Dave Barnett, Dept: Sales

Id: 4, Name: Charlie Herb, Dept: IT

Id: 5, Name: Tim Litton, Dept: Marketing

Id: 6, Name: Chris Preston, Dept: Sales

Id: 7, Name: David Simpson, Dept: IT

Обратите внимание на то, что при втором сквозном обходе элементов коллекции allEmployees выводимый на экран список включает работника David Simpson, даже при том что он был добавлен только после определения коллекции allEmployees.

Нажмите Ввод, чтобы вернуться в среду Visual Studio 2015.

Внесите изменения, показанные жирным шрифтом, в метод doWork, в инструкцию, создающую коллекцию allEmployees, чтобы происходило определение и немедленное кэширование данных:

var allEmployees = from e in empTree.ToList<Employee>()

                   select e;

Расширение LINQ предоставляет обобщенные и необобщенные версии методов ToList и ToArray. По возможности лучше использовать обобщенные версии этих методов, чтобы гарантировать безопасность результата в отношении типов. Данные, возвращаемые оператором select, являются Employee-объектом, а только что показанный код создает коллекцию-обобщение allEmployees типа List<Employee>.

Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что приложение выводит следующие данные:

All employees

Id: 1, Name: Kim Abercrombie, Dept: IT

Id: 2, Name: Jeff Hay, Dept: Marketing

Id: 3, Name: Dave Barnett, Dept: Sales

Id: 4, Name: Charlie Herb, Dept: IT

Id: 5, Name: Tim Litton, Dept: Marketing

Id: 6, Name: Chris Preston, Dept: Sales

 

Employee added

All employees

Id: 1, Name: Kim Abercrombie, Dept: IT

Id: 2, Name: Jeff Hay, Dept: Marketing

Id: 3, Name: Dave Barnett, Dept: Sales

Id: 4, Name: Charlie Herb, Dept: IT

Id: 5, Name: Tim Litton, Dept: Marketing

Id: 6, Name: Chris Preston, Dept: Sales

Обратите внимание на то, что при втором сквозном обходе элементов коллекции allEmployees в выведенном на экран списке отсутствует работник David Simpson. В данном случае запрос был вычислен, а результат кэширован до того, как David Simpson был добавлен к двоичному дереву empTree.

Нажмите Ввод, чтобы вернуться в среду Visual Studio 2015.

Выводы

В этой главе были изучены приемы использования расширением LINQ интерфейса IEnumerable<T>, а также применение методов расширения для предоставления механизма запроса данных. Вы также увидели, как эти функциональные средства поддерживают синтаксис выражений запросов в C#.

Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 22 «Перегрузка операторов».

Если сейчас вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Увидев диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.

Краткий справочник

Чтобы

Сделайте следующее

Выделить указанные поля из перечисляемой коллекции

Воспользуйтесь методом Select и укажите лямбда-выражение, определяющее выделяемые поля, например:

var customerFirstNames = customers.Select(cust =>

cust.FirstName);

Или же воспользуйтесь операторами запросов from и select, например:

var customerFirstNames =

    from cust in customers

    select cust.FirstName;

Отфильтровать строки из перечисляемой коллекции

Воспользуйтесь методом Where и укажите лямбда-выражение, содержащее критерий, которому должны соответствовать строки, например:

var usCompanies =

    addresses.Where(addr =>

      String.Equals(addr.Country, «United States»))

      .Select(usComp => usComp.CompanyName);

Или же воспользуйтесь оператором запросов where, например:

var usCompanies =

    from a in addresses

    where String.Equals(a.Country, «United States»)

    select a.CompanyName;

Перечислить данные в определенном порядке

Воспользуйтесь методом OrderBy и укажите лямбда-выражение, определяющее поле, используемое для упорядочения строк, например:

 

var companyNames =

    addresses.OrderBy(addr => addr.CompanyName)

    .Select(comp => comp.CompanyName);

Или же воспользуйтесь оператором запросов orderby, например:

var companyNames =

    from a in addresses

    orderby a.CompanyName

    select a.CompanyName;

Сгруппировать данные по значению в поле

Воспользуйтесь методом GroupBy и укажите лямбда-выражение, определяющее поле, используемое для группировки строк, например:

var companiesGroupedByCountry =

    addresses.GroupBy(addrs => addrs.Country);

Или же воспользуйтесь оператором запросов group by, например:

var companiesGroupedByCountry =

    from a in addresses

    group a by a.Country;

Объединить данные, хранящиеся в двух разных коллекциях

Воспользуйтесь методом Join, указав коллекцию, с которой нужно объединиться, критерий объединения и поля для вывода результата, например:

var countriesAndCustomers =

  customers

    .Select(c => new { c.FirstName,   

c.LastName,c.CompanyName }).

  Join(addresses, custs => custs.CompanyName,

       addrs => addrs.CompanyName,

       (custs, addrs) => new {custs.FirstName,

        custs.LastName, addrs.Country });

Или же воспользуйтесь оператором запросов join, например:

var countriesAndCustomers =

    from a in addresses

    join c in customers

    on a.CompanyName equals c.CompanyName

    select new { c.FirstName, c.LastName, a.Country

};

Добиться от LINQ-запроса немедленного создания результата

Воспользуйтесь методом ToList или методом ToArray для создания списка или массива, содержащего результаты, например:

var allEmployees =

    from e in empTree.ToList<Employee>()

    select e;

Назад: 20. Отделение логики приложения и обработка событий
Дальше: 22. Перегрузка операторов

Антон
Перезвоните мне пожалуйста 8(812)642-29-99 Антон.
Антон
Перезвоните мне пожалуйста по номеру 8(904) 332-62-08 Антон.
Антон
Перезвоните мне пожалуйста, 8 (904) 606-17-42 Антон.
Антон
Перезвоните мне пожалуйста по номеру. 8 (953) 367-35-45 Антон
Ксения
Текст от профессионального копирайтера. Готово через 1 день. Консультация бесплатно. Жми roholeva(точка)com