Общие принципы дизайна в объектно-ориентированном проектировании Задание: Обобщенное программирование Для заданной прикладной области реализовать обобщенный алгоритм для преобразования набора данных одного типа в набор данных другого типа. Описать полученные обобщенные методы с помощью встроенной документации XML. Поместить методы в библиотеку для повторного использования. Написать приложение, демонстрирующее заданное преобразование. Варианты прикладной области: 1 OrderBy – получает последовательность, отсортированную по некоторому столбцу; Where – фильтрует последовательность по некоторому предикату 2 Any – определяет существует ли хотя бы один элемент последовательности, удовлетворяющий условию; Concat – присоединяет две последовательности с одинаковыми типами элементов (f + s) 3 Contains – определяет, что последовательность содержит заданный элемент; Union – получает объединение двух последовательностей (f | s) 4 Intersect – получает пересечение двух последовательностей (f & s); Last – получает последний элемент последовательности, который соответствует заданному условию 5 Min – получает элемент последовательности, который является минимальным по заданному условию; Reverse – выполняет обращение элементов последовательности 6 Distinct – возвращает уникальные элементы последовательности (с использованием Хеш-таблицы); ElementAt – получает элемент по индексу 7 Max – получает элемент последовательности, который является максимальным по заданному условию; Repeat – формирует последовательность дублированием элемента заданное количество раз 8 Except – получает разницу двух последовательностей (f \ s); First – получает первый элемент последовательности который соответствует заданному условию 9 Single - получает элемент последовательности, который соответствует заданному условию, если таких элементов несколько, то генерирует исключение; SequenceEqual – проверяет, что две последовательности одинаковые 10 Cast – осуществляет приведение элементов последовательности к заданному типу; All – проверяет, что все элементы последовательности удовлетворяют заданному условию 11 SkipWhile – пропускает элементы последовательности, пока условие воз- вращает true и возвращает остальные элементы; Take – получает заданное количество первых элементов последовательности 12 TakeWhile – получает элементы последовательности, пока условие возвращает true и пропускает остальные элементы; ToArray – получает массив элементов последовательности Минимальные требования к лабораторной работе Методы должны быть полностью задокументированы. Библиотека должна быть подписана цифровой подписью издателя. Библиотека должна соответствовать общеязыковой спецификации. Окончательная версия библиотеки не должна содержать: ошибки и предупреждения компилятора C# по качеству кода; предупреждения компилятора C# по документированию; предупреждения статического анализатора кода. Сгенерировать документацию, используя один из генераторов документации NDoc или Sandcastle. Последовательность шагов при выполнении лабораторной работы Создать два проекта в одном решении. Проект, который будет содержать обобщенные алгоритмы должен быть Class Library. В проекте демонстрационного приложения добавить ссылку на проект общей библиотеки. Общая библиотека должна содержать разрабатываемые методы и классы. Включить генерацию документации для общей библиотеки. Реализовать методы прикладной области, включая документацию. Подписать общую сборку ключом, полученным во время выполнения второй лабораторной работы. Включить статический анализатор кода и исправить ошибки. Сгенерировать документацию, используя один из генераторов документации NDoc или Sandcastle. Реализовать демонстрационное приложение демонстрации заданного преобразования. Информация к выполнению лабораторной работы Обобщение (generic) – один из механизмов повторного использования кода, а именно повторным использованием алгоритма. Разработчик определяет алгоритм, например сортировку, поиск, замену, сравнение или преобразование, но не указывает конкретные типы данных, с которыми тот работает. Поэтому алгоритм может обобщенно применяться к объектам разных типов. Используя готовый алгоритм, другой разработчик применяет его к конкретным типам данных. Обобщение поддерживается не только на уровне компиляции, но и на уровне подсказок IntelliSense в Visual Studio. В курсе ООП был объявлен класс стек с параметром-типом T. public class Stack<T> { private const int length = 100; private T[] array = new T[length]; private int size = 0; public void Push(T item) { //... } public T Pop() { //... } } Поскольку параметр типа применен к классу Stack, то этот класс является обобщенным классом, а параметр типа может использоваться в пределах класса как символическая ссылка на тип. При использовании такого алгоритма указываются конкретные типы данных. Указанный тип данных называют аргументом-типом. Например, можно создать стек целых чисел или стек для работы со строками: Приведенные фрагменты показывают, как происходит подстановка типа в методе на примере метода Push(…). Параметры-типы могут применяться по отношению к классам, структурам интерфейсам, делегатам и методам. Рассмотрим пример задания. Реализовать следующие методы обработки последовательности: Получение проекции каждого элемента входной последовательности в новую форму (выбор заданных столбцов из входной таблицы – Select). Получение среднего значения некоторого столбца таблицы (Average). Получение суммы элементов некоторого столбца таблицы (Sum). Получение Хеш-таблицы с ключами некоторого столбца для определения значений таблицы по заданному ключу (ToDictionary). Действия этих методов рассмотрим на примере следующей таблицы: Company Coho Vineyard Lucerne Publishing Wingtip Toys Adventure Works Weight 25.2 18.7 6.0 33.8 Tracking Number 89453312 89112755 299456122 4665518773 Метод Select должен формировать из входной последовательности выходную последовательность с заданными столбцами. Например, если требуется выделить столбцы Company и Weight, то результат должен быть следующим: Company Coho Vineyard Lucerne Publishing Wingtip Toys Adventure Works Weight 25.2 18.7 6.0 33.8 Очевидно, должна существовать функция преобразования каждой строки входной последовательности в строку выходной последовательности. Методам Average и Sum для нахождения среднего значения и суммы требуется задать способ выделения данных из таблицы. Например, в столбце Company определить среднюю длину имен компаний. В этом случае тоже можно использовать функцию для получения длины строки названия компании. Аналогичную функцию можно использовать для задания ключа для Хеш-таблицы. Таким образом, обобщив условие задачи, можно прийти к выводу, что каждый из методов мог бы принимать функцию, соответствующую делегату: public delegate TResult Func<T, TResult>(T arg); Здесь T и TResult являются параметрами-типами. Т – тип аргумента функции. TResult – тип результата функции. Все интересующие нас методы можно разместить в статическом классе Processing. Метод Select должен для каждого элемента входной последовательности получать элементы выходной последовательности. Следовательно, такой метод должен работать обобщенно с двумя параметрами-типами (например, TSource и TResult). Тогда метод Select может быть объявлен следующим образом: public static IEnumerable<TResult> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector) { if (source == null) throw new ArgumentNullException("source", "Значение параметра source - пустая ссылка."); if (selector == null) throw new ArgumentNullException("selector", "Значение параметра selector - пустая ссылка."); foreach (TSource item in source) { yield return selector(item); } } Метод сначала проверяет входные аргументы, затем для каждого элемента входной последовательности получает элемент выходной последовательности с помощью переданной функции selector. Полученный элемент направляется в выходную последовательность. Работу такого обобщенного метода рассмотрим на примере. Пусть объявлены два класса Package и ExtendPackage: class Package { public string Company; public double Weight; public Package(string Company, double Weight) { this.Company = Company; this.Weight = Weight; } } class ExtendPackage : Package { public long TrackingNumber; public ExtendPackage(string Company, double Weight, long TrackingNumber) : base(Company, Weight) { this.TrackingNumber = TrackingNumber; } } Требуется выделить из таблицы {Company, Weight, TrackingNumber} столбцы {Company, Weight}. Для преобразования элементов входной последовательности в элементы выходной последовательности будет использована функция Translator: private static Package Translator(ExtendPackage item) { return new Package(item.Company, item.Weight); } В качестве входной последовательности может выступать динамический массив List<T>. List<ExtendPackage> extendPackages = new List<ExtendPackage>(); extendPackages.Add(new ExtendPackage("Coho Vineyard", 25.2, 89453312L)); extendPackages.Add(new ExtendPackage("Lucerne Publishing", 18.7, 89112755L)); extendPackages.Add(new ExtendPackage("Wingtip Toys", 6.0, 299456122L)); extendPackages.Add(new ExtendPackage("Adventure Works", 33.8, 4665518773L)); //Выделение элементов Package из ExtendPackage с использованием метода выделения Translator: IEnumerable<Package> packages1 = Processing.Select<ExtendPackage, Package>(extendPackages, Translator); В C# 2.0 появилась возможность объявить метод в теле другого метода с использованием ключевого слова delegate. В следующем примере функция преобразования объявляется при вызове метода Select: IEnumerable<Package> packages2 = Processing.Select<ExtendPackage, Package>(extendPackages, delegate(ExtendPackage item) { return new Package(item.Company, item.Weight); }); Нововведения языка C# 3.0 позволяют существенно упростить вызов метода Select, если дополнительно добавить к объявлению метода атрибут [Extension]: var packages5 = extendPackages.Select(item => new { item.Company, item.Weight }); Оценить преимущество спецификации C# 3.0 по отношению к C# 2.0 можно на следующем примере. Пусть требуется вычислить квадраты элементов некоторого динамического массива. С использованием языка C# 2.0 эту задачу можно решить следующим образом: List<int> numbers1 = new List<int>(new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }); IEnumerable<int> square1 = Processing.Select<int, int>(numbers1, delegate(int x) { return x * x; }); Processing.Print(square1); Здесь массив numbers1 содержит некоторую последовательность целых чисел. Затем вызывается метод Select с аргументами последовательности и анонимным методом, вычисляющим квадрат целого числа. Полученный объект является перечислимым. Он печатается на экран. Спецификация языка C# 3.0 позволяет писать наглядный и более готовый к изменениям код: var numbers2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7 }; var square2 = numbers2.Select(x => x * x); Processing.Print(square2); Если придется изменить тип элемента массива с int на double, то в первом случае придется вносить 7 изменений, а во втором случае только 1. Остальные методы не представляют трудностей. Рис. 5