Выражения запросов LINQ

C Sharp > Выражения запросов LINQ
23.11.2016 18:26:08


Наиболее часто встречающиеся слова в статье:

[запроса] [FirstName] [LastName] [примере] [Console] [student] [WriteLine] [следующем] [Student] [IEnumerable]


Статья:

 

LINQ — это название набора технологий, основанных на интеграции возможностей запроса непосредственно в язык C# (а также в Visual Basic и, возможно, в любые другие языки .NET). Благодаря LINQ запрос теперь является одним из основных структурных элементов языка, подобно классам, методам, событиям и т. д.

Для разработчиков, сознающих запросы, наиболее очевидная "встроенная в язык" часть LINQ — это выражение запросов. Выражения запросов составляются в соответствии с декларативным синтаксисом запроса, который был впервые предложен в C# 3.0. Синтаксис запроса позволяет выполнять достаточно сложную фильтрацию, упорядочение и операции группирования при работе с источниками данных, используя минимум программного кода. Можно использовать одинаковые базовые шаблоны запросов для запроса и преобразования данных в базах данных SQL, наборах данных ADO.NET, документах и потоках XML, а также в коллекциях .NET.

В следующем примере показана выполненная операция запроса. 

    class LINQQueryExpressions
    {
        static void Main()
        {
            
            // Specify the data source.
            int[] scores = new int[] { 97, 92, 81, 60 };

            // Define the query expression.
            IEnumerable<int> scoreQuery =
                from score in scores
                where score > 80
                select score;

            // Execute the query.
            foreach (int i in scoreQuery)
            {
                Console.Write(i + " ");
            }            
        }
    }
    // Output: 97 92 81

 

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

Обычно исходные данные логически организованы как последовательность элементов одного вида. База данных SQL содержит последовательность строк. Аналогично, ADO.NET DataTable содержит последовательность объектов DataRow. В файле XML содержится "последовательность" элементов XML (они организованы иерархически в древовидную структуру). Коллекция в памяти содержит последовательность объектов.

С точки зрения приложения определенные тип и структура оригинальных исходных данных не важны. Исходные данные всегда представляются приложению как коллекция IEnumerable<T> или IQueryable<T>. В LINQ to XML исходные становятся видимыми какIEnumerable<XElement>. В LINQ to DataSet — как IEnumerable<DataRow>. В LINQ to SQL — как IEnumerable или IQueryable любого из пользовательских объектов, которые были определены для представления данных в таблице SQL.

При такой исходной последовательности запрос может выполнять одно из трех возможных действий.

  • Извлечение подмножества элементов для получения новой последовательности без изменения отдельных элементов. Затем запрос может отсортировать или сгруппировать возвращаемую последовательность различными способами, как показано в следующем примере (предположим, что scores является int[]):

                IEnumerable<int> highScoresQuery =
                    from score in scores
                    where score > 80
                    orderby score descending
                    select score;
    

  • Извлечение последовательности элементов, как и в предыдущем примере, но с преобразованием элементов в новый вид объекта. Например, запрос может извлекать только фамилии из определенных записей клиентов в источнике данных. Запрос также может извлекать полную запись и использовать ее для создания другого типа объекта в памяти или даже данных XML перед созданием заключительной последовательности результатов. В следующем примере показана трансформация int в string. Обратите внимание на новый тип highScoresQuery.

                IEnumerable<string> highScoresQuery2 =
                    from score in scores
                    where score > 80
                    orderby score descending
                    select String.Format("The score is {0}", score);
    

  • Извлечение одноэлементного значения исходных данных, такого как:

    • Количество элементов, которое соответствует определенному условию.

    • Элемент, обладающий наибольшим или наименьшим значением.

    • Первый элемент, соответствующий условию, или сумма определенных значений в заданном наборе элементов. Например, следующий запрос возвращает количество оценок выше 80 из целочисленного массива scores:

                int highScoreCount =
                    (from score in scores
                     where score > 80
                     select score)
                     .Count();
    

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

                IEnumerable<int> highScoresQuery3 =
                    from score in scores
                    where score > 80
                    select score;
    
                int scoreCount = highScoresQuery3.Count();
    

В предыдущем примере запрос выполняется в вызове Count, так как Count должен выполнить итерацию результатов, чтобы определить количество элементов, возвращенных highScoresQuery.

