Поэлементные операции: LinqBegin4
Создание проекта-заготовки и ознакомление с заданием
Задания группы LinqBegin предназначены для освоения различных
методов преобразования последовательностей. Все эти методы являются
методами расширения класса System.Linq.Enumerable и образуют
интерфейс LINQ To Objects (методы расширения появились в версии 3.0
языка C#).
Задания охватывают большинство методов LINQ To Objects; они
разбиты на 4 подгруппы, каждая из которых посвящена определенным
видам преобразований: от простейших (поэлементные операции) до
наиболее сложных (объединение и группировка).
Рассмотрим одну из задач, связанных с поэлементными
операциями.
LinqBegin4°. Дан символ С и строковая последовательность A. Если A содержит единственный элемент, оканчивающийся символом C, то вывести этот элемент; если требуемых строк в A нет, то вывести пустую строку; если требуемых строк больше одной, то вывести строку «Error». Указание. Использовать try-блок для перехвата возможного исключения.
Выполнение задания с применением задачника Programming Taskbook
обычно начинается с создания проекта-заготовки для выбранного задания.
Для создания заготовки предназначен программный модуль PT4Load,
входящий в состав задачника. Вызвать этот модуль можно с помощью
ярлыка Load.lnk, который автоматически создается в рабочем каталоге
учащегося (по умолчанию рабочий каталог размещается на диске C и
имеет имя PT4Work; с помощью программы PT4Setup, также входящей в
состав задачника и доступной из его меню «Пуск | Программы |
Programming Taskbook 4», можно определить любое количество других
рабочих каталогов).
Заметим, что группы заданий, связанные с технологией LINQ, не
входят в базовый набор электронного задачника Programming Taskbook.
Для того чтобы эти группы были доступны, необходимо после установки
задачника Programming Taskbook установить его расширение Programming
Taskbook for LINQ задачник по технологии LINQ.
После запуска модуля PT4Load на экране появится его окно, в
котором будут перечислены все доступные группы заданий:
В заголовке окна указывается имя текущей среды программирования
и номер ее версии. Приведенный рисунок соответствует настройке, при
которой текущей средой является среда Microsoft Visual Studio 2022
для языка C#.
При выполнении заданий, посвященных технологии LINQ,
необходимо использовать среду Microsoft Visual Studio .NET, начиная с
версии 2008, поскольку эта технология появилась в версии .NET 3.5,
вышедшей одновременно с Visual Studio 2008. В качестве языка
программирования следует выбрать либо C#, либо Visual Basic .NET, либо F#. Если
указана среда, отличная от требуемой, следует вызвать контекстное меню
модуля PT4Load (щелкнув в его окне правой кнопкой мыши) и выбрать
нужную среду из появившегося списка.
В списке групп должна содержаться группа LinqBegin. Ее отсутствие
может объясняться двумя причинами: либо в качестве текущего языка
выбран язык, отличный от C#, Visual Basic .NET или F#, либо на компьютере не
установлено расширение Programming Taskbook for LINQ.
После ввода имени задания (в нашем случае LinqBegin4) кнопка
«Загрузка» в окне PT4Load станет доступной и, нажав ее (или клавишу
[Enter]), мы создадим проект-заготовку для указанного задания, которая
будет немедленно загружена в среду Visual Studio. Будем
предполагать, что в качестве языка программирования выбран язык C#.
Созданный проект будет состоять из нескольких файлов, однако для
решения задачи нам потребуется только файл с именем LinqBegin4.cs.
Именно этот файл будет загружен в редактор среды Visual Studio.
Приведем содержимое файла LinqBegin4.cs:
// File: "LinqBegin4"
using PT4;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PT4Tasks
{
public class MyTask : PT
{
// When solving tasks of the LinqBegin group, the following
// additional methods defined in the taskbook are available:
// (*) GetEnumerableInt() - input of a numeric sequence;
// (*) GetEnumerableString() - input of a string sequence;
// (*) Put() (extension method) - output of a sequence;
// (*) Show() and Show(cmt) (extension methods) - debug output
// of a sequence, cmt - string comment;
// (*) Show(e => r) and Show(cmt, e => r) (extension methods) -
// debug output of r values, obtained from elements e
// of a sequence, cmt - string comment.
public static void Solve()
{
Task("LinqBegin4");
}
}
}
Файл LinqBegin4.cs начинается с директив using, подключающих
основные пространства имен, в том числе System.Collections.Generic для
обобщенных коллекций, System.Linq для класса Enumerable,
содержащего методы расширения LINQ to Objects, System.Text для
классов, связанных с обработкой текста, и PT4 для класса PT,
обеспечивающего доступ к функциям ядра задачника Programming
Taskbook. Затем следует описание класса MyTask потомка класса PT.
Метод Solve, в котором требуется запрограммировать решение задачи,
содержит вызов метода Task, инициализирующего задачу с указанным
именем.
При выполнении заданий, связанных с технологией LINQ,
могут оказаться полезными дополнительные методы задачника. Краткое описание
этих методов приводится в комментариях, расположенных перед
описанием метода Solve.
Для запуска созданной программы достаточно нажать клавишу [F5].
При этом на экране появится окно задачника. Приведенный на
рисунке вид окна соответствует режиму с динамической компоновкой
разделов, который появился в версии 4.11 задачника Programming
Taskbook.
Поскольку в программе не выполняются действия по вводу и выводу
данных, запуск программы считается ознакомительным. При
ознакомительном запуске в окне задачника отображаются три раздела: с
формулировкой задания, исходными данными и примером верного
решения.
Все исходные данные выделяются особым цветом, чтобы отличить их
от комментариев: если для окна установлен цветовой режим «на черном
фоне» (как на приведенном выше рисунке),
то данные выделяются желтым цветом; в режиме «на белом фоне»
данные выделяются синим цветом. Для переключения
цветового режима достаточно нажать клавишу [F3].
Запуск программы позволяет ознакомиться
с образцом исходных данных и соответствующим этим исходным
данным примером правильного решения. В нашем случае в набор исходных
данных входит символ C и строковая последовательность А. Во всех
заданиях группы LinqBegin при определении последовательности вначале
указывается количество ее элементов, а затем значения самих
элементов (на экране количество элементов отделяется от списка значений
двоеточием), причем значений может занимать несколько экранных
строк. Символьные данные заключаются в одинарные кавычки, а
строковые в двойные, что соответствует представлению символьных и
строковых констант, принятому в языке C#. Использование кавычек
позволяет, в частности, «увидеть» пустые строки, входящие в наборы
исходных или результирующих данных (как в примере исходных данных,
приведенном на предыдущих рисунках).
Для выхода из программы достаточно нажать кнопку «Выход»,
клавишу [Esc] или клавишу [F5] (т. е. ту же клавишу, которая обеспечивает
запуск программы из среды Visual Studio).
Чтобы более подробно ознакомиться с заданием, можно использовать
два специальных режима задачника: демонстрационный режим и режим
отображения заданий в формате html.
Для запуска программы в демонстрационном режиме достаточно
дополнить параметр метода Task символом «?» (в нашем случае вызов
метода примет вид Task("LinqBegin4?"); ). Окно задачника в
демонстрационном режиме имеет дополнительные кнопки, позволяющие
переходить к предыдущему или последующему заданию, а также
отображать на экране различные наборы исходных данных и связанные с
ними образцы правильного решения (см. рисунок). На рисунке приведена
ситуация, в которой исходная последовательность содержит несколько
элементов, оканчивающихся символом С, равным «p» (это элементы
«3fwp» и «Q6kp»), поэтому для правильного решения требуется вывести
строку «Error».
Для отображения задания в формате html достаточно щелкнуть мышью на метке «Режим (F4)»
в окне задачника (или просто нажать клавишу [F4]). При этом
будет запущен html-браузер, используемый в системе по умолчанию, и в него
загрузится html-страница, содержащая формулировку задания и текст
преамбулы к соответствующей группе заданий:
В данном случае на экране, кроме формулировки задания LinqBegin4,
отображается преамбула ко всей группе LinqBegin и преамбула к подгруппе «Поэлементные
операции, агрегирование и генерирование последовательностей», в
которую входит задание LinqBegin4.
Отобразить задание в формате html можно сразу после запуска программы (не выводя на экран окно задачника),
если дополнить параметр метода Task символом «#» (например, Task("LinqBegin4#"); ).
Режим html-страницы удобен в нескольких отношениях. Во-первых,
только в этом режиме можно ознакомиться с преамбулами к группе заданий
и входящим в нее подгруппам и тем самым получить дополнительные
сведения, которые могут оказаться полезными при выполнении заданий.
Во-вторых, окно браузера с html-страницей не требуется закрывать для
того, чтобы продолжить работу над заданием; таким образом, с помощью
этого окна можно в любой момент ознакомиться с формулировкой
задания, даже если текущее состояние программы не позволяет
откомпилировать ее и запустить на выполнение.
Имеется возможность отобразить в html-режиме все задания,
входящие в некоторую группу. Для этого в параметре метода Task следует
удалить номер задания, оставив только имя группы и символ «#»
(например, Task("LinqBegin#"); ).
Закончив на этом обзор возможностей задачника, предназначенных
для формирования проекта-заготовки и ознакомления с выбранным
заданием, перейдем к описанию самого процесса решения.
Выполнение задания
Первым этапом при выполнении любого задания является ввод
исходных данных. Если задание выполняется с применением электронного
задачника, то именно задачник формирует набор исходных данных и
предоставляет его программе учащегося. Понятно, что для получения
исходных данных, подготовленных задачником, в программе необходимо
использовать специальные методы ввода. Эти статические методы
определены в классе PT, являющемся предком класса MyTask. Поэтому
при вызове методов ввода в функции Solve можно не указывать имя
класса, в котором они определены.
В соответствии с подходом к организации ввода, принятым в стандартной
библиотеке .NET, для ввода используются функции без параметров,
каждая из которых возвращает один элемент исходных данных определенного типа.
В задачнике Programming Taskbook предусмотрены функции
для ввода данных всех базовых типов:
- GetBool() для ввода логических данных (типа bool);
- GetInt() для ввода целочисленных данных (типа int);
- GetDouble() для ввода вещественных данных (типа double);
- GetChar() для ввода символьных данных (типа char);
- GetString() для ввода строковых данных (типа string).
Этих функций достаточно для того, чтобы организовать ввод любых
данных, входящих в задания групп LinqBegin, LinqObj и LinqXml.
Например, для ввода данных из задания LinqBegin4 можно использовать
следующий фрагмент (здесь и далее будем приводить только описание
функции Solve, поскольку остальная часть проекта не требует изменений):
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
string[] a = new string[GetInt()];
for (int i = 0; i < a.Length; i++)
a[i] = GetString();
}
В данном фрагменте вначале описывается и вводится символ c , затем
описывается и создается строковый массив a (при этом считывается размер
исходной последовательности, который указывается в конструкторе
массива), после чего организуется цикл для ввода значений самих
элементов массива.
При запуске программы окно задачника примет вид, подобный
приведенному на рисунке. Этот запуск уже не считается ознакомительным,
поскольку в программе выполняются действия по вводу-выводу данных.
Следует обратить внимание на дополнительную
панель (панель индикаторов), которая содержит информацию о количестве
введенных и выведенных данных, а также о количестве успешно
пройденных тестовых испытаний программы.
В нашем случае в программе выполнен ввод всех исходных данных (о
чем свидетельствует первый индикатор и связанный с ним текст), однако
результирующие данные не выведены. Можно сказать, что мы успешно прошли первый
этап решения, связанный с вводом исходных данных.
Поэтому информационная панель (расположенная рядом с кнопкой «Выход»)
содержит текст «Запуск с правильным вводом данных»,
а ее фон изменился на светло-синий.
Заметим, что при обнаружении различных ошибок
фон информационной панели также изменяется, причем в этом случае
используются различные оттенки красного цвета (например, для ошибок, связанных с
вводом или выводом недостаточного числа исходных или результирующих
данных, используется оранжевый цвет). Кроме того, этим же цветом выделяется
заголовок раздела, связанного с ошибкой (например, при выводе недостаточного числа результирующих данных
оранжевым цветом выделяется заголовок «Полученные результаты»).
В случае ошибочного решения в
окне задачника отображается не только раздел с полученными
результатами, но и раздел с примером верного решения.
Все эти дополнительные
элементы окна призваны упростить поиск и исправление ошибок,
выявленных задачником при выполнении программы.
Прежде чем перейти к обсуждению решения задачи, отметим одну
дополнительную возможность, доступную при выполнении заданий
группы LinqBegin, а именно наличие вспомогательных функций ввода
GetEnumerableInt() и GetEnumerableString(), описанных в классе MyTask
и обеспечивающих быстрый ввод целочисленных и строковых
последовательностей. Благодаря этим функциям упрощаются действия по
вводу исходных данных, а полученные решения становятся более
краткими и наглядными.
С применением функции GetEnumerableString фрагмент решения,
отвечающий за ввод исходных данных, будет состоять всего из двух строк:
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
var a = GetEnumerableString();
}
В приведенном варианте программы использована возможность языка
C#, появившаяся одновременно с технологией LINQ и тесно с ней
связанная: любая переменная может быть описана с помощью ключевого
слова var, если данная переменная сразу инициализируется некоторым
значением. В подобной ситуации тип переменной автоматически
определяется по типу инициализирующего выражения (говорят также, что
тип переменной выводится из типа инициализирующего выражения). В
нашем случае эта возможность позволяет использовать более краткую
запись слово var вместо типа Enumerable<string>, который имеет
возвращаемое значение функции GetEnumerableString. Заметим, что в
некоторых случаях (связанных с использованием так называемых
анонимных типов, также появившихся в языке C# одновременно с
технологией LINQ) без слова var при описании переменных
обойтись невозможно.
При запуске нового варианта решения будет выведено прежнее
сообщение («Запуск с правильным вводом данных»).
Приступим к обработке введенных данных, основанной на
применении технологии LINQ. При выборе подходящего метода LINQ
полезно обратиться к списку методов, указанному в преамбуле к
подгруппе, к которой относится задание,
поскольку именно эти методы должны применяться при решении задач
данной подгруппы. Впрочем, при выполнении заданий из последующих
подгрупп может потребоваться использовать не только те методы, которые
впервые появляются в этих подгруппах, но и какие-либо из методов,
рассмотренных в предшествующих подгруппах.
В нашем случае, очевидно, необходимо использовать один из
методов, связанных с выбором единственного элемента: Single или
SingleOrDefault (к поэлементным операциям относятся также методы First
и FirstOrDefault, связанные с выбором начального элемента
последовательности, и Last и LastOrDefault, связанные с выбором ее
конечного элемента). Надо отметить, что из всех методов, относящихся к
поэлементным операциям, методы Single и SingleOrDefault
характеризуются наиболее сложным поведением (именно по этой причине
мы выбрали для разбора задание LinqBegin4).
Для любой поэлементной операции можно указать параметр-предикат,
который позволяет отобрать те элементы, для которых
требуется выполнить соответствующую операцию. Подобный параметр
надо оформить в виде лямбда-выражения, принимающего элемент
последовательности и возвращающего логическое значение (true, если
элемент следует учитывать при выполнении операции, false в противном
случае).
По условию задания LinqBegin4 требуется проанализировать
элементы исходной последовательности, оканчивающиеся символом C. В
качестве предиката в данном случае можно использовать следующее
лямбда-выражение (перед символом лямбда-выражения =>
указывается его параметр, а после него возвращаемое значение):
e => e.Length != 0 && e[e.Length - 1] == c
Отметим, что без первого условия указанное логическое выражение
приводило бы к возбуждению исключительной ситуации
IndexOutOfRangeException при обработке пустых строк (которые могут
входить в исходную последовательность). Для связывания двух условий
необходимо использовать операцию && (логическое И с сокращенной
схемой вычисления), при которой в случае ложного первого операнда
второй операнд не анализируется (операцию & с полной схемой
вычисления в данном случае применять нельзя, как нельзя и менять
порядок следования данных условий). Вместо сравнения длин строк в
первом условии можно использовать сравнение самих строк (e != "" ),
однако данное сравнение будет выполняться медленнее, чем сравнение
длин.
Указанный вариант реализации предиката не является единственно
возможным. Если воспользоваться методом EndsWith класса string, то
предикат можно представить в виде единственного условия:
e => e.EndsWith(c.ToString())
В данном случае, однако, требуется явным образом преобразовать
символ c к строковому типу, вызвав для него метод ToString. Кроме того,
следует учитывать, что метод EndsWith требует сравнения строк, которое
выполняется дольше, чем сравнение символов.
Выберем один из описанных предикатов и используем его в методе
Single, возвращающем единственный элемент последовательности,
удовлетворяющий указанному предикату. Полученный элемент надо
передать задачнику для проверки правильности решения; для этого
следует использовать еще один статический метод класса PT метод Put,
который может принимать произвольное количество параметров базовых
типов (логического, целочисленного, вещественного, символьного,
строкового). В результате очередной вариант функции Solve с решением
задачи примет вид:
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
var a = GetEnumerableString();
Put(a.Single(e => e.Length != 0 && e[e.Length - 1] == c));
}
Полученная программа будет правильно обрабатывать
последовательности, содержащие единственный элемент, оканчивающийся
символом c , однако если последовательность не содержит таких элементов
или их имеется более одного, то будет возбуждаться исключительная
ситуация InvalidOperationException:
С исключительной ситуацией будут связываться два вида сообщений:
«Sequence contains more than one matching element» («Последовательность
содержит более одного соответствующего элемента» как на рисунке) и
«Sequence contains no matching element» («Последовательность не
содержит соответствующий элемент»).
Для перехвата и обработки исключительных ситуаций следует
использовать конструкцию trycatch. Для доступа к объекту-исключению
его надо описать в заголовке блока catch. Имея этот объект, можно
обратиться к его полю Message, содержащему текстовое описание
исключения, по этому полю определить причину возбуждения исключения
и вывести требуемую строку (напомним, что по условию задания в случае
отсутствия подходящих элементов надо вывести пустую строку, а в случае
двух и более подходящих элементов строку «Error»).
Получаем первый вариант правильного решения:
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
var a = GetEnumerableString();
try
{
Put(a.Single(e => e.Length != 0 && e[e.Length - 1] == c));
}
catch (InvalidOperationException ex)
{
if (ex.Message.Contains("более"))
Put("Error");
else
Put("");
}
}
Решение можно упростить, если воспользоваться вариантом метода
Single SingleOrDefault, который не приводит к возбуждению
исключения в случае, если в последовательности отсутствуют
требуемые элементы (заметим, что аналогичным образом ведут себя и
другие поэлементные операции, содержащие текст «OrDefault»:
FirstOrDefault и LastOrDefault). В подобной ситуации метод
SingleOrDefault возвращает значение по умолчанию (для числовых
последовательностей это 0, для строковых последовательностей, как и для
любых последовательностей со ссылочными данными, значение null).
Таким образом, при использовании метода SingleOrDefault
исключительная ситуация будет возбуждаться только в случае, если
исходная последовательность содержит более одного требуемого элемента.
Во втором варианте решения раздел catch стал более кратким, а раздел
try увеличился:
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
var a = GetEnumerableString();
try
{
string res = a.SingleOrDefault(e => e.Length != 0 &&
e[e.Length - 1] == c);
Put(res != null ? res : "");
}
catch
{
Put("Error");
}
}
Вместо тернарной операции ?: можно использовать операцию ??
появившуюся в версии C# 2.0 и часто используемую в программах,
применяющих технологию LINQ. Выражение a ?? b возвращает значение
a , если оно отлично от null, и значение b в противном случае. Таким
образом, оператор Put из раздела try можно переписать в виде
Put(res ?? "");
В подобной ситуации отпадает необходимость в переменной res , и
раздел try сокращается до единственного оператора:
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
var a = GetEnumerableString();
try
{
Put(a.SingleOrDefault(e => e.Length != 0 &&
e[e.Length - 1] == c) ?? "");
}
catch
{
Put("Error");
}
}
Поскольку переменная a используется в единственном месте
программы, в ней тоже нет необходимости. Это позволяет еще более
сократить полученное решение:
public static void Solve()
{
Task("LinqBegin4");
char c = GetChar();
try
{
Put(GetEnumerableString().SingleOrDefault(e =>
e.Length != 0 && e[e.Length - 1] == c) ?? "");
}
catch
{
Put("Error");
}
}
Следует заметить, что при использовании технологии LINQ очень
часто содержательная часть программы представляет собой цепочку
последовательных вызовов методов; в нашем случае цепочка состоит
всего из двух вызовов (GetEnumerableString и SingleOrDefault), однако
дополнительно включает операцию ?? . Заметим также, что при
использовании методов FirstOrDefault и LastOrDefault необходимости в
явной обработке исключений не возникает (хотя операция ?? для них
также может оказаться полезной).
Пройдя семь успешных тестовых испытаний программы, мы получим
сообщение о том, что задание выполнено (см. рисунок). При успешном тестовом
запуске в окне отсутствует раздел с примером верного решения, поскольку
этот раздел совпадал бы с разделом полученных результатов.
Нажав на клавишу [F2], можно отобразить окно с протоколом
выполнения данного задания (этот протокол в зашифрованном виде
хранится в файле результатов results.dat). В нашем случае информация о
ходе выполнения задания может иметь следующий вид:
LinqBegin4 S16/04 11:43 Ознакомительный запуск.
LinqBegin4 S16/04 11:55 Запуск с правильным вводом данных.
LinqBegin4 S16/04 12:03 Error InvalidOperationException.--3
LinqBegin4 S16/04 12:08 Задание выполнено!
Символ, указываемый перед информацией о дате и времени тестового
запуска, определяет язык программирования, на котором выполнялось
задание (символ S соответствует языку C# (C Sharp); для языка VB.NET
используется символ B). Числовое значение, указываемое в конце строки,
означает количество тестовых запусков, выполненных подряд и
завершившихся с одним и тем же результатом.
|