Всё для Учёбы — студенческий файлообменник
1 монета
pdf

Студенческий документ № 081011 из СИЮ

Оглавление

Введение 3

1. Класс "Рациональное число" 5

1.1. Переменные и методы класса "Рациональное число" 5

1.2. Конструкторы и деструктор класса "Рациональное число" 10

1.3. Перегрузка операций для класса "Рациональное число" 13

1.4. Быстрая сортировка массива дробей 20

Задания для самостоятельной работы 22

2. Методы решения уравнений 24

Задания для самостоятельной работы 30

3. Решение системы линейных уравнений 32

Задания для самостоятельной работы 44

4. Интерфейс для работы с математическими объектами 46

4.1. Разработка интерфейса 46

4.2. Раскрытие интерфейса для класса "Матрица" 47

4.3. Раскрытие интерфейса для класса "Полином" 49

4.4. Использование интерфейса IMathObject 53

Задания для самостоятельной работы 55

5. Множество точек на плоскости 57

5.1. Структура хранения системы ограничений 57

5.2. Иерархия классов кривых 1-ого и 2-ого порядков 60

Задания для самостоятельной работы 68

6. Классы - коллекции 70

6.1. Использование стеков и очередей 74

6.2. Использование списков для хранения разреженных матриц 81

6.3. Использование словарей для создания телефонной книги 90

6.4. Язык запросов LINQ на примере приложения "Магазин" 97

Задания для самостоятельной работы 109

Литература 112

Введение

Учебное пособие является руководством для ведения практических занятий при изучении студентами объектно-ориентированного программирования на языке C#. В пособии проиллюстрировано применение основных принципов объектно-ориентированного программирования (абстрагирования, инкапсуляции, наследования и полиморфизма) на разнообразных примерах. Помимо этого большое внимание уделено различным возможностям языка программирования С# в рамках объектноориентированного подхода. Этот язык программирования является сейчас одним из наиболее распространенных средств разработки приложений. Он был разработан в 1998-2001 годах группой инженеров под руководством Андерса Хейлсберга в компании Microsoft как основной язык разработки приложений для платформы Microsoft .NET Framework.

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

Данный практикум может использоваться совместно с учебным пособием тех же авторов "Объектно-ориентированное программирование на С#", являясь его практическим сопровождением.

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

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

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

В третьем разделе описывается получение решения системы линейных уравнений. На данном примере демонстрируется взаимодействие объектов различных классов (матрица, система уравнений) и разнообразные способы представления и решения системы уравнений.

В четвертом разделе впервые затрагивается принцип наследования. Использование этого принципа будет продемонстрировано на примере создания интерфейса математических объектов и раскрытия этого интерфейса на примерах классов "Матрица" и "Полином". С помощью обобщенной функции показывается, как единообразно можно осуществлять обработку объектов-потомков одного родителя, не анализируя их тип.

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

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

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

Microsoft Visual Studio 2010.

1. Класс "Рациональное число"

Рациональное число (лат. ratio - отношение, деление, дробь) - число,

m представляемое обыкновенной дробью , где m, n - целые числа. n

Правильной называется дробь, у которой модуль числителя меньше модуля знаменателя. Правильные дроби представляют рациональные числа, принадлежащие интервалу (-1, 1). Дробь, не являющаяся правильной, называется неправильной. У нее модуль числителя больше или равен модулю знаменателя.

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

В качестве примера разберем создание класса "Рациональное число", который должен реализовывать стандартные операции над числами: сложение, вычитание, умножение, деление и операции сравнения. В классе также необходимо предусмотреть средства приведения дроби к смешанному виду.

1.1. Переменные и методы класса "Рациональное число"

Из определения следует, что любое рациональное число в смешанном виде определяется четырьмя составляющими:

• знаком числа (число положительное или отрицательное);

• целой частью;

• числителем; ? знаменателем.

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

// класс "Рациональное число" class Fraction

{ int sign; // знак дроби (+ или -)

int intPart; // целая часть дроби int numerator; // числитель дроби int denominator;

. . .

} // знаменатель дроби

При описании операций с дробями предполагаем, что объекты класса Fraction находятся в смешанном виде. Результатом операции над дробями может быть неправильная дробь, которую, согласно предположению, необходимо перевести в смешанный вид. Для этого необходимы методы "преобразования в смешанный вид", "сокращения дроби" и "выделения целой части". Данные методы будут применяться при выполнении арифметических операций над дробями или при создании дроби, гарантируя, что дробь после завершения операции будет иметь смешанный вид. Таким образом, пользователю класса нет необходимости выполнять операции приведения дроби к смешанному виду, поскольку эта операция выполняется автоматически. Поэтому методы преобразования в смешанный вид, сокращения дроби и выделения целой части можно описать как закрытые элементы класса.

// класс "Рациональное число" class Fraction

{ int sign; // знак дроби (+ или -) int intPart; // целая часть дроби int numerator; // числитель дроби int denominator; // знаменатель дроби

// метод преобразование дроби в смешанный вид void GetMixedView()

{

. . .

} // метод сокращения дроби void Cancellation()

{ . . .

}

// метод выделения целой части дроби

void GetIntPart()

{ . . .

}

. . . }

Деструктор также должен относиться к закрытым элементам класса - это определено правилами языка С#.

К доступным элементам класса Fraction относятся конструкторы, методы, реализующие арифметические операции, методы сравнения, метод преобразования в вещественное число. Для ввода дроби необходимо разработать статический метод Parse для выделения дроби из символьной строки. Для вывода дроби удобно переопределить неявную операцию преобразования в символьную строку, которая будет осуществлять получение символьного представления дроби.

Таким образом, полный состав класса Fraction может выглядеть так:

class Fraction { int sign; // знак дроби (+ или -) int intPart; // целая часть дроби int numerator; // числитель дроби int denominator; // знаменатель дроби

// метод преобразования дроби в смешанный вид void GetMixedView()

{

. . .

} // метод сокращения дроби

void Cancellation()

{

. . . }

// метод выделения целой части дроби

void GetIntPart()

{

. . .

} // конструктор без параметров public Fraction()

{ . . .

}

// конструктор c параметрами

public Fraction(int n, int d, int i = 0, int s = 1)

{ . . . }

// деструктор

~Fraction() {

. . .

} // метод сложения двух дробей

static public Fraction operator + (Fraction ob1, Fraction ob2)

{ . . .

} // метод сложения дроби с целым числом

static public Fraction operator + (Fraction ob1, int a)

{

. . .

} // метод сложения целого числа и дроби

static public Fraction operator + (int a, Fraction ob1)

{

. . .

}

// метод изменение знака дроби на противоположный static public Fraction operator - (Fraction ob)

{

. . .

} // метод вычитания двух дробей

static public Fraction operator - (Fraction ob1, Fraction ob2)

{

. . .

} // метод вычитания из дроби целого числа

static public Fraction operator - (Fraction ob1, int a)

{

. . .

} // метод вычитания дроби из целого числа

static public Fraction operator - (int a, Fraction ob1)

{

. . .

}

// метод умножения двух дробей

static public Fraction operator * (Fraction ob1, Fraction ob2)

{ . . .

} // метод умножения дроби на целое число

static public Fraction operator * (Fraction ob1, int a)

{

. . .

} // метод умножения целого числа и дроби

static public Fraction operator * (int a, Fraction ob1)

{

. . .

} // метод деления двух дробей

static public Fraction operator / (Fraction ob1, Fraction ob2)

{

. . .

} // метод деления дроби на целое число

static public Fraction operator / (Fraction ob1, int a)

{

. . .

} // метод деления целого числа на дробь

static public Fraction operator / (int a, Fraction ob1)

{

. . .

} // метод преобразования дроби в тип double static public explicit operator double(Fraction ob)

{ . . .

}

// методы сравнения двух дробей

public static bool operator > (Fraction ob1, Fraction ob2)

{ . . .

}

public static bool operator = (Fraction ob1, Fraction ob2)

{ . . .

} public static bool operator = denominator)