Выражение запроса — запрос, выраженный с помощью синтаксиса запроса. Выражение запроса является конструкцией языка первого класса. Выражение запроса похоже на любое другое выражение, оно может использоваться в любом контексте, в котором выражение C# является допустимым. Выражение запроса состоит их набора предложений, написанных в декларативном синтаксисе, аналогичном SQL или XQuery. Каждое предложение, в свою очередь, содержит одно или несколько выражений C#, которые могут являться выражениями запроса или могут содержать выражение запроса.

Выражение запроса должно начинаться предложением from и оканчиваться предложением select или group. Между первым предложениемfrom и последним предложением select или group может содержаться одно или несколько необязательных предложений where, orderby,join, let или даже дополнительных предложений from. Также можно использовать ключевое слово into, чтобы результат предложения joinили group мог служить источником дополнительных предложений запроса в том же выражении запроса.

Переменная запроса

В LINQ переменная запроса — это любая переменная, сохраняющая запрос вместо результатов запроса. Говоря точнее, переменная запроса всегда является перечислимым типом и производит последовательность элементов, когда она используется в итерации оператораforeach или прямом вызове ее метода IEnumerator.MoveNext.

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

        static void Main()
        {
            // Data source.
            int[] scores = { 90, 71, 82, 93, 75, 82 };

            // Query Expression.
            IEnumerable<int> scoreQuery = //query variable
                from score in scores //required
                where score > 80 // optional
                orderby score descending // optional
                select score; //must end with select or group

            // Execute the query to produce the results
            foreach (int testScore in scoreQuery)
            {
                Console.WriteLine(testScore);
            }                  
        }
        // Outputs: 93 90 82 82      

В предыдущем примере scoreQuery  переменная запроса, которую иногда называют просто запросом. В переменной запроса не хранятся фактические данные результата, которые получаются с помощью цикла foreach. Когда выполняется оператор foreach, результаты запроса не возвращаются с помощью переменной запроса scoreQuery. В таком случае они возвращаются с помощью переменной итерацииtestScore. Итерация переменно scoreQuery может выполняться во втором цикле foreach. Результаты будет теми же, если ни они, ни источник данных не изменяются.

В переменной запроса может храниться запрос, выраженный с помощью синтаксиса запроса или метода запроса, или их комбинации. В следующих примерах queryMajorCities и queryMajorCities2 являются переменными запроса.

            //Query syntax
            IEnumerable<City> queryMajorCities =
                from city in cities
                where city.Population > 100000
                select city;

            
            // Method-based syntax
            IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

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

            int highestScore =
                (from score in scores
                 select score)
                .Max();

            // or split the expression
            IEnumerable<int> scoreQuery =
                from score in scores
                select score;

            int highScore = scoreQuery.Max();

            List<City> largeCitiesList =
                (from country in countries
                 from city in country.Cities
                 where city.Population > 10000
                 select city)
                   .ToList();

            // or split the expression
            IEnumerable<City> largeCitiesQuery =
                from country in countries
                from city in country.Cities
                where city.Population > 10000
                select city;

            List<City> largeCitiesList2 = largeCitiesQuery.ToList();

System_CAPS_ICON_note.jpg Примечание

В документации LINQ к именам переменных, в которых хранятся запросы, добавляется слово "query" (запрос). В именах переменных, в которых хранятся фактические результаты, слово "query" (запрос) отсутствует.

Дополнительные сведения о различных способах выражения запросов содержатся в разделе Query Syntax and Method Syntax in LINQ.

Явная и неявная типизация переменных запроса

В данной документации обычно явно указывается тип переменной запроса для того, чтобы продемонстрировать типичное отношение между переменной запроса и предложением select. Кроме того, также можно использовать ключевое слово var, для указания компилятору вывести тип переменной запроса (или любой другой локальной переменной) во время компиляции. Например, ранее приведенный в данном разделе пример запроса также может быть выражен путем неявной типизации:

            // Use of var is optional here and in all queries.
            // queryCities is an IEnumerable<City> just as 
            // when it is explicitly typed.
            var queryCities =
                from city in cities
                where city.Population > 100000
                select city;

Дополнительные сведения см. в разделах Неявно типизированные локальные переменные и Type Relationships in LINQ Query Operations.

Начало выражения запроса

Выражение запроса должно начинаться с предложения from. Оно задает источник данных вместе с переменной диапазона. Переменная диапазона предоставляет каждый последующий элемент в исходной последовательности во время ее обзора. Переменная диапазона строго типизируется на основе типа элементов в источнике данных. В следующем примере переменная диапазона типизируется как Country, так как countries является массивом объектов Country. Так как переменная диапазона строго типизируется, для доступа к любым доступным элементам типа можно использовать оператор-точку.

            IEnumerable<Country> countryAreaQuery =
                from country in countries
                where country.Area > 500000 //sq km
                select country;

Переменная диапазона находится в области до тех пор, пока запрос не завершится с помощью точки с запятой или предложенияпродолжения.

Выражение запроса может содержать несколько предложений from. Используйте дополнительные предложения from, если каждый элемент в источнике является коллекцией или содержит коллекцию. Например, предположим, что имеется коллекция объектов Country, каждый их которых содержит коллекцию объектов City с именем Cities. Для выполнения запросов к объектам City в каждой коллекцииCountry используйте два предложения from, как показано ниже:

            IEnumerable<City> cityQuery =
                from country in countries
                from city in country.Cities
                where city.Population > 10000
                select city;

Дополнительные сведения см. в разделе Предложение from.

Окончание выражения запроса

Выражение запроса должно завершаться предложением select или group.

Предложение "group"

Используйте предложение group для получения последовательности групп, организованной на основе указанного ключа. Ключом могут быть данные любого типа. Например, следующий запрос создает последовательность групп, содержащую один или несколько объектовCountry, ключ для которых имеет значение char.

            var queryCountryGroups =
                from country in countries
                group country by country.Name[0];

Дополнительные сведения о группировании см. в разделе Предложение group.

Предложение "select"

Используйте предложение select для получения всех других типов последовательностей. Простое предложение select просто создает последовательность с тем же типом объектов, что и у объектов, которые содержатся в источнике данных. В этом примере источник данных содержит объекты типа Country. Предложение orderby просто сортирует элементы в новом порядке, а предложение select создает последовательность переупорядоченных объектов Country.

            IEnumerable<Country> sortedQuery =
                from country in countries
                orderby country.Area
                select country;

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

            // Here var is required because the query
            // produces an anonymous type.
            var queryNameAndPop =
                from country in countries
                select new { Name = country.Name, Pop = country.Population };

Дополнительные сведения обо всех методах использования предложения select для преобразования исходных данных см. в разделеПредложение select.

Продолжения с использованием ключевого слова "into"

Ключевое слово into можно использовать в предложении select или group для создания временного идентификатора, в котором хранится запрос. Это действие рекомендуется выполнять, если требуется выполнить в запросе дополнительные операции запроса после операции группирования или выбора. В следующем примере объекты countries группируются в соответствии с численностью населения в диапазоны по 10 миллионов. После создания этих групп дополнительные предложения отфильтровывают некоторые группы, а затем сортируют группы в порядке возрастания. Чтобы выполнить эти дополнительные операции, требуется продолжение, представляемое с помощью countryGroup.

            // percentileQuery is an IEnumerable<IGrouping<int, Country>>
            var percentileQuery =
                from country in countries
                let percentile = (int) country.Population / 10000000
                group country by percentile into countryGroup
                where countryGroup.Key >= 20
                orderby countryGroup.Key
                select countryGroup;

            // grouping is an IGrouping<int, Country>
            foreach (var grouping in percentileQuery)
            {
                Console.WriteLine(grouping.Key);
                foreach (var country in grouping)
                    Console.WriteLine(country.Name + ":" + country.Population);
            }

Дополнительные сведения см. в разделе into.

Фильтрация, упорядочение и присоединение

Между открывающим предложением from и завершающим предложением select или group могут размещаться все остальные, необязательные предложения (where, join, orderby, from, let). Любое необязательное предложение может использоваться в теле запроса несколько раз или отсутствовать вообще.

Предложение "where"

Используйте предложение where для фильтрации элементов из источника данных по одному или нескольким выражениям предиката. У предложения where в следующем примере имеются два предиката.

            IEnumerable<City> queryCityPop =
                from city in cities
                where city.Population < 200000 && city.Population > 100000
                select city;