{

intPart += (numerator / denominator); numerator %= denominator;

} }

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

// метод сокращения рациональной дроби

void Cancellation()

{ if(numerator != 0)

{ int m = denominator, n = numerator, ost = m%n;

// вычисление НОД(числителя, знаменателя)

// алгоритмом Евклида

while(ost != 0)

{ m = n;

n = ost; ost = m % n;

} int nod = n; if(nod != 1)

{ numerator /= nod;

denominator /= nod;

}

} } Деструктор класса выводит сообщение о том, что уничтожен объект класса Fraction.

// деструктор

~Fraction() {

Console.WriteLine("Дробь " + this + " уничтожена.");

} Далее в функции Main() приведены различные способы создания объектов класса Fraction с помощью конструкторов.

static void Main(string[] args)

{ // создание дроби 2/3

Fraction d1 = new Fraction(2, 3, 0, 1);

// создание дроби -2 4/5

Fraction d2 = new Fraction(4, 5, 2, -1);

// создание дроби 2 1/3

Fraction d3 = new Fraction(4, 3, 1, 1);

// создание дроби 1 2/3

Fraction d4 = new Fraction(10, 6);

// создание дроби 3/7

Fraction d5 = new Fraction(3, 7);

// создание дроби 2 3/8

Fraction d6 = new Fraction(3, 8, 2);

// создание рационального числа 0 Fraction d7 = new Fraction(); . . .

}

1.3. Перегрузка операций для класса "Рациональное число"

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

Поскольку любая дробь является вещественным числом, переопределим оператор явного преобразования объекта класса Fraction к вещественному типу данных double:

// операция преобразования дроби в тип double public static explicit operator double(Fraction ob)

{

double res = (double)ob.sign*(ob.intPart * ob.denominator + ob.numerator) / ob.denominator;

return res; }

Данное преобразование удобно будет использовать при сравнении дробей.

Перегрузку операций сравнения двух дробей (больше, больше или равно, меньше, меньше или равно, равно, не равно) осуществим с помощью статических методов класса Fraction. Эти операторы должны возвращать значение типа bool. Заметим, что эти операторы следует перегружать "парами". Например, если класс содержит перегруженную операцию "==", то обязательно в нем должна быть перегружена и операция "!=".

Операторы "==" и "!=" осуществляют поэлементное сравнение двух дробей, которые представлены в смешанном виде. Остальные операторы сравнения используют преобразование дроби к вещественному числу и сравнивают полученные значения.

// операции сравнения двух дробей

public static bool operator == (Fraction ob1, Fraction ob2)

{ if (ob1.sign != ob2.sign || ob1.intPart != ob2.intPart || ob1.numerator * ob2.denominator != ob1.denominator * ob2.numerator)

return false;

return true;

} public static bool operator != (Fraction ob1, Fraction ob2)

{ if (ob1.sign == ob2.sign && ob1.intPart == ob2.intPart && ob1.numerator * ob2.denominator == ob1.denominator * ob2.numerator)

return false; return true;

}

public static bool operator > (Fraction ob1, Fraction ob2)

{ if ((double)ob1 = (double)ob2)

return false;

return true;

} public static bool operator >= (Fraction ob1, Fraction ob2)

{ if ((double)ob1 (double)ob2)

return false;

return true;

} Каждая арифметическая операция ("+", "-", "*", "/") перегружена тремя методами-операторами для случаев, когда:

• операндами операции являются объекты класса Fraction;

• первый операнд - дробь, второй - целое число;

• первый операнд - целое число, второй - объект-дробь.

Результатом выполнения этих операторов является новая дробь. Рассмотрим подробнее реализацию операторов сложения.

Оператор сложения двух дробей после формирования результата осуществляет преобразование к смешанному виду.

// операция сложения двух дробей

static public Fraction operator + (Fraction ob1, Fraction ob2)