Дополнительные сведения см. в разделе Предложение where.

Предложение "orderby"

Используйте предложение orderby, чтобы сортировать результаты в порядке возрастания или убывания. Также можно задать порядок дополнительной сортировки. В следующем примере выполняется основная сортировка объектов country по свойству Area. Затем выполняется дополнительная сортировка по свойству Population.

            IEnumerable<Country> querySortedCountries =
                from country in countries
                orderby country.Area, country.Population descending
                select country;

Ключевое слово ascending является необязательным, так как сортировка по умолчанию происходит по возрастанию, если не задан порядок сортировки. Дополнительные сведения см. в разделе Предложение orderby.

Предложение "join"

Используйте предложение join для связи или объединения элементов из одного источника данных с элементами из другого источника данных на основе сравнения на равенство определенных ключей в каждом элементе. В LINQ операции объединения выполняются над последовательностями объектов, элементы которых относятся к различным типам. После объединения двух последовательностей необходимо использовать оператор select или group, чтобы указать элемент для сохранения в выходной последовательности. Также можно использовать анонимный тип, чтобы объединить свойства каждого набора связанных элементов в новый тип для выходной последовательности. В следующем примере связываются объекты prod, свойство Category которых соответствует одной из категорий в массиве строк categories. Отфильтровываются продукты, свойство Category которых не соответствует ни одной строке в categories. Оператор select формирует новый тип, свойства которого берутся как из cat, так и из prod.

            var categoryQuery =
                from cat in categories
                join prod in products on cat equals prod.Category
                select new { Category = cat, Name = prod.Name };

Также можно выполнить групповое соединение путем сохранения результатов операции join во временную переменную, используя ключевое слово into. Дополнительные сведения см. в разделе Предложение join.

Предложение "let"

Используйте предложение let для сохранения результатов выражения, такого как вызов метода, в новую переменную диапазона. В следующем примере в переменную диапазона firstName сохраняется первый элемент массива строк, возвращенного с помощью Split.

            string[] names = { "Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia" };
            IEnumerable<string> queryFirstNames =
                from name in names
                let firstName = name.Split(new char[] { ' ' })[0]
                select firstName;

            foreach (string s in queryFirstNames)
                Console.Write(s + " ");
            //Output: Svetlana Claire Sven Cesar

Дополнительные сведения см. в разделе Предложение let.

Вложенные запросы в выражении запроса

Предложение запроса может само содержать выражение запроса, которое иногда называют вложенным запросом. Каждый вложенный запрос начинается собственным предложением from, которое может указывать на источник данных, отличный от источника данных первого предложения from. Например, в следующем запросе показано выражение запроса, которое используется в операторе "select" для извлечения результатов операции группирования.

            var queryGroupMax =
                from student in students
                group student by student.GradeLevel into studentGroup
                select new
                {
                    Level = studentGroup.Key,
                    HighestScore =
                        (from student2 in studentGroup
                         select student2.Scores.Average())
                         .Max()
                };

 

Лучше всего для создания запросов использовать синтаксис запроса, создавая выражения запросов. В следующем примере показано три выражения запроса. В первом выражении демонстрируется фильтрация или ограничение результатов путем применения условий в предложении where. Оно возвращает все элементы в исходной последовательности со значениями больше 7 и меньше 3. Второе выражение демонстрирует сортировку возвращаемых результатов. Третий запрос демонстрирует группировку результатов. Он возвращает две группы на основе первой буквы слова.

            // Query #1.
            List<int> numbers = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

            // The query variable can also be implicitly typed by using var
            IEnumerable<int> filteringQuery =
                from num in numbers
                where num < 3 || num > 7
                select num;

            // Query #2.
            IEnumerable<int> orderingQuery =
                from num in numbers
                where num < 3 || num > 7
                orderby num ascending
                select num;

            // Query #3.
            string[] groupingQuery = { "carrots", "cabbage", "broccoli", "beans", "barley" };
            IEnumerable<IGrouping<char, string>> queryFoodGroups =
                from item in groupingQuery
                group item by item[0];

Обратите внимание, что тип запросов — IEnumerable<T>. Все эти запросы можно написать с помощью var, как показано в примере ниже.

var query = from num in numbers...

В каждом из приведенных выше примеров фактическое выполнение запроса откладывается до использования переменной запроса в операторе foreach. Дополнительные сведения см. в разделе Introduction to LINQ Queries (C#).

Некоторые операции запросов должны быть выражены в виде вызова метода. Чаще всего используются методы, возвращающие одноэлементные числовые значения, например Sum, Max, Min, Average и т.д. Эти методы всегда должны быть вызваны последними в запросе, поскольку они представляют только одно значение и не могут служить источником дополнительных действий запроса. В следующем примере демонстрируется вызов метода в выражении запроса.

            List<int> numbers1 = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
            List<int> numbers2 = new List<int>() { 15, 14, 11, 13, 19, 18, 16, 17, 12, 10 };
            // Query #4.
            double average = numbers1.Average();

            // Query #5.
            IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

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

            // Query #6.
            IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

В предыдущих запросах только запрос №4 выполнялся немедленно. Причина заключается в том, что он возвращает одиночное значение, а не универсальную коллекцию IEnumerable<T>. Сам метод должен использовать foreach для вычисления значения.

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

            // var is used for convenience in these queries
            var average = numbers1.Average();
            var concatenationQuery = numbers1.Concat(numbers2);
            var largeNumbersQuery = numbers2.Where(c => c > 15);

В этом примере демонстрируется использование синтаксиса метода для результатов предложения запроса. Нужно всего лишь заключить выражение запроса в скобки, а затем применить оператор точки и вызвать метод. В следующем примере запрос #7 возвращает количество чисел, значение которых лежит в диапазоне от 3 до 7. Однако в общем случае лучше использовать вторую переменную для хранения результатов вызова метода. Таким образом, будет меньше вероятность перепутать запрос с результатами запроса.

            // Query #7.

            // Using a query expression with method syntax
            int numCount1 =
                (from num in numbers1
                 where num < 3 || num > 7
                 select num).Count();

            // Better: Create a new variable to store
            // the method call result
            IEnumerable<int> numbersQuery =
                from num in numbers1
                where num < 3 || num > 7
                select num;

            int numCount2 = numbersQuery.Count();

Запрос №7 возвращает одиночное значение, а не коллекцию, поэтому он выполняется мгновенно.

Предыдущий запрос можно написать с использованием неявной типизации с помощью var, как показано в следующем примере:

var numCount = (from num in numbers...  

Можно использовать синтаксис метода следующим образом.

var numCount = numbers.Where(n => n < 3 || n > 7).Count();  

Можно использовать явную типизацию следующим образом.

int numCount = numbers.Where(n => n < 3 || n > 7).Count();  

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

    class ExceptionsOutsideQuery
    {
        static void Main()
        {
            // DO THIS with a datasource that might
            // throw an exception. It is easier to deal with
            // outside of the query expression.
            IEnumerable<int> dataSource;
            try
            {
                dataSource = GetData();
            }
            catch (InvalidOperationException)
            {
                // Handle (or don't handle) the exception 
                // in the way that is appropriate for your application.
                Console.WriteLine("Invalid operation");
                goto Exit;
            }
            
            // If we get here, it is safe to proceed.
            var query = from i in dataSource
                        select i * i;

            foreach (var i in query)
                Console.WriteLine(i.ToString());

            //Keep the console window open in debug mode
            Exit:
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }

        // A data source that is very likely to throw an exception!
        static IEnumerable<int> GetData()
        {
            throw new InvalidOperationException();
        }
    }

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

Обратите внимание, что блок try заключает в себя цикл foreach, а не сам запрос. Причина заключается в том, что цикл foreach — это точка, в которой запрос фактически выполняется. Дополнительные сведения см. в разделе Introduction to LINQ Queries (C#).

    class QueryThatThrows
    {
        static void Main()
        {
            // Data source.
            string[] files = { "fileA.txt", "fileB.txt", "fileC.txt" };

            // Demonstration query that throws.
            var exceptionDemoQuery =
                from file in files
                let n = SomeMethodThatMightThrow(file)
                select n;

            // Runtime exceptions are thrown when query is executed.
            // Therefore they must be handled in the foreach loop.
            try
            {
                foreach (var item in exceptionDemoQuery)
                {
                    Console.WriteLine("Processing {0}", item);
                }
            }

            // Catch whatever exception you expect to raise
            // and/or do any necessary cleanup in a finally block
            catch (InvalidOperationException e)
            {
                Console.WriteLine(e.Message);
            }

            //Keep the console window open in debug mode
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }

        // Not very useful as a general purpose method.
        static string SomeMethodThatMightThrow(string s)
        {
            if (s[4] == 'C')
                throw new InvalidOperationException();
            return @"C:\newFolder\" + s;
        }
    }
    /* Output:
        Processing C:\newFolder\fileA.txt
        Processing C:\newFolder\fileB.txt
        Operation is not valid due to the current state of the object.
     */

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

    class Student
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int ID { get; set; }
        public List<int> ExamScores { get; set; }
    }

    class PopulateCollection
    {
        static void Main()
        {
            // These data files are defined in How to: Join Content from 
            // Dissimilar Files (LINQ).

            // Each line of names.csv consists of a last name, a first name, and an
            // ID number, separated by commas. For example, Omelchenko,Svetlana,111
            string[] names = System.IO.File.ReadAllLines(@"../../../names.csv");

            // Each line of scores.csv consists of an ID number and four test 
            // scores, separated by commas. For example, 111, 97, 92, 81, 60
            string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");

            // Merge the data sources using a named type.
            // var could be used instead of an explicit type. Note the dynamic
            // creation of a list of ints for the ExamScores member. We skip 
            // the first item in the split string because it is the student ID, 
            // not an exam score.
            IEnumerable<Student> queryNamesScores =
                from nameLine in names
                let splitName = nameLine.Split(',')
                from scoreLine in scores
                let splitScoreLine = scoreLine.Split(',')
                where splitName[2] == splitScoreLine[0]
                select new Student()
                {
                    FirstName = splitName[0],
                    LastName = splitName[1],
                    ID = Convert.ToInt32(splitName[2]),
                    ExamScores = (from scoreAsText in splitScoreLine.Skip(1)
                                  select Convert.ToInt32(scoreAsText)).
                                  ToList()
                };

            // Optional. Store the newly created student objects in memory
            // for faster access in future queries. This could be useful with
            // very large data files.
            List<Student> students = queryNamesScores.ToList();

            // Display each student's name and exam score average.
            foreach (var student in students)
            {
                Console.WriteLine("The average score of {0} {1} is {2}.",
                    student.FirstName, student.LastName,
                    student.ExamScores.Average());
            }

            //Keep console window open in debug mode
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
    /* Output: 
        The average score of Omelchenko Svetlana is 82.5.
        The average score of O'Donnell Claire is 72.25.
        The average score of Mortensen Sven is 84.5.
        The average score of Garcia Cesar is 88.25.
        The average score of Garcia Debra is 67.
        The average score of Fakhouri Fadi is 92.25.
        The average score of Feng Hanying is 88.
        The average score of Garcia Hugo is 85.75.
        The average score of Tucker Lance is 81.75.
        The average score of Adams Terry is 85.25.
        The average score of Zabokritski Eugene is 83.
        The average score of Tucker Michael is 92.
     */

В предложении select инициализатор объектов используется для создания каждого нового объекта Student, применяя данные из двух источников.

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

            // Merge the data sources by using an anonymous type.
            // Note the dynamic creation of a list of ints for the
            // ExamScores member. We skip 1 because the first string
            // in the array is the student ID, not an exam score.
            var queryNamesScores2 =
                from nameLine in names
                let splitName = nameLine.Split(',')
                from scoreLine in scores
                let splitScoreLine = scoreLine.Split(',')
                where splitName[2] == splitScoreLine[0]
                select new
                {
                    First = splitName[0],
                    Last = splitName[1],
                    ExamScores = (from scoreAsText in splitScoreLine.Skip(1)
                                  select Convert.ToInt32(scoreAsText))
                                  .ToList()
                };

            // Display each student's name and exam score average.
            foreach (var student in queryNamesScores2)
            {
                Console.WriteLine("The average score of {0} {1} is {2}.",
                    student.First, student.Last, student.ExamScores.Average());
            }

Группирование — одна из самых эффективных функций LINQ. В следующих примерах демонстрируются различные способы группирования данных:

  • По отдельному свойству.

  • По первой букве строкового свойства.

  • По расчетному числовому диапазону.

  • По логическому предикату или другому выражению.

  • По составному ключу.

Кроме того, два последних запроса передают свои результаты в новый анонимный тип, содержащий только имя и фамилию студента. Дополнительные сведения см. в разделе Предложение group.

Во всех примерах в данном разделе используются следующие вспомогательные классы и источники данных.

    public class StudentClass
    {
        #region data
        protected enum GradeLevel { FirstYear = 1, SecondYear, ThirdYear, FourthYear };
        protected class Student
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public int ID { get; set; }
            public GradeLevel Year;
            public List<int> ExamScores;
        }

        protected static List<Student> students = new List<Student>
        {
            new Student {FirstName = "Terry", LastName = "Adams", ID = 120, 
                Year = GradeLevel.SecondYear, 
                ExamScores = new List<int>{ 99, 82, 81, 79}},
            new Student {FirstName = "Fadi", LastName = "Fakhouri", ID = 116, 
                Year = GradeLevel.ThirdYear,
                ExamScores = new List<int>{ 99, 86, 90, 94}},
            new Student {FirstName = "Hanying", LastName = "Feng", ID = 117, 
                Year = GradeLevel.FirstYear, 
                ExamScores = new List<int>{ 93, 92, 80, 87}},
            new Student {FirstName = "Cesar", LastName = "Garcia", ID = 114, 
                Year = GradeLevel.FourthYear,
                ExamScores = new List<int>{ 97, 89, 85, 82}},
            new Student {FirstName = "Debra", LastName = "Garcia", ID = 115, 
                Year = GradeLevel.ThirdYear, 
                ExamScores = new List<int>{ 35, 72, 91, 70}},
            new Student {FirstName = "Hugo", LastName = "Garcia", ID = 118, 
                Year = GradeLevel.SecondYear, 
                ExamScores = new List<int>{ 92, 90, 83, 78}},
            new Student {FirstName = "Sven", LastName = "Mortensen", ID = 113, 
                Year = GradeLevel.FirstYear, 
                ExamScores = new List<int>{ 88, 94, 65, 91}},
            new Student {FirstName = "Claire", LastName = "O'Donnell", ID = 112, 
                Year = GradeLevel.FourthYear, 
                ExamScores = new List<int>{ 75, 84, 91, 39}},
            new Student {FirstName = "Svetlana", LastName = "Omelchenko", ID = 111, 
                Year = GradeLevel.SecondYear, 
                ExamScores = new List<int>{ 97, 92, 81, 60}},
            new Student {FirstName = "Lance", LastName = "Tucker", ID = 119, 
                Year = GradeLevel.ThirdYear, 
                ExamScores = new List<int>{ 68, 79, 88, 92}},
            new Student {FirstName = "Michael", LastName = "Tucker", ID = 122, 
                Year = GradeLevel.FirstYear, 
                ExamScores = new List<int>{ 94, 92, 91, 91}},
            new Student {FirstName = "Eugene", LastName = "Zabokritski", ID = 121,
                Year = GradeLevel.FourthYear, 
                ExamScores = new List<int>{ 96, 85, 91, 60}}
        };
        #endregion

        //Helper method, used in GroupByRange.
        protected static int GetPercentile(Student s)
        {
            double avg = s.ExamScores.Average();
            return avg > 0 ? (int)avg / 10 : 0;
        }

        

        public void QueryHighScores(int exam, int score)
        {
            var highScores = from student in students
                             where student.ExamScores[exam] > score
                             select new {Name = student.FirstName, Score = student.ExamScores[exam]};

            foreach (var item in highScores)
            {
                Console.WriteLine("{0,-15}{1}", item.Name, item.Score);
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            StudentClass sc = new StudentClass();
            sc.QueryHighScores(1, 90);

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }
    }

В следующем примере показывается группировка элементов источника с помощью использования отдельного свойства элемента в качестве ключа группы. В данном случае в качестве ключа используется string, фамилия учащегося. Для ключа можно также использовать подстроку. При операции группирования используется компаратор проверки на равенство, используемый по умолчанию для данного типа.

Вставьте приведенный ниже метод в класс StudentClass. Измените оператор вызова в методе Main на sc.GroupBySingleProperty().

        public void GroupBySingleProperty()
        {
            Console.WriteLine("Group by a single property in an object:");

            // Variable queryLastNames is an IEnumerable<IGrouping<string, 
            // DataClass.Student>>. 
            var queryLastNames =
                from student in students
                group student by student.LastName into newGroup
                orderby newGroup.Key
                select newGroup;

            foreach (var nameGroup in queryLastNames)
            {
                Console.WriteLine("Key: {0}", nameGroup.Key);
                foreach (var student in nameGroup)
                {
                    Console.WriteLine("\t{0}, {1}", student.LastName, student.FirstName);
                }
            }
        }
        /* Output:
            Group by a single property in an object:
            Key: Adams
                    Adams, Terry
            Key: Fakhouri
                    Fakhouri, Fadi
            Key: Feng
                    Feng, Hanying
            Key: Garcia
                    Garcia, Cesar
                    Garcia, Debra
                    Garcia, Hugo
            Key: Mortensen
                    Mortensen, Sven
            Key: O'Donnell
                    O'Donnell, Claire
            Key: Omelchenko
                    Omelchenko, Svetlana
            Key: Tucker
                    Tucker, Lance
                    Tucker, Michael
            Key: Zabokritski
                    Zabokritski, Eugene
        */

В следующем примере показывается группировка элементов источника, когда в качестве ключа группы используется не свойство объекта. В данном случае в качестве ключа используется первая буква фамилии учащегося.

Вставьте приведенный ниже метод в класс StudentClass. Измените оператор вызова в методе Main на sc.GroupBySubstring().

        public void GroupBySubstring()
        {            
            Console.WriteLine("\r\nGroup by something other than a property of the object:");

            var queryFirstLetters =
                from student in students
                group student by student.LastName[0];

            foreach (var studentGroup in queryFirstLetters)
            {
                Console.WriteLine("Key: {0}", studentGroup.Key);
                // Nested foreach is required to access group items.
                foreach (var student in studentGroup)
                {
                    Console.WriteLine("\t{0}, {1}", student.LastName, student.FirstName);
                }
            }           
        }
        /* Output:
            Group by something other than a property of the object:
            Key: A
                    Adams, Terry
            Key: F
                    Fakhouri, Fadi
                    Feng, Hanying
            Key: G
                    Garcia, Cesar
                    Garcia, Debra
                    Garcia, Hugo
            Key: M
                    Mortensen, Sven
            Key: O
                    O'Donnell, Claire
                    Omelchenko, Svetlana
            Key: T
                    Tucker, Lance
                    Tucker, Michael
            Key: Z
                    Zabokritski, Eugene
        */

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

        public void QueryNestedGroups()
        {
            var queryNestedGroups =
                from student in students
                group student by student.Year into newGroup1
                from newGroup2 in
                    (from student in newGroup1
                     group student by student.LastName)
                group newGroup2 by newGroup1.Key;

            // Three nested foreach loops are required to iterate 
            // over all elements of a grouped group. Hover the mouse 
            // cursor over the iteration variables to see their actual type.
            foreach (var outerGroup in queryNestedGroups)
            {
                Console.WriteLine("DataClass.Student Level = {0}", outerGroup.Key);
                foreach (var innerGroup in outerGroup)
                {
                    Console.WriteLine("\tNames that begin with: {0}", innerGroup.Key);
                    foreach (var innerGroupElement in innerGroup)
                    {
                        Console.WriteLine("\t\t{0} {1}", innerGroupElement.LastName, innerGroupElement.FirstName);
                    }
                }
            }
        }
        /*
         Output:
        DataClass.Student Level = SecondYear
                Names that begin with: Adams
                        Adams Terry
                Names that begin with: Garcia
                        Garcia Hugo
                Names that begin with: Omelchenko
                        Omelchenko Svetlana
        DataClass.Student Level = ThirdYear
                Names that begin with: Fakhouri
                        Fakhouri Fadi
                Names that begin with: Garcia
                        Garcia Debra
                Names that begin with: Tucker
                        Tucker Lance
        DataClass.Student Level = FirstYear
                Names that begin with: Feng
                        Feng Hanying
                Names that begin with: Mortensen
                        Mortensen Sven
                Names that begin with: Tucker
                        Tucker Michael
        DataClass.Student Level = FourthYear
                Names that begin with: Garcia
                        Garcia Cesar
                Names that begin with: O'Donnell
                        O'Donnell Claire
                Names that begin with: Zabokritski
                        Zabokritski Eugene        
         */