{ Fraction res=new Fraction();

res.numerator = ob1.sign * (ob1.intPart * ob1.denominator + ob1.numerator) * ob2.denominator + ob2.sign *(ob2.intPart * ob2.denominator + ob2.numerator) * ob1.denominator;

res.denominator = ob1.denominator * ob2.denominator;

if (res.numerator " для двух дробей if (r1 > r2)

Console.WriteLine("r1 > r2"); else

Console.WriteLine("r1 r2)

Console.WriteLine("r1 r2"); // вызов оператора "+" для двух дробей d = r1 + r2;

Console.WriteLine("r1 + r2 = " + d); // вызов оператора "+" для дроби и числа d = r1 + (-11);

Console.WriteLine("r1 + (-11) = " + d);

// вызов оператора "+" для числа и дроби d = 5 + r1;

Console.WriteLine("5 + r1 = " + d);

. . . }

Рис.1.1. Демонстрация операций с классом Fraction.

1.4. Быстрая сортировка массива дробей

Тип Fraction может использоваться в качестве подставляемого вместо обобщенного типа данных при генерации функций и классов на основе обобщений. Приведем пример использования обобщения функции быстрой сортировки для массива дробей. В сгенерированном методе с типом Fraction требуется организовать сравнение двух дробей. Для этого класс Fraction должен раскрывать интерфейс IComparable и иметь метод сравнения дробей CompareTo().

class Fraction : IComparable

{

int sign; // знак дроби (+ или -) int intPart; // целая часть дроби int numerator; // числитель дроби int denominator; // знаменатель дроби

// метод сравнения двух дробей public int CompareTo(object ob)

{ if (this (ob as Fraction)) return 1; return 0;

}

. . . }

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

class Program

{

static void QuickSort(T [] m, int l, int r) where T: IComparable

{ if(l == r) return; int i = l, j = r;

// выбирается элемент, делящий массив на две части

T selected = m[l];

// осуществляется поиск и перестановка элементов, меньших

// выбранного, с конца массива и больших выбранного // с начала массива.

while(i != j)

{

while(m[j].CompareTo(selected) >= 0 && j > i)

j--;

if(j > i)

{

m[i] = m[j];

while(m[i].CompareTo(selected) 0)

throw new Exception("Возможно, на этом отрезке корней нет");

// цикл метода - вычисления продолжается до тех пор,

// пока на одном из концов отрезка не будет получено

// значение функции с заданной точностью

while(Math.Abs(f(a))>eps && Math.Abs(f(b))>eps)

{

// поиск точки деления отрезка - вызов делегата double c = method(f,a,b);

// если найденная точка - корень, это решение if(f(c) == 0) return c;

// выбор следующего отрезка if(f(a)*f(c) 0)

Console.Write("+" + x[i, j] + "*x" +

(reoder[rang + j - 1] + 1));

else Console.Write(""+ x[i, j] + "*x" +

(reoder[rang + j - 1] + 1));

}

Console.WriteLine();

} }

else {

// получен единственный вектор решений Console.Write("(");

for(int i = 0; i 0; i++)

if (a[i] != 0)

{ // полином должен иметь i-ую степень,

// создаем новый массив и меняем // значения полей текущего объекта double[] b = new double[i + 1];

a.CopyTo(b, i); a = b; n = i; return;

} // все коэффициенты нулевые - в любом случае получаем

// полином нулевой степени n = 0;

double c = a[0];

a = new double[1]; a[0] = c; }

. . . }

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

// индексатор для получения коэффициента полинома по степени

double this[int i]

{ get { return a[i]; } set { a[i] = value; }

} // операция получения строкового представления полинома public static implicit operator string(Polynom ob)

{ string str = ""; for (int i = ob.n; i >0; i--)

str = str + string.Format("{0}*x^{1}+", ob[i], i); str = str + string.Format("{0}\n", ob[0]); return str;

}

// функция формирования полинома из // строки с коэффициентами полинома public static Polynom Parse(string str)

{ // разделение коэффициентов полинома в строке string[] s = str.Split(' ');

// формирование полинома по количеству элементов в

// массиве строк

Polynom res = new Polynom(s.Length - 1); for (int i = 0; i ob1.n)

{ // степень первого полинома больше, чем второго

Polynom res = new Polynom(n); for (int i = 0; i (T ob1, T ob2) where T : IMathObject

{

Console.WriteLine(ob1); Console.WriteLine(ob2);

Console.WriteLine("Сумма");

IMathObject res = ob1.Summa(ob2);

Console.WriteLine(res); Console.WriteLine("Вычитание"); res = ob1.Substract(ob2);

Console.WriteLine(res); Console.WriteLine("Умножение"); res = ob1.Multiply(ob2);

Console.WriteLine(res);

Console.WriteLine("Умножение на число");

res = ob1.Multiply(4); Console.WriteLine(res);

} Данный метод получает в качестве параметров два объекта одинакового типа, который раскрывает интерфейс IMathObject. Далее с этими объектами производятся те операции, которые определены в интерфейсе - суммирование этих объектов, получение их разности, умножение объектов и умножение первого из них на число. Продемонстрируем вызов этого метода:

static void Main(string[] args)

{ Console.WriteLine("Выберите режим работы с объектами:

1 - Матрица, 2 - Полином");

string str = Console.ReadLine(); switch (str)

{

case "1": {

// выбран режим работы с матрицами

Console.WriteLine("Введите две матрицы:"); Matrix a = new Matrix(3, 3);

a.Input();

Matrix b = new Matrix(3, 3);

b.Input();

Console.WriteLine("Демонстрация операций с матрицами:");

// вызов демо-функции

// для параметров-матриц

Demo(a, b); break; } case "2":

{ // выбран режим работы с полиномами

Console.WriteLine("Введите два полинома:");

Polynom a = Polynom.Parse(Console.ReadLine());

Polynom b = Polynom.Parse(Console.ReadLine()); Console.WriteLine("Демонстрация операций с полиномами:");

// вызов демо-функции

// для параметров-полиномов

Demo(a, b); break;

} }

} Рис. 4.1. Демонстрация режима работы с объектами-полиномами

Рис. 4.2. Демонстрация режима работы с объектами-матрицами

Задания для самостоятельной работы

1. Раскрыть интерфейс IMathObject на примере класса "Рациональное число". Протестировать обращение к наследуемым методам без явного указания на тип объектов.

2. Раскрыть интерфейс IMathObject на примере класса "Комплексное число". Протестировать обращение к наследуемым методам без явного указания на тип объектов.

3. Для массива объектов, которые раскрывают интерфейс IMathObject, создать метод-обобщение, который находит сумму объектов массива. Протестировать метод для массива матриц, массива комплексных чисел и массива полиномов.

4. Разработать интерфейс "Фигура на плоскости". Определить для него операции перемещения, поворота, определения площади, получения местоположения и пр. Раскрыть интерфейс в классах "Треугольник", "Прямоугольник", "Многоугольник".

5. Разработать класс "Линейная функция в n-мерном пространстве" ( f (x) ? b,x ? c). Определить конструктор, переопределить операции сложения и вычитания функций, умножения функции на число. Для организации ввода-вывода переопределить операцию преобразования в строку и статический метод Parse(). Написать методы вычисления значения функции в точке, получения градиента функции. Наследовать от этого класса класс "Квадратичная функция в n-мерном пространстве" ( f (x) ? Ax,x ? b,x ? c). Переопределить все указанные операции и методы для класса-наследника.

6. Разработать класс "Граф" в виде списка смежности. Определить конструкторы и деструктор. Переопределить операции ввода-вывода. Написать методы проверки связности графа, проверки полноты графа, проверки двудольности графа, получения дополнения графа, нахождения источника графа, нахождения стока графа. Наследовать от этого класса класс "Взвешенный граф". Написать методы получения кратчайшего пути между двумя вершинами алгоритмом Дейкстры, получения каркаса минимального веса алгоритмами Прима и Краскала.

5. Множество точек на плоскости

В задачах поиска экстремумом функции на некотором множестве точек множество зачастую задается в виде системы ограничений. Каждое ограничение представляет собой уравнение или неравенство, в котором левая часть записывается в виде некоторой функции, определенной в пространстве Rn (n?1,2,...), а правая часть - число.

Например, в пространстве R2 множество точек, заданное системой ограничений

?x2 ? y2 ? 4;

? ? y ? x;

? ? ? 2x ? y;

?? y ? 0 выглядит так:

Рис. 5.1. Множество допустимых точек задачи.

Создадим систему классов для описания какого-либо множества в пространстве R2.

5.1. Структура хранения системы ограничений

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

Ограничение может быть представлено следующим образом:

функция в левой части тип ограничения правая часть f (x,y) (=, , ?, ?, ? ) C (const)

Для задания ограничения требуется знать функцию его левой части, константу, стоящую в правой части и тип неравенства/равенства.

Определим функции 1-ого и 2-ого порядков, которые будут использоваться в ограничениях:

• линейная - f (x, y) ? ax ?by;

• эллиптическая - f (x, y) ? (x ?ax20)2 ? (y ?b2y0)2 ;

• гиперболическая - f (x, y) ? (x ?ax20)2 ? (y ?b2y0)2 ;

• параболическая - f (x, y) ? (y ? y0)2 ? 2px.

Линейная функция задается с помощью коэффициентов a и b. Для определения эллиптической и гиперболической функций требуется задать коэффициенты a и b, а также координаты точки (x0, y0), задающей смещение графика функции относительно начала координат. Параболическая функция задается параметром p и смещением графика по оси OY на величину y0.

Для определения функции в левой части ограничения можно объявить следующий класс Function:

// класс, задающий функцию в левой части ограничения

class Function

{ int typeFunction; // тип кривой: 1 - линейная,

// 2 - эллиптическая,

// 3 - гиперболическая,

// 4 - параболическая

// параметры, задающие функции разных типов

double a, b, p, x0, y0;

. . . }

// перечисление для определения типа ограничения // le - =, e - =, l - , n - <> enum TypeInequation { le, ge, e, l, g, n };

// класс, определяющий ограничение

class Constraint {

Function function; // объект, описывающий функцию

// в левой части ограничения double b; // правая часть TypeInequation type; // тип ограничения . . .

}

// класс, определяющий множество class Set

{ Constraint [] constraints; // массив ограничений

int n; // количество ограничений в системе . . .

}

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

// метод вычисления значения функции в заданной точке public double Calculate(double x, double y)

{ double value = 0.0; switch (typeFunction)

{

case 1: value = a * x + b * y; break; case 2:

value = (x - x0) * (x - x0) / (a * a) +

(y - y0) * (y - y0) / (b * b); break;

case 3:

value = (x - x0) * (x - x0) / (a * a) -

(y - y0) * (y - y0) / (b * b); break;

case 4: value = (y - y0) * (y - y0) - 2*p*x; break; default:

// неизвестен тип функции,

// поэтому генерируется исключение

throw new Exception("Неизвестен тип функции");

} return value;

}

Аналогичная структура программного кода (использование оператора switch) будет использоваться во всех методах класса Function. Недостатком этой структуры является необходимость внесения изменений во все методы класса, если добавляется новый тип функции или удаляется имеющийся. Если типов функций будет много, то методы становятся объемными и трудно читаемыми.

Еще одним недостатком является наличие неиспользуемых переменных класса Function. Например, для задания линейной функции достаточно использовать переменные typeFunction, a и b, а переменные p, x0 и y0 будут определены, но их значения игнорируются. Подобного неэффективного использования памяти следует избегать.

5.2. Иерархия классов кривых 1-ого и 2-ого порядков

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

Для каждого типа функции задается собственный класс, например,

Line, Ellipse, Hyperbola, Parabola. Все эти классы обладают одинаковым поведением - должно вычисляться значение функции, нужно вводить параметры функции и выводить представление функции на экран. Поэтому можно эти методы определить в отдельном классе Function, родительском для классов различных типов функций. Поскольку родительский класс "не знает", какая функция вычисляется, как ее распечатать, какие параметры ее определяют, все эти методы должны быть абстрактными или не иметь какой-либо содержательной обработки.

Соответственно, класс Function должен быть абстрактным:

// класс, задающий функцию в левой части ограничения abstract class Function

{ // абстрактный метод вычисления функции

public abstract double Сalculate(double x, double y); // виртуальный метод ввода параметров функции public virtual void Input()

{

Console.WriteLine("Введите данные, определяющие функцию");

} // абстрактный метод вывода функции на печать

public abstract string Output();

}

Классы, задающие кривые различных типов, имеют одинаковую структуру. Для примера приведем определение методов класса Ellipse:

// класс, задающий эллиптическую функцию

class Ellipse : Function

{

// параметры, задающие эллиптическую функцию double a, b;

double x0, y0;

// конструктор эллиптической функции public Ellipse(double a1= 1, double b1= 1, double x= 0, double y = 0)

{

// если параметр a или b равен нулю, эллиптическую

// функцию определить невозможно. // Поэтому генерируется исключение if (a1 == 0 || b1 == 0)

throw new Exception("Функция не может быть определена такими параметрами");

a = a1; b = b1; x0 = x; y0 = y;

} // переопределение виртуального метода // вычисления значения функции

public override double Сalculate(double x, double y)

{ return (x - x0) * (x - x0) / (a * a) +

(y - y0) * (y - y0) / (b * b);

} // переопределение виртуального метода ввода

// параметров функции public override void Input()

{ // вызов базовой версии метода base.Input();

// ввод параметром эллиптической функции a = double.Parse(Console.ReadLine()); b = double.Parse(Console.ReadLine()); x0 = double.Parse(Console.ReadLine());

y0 = double.Parse(Console.ReadLine());

} // переопределение виртуальной функции вывода на печать

public override string Output()

{ return "(x-" + x0 + ")^2/" + a * a +

"+ (y - " + y0 + ")^2/" + b * b;

} }

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

Теперь приведем объявление классов Constraint и Set с внесенными изменениями:

// класс, определяющий ограничение class Constraint

{ Function function; // ссылка на объект функции // в левой части ограничения

double b; // правая часть

TypeInequation type; // тип ограничения

// конструктор ограничения - параметры ограничения

// инициализируются путем ввода с клавиатуры public Constraint()

{ Input();

} // метод проверки выполнения ограничения public bool IsExecute(double x, double y)

{

// вычисление функции левой части ограничения double val = function.Сalculate(x, y);

// сравнение с правой частью согласно виду ограничения switch(type)

{ case TypeInequation.le: if (val = b)

return true;

break; case TypeInequation.e: if (val == b)

return true;

break; case TypeInequation.l: if (val b)

return true;

break; case TypeInequation.n: if (val != b)

return true;

break;

}

return false;

} // метод проверки выполнения равенства f(x,y) = b

// для ограничений типа "=", "=" public bool IsOnBound(double x, double y)

{

// для ограничений видов "", "<>" // равенство не должно выполняться if (type == TypeInequation.l || type == TypeInequation.g || type == TypeInequation.n)

return false;

// вычисление значения функции в точке double val = function.Сalculate(x,y);

// сравнение с правой частью на выполнение равенства

if (val == b) return true; return false;

} // метод ввода ограничения public void Input()

{ int choice;

// ввод типа ограничения

while(true)

{ Console.WriteLine("Линейная - 1, Эллиптическая - 2,

Гиперболическая - 3, Параболическая - 4");

choice = int.Parse(Console.ReadLine());

if(choice >= 1 && choice = - 1, = - 2,

- 4, <> - 5");

choice = int.Parse(Console.ReadLine());

if(choice >= 0 && choice ="; break; case TypeInequation.e: res = res + "="; break; case TypeInequation.l: res = res +""; break; case TypeInequation.n: res = res + "<>"; break; default:

throw new Exception

("Не существует такого вида ограничения");

} // вывод правой части ограничения res = res + ob.b;

return res;

}

} В классе Constraint используется принцип полиморфизма при работе с объектами функций левой части ограничения. Класс содержит ссылку на абстрактный класс Function, которая может хранить адрес объекта класса Line, Ellipse, Hyperbola или Parabola, задающего конкретную функцию. При вводе ограничения у пользователя запрашивается вид нужной функции и создается объект соответствующего класса, адрес которого сохраняется в переменной-ссылке function. При вызове методов Input(), Output() и Calculate() через ссылку function будут вызываться виртуальные функции того класса, адрес которого хранится в function. Такие вызовы выполняются как в функциях ввода/вывода ограничения, так и в методах проверки выполнения некоторых условий для точки. Таким образом, анализировать в этих методах тип функции левой части ограничения уже не потребуется.

Далее определим методы класса Set. Отметим, что в этом классе отсутствует метод ввода информации о системе ограничений. Ввод выполняется в конструкторе класса Set при создании массива ограничений (в конструкторе каждого ограничения).

// класс, задающий множество как систему ограничений class Set

{

Constraint[] constraints; // массив ограничений

int n; // количество ограничений

// конструктор, задающий количество ограничений

public Set(int n1)

{ n = n1; constraints = new Constraint [n]; for (int i = 0; i Стек - частный случай однонаправленного списка, действующий по принципу: последним пришел - первым вышел Queue Очередь - частный случай однонаправленного списка, действующего по принципу: первым пришел - первым вышел List Динамический массив, т.е. массив который при необходимости может увеличивать свой размер Dictionary

Хеш-таблица для пар ключ/значение и т.д.

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

Далее разберем принципы работы с указанными структурами данных на нескольких примерах.

6.1. Использование стеков и очередей

Стек - это динамическая линейная структура данных, в которой добавление элементов и их извлечение выполняются с одного конца, называемого вершиной стека (головой - head). При выборке элемент исключается из стека. Другие операции со стеком не определены. Говорят, что стек реализует принцип обслуживания LIFO ("last in - first out" - "последни пришел - первым вышел").

Среди коллекций языка C# реализованы классы стека как класса общего назначения, так и стека как класса-обобщения.

Среди методов и свойств класса Stack выделяют: ? Count - свойство для получения числа элементов в стеке;

• Push(object) - метод добавления элемента в стек;

• Pop() - метод извлечения элемента из стека;

• Peek() - метод получения элемента, который находится в вершине стека, не извлекая его;

• Contains(object) - метод проверки наличия в стеке заданного объекта; ? Clear() - очистка стека.

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

Пусть дано целое положительное число. Требуется перевести его в заданную систему счисления.

Напомним, что при переводе числа в другую систему счисления производится последовательное получение остатков от деления числа на основание системы счисления, которые при записи в обратном порядке образуют требуемое представление числа. Поэтому остатки от деления заносятся в стек, а затем извлекаются из него для формирования строкового представления записи числа. Если основание системы счисления больше 10 (в этом случае для записи числа используются буквенные обозначения), в строку заносится соответствующий символ (10 - 'A', 11 - 'B' и т.д.).

Решение данной задачи оформлено в виде функции, в которой используется объект класса Stack и его методы.

// определение функции перевода положительного целого числа // из десятичной системы счисления в любую заданную

// с основанием от 2 до 16.

// number - положительное целое число

// в десятичной системе счисления

// baseSS - основание системы счисления

// функция возвращает строковое представление записи числа static string PreobrChislo(int number, int baseSS)

{

// создается стек для хранения целых чисел Stack s = new Stack();

// вычисленные остатки от деления числа

// на основание системы счисления помещаются в стек

while(number != 0)

{ s.Push(number % baseSS);

number = number / baseSS;

} // формирование строкового представления записи числа

// путем извлечения значений из стека string res=""; try

{

while(true)

{ int n = s.Pop(); if(n s = new Stack(); Queue q = new Queue(); char c;

// строка анализируется посимвольно

for(int i=0;i s = new Stack(); for (int i = 0; i ), являются самыми распространенными классами-коллекциями, которые используются в приложениях. Они позволяют создавать линейный массив данных, который может легко менять свой размер путем добавления в него новых элементов или удаления из него существующих. Для списков реализован индексатор, который позволяет обращаться к элементам списка по индексу, как в массиве, что делает удобным обращение с его элементами. Основные свойства и методы классов-списков таковы:

• Count - свойство, которое задает количество элементов в списке;

• Add(object) - метод добавления объекта в конец списка;

• Clear() - удаление всех элементов из списка;

• Contains(object) - определение, присутствует ли заданный элемент в списке;

• IndexOf(object), LastIndexOf(object) - метод, который возвращает номер первого или последнего вхождения заданного элемента в список;

• Insert(int, object) - метод вставки элемента в список на заданную позицию;

• Remove(object) - метод удаления заданного элемента из списка;

• RemoveAt(int) - метод удаления элемента списка, находящегося на заданной позиции;

• и т.д., в том числе и методы поиска, сортировки, реверса элементов и прочие методы.

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

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

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

• класс для хранения одного элемента матрицы, который содержит индексы этого элемента и его значение (MatrixElement);

• класс представления всей матрицы в виде списка, содержащий размеры матрицы, количество ненулевых элементов и список ненулевых элементов матрицы (MatrixList);

• классы исключений BadIndexException, BadDimensionException, NonSquareMatrixException.

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

// класс для хранения одного элемента матрицы class MatrixElement

{

// индексы элемента матрицы int i, j;

// значение элемента матрицы double val;

// конструктор элемента матрицы

public MatrixElement(int i1, int j1, double v)

{ i = i1; j = j1; val = v;

} // свойство получения номера строки элемента public int I

{ get { return i; }

}

// свойство получения номера столбца элемента public int J

{ get { return j; }

} // свойство получения и установки значения элемента public double Value

{

get { return val; } set { val = value; }

} }

Разреженная матрица реализуется как отдельный класс, включающий в себя список элементов матрицы - объектов класса MatrixElement.

class MatrixList

{ // список элементов матрицы

List list; // размеры матрицы

int m,n; // количество ненулевых элементов матрицы

int count;

// конструктор разреженной матрицы из нулевых элементов public MatrixList(int m1, int n1)

{ m = m1; n = n1;

list = new List(); count = 0;

} . . .

} Для эффективного выполнения ряда операций над разреженными матрицами удобно хранить ее элементы, упорядоченными лексикографическим образом по номерам строк и столбцов. Такое представление однозначно определяет место каждого элемента в списке, что позволяет упростить процедуру поиска элемента, находящегося в заданной позиции матрицы. Поиск элемента матрицы, расположенного в i-ой строке и j-ом столбце заключается в том, что при просмотре списка пропускаются все элементы, расположенные в строках с меньшим номером, чем i, а затем - в заданной строке, но в столбцах с меньшим номером, чем j. Такая процедура поиска используется в индексаторе, осуществляющем доступ к элементу матрицы и в других методах класса MatrixList.

// индексатор для доступа к элементу матрицы по его индексам public double this[int i, int j]

{ // получение значения элемента матрицы get

{

// поиск позиции искомого элемента в списке int index = 0;

// пропускаем элементы, находящиеся // в строках с меньшим номером while (index list[index].I) index++;

if (index list[index].J)

index++;

} // если элемент в заданной позиции уже // имеется, возвращаем его значение if (index list[index].I) index++;

if (index list[index].J)

index++;

}

// если элемент в заданной позиции уже // имеется, изменяем его значение if (index list[index].I) index++;

if (index list[index].J)

index++;

}

// если элемент в заданной позиции уже // имеется, удаляем его из списка

if (index . Словари представляют собой коллекции, в которых для хранения объектов используется принцип хеширования. Суть хеширования состоит в том, что для определения уникального значения (хеш-кода), используется значение соответствующего ему ключа. Хеш-код затем используется в качестве индекса, по которому в словаре отыскиваются данные, соответствующие этому ключу. Преобразование ключа в хеш-код выполняется автоматически.

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

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

Классы, которые реализуют словари, обладают следующими методами и свойствами:

• ContainsKey(key) - метод проверки наличия записи с заданным ключом в словаре;

• ContainsValue(value) - метод проверки наличия записи с заданным значением в словаре;

• Keys - свойство, с помощью которого можно получить доступ к списку всех ключей словаря;

• Values - свойство, с помощью которого можно получить доступ к списку всех значений в словаре;

• Add(key, value) - метод добавления новой записи в словарь;

• Remove(key) - метод удаления записи, соответствующей заданному ключу ? и пр.

Обратиться к элементу хэш-таблицы по ключу можно с помощью следующего синтаксиса: имя_хэш_таблицы[ключ].

Продемонстрируем принципы работы со словарями на примере приложения работы с телефонной книгой.

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

Простым примером подобных приложений является "Телефонная книга". В этом приложении должно храниться произвольное количество записей о контактах (имя абонента и его телефон). Основной функцией приложения является поиск телефона нужного абонента.

Для хранения информации о контактах телефонной книги реализуем собственную хэш-таблицу, элементами которой будут являться "страницы" телефонной книги.

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

Для хранения телефонной книги будем использовать хэш-таблицу из 33 групп (по количеству букв русского алфавита). Ключевым значением поиска будет являться имя абонента. Хэш-функция будет определять номер группы по первой букве этого имени. Таким образом, как и в бумажных телефонных книгах, записи будут сгруппированы по начальным буквам имен абонентов.

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

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

? класс информации об абоненте (Info); ? класс для хранения хэш-таблицы (PhoneBook).

Приведем код класса информации об абоненте:

class Info

{ string fio; // имя абонента

string phone; // номер телефона

// конструктор с инициализацией данных об абоненте

public Info(string f, string p)

{ fio = f; phone = p;

}

// конструктор по умолчанию public Info()

{ fio = ""; phone = "";

} // свойство доступа к имени абонента public string Fio

{ get { return fio; }

} // свойство доступа к телефону public string Phone

{

get { return phone; }

} // операция получения строкового представления записи public static implicit operator string(Info ob)

{ return "Абонент: "+ob.fio + " Телефон: " + ob.phone;

}

} Как видно, класс Info не представляет особой сложности и может быть легко дополнен новыми полями и свойствами. Остановимся подробнее на классе PhoneBook. Класс содержит словарь, в котором буквам соответствуют списки записей об абонентах, чьи имена начинаются с этой буквы. Данный словарь, а также пустые списки записей об абонентах, создаются в конструкторе класса PhoneBook:

// класс хэш-таблицы в виде массива списков class PhoneBook

{ // хэш-таблица записей об абонентах Dictionary> book; // конструктор хэш-таблицы public PhoneBook()

{

book = new Dictionary>();

// создание списка (группы абонентов) для каждой буквы for (char c = 'А'; c ()); }

. . . }

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

// метод добавления новой записи об абоненте public void PushAbonent(string abonent, string phone)

{

// ключ группы соответствует первой букве в имени абонента

// буква задана в верхнем регистре char key = Char.ToUpper(abonent[0]);

// добавление записи c информацией об абоненте (объект Info)

// в группу с ключом key book[key].Add(new Info(abonent.ToUpper(), phone));

}

// метод удаления из списка записи по имени абонента public void DeleteAbonent(string abonent)

{ // ключ группы соответствует первой букве в имени абонента

char key = Char.ToUpper(abonent[0]); // поиск записи c информацией об абоненте

// и удаление его из списка с ключом key Info info;

if (FindAbonent(abonent, out info)) book[key].Remove(info);

} // метод поиска записи по имени абонента

// информация об абоненте возвращается через out-параметр info public bool FindAbonent(string abonent, out Info info)

{

// ключ группы соответствует первой букве в имени абонента

char key = Char.ToUpper(abonent[0]);

// возвращаем true, если абонент найден в группе key,

// false - в противном случае abonent = abonent.ToUpper(); for (int i = 0; i 6)

{

Console.WriteLine("----------- Меню ------------");

Console.WriteLine("Добавить абонента - 1,\n

Удалить абонента - 2,\n

Найти абонента - 3,\n

Проверка существования абонента - 4,\n

Печать телефонной книжки - 5,\n

Выход - 6"); Console.WriteLine("Введите команду:"); k = int.Parse(Console.ReadLine());

} return k;

} static void Main(string[] args)

{ PhoneBook phoneBook = new PhoneBook(); string phone; string str; Info i; while (true)

{ switch (Menu())

{ case 1: // вставка новой записи об абоненте

Console.WriteLine("--- Добавление абонента ---");

Console.Write("Введите имя:"); str = Console.ReadLine();

if (phoneBook.HasAbonent(str) == true)

{

Console.WriteLine("Такой абонент уже есть"); break;

}

Console.Write("Введите номер телефона:"); phone = Console.ReadLine(); phoneBook.PushAbonent(str, phone); break; case 2: // удаление записи по имени абонента Console.WriteLine("--- Удаление абонента -----");

Console.Write("Введите имя:"); str = Console.ReadLine(); phoneBook.DeleteAbonent(str); break; case 3: // поиск телефона заданного абонента Console.WriteLine("---- Поиск абонента -------");

Console.Write("Введите имя:"); str = Console.ReadLine();

if (phoneBook.FindAbonent(str, out i) == false) Console.WriteLine("Абонента не существует"); else

Console.WriteLine(i); break;

case 4: // проверка существования абонента

// с заданным именем

Console.WriteLine("- Существование абонента -");

Console.Write("Введите имя:"); str = Console.ReadLine();

if (phoneBook.HasAbonent(str) == false) Console.WriteLine("Абонента не существует");

else

Console.WriteLine("Абонент существует");

break;

case 5: // печать телефонной книги Console.WriteLine(phoneBook);

break;

case 6: // выход из приложения

return;

} }

}

6.4. Язык запросов LINQ на примере приложения "Магазин"

Одна из типовых задач работы с коллекциями - это задача поиска данных, удовлетворяющих определенным условиям. Именно для этих целей в языке C# был создан специальный язык запросов LINQ (Language Integrated Query), в чем-то схожий с языком запросов для баз данных SQL. Язык LINQ предоставляет универсальный способ выборки данных независимо от того, каков их источник - различные коллекции (массивы, списки, словами), xml-документы, базы данных и пр.

Запрос указывает, какую информацию нужно извлечь из источника данных. В запросе могут быть указаны способ сортировки и группировки данных. Запрос хранится в переменной и инициализируется выражением запроса, которое содержит три основные части: from, where и select. Часть from указывает, из каких источников следует выбирать данные, часть where предназначена для задания условий отбора данных, наконец, часть select указывает, каким образом выбранные данные должны храниться в результате запроса.

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

// формирование источника данных для запроса

int[] array = new int[] { 4, 5, 2, 0, 3, 9, 1, 7, 8, 6 };

// формирование запроса получения всех четных элементов массива var query = from el in array where (el % 2) == 0 select el;

// выполнение запроса и печать элементов из выборки

foreach (int a in query)

Console.Write("{0} ", a);

В части from данного запроса указывается, что просматриваются все элементы el из массива array. В части where задано условие отбора элементов (для которых el%2==0). Часть select указывает, что в выборку входят сами элементы el. Собственно выполнение запроса осуществляется при выполнении следующего за запросом цикла.

Можно выполнять запрос и без цикла foreach. Например, это можно сделать с помощью агрегирующих функций Count(), Average(), Max() и др. Так, при получении максимального четного числа из массива выполнение запроса могло бы быть таким:

// формирование источника данных для запроса

int[] array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// формирование запроса получения всех четных элементов массива var query = from el in array where (el % 2) == 0 select el;

// выполнение запроса и печать максимального

// из элементов в выборке

Console.WriteLine("Максимальный четный элемент - {0}", query.Max());

Другим способом выполнения запроса без цикла является кэширование результата запроса в массиве или списке, который можно обработать позднее. Это делается с помощью методов запроса ToArray() или

ToList(). Например,

int[] res = query.ToArray();

Подробнее про язык запросов можно прочитать, например, в [9]. Мы же рассмотрим пример приложения, в котором используются коллекции и требуется выбор данных из этих коллекций. Для осуществления этого выбора будем использовать язык запросов.

Требуется написать приложение, которое отслеживает работу магазина. Имеется каталог товаров, которые могут продаваться в магазине. Каждый товар характеризуется категорией, названием и ценой. Данные о товарах хранятся в файле:

Рис.6.4. Файл с информацией о товарах

Для каждого товара в файле отводится одна строка, в которой данные разделены символом табуляции.

Для хранения информации о товаре в приложении создан класс Tovar:

// класс описания товара class Tovar

{

string category; // категория товара string name; // название товара int price; // цена товара

// конструктор класса public Tovar(string c, string n, int p)

{ category = c; name = n; price = p;

}

// свойства для доступа к полям класса public string Category

{ get { return category; }

} public string Name

{

get { return name; }

} public int Price

{ get { return price; }

}

// получение информации о товаре из символьной строки static public Tovar Parse(string str)

{ // разделение строки по символу табуляции string[] s = str.Split('\t');

// создание и возврат объекта-товара из данных строки Tovar t = new Tovar(s[0], s[1], int.Parse(s[2]));

return t;

} // операция получения строки с информацией о товаре для печати static public implicit operator string(Tovar t)

{ return t.category + " " + t.name + " Цена:" +

t.price + " рублей";

}

// переопределение функции получения строки с // информацией о товаре для сохранения в файл public override string ToString()

{ return category + "\t" + name + "\t" + price;

} } Для работы с каталогом товаров и поиска в нем нужной информации создан класс PriceList, который хранит список объектов Tovar, которые могут продаваться в магазине, и имеет методы для поиска в каталоге товаров, удовлетворяющих различным критериям поиска.

// класс для описания списка товаров, которые продаются в магазине class PriceList

{

List list;

// конструктор

public PriceList(string file)

{

// создание списка товаров по информации из файла list = new List(); // открытие файла для чтения

StreamReader sr = new StreamReader(file); string str;

// считывание файла построчно while ((str = sr.ReadLine()) != null) {

// получение товара из строки с его информацией

Tovar t = Tovar.Parse(str); // добавление товара в список list.Add(t);

} sr.Close(); }

. . . }

Методы печати информации по выбранным критериям имеют единую схему:

• формируется запрос на выбор из списка тех товаров, которые удовлетворяют требуемым условиям;

• проверяется, не пуст ли результат выборки;

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

По той же схеме работает и метод получения заданного товара (по категории и названию).

// получение объекта товара по названию и категории public Tovar GetTovar(string c, string n)

{

// формирование и кэширование в список запроса

// но поиск в списке товаров того, который

// имеет заданную категорию и название List items = (from s in list where s.Category.Equals(c) &&

s.Name.Equals(n) select s).ToList(); // если товаров не найдено, генерируется исключение if (items.Count() == 0)

throw new Exception("Такого товара нет на складе");

// товар найден - возвращаем его объект return items[0];

} Магазин имеет склад товаров. Информация о товарах, которые хранятся на складе, записана в текстовый файл (Рис. 6.5).

Через символ-разделитель, которым в данном случае является '!', в файл записаны категория товара, его название и количество на складе.

Рис.6.5. Файл с информацией склада.

Для управления складом в приложение добавим класс Sklad, содержащий словарь, в котором объекту товара ставится в соответствие количество данного товара на складе. Информация в словарь загружается из файла в конструкторе класса Sklad:

// класс для описания работы склада

class Sklad {

// словарь, который содержит информацию

// о наличии товаров на складе

Dictionary sklad;

// конструктор класса public Sklad(string file, PriceList l)

{

sklad = new Dictionary(); StreamReader sr = new StreamReader(file); string str;

// считывание строки из файла while ((str = sr.ReadLine()) != null)

{ string[] s = str.Split('!');

// получение объекта-товара по категории и названию

Tovar t = l.GetTovar(s[0],s[1]);

// добавление в словарь записи о товаре sklad.Add(t, int.Parse(s[2]));

} sr.Close(); }

. . . } Класс Sklad также имеет методы, которые регистрируют операции поступления товара на склад (AddTovar()) и реализации товара покупателю (SaleTovar()). Метод AddTovar() с помощью запроса находит в словаре запись о поступившем товаре. Если такого товара на складе не было, добавляется новая запись об этом товаре, в противном случае в найденной записи корректируется количество с учетом поступления.

// метод, регистрирующий поступление товара на склад public void AddTovar(Tovar t, int count)

{

// запрос к словарю на поиск записи с заданным ключом-товаром List items = (from s in sklad where s.Key.Equals(t) select s.Key).ToList();

if (items.Count() == 0)

{ // товара не было найдено -

// добавляем информацию о его поступлении sklad.Add(t, count); return;

}

// товар уже есть на складе - увеличиваем его количество sklad[t] = sklad[t] + count;

} Алгоритм метода SaleTovar() предусматривает поиск записи в словаре, соответствующей продаваемому товару. Отсутствие такой записи приводит к генерации исключения (товара нет на складе). Аналогичное исключение возникает, когда товар имеется, но в недостаточном количестве. Если же данные корректны, в найденной записи словаря изменяется количество товара с учетом реализации.

// метод, регистрирующий покупку товара public void SaleTovar(Tovar t, int count)

{

List> items =

(from s in sklad where s.Key.Equals(t) select s).ToList >();

if(items.Count() == 0)

throw new Exception("Необходимого товара нет на складе");

foreach (KeyValuePair p in items)

{ if (p.Value count = (from s in sklad where s.Key.Equals(t) select s.Value).ToList(); // если результат запроса пуст, товара нет на складе if (count.Count == 0)

return 0;

// возвращаем найденное количество return count[0];

} Помимо указанных методов удобно добавить в класс Sklad метод получения символьного представления списка товаров на складе, метод записи в файл и метод получения списка всех товаров, которые имеются на складе. Сложностей в написании данные методы не представляют.

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

// класс для описания заказа покупателя class Zakaz

{

Tovar t; // заказанный товар int count; // количество

// конструктор класса public Zakaz(Tovar a, int c)

{ t = a; count = c;

}

// свойства для получения доступа к полям заказа public Tovar tovar

{ get { return t; }

} public int Count

{

get { return count; } set { count = value; }

} // операция получения строкового представления заказа static public implicit operator string(Zakaz ob)

{ return ""+ob.t.Category+"!"+ob.t.Name + "!" + ob.count;

}

} Поступившие заказы фиксируются в списке заказов, для которого имеется соответствующий класс. Список текущих заказов сохраняется и загружается в текстовый файл:

// класс для описания списка заказов class ListZakaz

{ List list;

// конструктор, считывающий информацию // о невыполненных заказах из файла public ListZakaz(string file, PriceList l)

{

list = new List();

StreamReader sr = new StreamReader(file); string str;

while ((str = sr.ReadLine()) != null)

{

string[] s = str.Split('!'); Tovar t = l.GetTovar(s[0],s[1]);

Zakaz z = new Zakaz(t,int.Parse(s[2]));

} sr.Close(); }

. . .

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

// метод добавления заказа в список public void AddZakaz(Tovar t, int count)

{ Zakaz z = new Zakaz(t, count); list.Add(z);

}

// метод выполнения заказа и удаления его из списка public void RemoveZakaz(Tovar t, int count)

{ // заказов может быть выполнено несколько, // поэтому обращаемся к ним в цикле

while (true)

{

// если количество исчерпано, заканчиваем метод if (count == 0) return; // ищем заказы на данный товар List items = (from s in list where s.tovar.Equals(t) select s).ToList(); // если заказов нет, заканчиваем реализацию

if (items.Count() == 0) return; foreach (Zakaz z in items)

{ if (z.Count items = (from s in list where s.tovar.Equals(t)

select s.Count).ToList();

// подсчет суммы этого количества

int count = 0; foreach (int a in items) count += a; return count;

} Основной класс приложения - класс Shop, который объединяет в себе все операции, выполняемые в магазине.

// класс описания магазина class Shop

{ Sklad sklad; // объекта склада

ListZakaz list; // объект для списка заказов

PriceList tovars; // каталог товаров

// конструктор public Shop()

{ // создание объектов и загрузка данных из файлов tovars = new PriceList("../../0.txt"); sklad = new Sklad("../../1.txt", tovars); list = new ListZakaz("../../2.txt", tovars);

} // деструктор - записывает последнюю информацию // о заказах и состоянии склада в файлы

~Shop() {

sklad.WriteFile("../../1.txt"); list.WriteFile("../../2.txt"); }

. . . }

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

// оформление заказа

public void Zakaz(string cat,string tov,int c)

{ try {

Tovar t = tovars.GetTovar(cat, tov); // определение количества товаров на складе int count_sklad = sklad.CountTovar(t); if (count_sklad >= c)

{ // если товар на складе есть, обслуживаем заказ сразу

Console.WriteLine("Заказ обработан"); // меняем количество товара на складе

sklad.SaleTovar(t, c);

} else {

if (count_sklad > 0)

{ Console.WriteLine("Заказ обработан частично

({0})", count_sklad);

// запись оставшейся части заказа list.AddZakaz(t, c - count_sklad); // уменьшение товара на складе sklad.SaleTovar(t, count_sklad);

} else

{ // добавление заказа в список list.AddZakaz(t, c);

}

} }

catch (Exception e)

{ Console.WriteLine(e.Message);

}

} // поступление товаров от поставщиков

public void Postavka(string cat,string tov,int c)

{ try {

Tovar t = tovars.GetTovar(cat, tov);

// определение количества товара, на которое

// уже есть заказы int count_zakaz = list.CountTovar(t);

if (c 0)

{

// можно частично обработать заказы list.RemoveZakaz(t, count_zakaz); // оставшееся количество товара

// отправляем на склад

sklad.AddTovar(t, c - count_zakaz);

} else

{ // заказов на данный товар нет, // приходуем все на склад sklad.AddTovar(t, c);

} } }

catch (Exception e)

{

Console.WriteLine(e.Message);

} }

Задания для самостоятельной работы

1. Разработать класс "Полином", в котором информация о коэффициентах хранится в виде списка. Реализовать для класса методы ввода-вывода, сложения и умножения полиномов, умножения полинома на число, интегрирования и дифференцирования полинома.

2. Использовать классы стека и очереди для решения следующих задач:

• Дана символьная строка с некоторым выражением, в котором могут содержаться скобки трех видов - (), {}, []. Написать метод проверки правильности расстановки скобок в этой строке.

• Дана символьная строка, которая содержит правильное скобочное выражение. Для каждой пары скобок (открывающей и соответствующей ей закрывающей) распечатать номера их позиций в строке, упорядочив пары: а) по возрастанию номеров открывающих скобок; б) по возрастанию номеров закрывающих скобок.

• Дана символьная строка, содержащая правильно записанное математическое выражение следующего вида:

::=|M(,)

|m(,)

M - операция вычисления max из двух выражений, m - операция вычисления min из двух выражений. Написать функцию вычисления значения этого выражения.

• Дана символьная строка, содержащая правильно записанное логическое выражение следующего вида:

::= T | F | And( , ) | Or( , ) | Not ()

And - операция логического И, Or - операция логического ИЛИ, Not - операция логического НЕ.

Написать функцию вычисления этого выражения (функция должна возвращать true, если значение выражения равно T, false - в противном случае).

3. Описать класс "Предметный указатель". Каждый компонент указателя содержит слово и номера страниц, на которых это слово встречается. Предусмотреть возможность формирования указателя с клавиатуры и из файла, печати предметного указателя, сохранения в файл, вывода номеров страниц для заданного слова, добавления и удаления элемента из указателя.

4. Описать класс "Каталог библиотеки". Каждая запись каталога содержит информацию о книге - название, автор, количество экземпляров, количество экземпляров "на руках". Предусмотреть возможность формирования каталога с клавиатуры и из файла, печати каталога, сохранения в файл, поиска книги по какому-либо признаку (например, автору или названию), добавления книг в библиотеку, удаления книг из нее, операции получения или возврата книги читателем.

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

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

Литература

1. Пышкин, Е.В. Основные концепции и механизмы объектноориентированного программирования [Текст]/ Е.В.Пышкин. - СПб: БХВ-Петербург, 2005. - 640 с.

2. Шилдт, Г.. С# 4.0: полное руководство [Текст]: Пер. с англ. / Герберт Шилдт. - М.: ООО "И.Д. Вильямс", 2011. - 1056 с.

3. Дейтел, Х. C# в подлиннике. Наиболее полное руководство [Текст]: пер. с англ./ Харви Дейтел, Пол Дейтел. - СПб: БХВ-Петербург, 2006 г.. - 1056 с.

4. Троелсен, Э. Язык программирования C# 2010 и платформа .NET 4

[Текст]: Пер. с англ. / Эндрю Троелсен. - М.: ООО "И.Д. Вильямс", 2011. - 1392 с.

5. Уотсон, К. Visual C# 2010: полный курс [Текст]: Пер. с англ./ Карли Уотсон, Кристиан Нейгел, Якоб Хаммер Педерсен, Джон Д. Рид, Морган Скиннер. - М.: Диалектика, 2010. - 960 с.

6. Трей, Нэш C# 2010: ускоренный курс для профессионалов [Текст]: Пер.

с англ./ Нэш Трей. - М.: ООО "И.Д. Вильямс", 2011. - 592 с.

7. Кубенский, А.А. Структуры и алгоритмы обработки данных: объектноориентированный подход и реализация на С++ [Текст] / А.А. Кубенский. - СПб: БХВ-Петербург, 2004. - 464 с.

8. Вирт, Н. Алгоритмы и структуры данных [Текст]: пер. с англ. / Никлаус Вирт. - СПб: Невский Диалект, 2008. - 352 с.

9. Сайт центра разработки на Visual C# [Интернет-ресурс]. URL: http://msdn.microsoft.com/ru-ru/vcsharp/. Дата обращения: 10.11.2011.

113 114 115

2 3 2

Показать полностью… https://vk.com/doc313027694_443410081
2 Мб, 18 марта 2017 в 8:56 - Россия, Москва, СИЮ, 2017 г., pdf
Рекомендуемые документы в приложении