for(i=2; i { // ошибка: коллизия имен: у // float у = 7.7f; sum +=i; } Console.WriteLine("x= {0}; sum ={1}", x,sum); } //ScopeVar Заметьте, в теле метода встречаются имена полей, аргументов и локальных переменных. Эти имена могут совпадать. Например, имя В процедурный блок вложены два блока, порожденные оператором Выражение, проверяемое в операторе Во многих языках программирования разрешено иметь локальные статические переменные, у которых область видимости определяется блоком, но время их жизни совпадает со временем жизни проекта. При каждом повторном входе в блок такие переменные восстанавливают значение, полученное при предыдущем выходе из блока. В языке C# статическими могут быть только поля, но не локальные переменные. Незаконная попытка объявления Глобальные переменные уровня процедуры. Существуют ли? Поскольку процедурный блок имеет сложную структуру с вложенными внутренними блоками, то и здесь возникает тема глобальных переменных. Переменная, объявленная во внешнем блоке, рассматривается как глобальная по отношению к внутренним блокам. Во всех известных мне языках программирования во внутренних блоках разрешается объявлять переменные с именем, совпадающим с именем глобальной переменной. Конфликт имен снимается за счет того, что локальное внутреннее определение сильнее внешнего. Поэтому область видимости внешней глобальной переменной сужается и не распространяется на те внутренние блоки, где объявлена переменная с подобным именем. Внутри блока действует локальное объявление этого блока, при выходе восстанавливается область действия внешнего имени. В языке C# этот гордиев узел конфликтующих имен разрублен, — во внутренних блоках запрещено использование имен, совпадающих с именем, использованным во внешнем блоке. В нашем примере незаконная попытка объявить во внутреннем блоке уже объявленное имя у закомментирована. Обратите внимание, что подобные решения, принятые создателями языка С#, не только упрощают жизнь разработчикам транслятора. Они способствуют повышению эффективности программ, а самое главное, повышают надежность программирования на С#. Отвечая на вопрос, вынесенный в заголовок, следует сказать, что глобальные переменные на уровне процедуры в языке С#, конечно же, есть, но нет конфликта имен между глобальными и локальными переменными на этом уровне. Область видимости глобальных переменных процедурного блока распространяется на весь блок, в котором они объявлены, начиная отточки объявления, и не зависит от существования внутренних блоков. Когда говорят, что в C# нет глобальных переменных, то, прежде всего, имеют в виду их отсутствие на уровне модуля. Уже во вторую очередь речь идет об отсутствии конфликтов имен на процедурном уровне. Константы Константы C# могут появляться, как обычно, в виде литералов и именованных констант. Вот пример константы, заданной литералом и стоящей в правой части оператора присваивания: y = 7. 7 f; Значение константы Если возникает необходимость уточнить, как записываются литералы, то достаточно получить справку по этой теме. Делается все так же, как и в языке C++. Всюду, где можно объявить переменную, можно объявить и именованную константу. Синтаксис объявления схож. В объявление добавляется модификатор /// /// Константы /// public void Constants () { const int SmallSize = 38, LargeSize = 58; const int Midsize = (SmallSize + LargeSize)/2; const double pi = 3.141593; //LargeSize = 60; //Значение константы нельзя изменить. Console.WriteLine("MidSize= {0}; pi={1}", Midsize, pi); }//Constants
6. Выражения. Операции в выражениях Построение выражений. Операции и их приоритеты. Описание операций. Выражения строятся из операндов — констант, переменных, функций, — объединенных знаками операций и скобками. При вычислении выражения определяется его значение и тип. Эти характеристики однозначно задаются значениями и типами операндов, входящих в выражение, и правилами вычисления выражения. Правила также задают: • приоритет операций; • для операций одного приоритета порядок применения — слева направо или справа налево; • преобразование типов операндов и выбор реализации для перегруженных операций; • тип и значение результата выполнения операции над заданными значениями операндов определенного типа. Программист, записывающий выражение, должен знать, по каким правилам оно будет вычисляться. Сложность в том, что эти правила, начиная с приоритета операций, варьируются от языка к языку. Давайте посмотрим, как это делается в С#. Приоритет и порядок выполнения операций Большинство операций в языке С#, их приоритет и порядок наследованы из языка C++. Однако имеются и различия: например, нет операции", ", позволяющей вычислять список выражений; добавлены уже упоминавшиеся операции Как это обычно делается, приведем таблицу приоритетов операций, в каждой строке которой собраны операции одного приоритета, а строки следуют в порядке приоритетов, от высшего к низшему. Перегрузка операций Под /// /// Анализ выражений /// public void Express() { //перегрузка операций byte b1=1, b2 =2, Ь3; short sh1; int ini; //b3 = b1 + Ь2; //ошибка: результат типа int b3 = (byte) (b1+Ь2); //sh1 = b1 + Ь2; //ошибка: результат типа int sh1 = (short)(b1+b2); in1 = b1+ b2 + sh1; Console.WriteLine("Ь3= " + Ь3 + " sh1= "+ sh1 +" in1= " + in1); }//Express Разберем этот фрагмент. Начнем с первого закомментированного оператора присваивания Давайте разберем, как в данном примере организован вывод в методе Полагаю, что разбор данного примера и материалы предыдущей лекции, где приводилась иерархия преобразований внутри арифметического типа и обсуждались вопросы выбора реализации перегруженного метода, дают необходимое представление о том, как работает перегрузка операций при вычислении выражений. В деталях, как всегда, может помочь справочная система. С чего начинается выполнение выражения Вычисление выражения начинается с выполнения операций высшего приоритета. Первым делом вычисляются выражения в круглых скобках — ( Операции "увеличить" и "уменьшить" ( Операции "увеличить на единицу" и "уменьшить на единицу" могут быть префиксными и постфиксными. К высшему приоритету относятся постфиксные операции //операции increment и decrement //Следующее выражение допустимо, но писать подобное никогда не следует in1 = ++in1 +in1+ in1++; //in2 = ++in1 + in1++ + in1; Console.WriteLine(" in1= " + in1); Обратите внимание, что хотя у постфиксной операции высший приоритет, это вовсе не означает, что при вычислении выражения вначале выполнится операция 7(7) +7 + 7(8), где в скобках записан побочный эффект операции. Так что консольный вывод даст следующий результат: in1 = 21 Операциями "увеличить" и "уменьшить" не следует злоупотреблять. Уже оператор, приведенный в нашем фрагменте, сложен для понимания из-за побочного эффекта. Понимаете ли вы, что при изменении порядка записи слагаемых, как это сделано в закомментированном операторе, результат вычисления выражения будет уже не 21, а 22? Разный приоритет префиксных и постфиксных операций носит условный характер. Эти операции применимы только к переменным, свойствам и индексаторам класса, то есть к выражениям, которым отведена область памяти. В языках C++ и C# такие выражения называются I-value, поскольку они могут встречаться в левых частях оператора присваивания. Как следствие, запись в C# выражения Операции Далее необходимо создать небезопасный блок, например, метод класса, помеченный как unsafe, в котором уже можно вызывать эту функцию (операцию). Приведу пример такого метода, созданного в классе Рис. 6.1. /// /// определение размеров и типов /// unsafe public static void SizeMethod() { Console.WriteLine("Размер типа Boolean = " + sizeof (bool)); Console.WriteLine("Размер типа double = " + sizeof(double)); Console.WriteLine("Размер типа char = " + sizeof(System.Char)); int b1=1; Type t = b1.GetType(); Console.WriteLine("Тип переменной b1: {0}", t); //Console.WriteLine("Размер переменной b1: {0}", sizeof (t)); }//SizeMethod В этом примере операция применяется к трем встроенным типам — t = typeof(Ciassi); Console.WriteLine("Тип класса Ciassi: {0}", t); t = typeof(Testing); Console.WriteLine("Тип класса Testing: {0}", t); Как получить подробную информацию о классе? Пожалуй, следует рассказать не только о том, как можно получить переменную типа Этот и последующий раздел прерывают последовательное рассмотрение темы операций языка С#. Полагаю, понимание того, с какой целью выполняются те или иные операции, не менее важно, чем знание самой операции, И я не стал откладывать изложение этого материала на последующие лекции. Можно ли, зная тип (класс), получить подробную информацию обо всех методах и полях класса? Ясно, что такая информация может быть весьма полезной, если класс поставлен сторонней фирмой. Оказывается, это сделать нетрудно. Вся необходимая информация содержится в метаданных, поставляемых вместе с классом. Процесс получения метаданных называется отражением (reflection). Об отражении и метаданных уже говорилось в первой вводной лекции, и эта тема будет обсуждаться и далее. А сейчас я приведу пример, демонстрирующий получение подробной информации о методах и полях класса. Первым делом следует упростить в проекте использование классов пространства имен В класс /// /// Подробная информация о классе объекта, его значении, /// методах класса, всех членов класса /// /// имя объекта /// объект любого типа public void WhoIsWho(string name,object any) { Type t = any.GetType(); Console.WriteLine("Тип {0}: {1}, значение: {2}", name, any.GetType(), any.ToString ()); Console.WriteLine("Методы класса: "); MethodInfo[] ClassMethods = t.GetMethods(); foreach (Methodlnfo curMethod in ClassMethods) { Console.WriteLine(curMethod); } Console.WriteLine("Все члены класса: "); MemberInfo[] ClassMembers = t.GetMembers(); foreach (Memberlnfo curMember in ClassMembers) { Console.WriteLine(curMember.ToString ()); } }//WhoIsWho Коротко прокомментирую эту процедуру. Вначале создается переменная В процедуре Main дважды вызывается процедура ts.WhoIsWho("2+2.5", 2+2.5); ts.WhoIsWho("ts", ts); И класс Рис. 6.2. Рассмотрим выводимую информацию о классах. Для созданного в проекте класса Класс Статические поля и методы арифметических классов Все арифметические классы, в том числе класс //Min и Мах значения типов Console.WriteLine("Class int"); Console.WriteLine("Мин. значение int = " + int.MinVaiue); Console.WriteLine("Макс. значение int = " + int.MaxVaiue); Console.WriteLine("Class double"); Console.WriteLine("Мин. значение double = " + double.MinVaiue); Console.WriteLine("Макс. значение double = " + double.MaxVaiue); Все арифметические классы, в том числе класс /// /// Преобразования типа с использованием метода Parse /// public void Parsing () { //method Parse Console.WriteLine("Введите целое"); string strdata = Console.ReadLine(); int intdata = int.Parse(strdata); Console.WriteLine("Введите число с дробной частью и порядком"); strdata = Console.ReadLine(); double doubdata = double.Parse(strdata); Console.WriteLine("intdata = {0}; doubdata = {1}", intdata, doubdata); } //Parsing Как видите, метод Parse с успехом заменяет соответствующий метод класса На рис. 6.3 можно увидеть консольный вывод, полученный в результате работы процедуры Parsing. Рис. 6.3. Операция Пора вернуться к основной теме — операциям, допустимым в языке С#. Последней из еще не рассмотренных операций высшего уровня приоритета является операция new. Ключевое слово new используется в двух контекстах — как модификатор и как операция в инициализирующих выражениях объявителя. Во втором случае результатом выполнения операции new является создание нового объекта и вызов соответствующего конструктора. Примеров подобного использования операции new было приведено достаточно много, в том числе и в этой лекции. Арифметические операции В языке C# имеются обычные для всех языков арифметические операции — /// /// Арифметические операции /// public void Ariphmetica() { int n = 7,m =3, p,q; p= n/m; q= p*m + n%m; if (q==n) Console.WriteLine("q=n"); else Console.WriteLine("q!=n"); double x=7, у =3, u,v,w; u = x/y; v= u*y; w= x%y; if (v==x) Console.WriteLine("v=x"); else Console.WriteLine("v!=x"); decimal d1=7, d2 =3, d3,d4,d5; d3 = d1/d2; d4= d3*d2; d5= d1%d2; if (d4==d1) Console.WriteLine("d4=d1"); else Console.WriteLine("d4!=d1"); }//Ariphmetica При проведении вычислений в двух первых случаях проверяемое условие оказалось истинным, в третьем — ложным. Для целых типов можно исходить из того, что равенство Операции отношения Операции отношения можно просто перечислить — в объяснениях они не нуждаются. Всего операций 6 Операции проверки типов Операции проверки типов Операции сдвига Операции сдвига вправо /// /// операции сдвига /// public void Shift() { int n = 17,m =3, p,q; p= n>>2; q = m<<2; Console.WriteLine("n= " + n + m= " + m + p=n>>2 = "+p + q=m<<2 " + q); long x=-75, у =-333, u,v,w; u = x>>2; v = y<<2; w = x/4; Console.WriteLine("x= " + x + y= " + у + u=x>>2 = "+u + v=y<<2 " + v + w = x/4 = " + w); }//Shift Логические операции Начну с предупреждения тем, кто привык к языку C++. Правила работы с логическими выражениями в языках C# и C++ имеют принципиальные различия. В языке C++ практически для всех типов существует неявное преобразование в логический тип. Правило преобразования простое, — ненулевые значения трактуются как истина, нулевое — как ложь. В языке C# неявных преобразований к логическому типу нет даже для целых арифметических типов. Поэтому вполне корректная в языке C++ запись: int k1 = 7; if (k1) Console.WriteLine("ok!"); незаконна в программах на С#. На этапе трансляции возникнет ошибка, поскольку вычисляемое условие имеет тип В языке C# более строгие правила действуют и для логических операций. Так, запись if(k1 && (х>у)), корректная в языке C++, приводит к ошибке в программах на С#, поскольку операция if(k1>0) if((k1>0) && (x>y)) После этого важного предупреждения перейду к более систематическому изложению некоторых особенностей выполнения логических операций. Так же, как и в языке C++, логические операции делятся на две категории: одни выполняются над логическими значениями операндов, другие осуществляют выполнение логической операции над битами операндов. По этой причине в C# существуют две унарные операции отрицания — логическое отрицание, заданное операцией"!", и побитовое отрицание, заданное операцией "~". Первая из них определена над операндом типа /// /// Логические выражения /// public void Logic() { //операции отрицания ~,! bool b1,b2; b1= 2 *2==4; b2 =!b1; //Ь2= ~1; uint j1 = 7, j2; j2= ~j1; //j2 =!j1; int j4 = 7, j5; j5 = ~j4; Console.WriteLine("uint j2 = " + j2 +" int j5 = " + j5); }//Logic В этом фрагменте закомментированы операторы, приводящие к ошибкам. В первом случае была сделана попытка применения операции побитового отрицания к выражению типа uint j2 = 4294967288 int j5 = -8 Бинарные логические операции " //Условное And — && int[] ar= {1,2,3}; int search = 7; int i=0; while ((i < ar.Length) && (ar[i]!= search)) i++; if(i else Console.WriteLine("Образец не найден"); Если значение переменной В этом случае первый операнд получит значение Три бинарные побитовые операции — " Вот пример первого их использования: //Логические побитовые операции And, Or, XOR (&, |, ^) int k2 = 7, k3 = 5, k4, k5, k6; k4 = k2 & kЗ; k5 = k2 | k3; k6 = k2^kЗ; Console. WriteLine ("k4 = " + k4 + " k5 = " + k5 + " k6 = " + k6); Приведу результаты вывода: k4 = 5 k5 = 7 k6 =2 Приведу пример поиска по образцу с использованием логического AND: i=0; search = ar[ar.Length — 1]; while ((i < ar.Length) & (ar [i]! = search)) i + +; if(i else Console.WriteLine("Образец не найден"); В данном фрагменте гарантируется наличие образца поиска в массиве, и фрагмент будет успешно выполнен. В тех же случаях, когда массив не содержит элемента Условное выражение В С#, как и в C++, разрешены условные выражения. Конечно, без них можно обойтись, заменив их условным оператором. Вот простой пример их использования, поясняющий синтаксис их записи: //Условное выражение int а = 7, Ь= 9, max; max= (a>b)? a: b; Console.WriteLine("a = " + a +"; b= " + b + "; max(a,b) = " + max); Условное выражение начинается с условия, заключенного в круглые скобки, после которого следует знак вопроса и пара выражений, разделенных двоеточием": ". Условием является выражение типа Операция приведения к типу Осталось рассмотреть еще одну операцию — (type) <унарное выражение> Она задает явное преобразование типа, определенного выражением, к типу, указанному в скобках. Чтобы операция была успешной, необходимо, чтобы такое явное преобразование существовало. Напомню, существуют явные преобразования внутри арифметического типа, но не существует, например, явного преобразования арифметического типа в тип //cast int р; р = (int)x; //Ь = (bool)x; В данном примере явное преобразование из типа
7. Присваивание и встроенные функции Присваивание. Новинка C# — определенное присваивание. Классы Math, Random и встроенные функции. В большинстве языков программирования присваивание — это оператор, а не операция. В языке C# присваивание унаследовало многие особенности присваивания языка C++. В C# оно толкуется как операция, используемая в выражениях. Однако в большинстве случаев присваивание следует рассматривать и использовать как обычный оператор. Возьмем полезный случай реального использования присваивания как операции. В ситуации, называемой множественным присваиванием, списку переменных присваивается одно и тоже значение. Вот пример: /// /// анализ присваивания /// public void Assign() { double x,y,z,w =1, u =7, v= 5; x=y=z=w=(u+v+w)/(u-v-w); }//Assign По мере изложения в метод О семантике присваивания говорилось уже достаточно много. Но следует внести еще некоторые уточнения. Правильно построенное выражение присваивания состоит из левой и правой части. Левая часть — это список переменных, в котором знак равенства выступает в качестве разделителя. Правая часть — это выражение. Выражение правой части вычисляется, при необходимости приводится к типу переменных левой части, после чего все переменные левой части получают значение вычисленного выражения. Последние действия можно рассматривать как побочный эффект операции присваивания. Заметьте, все переменные в списке левой части должны иметь один тип или неявно приводиться к одному типу. Операция присваивания выполняется справа налево, поэтому вначале значение выражения получит самая правая переменная списка левой части, при этом значение самого выражения не меняется. Затем значение получает следующая справа по списку переменная — и так до тех пор, пока не будет достигнут конец списка. Так что реально можно говорить об одновременном присваивании, в котором все переменные списка получают одно и то же значение. В нашем примере, несмотря на то, что переменная bool b; х=5; у=6; //Ь= х=у; //if (х=у) z=1;else z=-1; В программе на языке C++ можно было снять комментарии с операторов, и этот фрагмент кода компилировался и выполнялся бы без ошибок. Другое дело, что результат мог быть некорректен, поскольку, вероятнее всего, операция присваивания x =у; Ь= (у! = 0); if(у! = 0) z = 1; else z = -1; В программе появился лишний оператор, но исчезла двусмысленность, порождаемая операцией присваивания. Специальные случаи присваивания В языке C++ для двух частных случаев присваивания предложен отдельный синтаксис. Язык C# наследовал эти полезные свойства. Для присваиваний вида x = x Для таких присваиваний используется краткая форма записи: x В качестве операции разрешается использовать арифметические, логические (побитовые) операции и операции сдвига языка С#. Семантика такого присваивания достаточно очевидна, и я ограничусь простым примером: х += u+v; у /=(u-v); b &= (х<у); Однако и здесь есть один подводный камень, когда х= х+а не эквивалентно х +=а. Рассмотрим следующий пример: byte Ь3 = 21; Ь3 +=1; //Это допустимо //Ь3 = Ь3+1; //А это недопустимо: результат типа int Закомментированный оператор приведет к ошибке компиляции, поскольку правая часть имеет тип Определенное присваивание Присваивание в языке C# называется //определенное присваивание int аn =0; //переменные должны быть инициализированы for (int i= 0;i<5;i++) {an =i + 1; } x+=an; z+=an; у = an; string[] ars = new string[3]; doublet] ard = new double[3]; for (int i= 0;i<3;i++) { //массивы могут быть без инициализации ard[i] += i+1; ars[i] += i.ToString()+1; Console.WriteLine("ard[" +i + "]=" +ard[i] + "; ars[" +i + "]=" +ars[i]); } Заметьте, в этом фрагменте переменная Еще раз о семантике присваивания Подводя итоги рассмотрения присваивания Рассмотрим объявления: int x=3, y=5; object obj1, obj 2; Здесь объявлены четыре сущности: две переменные значимого типа и две — объектного. Значимые переменные obj1 = х; obj2 = у; Эти присваивания ссылочные (из-за типа левой части), поэтому правая часть приводится к ссылочному типу. В результате неявного преобразования — операции Класс Math и его функции Кроме переменных и констант, первичным материалом для построения выражений являются функции. Большинство их в проекте будут созданы самим программистом, но не обойтись и без встроенных функций. Умение работать в среде Visual Studio.Net предполагает знание встроенных возможностей этой среды, знание возможностей каркаса Framework.Net, пространств имен, доступных при программировании на языке С#, а также соответствующих встроенных классов и функций этих классов. Продолжим знакомство с возможностями, предоставляемыми пространством имен • тригонометрические функции — • обратные тригонометрические функции — • гиперболические функции — • экспоненту и логарифмические функции — • модуль, корень, знак — • функции округления — • минимум, максимум, степень, остаток — В особых пояснениях эти функции не нуждаются. Приведу пример: /// /// работа с функциями класса Math /// public void MathFunctions() { double a, b,t,t0,dt,y; string NameFunction; Console.WriteLine("Введите имя F(t)исследуемой функции a*F(b*t)" + " (sin, cos, tan, cotan)"); NameFunction = Console.ReadLine(); Console.WriteLine("Введите параметр a (double)"); a= double.Parse(Console.ReadLine ()); Console.WriteLine("Введите параметр b (double)"); b= double.Parse(Console.ReadLine()); Console.WriteLine("Введите начальное время t0(double)"); t0= double.Parse(Console.ReadLine()); const int points = 10; dt = 0.2; for(int i = 1; i<=points; i++) { t = t0 + (i-1)* dt; switch (NameFunction) { case ("sin"): у = a*Math.Sin(b*t); break; case ("cos"): у = a*Math.Cos(b*t); break; case ("tan"): у = a*Math.Tan(b*t); break; case ("cotan"): у = a/Math.Tan(b*t); break; case ("In"): у = a*Math.Log(b*t); break; case ("tanh"): у = a*Math.Tanh(b*t); break; default: y= 1; break; }//switch Console.WriteLine ("t = " + t +"; " + a +"*" + NameFunction +"(" + b + "*t)= " + у +";"); }//for double u = 2.5, v = 1.5, p,w; p= Math.Pow(u,v); w = Math.IEEERemainder(u,v); Console.WriteLine ("u = " + u +"; v= " + v + "; power(u,v)= " + p +"; reminder(u,v)= " + w); }//MathFunctions Заметьте, в примерах программного кода я постепенно расширяю диапазон используемых средств. Часть из этих средств уже описана, а часть (например, оператор цикла Коротко прокомментирую этот код. В данном примере пользователь определяет, какую функцию он хочет вычислить и при каких значениях ее параметров. Некоторые параметры задаются константами и инициализированными переменными, но для большинства их значения вводятся пользователем. Одна из целей этого фрагмента состоит в демонстрации консольного ввода данных разного типа, при котором используется описанный ранее метод Функция, заданная пользователем, вычисляется в операторе Вызов еще двух функций из класса Math содержится в двух последних строчках этой процедуры. На рис. 7.1 можно видеть результаты ее работы. Рис. 7.1. Класс Умение генерировать случайные числа требуется во многих приложениях. Как и всякий "настоящий" класс, класс Random является наследником класса Начнем рассмотрение с конструктора класса. Он перегружен и имеет две реализации. Одна из них позволяет генерировать неповторяющиеся при каждом запуске серии случайных чисел. Начальный элемент такой серии строится на основе текущей даты и времени, что гарантирует уникальность серии. Этот конструктор вызывается без параметров. Он описан как Перегруженный метод • • • Метод Еще один полезный метод класса Random позволяет при одном обращении получать целую серию случайных чисел. Метод имеет параметр — массив, который и будет заполнен случайными числами. Метод описан как Приведу теперь пример работы со случайными числами. Как обычно, для проведения экспериментов по генерации случайных чисел я создал метод /// /// Эксперименты с классом Random I /// public void Rand() { const int initRnd = 77; Random realRnd = new Random(); Random repeatRnd = new Random(initRnd); // случайные числа в диапазоне [0,1) Console.WriteLine("случайные числа в диапазоне[0,1)"); for (int i =1; i <= 5; i + +) { Console.WriteLine("Число " + i + "= " + realRnd.NextDoubie()); } // случайные числа в диапазоне[min,max] int min = -100, max=-10; Console.WriteLine("случайные числа в диапазоне [" + min +"," + max + "]"); for (int i =1; i <= 5; i + +) { Console.WriteLine("Число " + i + "= " + realRnd.Next(min,max)); } // случайный массив байтов byte[] bar = new byte[10]; repeatRnd.NextBytes (bar); Console.WriteLine("Массив случайных чисел в диапазоне [0, 255]"); for(int i =0; i < 10; i++) { Console.WriteLine("Число " + i + "= " +bar[i]); } }//Rand Приведу краткий комментарий к тексту программы. Вначале создаются два объекта класса Random. У этих объектов разные конструкторы. Объекте именем Рис. 7.2. На этом заканчивается рассмотрение темы выражений языка С#.
8. Операторы языка С# Операторы языка С#. Оператор присваивания. Составной оператор. Пустой оператор. Операторы выбора. If-оператор. Switch-оператор. Операторы перехода. Оператор goto. Операторы break, continue. Операторы цикла. For-оператор. Циклы while. Цикл foreach. Состав операторов языка С#, их синтаксис и семантика унаследованы от языка C++. Как и положено, потомок частично дополнил состав, переопределил синтаксис и семантику отдельных операторов, постарался улучшить характеристики языка во благо программиста. Посмотрим, насколько это удалось языку С#. Оператор присваивания Как в языке C++, так и в C# присваивание формально считается операцией. Вместе с тем запись: X = expr; следует считать настоящим оператором присваивания, так же, как и одновременное присваивание со списком переменных в левой части: X1 = X2 =… = Xk = expr; В отличие от языка C++ появление присваивания в выражениях C# хотя и допустимо, но практически не встречается. Например, запись: if(х = expr)… часто используемая в C++, в языке C# в большинстве случаев будет воспринята как ошибка еще на этапе компиляции. В предыдущих лекциях семантика присваивания разбиралась достаточно подробно, поэтому сейчас я на этом останавливаться не буду. Блок или составной оператор С помощью фигурных скобок несколько операторов языка (возможно, перемежаемых объявлениями) можно объединить в единую синтаксическую конструкцию, называемую { оператор_1 … оператор_N } В языках программирования нет общепринятой нормы для использования символа точки с запятой при записи последовательности операторов. Есть три различных подхода и их вариации. Категорические противники точек с запятой считают, что каждый оператор должен записываться на отдельной строке (для длинных операторов определяются правила переноса). В этом случае точки с запятой (или другие аналогичные разделители) не нужны. Горячие поклонники точек с запятой (к ним относятся языки C++ и С#) считают, что точкой с запятой должен оканчиваться каждый оператор. В результате в операторе Синтаксически блок воспринимается как единичный оператор и может использоваться всюду в конструкциях, где синтаксис требует одного оператора. Тело цикла, ветви оператора /// /// демонстрация блоков (составных операторов) /// public void Block() { int limit = 100; int x = 120, у = 50; int sum1 =0, sum2=0; for (int i = 0; i< 11; i++) { int step = Math.Abs(limit — x)/10; if (x > limit) {x — = step; у += step;} else {x += step; у — = step;} sum1 += x; sum2 +=y; } //limit = step; //переменная step перестала существовать //limit = i; // переменная i перестала существовать Console.WriteLine("x= {0}, y= {1}, sum1 ={2}, sum2 = {3}", x, у, sum1,sum2); } Заметьте, здесь в тело основного блока вложен блок, задающий тело цикла, в котором объявлены две локальные переменные — В свою очередь, в тело цикла вложены блоки, связанные с ветвями Приведенная процедура /// /// Класс Testing — тестирующий класс. Представляет набор /// скалярных переменных и методов, тестирующих работу /// с операторами, процедурами и функциями С#. /// public class Testing { public Testing(string name, int age) { this.age = age; this.name = name; } //поля класса public string name; public int age; private int period; private string status; } Пустой оператор Пустой оператор — это "пусто", завершаемое точкой с запятой. Иногда полезно рассматривать отсутствие операторов как существующий пустой оператор. Синтаксически допустимо ставить лишние точки с запятой, полагая, что вставляются пустые операторы. Например, синтаксически допустима следующая конструкция: for (int j=1; j<5; j++) {;;;}; Она может рассматриваться как задержка по времени, работа на холостом ходе. Операторы выбора Как в C++ и других языках программирования, в языке C# для выбора одной из нескольких возможностей используются две конструкции — Оператор Начнем с синтаксиса оператора if(выражение_1) оператор_1 else if(выражение_2) оператор_2 … else if(выражение_К) оператор_К else оператор_N Какие особенности синтаксиса следует отметить? Выражения if(выражение 1) if(выражение2) if(выражение 3)… Ветви Семантика оператора Оператор Частным, но важным случаем выбора из нескольких вариантов является ситуация, при которой выбор варианта определяется значениями некоторого выражения. Соответствующий оператор С#, унаследованный от C++, но с небольшими изменениями в синтаксисе, называется оператором switch. Вот его синтаксис: switch(выражение) { case константное_выражение_1: [операторы_1 оператор_перехода_1] … case константное_выражение_К: [операторы_К оператор_перехода_К] [default: операторы_N оператор_пepexoда_N] } Ветвь Семантика оператора Семантика осложняется еще и тем, что case-ветвь может быть пустой последовательностью операторов. Тогда в случае совпадения константного выражения этой ветви со значением switch-выражения будет выполняться первая непустая последовательность очередной case-ветви. Если значение switch-выражения не совпадает ни с одним константным выражением, то выполняется последовательность операторов ветви Полагаю, что оператор s Еще одна неудача в синтаксической конструкции switch связана с существенным ограничением, накладываемым на case-выражения, которые могут быть только константным выражением. Уж если изменять оператор, то гораздо лучше было бы использовать синтаксис и семантику Visual Basic, где в case-выражениях допускается список, каждое из выражений которого может задавать диапазон значений. Разбор случаев — это часто встречающаяся ситуация в самых разных задачах. Применяя оператор Когда разбор случаев предполагает проверку попадания в некоторый диапазон значений, приходится прибегать к оператору /// /// Определяет период в зависимости от возраста — аge /// Использование ветвящегося оператора if /// public void SetPeriod() { if ((age > 0)&& (age <7))period=1; else if ((age >= 7)&& (age <17))period=2; else if ((age >= 17)&& (age <22))period=3; else if ((age >= 22)&& (age <27))period=4; else if ((age >= 27)&& (age <37))period=5; else period =6; } Этот пример демонстрирует применение ветвящегося оператора /// /// Определяет статус в зависимости от периода — period /// Использование разбора случаев — оператора Switch /// public void SetStatus() { switch (period) { case 1: status = "child"; break; case 2: status = "schoolboy"; break; case 3: status = "student"; break; case 4: status = "junior researcher"; break; case 5: status = "senior researcher"; break; case 6: status = "professor"; break; default: status = "не определен"; break; } Console.WriteLine("Имя = {0}, Возраст = {1}, Статус = {2}", name, age, status); }//SetStatus Этот пример демонстрирует корректный стиль использования оператора /// /// Разбор случаев с использованием списков выражений /// /// операция над аргументами /// первый аргумент бинарной операции /// второй аргумент бинарной операции /// результат бинарной операции public void ExprResult(string operation,int argl, int arg2, ref int result) { switch (operation) { case "+": case "Plus": case "Плюс": result = arg1 + arg2; break; case "-": case "Minus": case "Минус": result = arg1 — arg2; break; case "*": case "Mult": case "Умножить": result = arg1 * arg2; break; case "/": case "Divide": case "Div": case "разделить": case "Делить": result = arg1/arg2; break; default: result = 0; Console.WriteLine("Операция не определена"); break; } Console.WriteLine ("{0} ({1}, {2}) = {3}", operation, arg1, arg2, result); }//ExprResult Операторы перехода Операторов перехода, позволяющих прервать естественный порядок выполнения операторов блока, в языке C# имеется несколько. Оператор Оператор goto [метка] case константное_выражение|default]; Все операторы языка C# могут иметь метку — уникальный идентификатор, предшествующий оператору отделенный от него символом двоеточия. Передача управления помеченному оператору — это классическое использование оператора goto. Два других способа использования "О вреде оператора goto" и о том, как можно обойтись без него, писал еще Эдгар Дейкстра при обосновании принципов структурного программирования. Я уже многие годы не применяю этот оператор и считаю, что хороший стиль программирования не предполагает использования этого оператора в C# ни в каком из вариантов — ни в операторе Операторы В структурном программировании признаются полезными "переходы вперед" (но не назад), позволяющие при выполнении некоторого условия выйти из цикла, из оператора выбора, из блока. Для этой цели можно использовать оператор Оператор public void Jumps() { int i = 1, j=1; for(i =1; i<100; i++) { for(j = 1; j<10;j++) { if (j>=3) break; } Console.WriteLine("Выход из цикла j при j = {0}", j); if (i>=3) break; } Console.WriteLine("Выход из цикла i при i= {0}", i); }//Jumps Оператор Оператор Еще одним оператором, относящимся к группе операторов перехода, является оператор return [выражение]; Для функций его присутствие и аргумент обязательны, поскольку выражение в операторе Операторы цикла Без циклов жить нельзя в программах, нет. Оператор Наследованный от C++ весьма удобный оператор цикла for обобщает известную конструкцию цикла типа арифметической прогрессии. Его синтаксис: for(инициализаторы; условие; список_выражений) оператор Оператор, стоящий после закрывающей скобки, задает тело цикла. В большинстве случаев телом цикла является блок. Сколько раз будет выполняться тело цикла, зависит от трех управляющих элементов, заданных в скобках. Инициализаторы задают начальное значение одной или нескольких переменных, часто называемых счетчиками или просто переменными цикла. В большинстве случаев цикл Счетчики цикла зачастую объявляются непосредственно в инициализаторе и соответственно являются переменными, локализованными в цикле, так что после завершения цикла они перестают существовать. В тех случаях, когда предусматривается возможность преждевременного завершения цикла с помощью одного из операторов перехода, счетчики объявляются до цикла, что позволяет анализировать их значения при выходе из цикла. В качестве примера рассмотрим классическую задачу: является ли строка текста палиндромом. Напомню, палиндромом называется симметричная строка текста, читающаяся одинаково слева направо и справа налево. Для ее решения цикл /// /// Определение палиндромов. Демонстрация цикла for /// /// текст /// public bool Palindrom(string str) { for (int i = 0,j =str.Length-1; i if(str [i]!=str [j]) return(false); return(true); }//Palindrom Циклы while(выражение) оператор Эта модификация соответствует стратегии: "сначала проверь, а потом делай". В результате проверки может оказаться, что и делать ничего не нужно. Тело такого цикла может ни разу не выполняться. Конечно же, возможно и зацикливание. В нормальной ситуации каждое выполнение тела цикла — это очередной шаг к завершению цикла. Цикл, проверяющий условие завершения в конце, соответствует стратегии: "сначала делай, а потом проверь". Тело такого цикла выполняется, по меньшей мере, один раз. Вот синтаксис этой модификации: do оператор while(выражение); Приведу пример, в котором участвуют обе модификации цикла while. Во внешнем цикле проверка выполняется в конце, во внутреннем — в начале. Внешний цикл представляет собой типичный образец организации учебных программ, когда в диалоге с пользователем многократно решается некоторая задача. На каждом шаге пользователь вводит новые данные, решает задачу и анализирует полученные данные. В его власти, продолжить вычисления или нет, но хотя бы один раз решить задачу ему приходится. Внутренний цикл /// /// Два цикла: с проверкой в конце и в начале. /// Внешний цикл — образец многократно решаемой задачи. /// Завершение цикла определяется в диалоге /// с пользователем. /// public void Loop() { string answer, text; do { Console.WriteLine("Введите слово"); text = Console.ReadLine(); int i =0, j = text.Length-1; while ((i {i + +; j-; } if (text[i] == text[j]) Console.WriteLine (text +" — это палиндром!"); else Console.WriteLine(text +" — это не палиндром!"); Console.WriteLine("Продолжим? (yes/по)"); answer = Console.ReadLine(); } while (answer =="yes"); }//Loop Цикл Новым видом цикла, не унаследованным от C++, является цикл foreach, удобный при работе с массивами, коллекциями и другими подобными контейнерами данных. Его синтаксис: foreach (тип идентификатор in контейнер) оператор Цикл работает в полном соответствии со своим названием — тело цикла выполняется для каждого элемента в контейнере. Тип идентификатора должен быть согласован с типом элементов, хранящихся в контейнере данных. Предполагается также, что элементы контейнера (массива, коллекции) упорядочены. На каждом шаге цикла идентификатор, задающий текущий элемент контейнера, получает значение очередного элемента в соответствии с порядком, установленным на элементах контейнера. С этим текущим элементом и выполняется тело цикла — выполняется столько раз, сколько элементов находится в контейнере. Цикл заканчивается, когда полностью перебраны все элементы контейнера. Серьезным недостатком циклов foreach в языке C# является то, что цикл работает только на чтение, но не на запись элементов. Так что наполнять контейнер элементами приходится с помощью других операторов цикла. В приведенном ниже примере показана работа с трехмерным массивом. Массив создается с использованием циклов типа /// /// Демонстрация цикла foreach. Вычисление суммы, /// максимального и минимального элементов /// трехмерного массива, заполненного случайными числами. /// public void SumMinMax() { int [,,] arr3d = new int[10,10, 10]; Random rnd = new Random(); for (int i =0; i<10; i++) for (int j =0; j<10; j++) for (int k =0; k<10; k++) arr3d[i,j,k]= rnd.Next(100); long sum =0; int min=arr3d[0,0,0], max=arr3d[0,0,0]; foreach (int item in arr3d) { sum +=item; if (item > max) max = item; else if (item < min) min = item; } Console.WriteLine("sum = {0}, min = {1}, max = {2}", sum, min, max); }//SumMinMax
9. Процедуры и функции — методы класса Процедуры и функции — две формы функционального модуля. Чем отличаются эти формы? Процедуры и функции — это методы класса. Описание методов (процедур и функций). Синтаксис. Атрибуты доступа. Статические и динамические методы. Формальные аргументы. Статус аргументов. Тело методов. Вызов процедур и функций. Фактические аргументы. Семантика вызова. Поля класса или аргументы метода? Поля класса или функции без аргументов? Проектирование класса Account. Функции с побочным эффектом. Перегрузка методов. Первыми формами модульности, появившимися в языках программирования, были Процедуры и функции — методы класса Долгое время Процедуры и функции связываются теперь с классом, они обеспечивают функциональность данных класса и называются методами класса. Главную роль в программной системе играют данные, а функции лишь служат данным. Напомню здесь, что в C# процедуры и функции существуют только как методы некоторого класса, они не существуют вне класса. В данном контексте понятие класс распространяется и на все его частные случаи — структуры, интерфейсы, делегаты. В языке C# нет специальных ключевых слов — procedure и function, но присутствуют сами эти понятия. Синтаксис объявления метода позволяет однозначно определить, чем является метод — процедурой или функцией. Прежнюю роль библиотек процедур и функций теперь играют библиотеки классов. Библиотека классов FCL, доступная в языке С#, существенно расширяет возможности языка. Знание классов этой библиотеки и методов этих классов совершенно необходимо для практического программирования на C# с использованием всей его мощи. Уже в лекции 1 мы говорили о роли библиотеки FCL — статическом компоненте Framework.Net. В лекции 4 рассматривались возможности класса Процедуры и функции. Отличия • всегда вычисляет некоторое значение, возвращаемое в качестве результата функции-, • вызывается в выражениях. • возвращает формальный результат • вызов процедуры является оператором языка; • имеет входные и выходные аргументы, причем выходных аргументов — ее результатов — может быть достаточно много. Хорошо известно, что одновременное существование в языке процедур и функций в каком-то смысле избыточно. Добавив еще один выходной аргумент, любую функцию можно записать в виде процедуры. Справедливо и обратное. Если допускать функции с побочным эффектом, то любую процедуру можно записать в виде функции. В языке С — дедушке C# — так и сделали, оставив только функции. Однако значительно удобнее иметь обе формы реализации метода: и процедуры, и функции. Обычно метод предпочитают реализовать в виде функции тогда, когда он имеет один выходной аргумент, рассматриваемый как результат вычисления значения функции. Возможность вызова функций в выражениях также влияет на выбор в пользу реализации метода в виде функции. В других случаях метод реализуют в виде процедуры. Описание методов (процедур и функций). Синтаксис Синтаксически в описании метода различают две части — описание заголовка и описание тела метода: заголовок_метода тело_метода Рассмотрим синтаксис заголовка метода: [атрибуты][модификаторы]{void| тип_результата_функции} имя_метода([список_формальных_аргументов]) Имя метода и список формальных аргументов составляют Квадратные скобки (метасимволы синтаксической формулы) показывают, что атрибуты и модификаторы могут быть опущены при описании метода. Подробное их рассмотрение будет дано в лекциях, посвященных описанию классов. Сейчас же упомяну только об одном из модификаторов — модификаторе доступа. У него четыре возможных значения, из которых пока рассмотрим только два — Обязательным при описании заголовка является указание типа результата, имени метода и круглых скобок, наличие которых необходимо и в том случае, если сам список формальных аргументов отсутствует. Формально тип результата метода указывается всегда, но значение void А() {…}; int В (){…}; public void С(){…}; Методы Список формальных аргументов Как уже отмечалось, список формальных аргументов метода может быть пустым, и это довольно типичная ситуация для методов класса. Список может содержать фиксированное число аргументов, разделяемых символом запятой. Рассмотрим теперь синтаксис объявления формального аргумента: [ref|out|params]тип_аргумента имя_аргумента Обязательным является указание типа и имени аргумента. Заметьте, никаких ограничений на тип аргумента не накладывается. Он может быть любым скалярным типом, массивом, классом, структурой, интерфейсом, перечислением, функциональным типом. Несмотря на фиксированное число формальных аргументов, есть возможность при вызове метода передавать ему произвольное число фактических аргументов. Для реализации этой возможности в списке формальных аргументов необходимо задать ключевое слово При вызове метода этому формальному аргументу соответствует произвольное число фактических аргументов. Содержательно, все аргументы метода разделяются на три группы: входные, выходные и обновляемые. Аргументы первой группы передают информацию методу, их значения в теле метода только читаются. Аргументы второй группы представляют собой результаты метода, они получают значения в ходе работы метода. Аргументы третьей группы выполняют обе функции. Их значения используются в ходе вычислений и обновляются в результате работы метода. Выходные аргументы всегда должны сопровождаться ключевым словом out, обновляемые — Для иллюстрации давайте рассмотрим группу методов класса /// /// Группа перегруженных методов А() /// первый аргумент представляет сумму кубов /// произвольного числа оставшихся аргументов /// Аргументы могут быть разного типа. /// void A(out long p2, int p1) { p2 =(long) Math.Pow(p1,3); Console.WriteLine("Метод A-1"); } void A(out long p2, params int [] p) { p2 = 0; for (int i=0; i Console.WriteLine("Метод A-2"); } void A(out double p2, double p1) { p2 = Math.Pow(pi,3); Console.WriteLine("Метод A-3"); } void A(out double p2, params doublet] p) { p2=0; for(int i=0; i Console.WriteLine("Метод A-4"); } /// /// Функция с побочным эффектом /// /// Увеличивается на 1 /// int f(ref int a) { return(a++); } Четыре перегруженных метода с именем Тело метода Синтаксически тело метода является блоком, который представляет собой последовательность операторов и описаний переменных, заключенную в фигурные скобки. Если речь идет о теле функции, то в блоке должен быть хотя бы один оператор перехода, возвращающий значение функции в форме return (выражение). Оператор return описан в лекции 8. Переменные, описанные в блоке, считаются локализованными в этом блоке. В записи операторов блока участвуют имена локальных переменных блока, имена полей класса и имена аргументов метода. Область видимости, время жизни переменных, конфликты имен рассмотрены в лекции 5, семантика операторов — в лекции 8. Дополнительные сведения о семантике выполнения метода будут даны в этой лекции. Знания семантики описаний и операторов достаточно для понимания семантики блока. Необходимые уточнения будут даны чуть позже. Вызов метода. Синтаксис Как уже отмечалось, метод может вызываться в выражениях или быть вызван как оператор. В качестве оператора может использоваться любой метод — как процедура, так и функция. Конечно, функцию разумно вызывать как оператор, только если она обладает побочным эффектом. В последнем случае она вызывается ради своего побочного эффекта, а возвращаемое значение никак не используется. Подобную роль играет использование некоторых выражений с побочным эффектом в роли оператора, классическим примером является оператор Если же попытаться вызвать процедуру в выражении, то это приведет к ошибке еще на этапе компиляции. Возвращаемое процедурой значение Сам вызов метода, независимо от того, процедура это или функция, имеет один и тот же синтаксис: имя_метода([список_фактических_аргументов]) Если это оператор, то вызов завершается точкой с запятой. Формальный аргумент, задаваемый при описании метода — это всегда имя аргумента (идентификатор). Фактический аргумент — это выражение, значительно более сложная синтаксическая конструкция. Вот точный синтаксис фактического аргумента: [ref|out]выражение О соответствии списков формальных и фактических аргументов Между списком формальных и списком фактических аргументов должно выполняться определенное соответствие по числу, порядку следования, типу и статусу аргументов. Если в первом списке Если формальный аргумент объявлен с ключевым словом ref или out, то фактический аргумент должен сопровождаться таким же ключевым словом в точке вызова (соответствие по статусу). Появление ключевых слов при вызове методов — это особенность языка С#, отличающая его от большинства других языков. Такой синтаксис следует приветствовать, поскольку он направлен на повышение надежности программной системы, напоминая программисту о том, что данный фактический аргумент является выходным и значение его наверняка изменится после вызова метода. Однако из-за непривычности синтаксиса при вызове методов эти слова часто забывают писать, что приводит к появлению синтаксических ошибок. Если формальный аргумент объявлен с типом Если формальный аргумент является выходным — объявлен с ключевым словом Вызов метода. Семантика Что происходит в момент вызова метода? Выполнение начинается с вычисления фактических аргументов, которые, как мы знаем, являются выражениями. Вычисление этих выражений может приводить, в свою очередь, к вызову других методов, так что этот первый этап может быть довольно сложным и требовать больших временных затрат. В чисто функциональном программировании все вычисление по программе сводится к вызову одной функции, фактическими аргументами которой являются вызовы функций и так далее и так далее. Для простоты понимания семантики вызова можно полагать, что в точке вызова создается блок, соответствующий телу метода (в реальности все значительно эффективнее). В этом блоке происходит замена имен формальных аргументов фактическими аргументами. Для выходных аргументов, для которых фактические аргументы также являются именами, эта замена или передача аргументов осуществляется по ссылке, то есть заменяет формальный аргумент ссылкой на реально существующий объект, заданный фактическим аргументом. Чуть более сложную семантику имеет вызов по значению, применяемый к формальным аргументам, которые объявлены без ключевых слов ref и out. При вычислении выражений, заданных такими фактическими аргументами, их значения присваиваются специально создаваемым переменным, локализованным в теле исполняемого блока. Имена этих локализованных переменных и подставляются вместо имен формальных аргументов. Понятно, что тип локализованных переменных определяется типом соответствующего формального аргумента. Понятно также, что семантика замены формальных аргументов фактическими — это, по сути, семантика оператора присваивания. Семантика присваивания рассматривалась в лекциях 3, 6 и 7. Каково следствие семантики вызова по значению! Если вы забыли указать ключевое слово ref или out для аргумента, фактически являющегося выходным, то к нему будет применяться вызов по значению. Даже если в теле метода происходит изменение значения этого аргумента, то оно действует только на время выполнения тела метода. Как только метод заканчивает свою работу (завершается блок), все локальные переменные (в том числе, созданные для замены формальных аргументов) оканчивают свое существование, так что изменения не затронут фактических аргументов и они сохранят свои значения, бывшие у них до вызова. Отсюда вывод: все выходные аргументы, значения которых предполагается изменить в процессе работы, должны иметь ключевое слово Говоря о семантике вызова по ссылке и по значению, следует сделать одно важное уточнение. В объектном программировании, каковым является и программирование на С#, основную роль играют ссылочные типы — мы работаем с классами и объектами. Когда методу передается объект ссылочного типа, то все поля этого объекта могут меняться в методе самым беззастенчивым образом. И это несмотря на то, что объект формально не является выходным, не имеет ключевых слов Что нужно знать о методах? Знания формального синтаксиса и семантики недостаточно, чтобы эффективно работать с методами. Рассмотрим сейчас несколько важных вопросов, касающихся различных сторон работы с методами класса. Методы класса имеют значительно меньше аргументов, чем процедуры и функции в классическом процедурном стиле программирования, когда не используется концепция классов. За счет чего происходит уменьшение числа аргументов у методов? Ведь аргументы играют важную роль: они передают методу информацию, нужную ему для работы, и возвращают информацию — результаты работы метода — программе, вызвавшей его. Все дело в том, что методы класса — это не просто процедуры, это процедуры, обслуживающие данные. Все поля доступны любому методу по определению. Нужно четко понимать, что в момент выполнения программной системы работа идет не с классом, а с объектами — экземплярами класса. Из полей соответствующего объекта — цели вызова — извлекается информация, нужная методу в момент вызова, а работа метода чаще всего сводится к обновлению значений полей этого объекта. Поэтому очевидно, что методу не нужно через входные аргументы передавать информацию, содержащуюся в полях. Если в результате работы метода обновляется значение некоторого поля, то, опять-таки, не нужен никакой выходной аргумент. Поля хранят информацию о состоянии объектов класса. Состояние объекта динамически изменяется в ходе вычислений — обновляются значения полей. Часто возникающая дилемма при проектировании класса: что лучше — создать ли поле, хранящее информацию, или создать функцию без аргументов, вычисляющую значение этого поля всякий раз, когда это значение понадобится. Решение дилеммы — это вечный для программистов выбор между памятью и временем. Если предпочесть поле, то это приводит к дополнительным расходам памяти. Они могут быть значительными, когда создается большое число объектов — ведь свое поле должен иметь каждый объект. Если предпочесть функцию, то это потребует временных затрат на вычисление значения, и затраты могут быть значительными в сравнении с выбором текущего значения поля. Если бы синтаксис описания метода допускал отсутствие скобок у функции (метода), в случае, когда список аргументов отсутствует, то клиент класса мог бы и не знать, обращается ли он к полю или к методу. Такой синтаксис принят, например, в языке Eiffel. Преимущество этого подхода в том, что изменение реализации никак не сказывается на клиентах класса. В языке C# это не так. Когда мы хотим получить длину строки, то пишем Проиллюстрируем рассмотренные выше вопросы на примере проектирования классов /// /// Класс Account определяет банковский счет. Простейший /// вариант с возможностью трех операций: положить деньги /// на счет, снять со счета, узнать баланс. Вариант с полями /// public class Account { //закрытые поля класса int debit=0, credit=0, balance =0; int sum =0, result=0; /// /// Зачисление на счет с проверкой /// /// зачисляемая сумма public void putMoney(int sum) { this.sum = sum; if (sum >0) { credit += sum; balance = credit — debit; result =1; } else result = -1; Mes (); }//putMoney /// /// Снятие со счета с проверкой /// /// снимаемая сумма public void getMoney(int sum) { this.sum = sum; if(sum <= balance) { debit += sum; balance = credit — debit; result =2; } else result = -2; Mes (); }//getMoney /// /// Уведомление о выполнении операции /// void Mes() { switch (result) { case 1: Console.WriteLine("Операция зачисления денег прошла успешно!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}",sum, balance); break; case 2: Console.WriteLine("Операция снятия денег прошла успешно!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance); break; case -1: Console.WriteLine("Операция зачисления денег не выполнена!"); Console.WriteLine("Сумма должна быть больше нуля!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance); break; case -2: Console.WriteLine("Операция снятия денег не выполнена!"); Console.WriteLine("Сумма должна быть не больше баланса!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance); break; default: Console.WriteLine("Неизвестная операция!"); break; } } }//Account Как можно видеть, только у методов А теперь спроектируем аналогичный класс /// /// Класс Account1 определяет банковский счет. /// Вариант с аргументами и функциями /// public class Account1 { //закрытые поля класса int debit=0, credit=0; /// /// Зачисление на счет с проверкой /// /// зачисляемая сумма public void putMoney(int sum) { int res =1; if (sum >0)credit += sum; else res = -1; Mes(res,sum); }//putMoney /// /// Снятие со счета с проверкой /// /// снимаемая сумма public void getMoney(int sum) { int res=2; if(sum <= balance())debit += sum; else res = -2; balance(); Mes(res, sum); }//getMoney /// /// вычисление баланса /// /// int balance() { return(credit — debit); } /// /// Уведомление о выполнении операции /// void Mes(int result, int sum) { switch (result) { case 1: Console.WriteLine("Операция зачисления денег прошла успешно!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance()); break; case 2: Console.WriteLine("Операция снятия денег прошла успешно!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance()); break; case -1: Console.WriteLine("Операция зачисления денег не выполнена!"); Console.WriteLine("Сумма должна быть больше нуля!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance()); break; case -2: Console.WriteLine("Операция снятия денег не выполнена!"); Console.WriteLine("Сумма должна быть не больше баланса!"); Console.WriteLine("Сумма={0}, Ваш текущий баланс={1}", sum,balance()); break; default: Console.WriteLine("Неизвестная операция!"); break; } } }//Account1 Сравнивая этот класс с классом Приведу процедуру класса public void TestAccounts () { Account myAccount = new Account(); myAccount.putMoney(6000); myAccount.getMoney(2500); myAccount.putMoney(1000); myAccount.getMoney(4000); myAccount.getMoney(1000); //Аналогичная работа с классом Account1 Console.WriteLine("Новый класс и новый счет!"); Accountl myAccount1 = new Account1(); myAccount1.putMoney(6000); myAccount1.getMoney(2500); myAccount1.putMoney(1000); myAccount1.getMoney(4000); myAccount1.getMoney(1000); } На рис. 9.1 показаны результаты работы этой процедуры. Рис. 9.1. Функции с побочным эффектом Функция называется Примером такой функции является функция /// /// тестирование побочного эффекта /// public void TestSideEffect() { int a = 0, b=0, c=0; a =1; b = a + f(ref a); a =1; с = f(ref a)+ a; Console.WriteLine("a={0}, b={1}, c={2}",a,b,c); } На рис. 9.2 показаны результаты работы этого метода. Рис. 9.2. Обратите внимание на полезность указания ключевого слова ref в момент вызова. Его появление хоть как-то оправдывает не коммутативность сложения. Методы. Перегрузка Должно ли быть уникальным имя метода в классе? Нет, этого не требуется. Более того, проектирование методов с одним и тем же именем является частью стиля программирования на C++ и стиля С#. Существование в классе методов с одним и тем же именем называется перегрузкой, а сами одноименные методы называются перегруженными. Перегрузка методов полезна, когда требуется решать подобные задачи с разным набором аргументов. Типичный пример — это нахождение площади треугольника. Площадь можно вычислить потрем сторонам, по двум углам и стороне, по двум сторонам и углу между ними и при многих других наборах аргументов. Считается удобным во всех случаях иметь для метода одно имя, например Перегрузка характерна и для знаков операций. В зависимости от типов аргументов, один и тот же знак может выполнять фактически разные операции. Классическим примером является знак операции сложения +, который играет роль операции сложения не только для арифметических данных разных типов, но и выполняет конкатенацию строк. О перегрузке операций при определении класса будет подробно сказано в лекции, посвященной классам. Перегрузка требует уточнения семантики вызова метода. Когда встречается вызов неперегруженного метода, то имя метода в вызове однозначно определяет, тело какого метода должно выполняться в точке вызова. Когда же метод перегружен, то знания имени недостаточно — оно не уникально. Уникальной характеристикой перегруженных методов является их сигнатура. Перегруженные методы, имея одинаковое имя, должны отличаться либо числом аргументов, либо их типами, либо ключевыми словами (заметьте: с точки зрения сигнатуры, ключевые слова Выше уже были приведены четыре перегруженных метода с именем а, различающиеся по сигнатуре. Эти методы отличаются типами аргументов и ключевым словом Тема поиска подходящего перегруженного метода уже рассматривалась в лекции 3, где шла речь о преобразованиях арифметического типа. Стоит вернуться к примеру, который был рассмотрен в этом разделе и демонстрировал возможность возникновения конфликта: один фактический аргумент требует выбора некоей реализации, для другого — предпочтительнее реализация иная. Для устранения таких конфликтов требуется вмешательство программиста. Насколько полезна перегрузка методов? Здесь нет экономии кода, поскольку каждую реализацию нужно задавать явно; нет выигрыша по времени — напротив, требуются определенные затраты на поиск подходящей реализации, который может приводить к конфликтам, — к счастью, обнаруживаемым на этапе компиляции. В нашем примере вполне разумно иметь четыре метода с разными именами и осознанно вызывать метод, применимый к данным аргументам. Все-таки есть ситуации, где перегрузка полезна, недаром она широко используется при построении библиотеки FCL. Возьмем, например, класс Convert, у которого 16 методов с разными именами, зависящими от целевого типа преобразования. Каждый из этих 16 методов перегружен, и в свою очередь, имеет 16 реализаций в зависимости от типа источника. Согласитесь, что неразумно было бы иметь в классе В заключение этой темы посмотрим, как проводилось тестирование работы с перегруженными методами: /// /// Тестирование перегруженных методов А() /// public void TestLoadMethods() { long u=0; double v =0; A(out u, 7); A(out v, 7.5); Console.WriteLine ("u= {0}, v= {1}", u,v); A(out v,7); Console.WriteLine("v= {0}",v); A(out u, 7,11,13); A(out v, 7.5, Math.Sin(11.5)+Math.Cos(13.5), 15.5); Console.WriteLine ("u= {0}, v= {1}", u,v); }//TestLoadMethods На рис. 9.3 показаны результаты этого тестирования. Рис. 9.3.
10. Корректность методов Корректность метода. Спецификации. Триады Хоара. Предусловие метода. Постусловие метода. Корректность метода по отношению к предусловию и постусловию. Частичная корректность. Завершаемость. Полная корректность. Инвариант цикла. Вариант цикла. Подходящий инвариант. Корректность циклов. Рекурсия. Прямая и косвенная рекурсия. Стратегия "разделяй и властвуй". Сложность рекурсивных алгоритмов. Задача "Ханойские башни". Быстрая сортировка Хоара. Написать метод, задающий ту или иную функциональность, нетрудно. Это может сделать каждый. Значительно сложнее написать метод, корректно решающий поставленную задачу. Корректность метода — это не внутреннее понятие, подлежащее определению в терминах самого метода. Корректность определяется по отношению к внешним спецификациям метода. Если нет спецификаций, то говорить о корректности "некорректно". Спецификации можно задавать по-разному. Мы определим их здесь через понятия предусловий и постусловий метода, используя символику триад Хоара, введенных Чарльзом Энтони Хоаром — выдающимся программистом и ученым, одну из знаменитых программ которого приведем чуть позже в этой лекции. Пусть Условие частичной корректности записывается в виде триады Хоара, связывающей программу с ее предусловием и постусловием: [Pre(x)] P(x,z) [Post(х, z)] Условие полной корректности записывается в виде триады Хоара, связывающей программу с ее предусловием и постусловием: {Pre(x) } Р (х, z) {Post(x,z) } Доказательство полной корректности обычно состоит из двух независимых этапов — доказательства частичной корректности и доказательства завершаемости программы. Заметьте, полностью корректная программа, которая запущена на входе, не удовлетворяющем ее предусловию, вправе зацикливаться, а также возвращать любой результат. Любая программа корректна по отношению к предусловию, заданному тождественно ложным предикатом Корректная программа говорит своим клиентам: если вы хотите вызвать меня и ждете гарантии выполнения постусловия после моего завершения, то будьте добры гарантировать выполнение предусловия на входе. Задание предусловий и постусловий методов — это такая же важная часть работы программиста, как и написание самого метода. На языке C# пред- и постусловия обычно задаются в теге Формальное доказательство корректности метода — задача ничуть не проще, чем написание корректной программы. Но вот парадокс. Чем сложнее метод, его алгоритм, а следовательно, и само доказательство, тем важнее использовать понятия предусловий и постусловий, понятия инвариантов циклов в процессе разработки метода. Рассмотрение этих понятий параллельно с разработкой метода может существенно облегчить построение корректного метода. Этот подход будет продемонстрирован в нашей лекции при рассмотрении метода Инварианты и варианты цикла Циклы, как правило, являются наиболее сложной частью метода — большинство ошибок связано именно с ними. При написании корректно работающих циклов крайне важно понимать и использовать понятия инварианта и варианта цикла. Без этих понятий не обходится и формальное доказательство корректности циклов. Ограничимся рассмотрением цикла в следующей форме: Init(x,z); while(В)S(х, z); Здесь Синтаксически было бы правильно, чтобы {Inv(x, z) & B} S(x,z){Inv(x,z)} Содержательно это означает, что из истинности инварианта цикла до начала выполнения тела цикла и из истинности условия цикла, гарантирующего выполнение тела, следует истинность инварианта после выполнения тела цикла. Сколько бы раз ни выполнялось тело цикла, его инвариант остается истинным. Для любого цикла можно написать сколь угодно много инвариантов. Любое тождественное условие (2*2 =4) является инвариантом любого цикла. Поэтому среди инвариантов выделяются так называемые подходящие инварианты цикла. Они называются подходящими, поскольку позволяют доказать корректность цикла по отношению к его пред- и постусловиям. Как доказать корректность цикла? Рассмотрим соответствующую триаду: {Рге(х)} Init(x,z); while(В)S(х, z);{Post(х, z)} Доказательство разбивается на три этапа. Вначале доказываем истинность триады: (*) {Рге(х)} Init(х, z){RealInv(х, z)} Содержательно это означает, что предикат (**) {Reallnv(x, z)& В} S(х, z){ReaLInv(х, z)} На последнем шаге доказывается, что наш инвариант обеспечивает решение задачи после завершения цикла: (***) ~B RealInv(x, z) — > Post(x,z) Это означает, что из истинности инварианта и условия завершения цикла следует требуемое постусловие. С циклом связано еще одно важное понятие — варианта цикла, используемое для доказательства завершаемости цикла. {(Var(x,z)= n) & В} S (х, z) { (Var(х, z)= m) & (m < n) } Содержательно это означает, что каждое выполнение тела цикла приводит к уменьшению значения его варианта. После конечного числа шагов вариант достигает своей нижней границы, и цикл завершается. Простейшим примером варианта цикла является выражение for(i=1; i<=n; i++) S(x, z); Пользоваться инвариантами и вариантами цикла нужно не только и не столько для того, чтобы проводить формальное доказательство корректности циклов. Они способствуют написанию корректных циклов. Правило корректного программирования гласит: "При написании каждого цикла программист должен определить его подходящий инвариант и вариант". Задание предусловий, постусловий, вариантов и инвариантов циклов является такой же частью процесса разработки корректного метода, как и написание самого кода. Рекурсия Рекурсия является одним из наиболее мощных средств в арсенале программиста. Рекурсивные структуры данных и рекурсивные методы широко используются при построении программных систем. Рекурсивные методы, как правило, наиболее всего удобны при работе с рекурсивными структурами данных — списками, деревьями. Рекурсивные методы обхода деревьев служат классическим примером. Рекурсия может быть прямой, если вызов Для того чтобы рекурсия не приводила к зацикливанию, в тело нормального рекурсивного метода всегда встраивается оператор выбора, одна из ветвей которого не содержит рекурсивных вызовов. Если в теле рекурсивного метода рекурсивный вызов встречается только один раз, значит, что рекурсию можно заменить обычным циклом, что приводит к более эффективной программе, поскольку реализация рекурсии требует временных затрат и работы со стековой памятью. Приведу вначале простейший пример рекурсивного определения функции, вычисляющей факториал целого числа: public long factorial (int n) { if (n<=1) return(1); else return(n*factorial(n-1)); }//factorial Функция public long fact(int n) { long res =1; for (int i = 2; i <=n; i + +) res* = i; return(res); }//factorial Конечно, циклическое определение проще, понятнее и эффективнее, и применять рекурсию в подобных ситуациях не следует. Интересно сравнить время вычислений, дающее некоторое представление о том, насколько эффективно реализуется рекурсия. Вот соответствующий тест, решающий эту задачу: public void TestTailRec() { Hanoi han = new Hanoi (5); long time1, time2; long f=0; time1 = getTimeInMilliseconds(); for (int i = 1; i <1000000; i + +)f =han.fact(15); time2 =getTimeInMilliseconds (); Console.WriteLine(" f= {0}, " + "Время работы циклической процедуры: {1}",f,time2 — time1); time1 = getTimeInMilliseconds (); for(int i = 1; i <1000000; i + +)f =han.factorial (15); time2 =getTimeInMilliseconds (); Console.WriteLine(" f= {0}, " + "Время работы рекурсивной процедуры: 1}",f,time2 — time1); Каждая из функций вызывается в цикле, работающем 1000000 раз. До начала цикла и после его окончания вычисляется текущее время. Разность этих времен и дает оценку времени работы функций. Обе функции вычисляют факториал числа 15. Проводить сравнение эффективности работы различных вариантов — это частый прием, используемый при разработке программ. И я им буду пользоваться неоднократно. Встроенный тип long getTimeInMilliseconds () { DateTime time = DateTime.Now; return(((time.Hour*60 + time.Minute)*60 + time.Second)*1000 + time.Millisecond); } Результаты измерений времени работы рекурсивного и циклического вариантов функций слегка отличаются от запуска к запуску, но порядок остается одним и тем же. Эти результаты показаны на рис. 10.1. Рис. 10.1. Вовсе не обязательно, что рекурсивные методы будут работать медленнее нерекурсивных. Классическим примером являются методы сортировки. Известно, что время работы нерекурсивной пузырьковой сортировки имеет порядок На примере сортировки слиянием покажем, как можно оценить время работы рекурсивной процедуры. Обозначим через Т(n) = 2Т(n/2) + сn Предположим для простоты, что п задается степенью числа 2, то есть Т(2k) = 2Т(2k—1) + с2k Полагая, что T(1) =с, путем несложных преобразований, используя индукцию, можно получить окончательный результат: T(2k) = с*k*2k = c*n*log(n) Известно, что это — лучшее по порядку время решения задачи сортировки. Когда исходную задачу удается разделить на подзадачи одинаковой размерности, то, при условии существования линейного алгоритма слияния, рекурсивный алгоритм имеет аналогичный порядок сложности. К сожалению, не всегда удается исходную задачу разбить на к подзадач одинаковой размерности n/k. Часто такое разбиение не представляется возможным. Рассмотрим известную задачу о конце света — "Ханойские башни". Ее содержательная постановка такова. В одном из буддийских монастырей монахи уже тысячу лет занимаются перекладыванием колец. Они располагают тремя пирамидами, на которых надеты кольца разных размеров. В начальном состоянии 64 кольца были надеты на первую пирамиду и упорядочены по размеру. Монахи должны переложить все кольца с первой пирамиды на вторую, выполняя единственное условие — кольцо нельзя положить на кольцо меньшего размера. При перекладывании можно использовать все три пирамиды. Монахи перекладывают одно кольцо за одну секунду. Как только они закончат свою работу, наступит конец света. Беспокоиться о близком конце света не стоит. Задача эта не под силу и современным компьютерам. Число ходов в ней равно 264, а это, как известно, большое число, и компьютер, работающий в сотню миллионов раз быстрее монахов, не справится с этой задачей в ближайшие тысячелетия. Рассмотрим эту задачу в компьютерной постановке. Я спроектировал класс Hanoi, в котором роль пирамид играют три массива, а числа играют роль колец. Вот описание данных этого класса и некоторых его методов: public class Hanoi { int size,moves; int[] tower1, tower2,tower3; int top1,top2,top3; Random rnd = new Random(); public Hanoi(int size) { this.size = size; tower1 = new int [size]; tower2 = new int[size]; tower3 = new int[size]; top1 = size; top2=top3=moves =0; } public void Fill() { for (int i =0; i< size; i + +) tower1[i]= size — i; } }//Hanoi Массивы Займемся теперь непосредственно методом, реализующим нашу игру и перекладывающим кольца в соответствии с правилами игры. Заметьте, написать нерекурсивный вариант ханойских башен совсем не просто. Можно, конечно, написать цикл, завершающийся по достижению требуемой конфигурации, на каждом шаге которого выполняется очередной ход. Но даже первый ход не тривиален. Поскольку фиксирована пирамида, где должны быть собраны кольца, то неясно, куда нужно переложить первое кольцо — на вторую или третью пирамиду? Рекурсивный вариант решения задачи прозрачен, хотя и напоминает некоторый род фокуса, что характерно для рекурсивного стиля мышления. Базис рекурсии прост. Для перекладывания одного кольца задумываться о решении не нужно — оно делается в один ход. Если есть базисное решение, то оставшаяся часть также очевидна. Нужно применить рекурсивно алгоритм, переложив public void HanoiTowers() { НТ(ref tower1,ref tower2, ref tower3, ref top1, ref top2, ref top3,size); Console.WriteLine("\nBcero ходов 2^n -1 = {0}",moves); } Как обычно в таких случаях, вначале пишется нерекурсивная процедура, вызывающая рекурсивный вариант с аргументами. В качестве фактических аргументов процедуре /// /// Перенос count колец с tower1 на tower2, соблюдая /// правила и используя tower3. Свободные вершины /// башен — top1, top2, top3 /// void HT (ref int[] t1, ref int[] t2,ref int [] t3, ref int top1,ref int top2, ref int top3, int count) { if (count == 1)Move(ref t1,ref t2,ref top1,ref top2); else { HT(ref t1,ref t3,ref t2,ref top1, ref top3, ref top2,count-1); Move(ref t1,ref t2,ref top1, ref top2); HT (ref t3,ref t2,ref t1,ref top3,ref top2, ref top1,count-1); } }//HT Процедура void Move(ref int[]t1, ref int [] t2, ref int top1, ref int top2) { t2[top2] = t1[top1-1]; top1--; top2++; moves++; //PrintTowers(); }//Move Метод public void TestHanoiTowers () { Hanoi han = new Hanoi (10); Console.WriteLine("Ханойские башни"); han.Fill(); han.PrintTowers (); han.HanoiTowers(); han.PrintTowers(); } На рис. 10.2 показаны результаты работы с включенной печатью каждого хода для случая переноса трех колец. Рис. 10.2. В рекурсивном варианте исчезли все трудности, связанные с выбором хода и соблюдением правил. Выбор выполняется почти автоматически, поскольку слияние частных решений не нарушает правил. В этом еще одна мощь рекурсии. Решение исходной задачи свелось к решению двух подзадач и одному ходу. В отличие от задачи сортировки слиянием, обе подзадачи имеют не половинный размер, а размер, лишь на единицу меньший исходного. Это, казалось бы, незначительное изменение приводит к серьезным потерям эффективности вычислений. Если сложность в первом случае имела порядок T(n) = 2Т(n-1) +1 Простое доказательство по индукции дает: T(n) = 2n-1 + 2n-2 +… + 2 +1 = 2n -1 Можно показать, что последовательность ходов, реализуемая рекурсивным алгоритмом, является оптимальной, так что никакой другой алгоритм не может решить задачу за меньшее число ходов. Продолжая тему рекурсии, познакомимся с реализацией на C# еще одного известного рекурсивного алгоритма, применяемого при сортировке массивов. Описанный ранее рекурсивный алгоритм сортировки слиянием имеет один существенный недостаток — для слияния двух упорядоченных массивов за линейное время необходима дополнительная память. Разработанный Ч. Хоаром метод сортировки, получивший название быстрого метода сортировки — Хотя этот метод и не является самым быстрым во всех случаях, но на практике он обеспечивает хорошие результаты. Нужно отметить, что именно этот метод сортировки встроен в класс Идея алгоритма быстрой сортировки состоит в том, чтобы выбрать в исходном массиве некоторый элемент Несмотря на простоту идеи, алгоритм сложен в своей реализации, поскольку весь построен на циклах и операторах выбора. Я проводил построение алгоритма параллельно с обоснованием его корректности, введя инварианты соответствующих циклов. Текст обоснования встроен в текст метода. Приведу его, а затем дам некоторые объяснения. Вначале, как обычно, приведу нерекурсивную процедуру, вызывающую рекурсивный метод: /// /// Вызывает рекурсивную процедуру QSort, /// передавая ей границы сортируемого массива. /// Сортируемый массив tower1 задается /// соответствующим полем класса, public void Quicksort () { QSort(0,size-1); } Вот чистый текст рекурсивной процедуры быстрой сортировки Хоара: void QSort(int start, int finish) { if (start!= finish) { int ind = rnd.Next(start,finish); int item = tower1[ind]; int ind1 = start, ind2 = finish; int temp; while (ind1 <=ind2) { while((ind1 <=ind2)&& (tower1[ind1] < item)) ind1++; while ((ind1 <=ind2)&&(tower1[ind2] >= item)) ind2--; if (ind1 < ind2) { temp = tower1[ind1]; tower1[ind1] = tower1[ind2]; tower1[ind2] = temp; ind1++; ind2--; } } if (ind1 == start) { temp = tower1[start]; towerl[start] = item; tower1[ind] = temp; QSort(start+1,finish); } else { QSort(start,ind1-1); QSort(ind2+1, finish); } } }//QuckSort Проведите эксперимент — закройте книгу и попробуйте написать эту процедуру самостоятельно. Если вам удастся сделать это без ошибок и она пройдет у вас с первого раза, то вы — блестящий программист и вам нужно читать другие книги. Я полагаю, что в таких процедурах ошибки неизбежны и для их исправления требуется серьезная отладка. Полагаю также, что помимо обычного тестирования полезно применять обоснование корректности, основанное на предусловиях и постусловиях, инвариантах цикла. Проектируя эту процедуру, я параллельно встраивал обоснование ее корректности. Это не строгое доказательство, но, дополняя тестирование, оно достаточно, чтобы автор поверил в корректность процедуры и представил ее на суд зрителей, как это сделал я. /// /// Небольшая по размеру процедура содержит три /// вложенных цикла while, два оператора if и рекурсивные /// вызовы. Для таких процедур задание инвариантов и /// обоснование корректности облегчает отладку. /// /// нaчaльный индекс сортируемой части /// массива tower /// конечный индекс сортируемой части /// массива tower /// Предусловие: (start <= finish) /// Постусловие: массив tower отсортирован по возрастанию void QSort (int start, int finish) { if (start!= finish) //если (start = finish), то процедура ничего не делает, //но постусловие выполняется, поскольку массив из одного //элемента отсортирован по определению. Докажем истинность //постусловия для массива с числом элементов >1. { int ind = rnd.Next(start,finish); int item = tower1[ind]; int ind1 = start, ind2 = finish; int temp; /// Введем три непересекающихся множества: /// S1: {tower1(i), start <= i =< ind1-1} /// S2: {tower1(i), ind1 <= i =< ind2} /// S3: {tower1(i), ind2+1 <= i =< finish} /// Введем следующие логические условия, /// играющие роль инвариантов циклов нашей программы: /// Р1: объединение S1, S2, S3 = towerl /// Р2: (S1 (i) < item) Для всех элементов S1 /// Р3: (S3 (i) >= item) Для всех элементов S3 /// Р4: item — случайно выбранный элемент tower1 /// Нетрудно видеть, что все условия становятся /// истинными после завершения инициализатора цикла. /// Для пустых множеств S1 и S3 условия Р2 и РЗ /// считаются истинными по определению. /// Inv = P1 & Р2 & РЗ & Р4 while (ind1 <=ind2) { while((ind1 <=ind2)&& (tower1[ind1] < item)) ind1++; // (Inv == true) & ~B1 (B1 — условие цикла while) while ((ind1 <=ind2)&& (tower1[ind2] >= item)) ind2-; // (Inv == true) & ~B2 (B2 — условие цикла while) if (ind1 < ind2) // Из Inv & ~B1 & ~B2 & ВЗ следует истинность: // ((tower1[ind1] >= item)&&(tower1[ind2] // Это условие гарантирует, что последующий обмен // элементов обеспечит выполнение инварианта Inv { temp = tower1[ind1]; tower1[ind1] = tower1[ind2]; tower1[ind2] = temp; ind1++; ind2-; } //(Inv ==true) } // из условия окончания цикла следует: (S2 — пустое множество) if (ind1 == start) // В этой точке S1 и S2 — это пустые множества, — > //(S3 = tower1) // Нетрудно доказать, что отсюда следует истинность: // (item = min) // Как следствие, можно минимальный элемент сделать первым, // а к оставшемуся множеству применить рекурсивный вызов. { temp = tower1[start]; towerl[start] = item; tower1[ind] = temp; QSort(start+1,finish); } else // Здесь оба множества S1 и S3 не пусты. // К ним применим рекурсивный вызов. { QSort(start,ind1-1); QSort(ind2+1, finish); } // Индукция по размеру массива и истинность инварианта // доказывает истинность постусловия в общем случае. } }// Quicksort Приведу некоторые пояснения к этому доказательству. Задание предусловия и постусловия процедуры Почему обоснование полезно практически? Дело в том, что в данном алгоритме приходится следить за границами множеств (чтобы они не пересекались), за пустотой множеств (служащих условием окончания циклов), за выполнением условий, накладываемых на элементы множеств. Если явно не ввести эти понятия, то вероятность ошибки существенно возрастает. В заключение следует все-таки привести результат сортировки хотя бы одного массива. Рис. 10.3.
11. Массивы языка С# Общий взгляд на массивы. Сравнение с массивами C++. Почему массивы C# лучше, чем массивы C++. Виды массивов — одномерные, многомерные и изрезанные. Динамические массивы. Массив задает способ организации данных. В языке C++ все массивы являются статическими; более того, все массивы являются В языке C# снято существенное ограничение языка C++ на статичность массивов. Массивы в языке C# являются настоящими динамическими массивами. Как следствие этого, напомню, массивы относятся к ссылочным типам, память им отводится динамически в "куче". К сожалению, не снято ограничение 0-базируемости, хотя, на мой взгляд, в таком ограничении уже нет логики из-за отсутствия в C# адресной арифметики. Было бы гораздо удобнее во многих задачах иметь возможность работать с массивами, у которых нижняя граница не равна нулю. В языке C++ "классических" многомерных массивов нет. Здесь введены одномерные массивы и массивы массивов. Последние являются более общей структурой данных и позволяют задать не только многомерный куб, но и изрезанную, ступенчатую структуру. Однако использование массива массивов менее удобно, и, например, классик и автор языка C++ Бьерн Страуструп в своей книге "Основы языка C++ " пишет: "Встроенные массивы являются главным источником ошибок — особенно когда они используются для построения многомерных массивов. Для новичков они также являются главным источником смущения и непонимания. По возможности пользуйтесь шаблонами Шаблоны, определенные в стандартных библиотеках, конечно, стоит использовать, но все-таки странной является рекомендация не пользоваться структурами, встроенными непосредственно в язык. Замечу, что в других языках массивы являются одной из любимых структур данных, используемых программистами. В языке С#, соблюдая преемственность, сохранены одномерные массивы и массивы массивов. В дополнение к ним в язык добавлены многомерные массивы. Динамические многомерные массивы языка C# являются весьма мощной, надежной, понятной и удобной структурой данных, которую смело можно рекомендовать к применению не только профессионалам, но и новичкам, программирующим на С#. После этого краткого обзора давайте перейдем к более систематическому изучению деталей работы с массивами в С#. Рассмотрим, как объявляются одномерные массивы, массивы массивов и многомерные массивы. Объявление одномерных массивов Напомню общую структуру объявления: [<атрибуты>] [<модификаторы>] <тип> <объявители>; Забудем пока об атрибутах и модификаторах. Объявление одномерного массива выглядит следующим образом: <тип> [] <объявители>; Заметьте, в отличие от языка C++ квадратные скобки приписаны не к имени переменной, а к типу. Они являются неотъемлемой частью определения класса, так что запись Что же касается границ изменения индексов, то эта характеристика к классу не относится, она является характеристикой переменных — экземпляров, каждый из которых является одномерным массивом со своим числом элементов, задаваемых в объявителе переменной. Как и в случае объявления простых переменных, каждый объявитель может быть именем или именем с инициализацией. В первом случае речь идет об отложенной инициализации. Нужно понимать, что при объявлении с отложенной инициализацией сам массив не формируется, а создается только ссылка на массив, имеющая неопределенное значение int[] а, Ь, с; Чаще всего при объявлении массива используется имя с инициализацией. И опять-таки, как и в случае простых переменных, могут быть два варианта инициализации. В первом случае инициализация является явной и задается константным массивом. Вот пример: double[] х= {5.5, 6.6, 7.7}; Следуя синтаксису, элементы константного массива следует заключать в фигурные скобки. Во втором случае создание и инициализация массива выполняется в объектном стиле с вызовом конструктора массива. И это наиболее распространенная практика объявления массивов. Приведу пример: int[] d= new int [5]; Итак, если массив объявляется без инициализации, то создается только висячая ссылка со значением Как обычно задаются элементы массива, если они не заданы при инициализации? Они либо вычисляются, либо вводятся пользователем. Давайте рассмотрим первый пример работы с массивами из проекта с именем public void TestDeclaration () { // объявляются три одномерных массива А,В,С int[] А = new int [5], В= new int [5], С= new int [5]; Arrs.CreateOneDimAr(A); Arrs.CreateOneDimAr(B); for(int i = 0; i<5; i++) С[i] = A[i] + В[i]; // объявление массива с явной инициализацией int[] х ={5,5,6,6,7,7}; //объявление массивов с отложенной инициализацией int[] u,v; u = new int [3]; for(int i=0; i<3; i++) u[i] =i+1; // v= {1,2,3}; // присваивание константного массива // недопустимо v = new int [4]; v=u; // допустимое присваивание int [,] w = new int[3,5]; // v=w; // недопустимое присваивание: объекты разных классов Arrs.PrintAr1("А", A); Arrs.PrintAr1("В", В); Arrs.PrintAr1("С", С); Arrs.PrintAr1("X", x); Arrs.PrintAr1("U", u); Arrs.PrintAr1("V", v); } На что следует обратить внимание, анализируя этот текст: • В процедуре показаны разные способы объявления массивов. Вначале объявляются одномерные массивы A, • Массив • Массивы • Обратите внимание на закомментированный оператор присваивания. В отличие от инициализации, использовать константный массив в правой части оператора присваивания недопустимо. Эта попытка приводит к ошибке, поскольку • Далее определяется двумерный массив w и делается попытка выполнить оператор присваивания • Для поддержки работы с массивами создан специальный класс public static void CreateOneDimAr (int [] A) { for(int i = 0; i A[i] = rnd.Next(1,100); }//CreateOneDimAr Здесь private static Random rnd = new Random(); Процедура печати массива с именем name выглядит так: public static void PrintArl(string name,int[] A) { Console.WriteLine(name); for (int i = 0; i Console.Write("\t" + name + "[{0}]={1}", i, A[i]); Console.WriteLine(); }//PrintArl На рис. 11.1 показан консольный вывод результатов работы процедуры Рис. 11.1. Особое внимание обратите на вывод, связанный с массивами Динамические массивы Во всех вышеприведенных примерах объявлялись статические массивы, поскольку нижняя граница равна нулю по определению, а верхняя всегда задавалась в этих примерах константой. Напомню, что в C# все массивы, независимо оттого, каким выражением описывается граница, рассматриваются как динамические, и память для них распределяется в "куче". Полагаю, что это отражение разумной точки зрения: ведь статические массивы, скорее исключение, а правилом является использование динамических массивов. В действительности реальные потребности в размере массива, скорее всего, выясняются в процессе работы в диалоге с пользователем. Чисто синтаксически нет существенной разницы в объявлении статических и динамических массивов. Выражение, задающее границу изменения индексов, в динамическом случае содержит переменные. Единственное требование — значения переменных должны быть определены в момент объявления. Это ограничение в C# выполняется автоматически, поскольку хорошо известно, сколь требовательно C# контролирует инициализацию переменных. Приведу пример, в котором описана работа с динамическим массивом-. public void TestDynAr() { //объявление динамического массива А1 Console.WriteLine("Введите число элементов массива А1"); int size = int.Parse(Console.ReadLine()); int[] A1 = new int[size]; Arrs.CreateOneDimAr(A1); Arrs.PrintAr1("A1",A1); }//TestDynAr В особых комментариях эта процедура не нуждается. Здесь верхняя граница массива определяется пользователем. Многомерные массивы Уже объяснялось, что разделение массивов на одномерные и многомерные носит исторический характер. Никакой принципиальной разницы между ними нет. Одномерные массивы — это частный случай многомерных. Можно говорить и по-другому: многомерные массивы являются естественным обобщением одномерных. Одномерные массивы позволяют задавать такие математические структуры как векторы, двумерные — матрицы, трехмерные — кубы данных, массивы большей размерности — многомерные кубы данных. Замечу, что при работе с базами данных многомерные кубы, так называемые кубы OLAP, встречаются сплошь и рядом. В чем особенность объявления многомерного массива? Как в типе указать размерность массива? Это делается достаточно просто, за счет использования запятых. Вот как выглядит объявление многомерного массива в общем случае: <тип>[, …,] <объявители>; Число запятых, увеличенное на единицу, и задает размерность массива. Что касается объявителей, то все, что сказано для одномерных массивов, справедливо и для многомерных. Можно лишь отметить, что хотя явная инициализация с использованием многомерных константных массивов возможна, но применяется редко из-за громоздкости такой структуры. Проще инициализацию реализовать программно, но иногда она все же применяется. Вот пример: public void TestMultiArr() { int[,]matrix = {{1,2}, {3,4} }; Arrs.PrintAr2("matrix", matrix); }//TestMultiArr Давайте рассмотрим классическую задачу умножения прямоугольных матриц. Нам понадобится три динамических массива для представления матриц и три процедуры, одна из которых будет заполнять исходные матрицы случайными числами, другая — выполнять умножение матриц, третья — печатать сами матрицы. Вот тестовый пример: public void TestMultiMatr() { int n1, m1, n2, m2,n3, m3; Arrs.GetSizes("MatrA",out n1,out m1); Arrs.GetSizes("MatrB",out n2,out m2); Arrs.GetSizes("MatrC",out n3,out m3); int[,]MatrA = new int[n1,m1], MatrB = new int[n2,m2]; int[,]MatrC = new int[n3,m3]; Arrs.CreateTwoDimAr(MatrA);Arrs.CreateTwoDimAr(MatrB); Arrs.MultMatr(MatrA, MatrB, MatrC); Arrs.PrintAr2("MatrA",MatrA); Arrs.PrintAr2("MatrB",MatrB); Arrs.PrintAr2("MatrC", MatrC); }//TestMultiMatr Три матрицы — Метод public void MuitMatr(int[,]A, int[,]B, int[,]C) { if (A.GetLength(1)!= В.GetLength(0)) Console.WriteLine("MuitMatr: ошибка размерности!"); else for (int i = 0; i < A.GetLength(0); i + +) for (int j = 0; j < В.GetLength(1); j++) { int s=0; for (int k = 0; k < A.GetLength(1); k++) s+= A[i,k]*B[k,j]; С [i, j] = s; } }//MuitMatr В особых комментариях эта процедура не нуждается. Замечу лишь, что прежде чем проводить вычисления, производится проверка корректности размерностей исходных матриц при их перемножении, — число столбцов первой матрицы должно быть равно числу строк второй матрицы. Обратите внимание, как выглядят результаты консольного вывода на данном этапе работы (рис. 11.2). Рис. 11.2. Массивы массивов Еще одним видом массивов C# являются В каких ситуациях может возникать необходимость в таких структурах данных? Эти массивы могут применяться для представления деревьев, у которых узлы могут иметь произвольное число потомков. Таковым может быть, например, генеалогическое дерево. Вершины первого уровня — Есть некоторые особенности в объявлении и инициализации таких массивов. Если при объявлении типа многомерных массивов для указания размерности использовались запятые, то для изрезанных массивов применяется более ясная символика — совокупности пар квадратных скобок; например, Сложнее с созданием самих массивов и их инициализацией. Здесь нельзя вызвать конструктор //массив массивов — формальный пример //объявление и инициализация int[] [] jagger = new int[3] [] { new int [] {5,7,9,11}, new int [] {2,8}, new int [] {6,12,4} }; Массив int[] [] j agger1 = new int[3] [] { new int [4], new int [2], new int [3] }; В этом случае элементы массива получат при инициализации нулевые значения. Реальную инициализацию нужно будет выполнять программным путем. Стоит заметить, что в конструкторе верхнего уровня константу 3 можно опустить и писать просто int [] [] j agger2 = { new int [4], new int [2], new int [3] }; А вот конструкторы нижнего уровня необходимы. Еще одно важное замечание — динамические массивы возможны и здесь. В общем случае, границы на любом уровне могут быть выражениями, зависящими от переменных. Более того, допустимо, чтобы массивы на нижнем уровне были многомерными. Но это уже "от лукавого" — вряд ли стоит пользоваться такими сложными структурами данных, ведь с ними предстоит еще и работать. Приведу теперь чуть более реальный пример, описывающий простое генеалогическое дерево, которое условно назову "отцы и дети": //массив массивов — "Отцы и дети" int Fcount =3; string[] Fathers = new string[Fcount]; Fathers[0] ="Николай"; Fathers[1] = "Сергей"; Fathers[2] = "Петр"; string[][] Children = new string[Fcount][]; Children[0] = new string[] {"Ольга", "Федор"}; Children[1] = new string[] {"Сергей","Валентина","Ира","Дмитрий"}; Children[2] = new string[]{"Мария","Ирина","Надежда"}; myar.PrintAr3(Fathers,Children); Здесь отцов описывает обычный динамический одномерный массив Я не буду демонстрировать работу с генеалогическим деревом, ограничусь лишь печатью этого массива. Здесь есть несколько поучительных моментов. В классе public void PrintAr3(string [] Fathers, string[][] Children) { for (int i = 0; i < Fathers.Length; i + +) { Console.WriteLine("Отец: {0}; Его дети: ", Fathers[i]); for (int j = 0; j < Children[i].Length; j++) Console.Write(Children[i][j] + " "); Console.WriteLine (); } }//PrintAr3 Приведу некоторые комментарии к этой процедуре: • Внешний цикл по • В этом цикле с тем же успехом можно было бы использовать и имя массива • Во внутреннем цикле свойство • Остальные детали, надеюсь, понятны. Приведу вывод, полученный в результате работы процедуры Рис. 11.3. Процедуры и массивы В наших примерах массивы неоднократно передавались процедурам в качестве входных аргументов и возвращались в качестве результатов. В лекции 9 подробно описывались особенности передачи аргументов в процедуру. Остается подчеркнуть только некоторые детали: • В процедуру достаточно передавать только сам объект — массив. Все его характеристики {размерность, границы) можно определить, используя свойства и методы этого объекта. • Когда массив является выходным аргументом процедуры, как аргумент • Может ли процедура-функция возвращать массив в качестве результата? В C# ответ на этот вопрос положителен. В следующей лекции будет приведен пример подобной функции.
12. Класс Array и новые возможности массивов Семейство классов-массивов. Родительский класс Array и наследуемые им интерфейсы. Новые возможности массивов в С#. Как корректно работать с массивами объектов? Класс Нельзя понять многие детали работы с массивами в С#, если не знать устройство класса //Класс Array int[] ar1 = new int [5]; doublet] ar2 ={5.5, 6.6, 7.7}; int [,] ar3 = new Int32[3,4]; Зададимся естественным вопросом: к какому или к каким классам принадлежат объекты У всех классов, являющихся массивами, много общего, поскольку все они являются потомками класса Рис. 12.1. Благодаря такому мощному родителю, над массивами определены самые разнообразные операции — копирование, поиск, обращение, сортировка, получение различных характеристик. Массивы можно рассматривать как коллекции и устраивать циклы Рассмотрим пример подобной процедуры. Ранее я для печати элементов массива использовал различные процедуры public static void PrintAr(string name, Array A) { Console.WriteLine(name); switch (A.Rank) { case 1: for (int i = 0; i Console.Write("\t" + name + "[{0}]={1}", i, A.GetValue (i)); Console.WriteLine (); break; case 2: for (int i = 0; i { for (int j = 0; j Console.Write("\t" + name + "[{0},{1}]={2 } ", i,j, A.GetValue(i,j)); Console.WriteLine (); } break; default: break; } }//PrintAr Вот как выглядит создание массивов и вызов процедуры печати: public void TestCommonPrint() { // Класс Array int[] ar1 = new int [5]; double[] ar2 ={5.5, 6.6, 7.7}; int [,] ar3 = new Int32[3,4]; Arrs.CreateOneDimAr(ar1);Arrs.PrintAr("ar1", ar1); Arrs.PrintAr("ar2", ar2); Arrs.CreateTwoDimAr(ar3);Arrs.PrintAr("ar3", ar3); }//TestCommonPrint Вот результаты вывода массивов Рис. 12.2. Приведу некоторые комментарии. Первое, на что следует обратить внимание: формальный аргумент процедуры принадлежит базовому классу Для того чтобы сохранить возможность работы с индексами, как в одномерном, так и в двумерном случае, пришлось организовать разбор случаев. Свойство Rank, возвращающее размерность массива, используется в этом разборе. К элементам массива а, имеющего класс Естественно, разбор случаев можно продолжить, придав процедуре большую функциональность. Заметьте, если разбор случаев вообще не делать, а использовать Массивы как коллекции В ряде задач массивы C# целесообразно рассматривать как коллекции, не используя систему индексов для поиска элементов. Это, например, задачи, требующие однократного или многократного прохода по всему массиву — нахождение суммы элементов, нахождение максимального элемента, печать элементов. В таких задачах вместо циклов типа public static void PrintCollection(string name,Array A) { Console.WriteLine(name); foreach (object item in A) Console.Write("\t {0}", item); Console.WriteLine(); }//PrintCollection Конечно, за все нужно платить. Платой за универсальность процедуры печати является то, что многомерный массив печатается как одномерный без разделения элементов на строки. К сожалению, ситуация с чтением и записью элементов массива не симметрична. Приведу вариант процедуры public static void CreateCollection(Array A) { int i=0; foreach (object item in A) //item = rnd.Next(1,10); //item read only A.SetValue(rnd.Next(1,10), i++); }//CreateCollection Заметьте, эту процедуру сделать универсальной не удается, поскольку невозможно модифицировать элементы коллекции. Поэтому цикл Сортировка и поиск. Статические методы класса Статические методы класса Array позволяют решать самые разнообразные задачи: 1. 2. 3. 4. 5. Все методы перегружены и имеют ряд модификаций. Большинство из этих методов применимо только к одномерным массивам. Приведу примеры различных операций, доступных при работе с массивами, благодаря наследованию от класса Array: public void TestCollection () { //операции над массивами int nc = 7; int[] col1 = new int[nc], col2 = new int[nc]; doublet] col3 = new double[nc]; int[,] col4 = new int [2,2]; Arrs.CreateCollection(col1); Arrs.PrintCollection("col1", col1); Arrs.CreateCollection(col2); Arrs.PrintCollection("соl2",соl2); Arrs.CreateCollection (col3); Arrs.PrintCollection("соl3",соl3); Arrs.CreateTwoDimAr (col4); Arrs.PrintCollection("соl4",соl4); // сортировка, поиск, копирование // поиск элемента int first = Array.IndexOf(coll, 2); int last = Array.LastlndexOf(col1,2); if (first == -1) Console.WriteLine("Нет вхождений 2 в массив col1"); else if (first ==last) Console.WriteLine("Одно вхождение 2 в массив col1"); else Console.WriteLine("Несколько вхождений 2 в массив col1"); //first = Array.IndexOf(col4, 4); // только одномерный массив Array.Reverse(col1); Console.WriteLine("Обращение массива col1: "); Arrs.PrintCollection("col1",col1); // Копирование Array.Copy(col1, col3, col1.Length); Console.WriteLine(" Массив col3 после копирования массива col1: "); Arrs. PrintCollection ("col3", col3); Array.Copy(col1,1,соl2,1,2); Console.WriteLine("копирование двух элементов col1 в col2:"); Arrs.PrintCollection("col1", col1); Arrs.PrintCollection("col2",col2); // быстрая сортировка Хоара Array.Sort(col1); Console.WriteLine("Отсортированный массив col1: "); Arrs.PrintCollection("col1",col1); first = Array.BinarySearch(col1, 2); Console.WriteLine("Индекс вхождения 2 в col1: {0}",first); //Создание экземпляра (массива) Array my2Dar = Array.Createlnstance(typeof(double), 2,3); Arrs.PrintCollection("my2Dar",my2Dar); //клонирование my2Dar = (Array)col4.Clone(); Console.WriteLine("Массив my2Dar после клонирования col4: "); Arrs.PrintCollection("my2Dar",my2Dar); //копирование CopyTo col1.CopyTo (соl2, 0); Console.WriteLine("Массив col2 после копирования col1: "); Arrs.PrintCollection("col2",col2); } В этой процедуре продемонстрированы вызовы различных статических методов класса Array. Для метода Сору показан вызов двух реализаций этого метода, когда копируется весь массив и часть массива. Закомментированный оператор вызова метода IndexOf напоминает о невозможности использования методов поиска при работе с многомерными массивами. Приведу результаты вывода, порожденные этим кодом. Рис. 12.3. Свойство ∙ Родитель ∙ Описание IsFixedSize ∙ Интерфейс IsReadOnly ∙ Интерфейс IsSynchronized ∙ Интерфейс SyncRoot ∙ Интерфейс Собственный метод синхронизации доступа к массиву. При работе с массивом его можно закрыть на время обработки, что запрещает его модификацию каким-либо потоком: Array myCol = new int []; lock(myCol.SyncRoot) { foreach (Object item in myCol) { // безопасная обработка массива } Length ∙ Число элементов массива ∙ Rank ∙ Размерность массива Метод ∙ Описание Array my2Dar = Array.CreateInstance(typeof(double), 2,2) Сводка свойств и методов класса Многие возможности, которыми можно пользоваться при работе с массивами, уже обсуждены. В завершение этой темы в таблицах 12.1-12.3 приведем сводку всех свойств и методов класса Метод ∙ Родитель ∙ Описание ∙ col1.CopyTo(со12,0); Класс Давайте обсудим допустимость преобразований между классами-массивами и классом В этой лекции и ранее обсуждался вопрос о создании универсальных процедур, которые могли бы работать с данными разных типов. Серьезный разговор об универсализации классов еще предстоит, сейчас же лишь напомню, что уже рассматривался такой прием, как перегрузка метода. У клиента, использующего перегруженный метод, создается впечатление, что он вызывает универсальный метод, работающий с аргументами разного типа. Создатель перегруженного метода должен, конечно, написать множество реализаций для поддержки такой универсальности. Другой уже обсуждавшийся прием состоит в том, что формальный аргумент метода принадлежит родительскому классу, тогда методу при вызове может быть передан аргумент любого из потомков. Приведу в качестве примера многострадальную процедуру печати объектов, многократные варианты которой уже были рассмотрены. На этот раз формальный аргумент процедуры будет иметь тип public static void PrintObj (object A) { Console.WriteLine("A.GetType()={0})", A.GetType()); if (A.GetType()==typeof(System.Int32[])) { int[] temp; temp = (int[])A; for(int i = 0; i Console.Write("\t temp[{0}]={1}", i,temp[i]); Console.WriteLine (); } else Console.WriteLine("Аргумент не является массивом целых"); }//PrintObject Несколько замечаний к реализации. Метод На каждой ветви разбора можно создать временный объект нужного типа и скопировать в него переданный аргумент. В данном примере рассматривается только одна ветвь, в которой создается целочисленный одномерный массив Заметьте, при присваивании значения переменной temp выполняется явное преобразование из класса При наличии переменной temp, выполнение нужных действий над массивом не представляет никаких трудностей. Приведу два примера вызова этой процедуры: //работа с процедурой PrintObject //Корректный и некорректный вызовы Arrs.PrintObj (col1); Arrs.PrintObj (соl3); Вот какой вывод порождается этим фрагментом кода: Рис. 12.4. Массивы объектов Во всех рассмотренных примерах этой главы нам встречались массивы, элементы которых имели только простые значимые типы. В реальных программах массивы объектов и других ссылочных типов встречаются не менее часто. Каков бы ни был тип элементов, большой разницы при работе с массивами нет. Но один важный нюанс все же есть, и его стоит отметить. Он связан с инициализацией элементов по умолчанию. Уже говорилось о том, что компилятор не следит за инициализацией элементов массива и доверяет инициализации, выполненной конструктором массива по умолчанию. Но для массивов ссылочного типа инициализация по умолчанию присваивает ссылкам значение Рассмотрим детали этой проблемы на примере. Определим достаточно простой и интуитивно понятный класс, названный /// /// Класс победителей с именем и премией /// public class Winners { //поля класса string name; int price; //статическое или динамическое поле rnd? //static Random rnd = new Random(); Random rnd = new Random(); // динамические методы public void SetVals(string name) { this.name = name; this.price = rnd.Next(5,10)* 1000; }//SetVals public void PrintWinner(Winners win) { Console.WriteLine("Имя победителя: {0}," + " его премия — {1}", win.name, win.price); }//PrintWinner }//class Winners Коротко прокомментирую этот текст. 1. Свойство 2. Свойство 3. Метод 4. Метод 5. В классе появится еще один статический метод I Пусть теперь в одном из методов нашего тестирующего класса public void TestWinners() { //массивы объектов int nwin = 3; Winners [] wins = new Winners[nwin]; string[] winames = {"Т. Xoap", "H. Вирт", "Э. Дейкстра"}; В результате создан массив //создание значений элементов массива for (int i=0; i < wins.Length; i + +) wins[i].SetVals(winames[i]); На этапе выполнения будет сгенерировано исключение — нулевая ссылка. Причина понятна: хотя массив Как же создавать эти объекты? Конечно, можно возложить эту обязанность на пользователя, объявившего массив //статический метод public static Winners[] InitAr(Winners[] Winar) { for (int i=0; i < Winar.Length; i + +) Winar[i] = new Winners(); return(Winar); }//InitAr Методу передается массив объектов, возможно, с нулевыми ссылками. Он возвращает тот же массив, но уже с явно определенными ссылками на реально созданные объекты. Теперь достаточно вызвать этот метод, после чего можно спокойно вызывать и метод Winners.InitAr(wins); //создание значений элементов массива for(int i=0; i < wins.Length; i++) wins[i].SetVals(winames[i]); //печать значений элементов массива for (int i=0; i < wins.Length; i + +) wins[i].PrintWinner(wins[i]); }//TestWinners Теперь все корректно, массивы создаются, элементы заполняются нужными значениями, их можно распечатать: Рис. 12.5. Обратите внимание, что всем победителям назначена одна и та же премия. Хотя понятно, что дело в программной ошибке, но в ней можно видеть и знак свыше. Коль скоро для победителей выбраны такие имена, почитаемые всеми программистами, то негоже пытаться расставить их по ранжиру даже в примере. Что же касается ошибки, то она связана с тем, что в данном случае свойство Массивы. Семантика присваивания Преобразования между классами массивов и родительскими классами Сформулируем теперь точные правила, справедливые для присваивания и передачи аргументов в процедуру. Для того, чтобы было возможным неявное преобразование массива с элементами класса • классы • размерности массивов должны совпадать; • должно существовать неявное преобразование элементов класса Заметьте, если Правило для явного преобразования можно сформулировать, например, так. Если существует неявное преобразование массива с элементами класса Для демонстрации преобразований между массивами написана еще одна процедура печати. Вот ее текст: public static void PrintArObj (string name,object[] A) { Console.WriteLine(name); foreach (object item in A) Console.Write("\t {0}", item); Console.WriteLine(); }//PrintArObj Как видите, формальный аргумент этой процедуры принадлежит классу public void TestMas() { string[] winames = ("Т. Xoap", "H. Вирт", "Э. Дейкстра"}; Arrs.PrintArObj("winames", winames); object[] cur = new object[5]; cur = winames; Arrs.PrintArObj("cur", cur); winames = (string[])cur; Arrs.PrintArObj("winames", winames); }//TestMas Взгляните на результаты работы этой процедуры. Рис. 12.6. Приступая к описаниям массивов, я полагал, что 10 страниц одной лекции будет вполне достаточно. Оказалось, что массивы C# более интересны. Надеюсь, с этим согласятся и читатели. … В исходнике 13 лекция пропущена…
14. Строки С#. Классы Строки С#. Класс String. Изменяемые и неизменяемые строковые классы. Классы Net Framework, расширяющие строковый тип. Класс StringBuilder. В предыдущей лекции мы говорили о символьном типе char и строках постоянной длины, задаваемых массивом символов. Основным типом при работе со строками является тип Объявление строк. Конструкторы класса Объекты класса • символа, повторенного заданное число раз; • массива символов • части массива символов. Некоторым конструкторам в качестве параметра инициализации можно передать строку, заданную типом char*. Но все это небезопасно, и подобные примеры приводиться и обсуждаться не будут. Приведу примеры объявления строк с вызовом разных конструкторов: public void TestDeclStrings() { //конструкторы string world = "Мир"; string s1 = new string ("s1"); //string s2 = new string (); string sssss = new string('s',5); char[] yes = "Yes".ToCharArray(); string stryes = new string(yes); string strye = new string(yes,0,2); Console.WriteLine("world = {0}; sssss={1}; stryes={2};"+ " strye= {3}", world, sssss, stryes, strye); } Объект Заметьте, не допускается явный вызов конструктора по умолчанию — конструктора без параметров. Нет также конструктора, которому в качестве аргумента можно передать обычную строковую константу. Соответствующие операторы в тексте закомментированы. Операции над строками Над строками определены следующие операции: • присваивание (=); • две операции проверки эквивалентности (==) и (!=); • конкатенация или сцепление строк (+); • взятие индекса ([]). Начну с присваивания, имеющего важную особенность. Поскольку В отличие от других ссылочных типов операции, проверяющие эквивалентность, сравнивают значения строк, а не ссылки. Эти операции выполняются как над значимыми типами. Бинарная операция "+" сцепляет две строки, приписывая вторую строку к хвосту первой. Возможность взятия индекса при работе со строками отражает тот приятный факт, что строку можно рассматривать как массив и получать без труда каждый ее символ. Каждый символ строки имеет тип Вот пример, в котором над строками выполняются данные операции: public void TestOpers() { //операции над строками string s1 ="АВС", s2 ="CDE"; string s3 = s1+s2; bool b1= (s1==s2); char ch1 = s1 [0], ch2 = s2[0]; Console.WriteLine("s1={0}, s2={1}, b1={2}," + "ch1={3}, ch2={4}", s1,s2,b1,chi,ch2); s2 = s1; b1 = (s1!=s2); ch2 = s2 [0]; Console.WriteLine("s1={0}, s2={1}, b1={2}," + "ch1={3 }, ch2={4 } ", s1,s2,b1,ch1,ch2); //Неизменяемые значения s1= "Zenon"; //s1[0]='L'; } Строковые константы Без констант не обойтись. В C# существуют два вида строковых констант: • обычные константы, которые представляют строку символов, заключенную в кавычки; • @-константы, заданные обычной константой с предшествующим знаком В обычных константах некоторые символы интерпретируются особым образом. Связано это прежде всего с тем, что необходимо уметь задавать в строке непечатаемые символы, такие, как, например, символ табуляции. Возникает необходимость задавать символы их кодом — в виде escape-последовательностей. Для всех этих целей используется комбинация символов, начинающаяся символом В @-константах все символы трактуются в полном соответствии с их изображением. Поэтому путь к файлу лучше задавать @-константой. Единственная проблема в таких случаях: как задать символ кавычки, чтобы он не воспринимался как конец самой константы. Решением является удвоение символа. Вот соответствующие примеры: //Два вида констант s1= "\х50"; s2 = @"\х50"""; b1 (s1==s2); Console.WriteLine("s1={0}, s2={1}, b1={2}", s1,s2,b1); s1 = "c: \\c#book\\ch5\\chapter5.doc"; s2 = @"c: \c#book\ch5\chapter5.doc"; b1= (s1==s2); Console.WriteLine("s1={0}, s2={1}, b1={2}", s1,s2,b1); s1= "\" A\""; s2=@"""A"""; bl= (s1==s2); Console.WriteLine("s1={0}, s2={1}, b1={2}", s1,s2,b1); Взгляните на результаты работы приведенных фрагментов кода, полученные при вызове процедур Рис. 14.1. Неизменяемый класс В языке C# существует понятие К таким неизменяемым классам относится и класс //Неизменяемые значения s1= "Zenon"; ch1 = s1 [0]; //s1[0]='L'; Статические свойства и методы класса Метод ∙ Описание Метод Метод Format в наших примерах встречался многократно. Всякий раз, когда выполнялся вывод результатов на консоль, неявно вызывался и метод Console.WriteLine("s1={0}, s2={1}", s1,s2); Здесь строка, задающая первый аргумент метода, помимо обычных символов, содержит форматы, заключенные в фигурные скобки. В данном примере используется простейший вид формата — он определяет объект, который должен быть подставлен в участок строки, занятый данным форматом. Помимо неявных вызовов, нередко возникает необходимость явного форматирования строки. Давайте рассмотрим общий синтаксис как самого метода public static string Format(string, object); public static string Format(IFormatProvider, string, params object[]); Параметр типа string задает форматируемую строку. Заданная строка содержит один или несколько форматов, они распознаются за счет окружающих формат фигурных скобок. Число форматов, вставленных в строку, определяет и число объектов, передаваемых при вызове метода Format. Каждый формат определяет форматирование объекта, на который он ссылается и который, после преобразования его в строку, будет подставлен в результирующую строку вместо формата. Метод Format в качестве результата возвращает переданную ему строку, где все спецификации формата заменены строками, полученными в результате форматирования объектов. Общий синтаксис, специфицирующий формат, таков: {N [,М [:<коды_форматирования>]]} Обязательный параметр N задает индекс объекта, заменяющего формат. Можно считать, что методу всегда передается массив объектов, даже если фактически передан один объект. Индексация объектов начинается с нуля, как это принято в массивах. Второй параметр Все становится ясным, когда появляются соответствующие примеры. Вот они: public void TestFormat() { //метод Format int x=77; string s= string.Format("x={0}",x); Console.WriteLine(s + "\tx={0}",x); s= string.Format("Итого:{0,10} рублей",x); Console.WriteLine(s); s= string.Format("Итого:{0,6:######} рублей",x); Console.WriteLine(s); s= string.Format("Итого:{0:P} ",0.77); Console.WriteLine(s); s= string.Format("Итого:{0,4:С} ",77.77); Console.WriteLine(s); //Национальные особенности System.Globalization.Culturelnfo ci = new System.Globalization.Culturelnfo("en-US"); s= string.Format(ci,"Итого:{0,4:С} ",77.77); Console.WriteLine(s); }//TestFormat Приведу некоторые комментарии к этой процедуре. Вначале демонстрируется, что и явный, и неявный вызовы метода Рис. 14.2. Методы Методы Заданный строкой текст зачастую представляет собой совокупность структурированных элементов — абзацев, предложений, слов, скобочных выражений и т. д. При работе с таким текстом необходимо разделить его на элементы, пользуясь специальными разделителями элементов, — это могут быть пробелы, скобки, знаки препинания. Практически подобные задачи возникают постоянно при работе со структурированными текстами. Методы Динамический метод public string[] Split(params char[]) На вход методу Синтаксис статического метода Join таков: public static string Join(string delimiters, string[] items) В качестве результата метод возвращает строку, полученную конкатенацией элементов массива items, между которыми вставляется строка разделителей Рассмотрим примеры применения этих методов. В первом из них строка представляет сложноподчиненное предложение, которое разбивается на простые предложения. Во втором предложение разделяется на слова. Затем производится обратная сборка разобранного текста. Вот код соответствующей процедуры: public void TestSplitAndJoin() { string txt = "А это пшеница, которая в темном чулане хранится," +" в доме, который построил Джек!"; Console.WriteLine("txt={0}", txt); Console.WriteLine("Разделение текста на простые предложения: "); string[] SimpleSentences, Words; //размерность массивов SimpleSentences и Words //устанавливается автоматически в соответствии с //размерностью массива, возвращаемого методом Split SimpleSentences = txt.Split (','); for (int i=0;i< SimpleSentences.Length; i + +) Console.WriteLine("SimpleSentences[{0}]= {1}", i, SimpleSentences[i]); string txtjoin = string.Join(",",SimpleSentences); Console.WriteLine("txtj oin={0 } ", txtjоin); Words = txt.Split (',', ' '); for (int i=0;i< Words.Length; i ++) Console.WriteLine("Words[{0}]= {1}",i, Words[i]); txtjoin = string.Join(" ",Words); Console.WriteLine("txtjoin={0}", txtjоin); }//TestSplitAndJoin Результаты выполнения этой процедуры показаны на рис. 14.3. Рис. 14.3. Обратите внимание, что методы • невозможно при сборке восстановить строку в прежнем виде, поскольку не сохраняется информация о том, какой из разделителей был использован при разборе строки. Поэтому при сборке между элементами вставляется один разделитель, возможно, состоящий из нескольких символов; • при разборе двух подряд идущих разделителей предполагается, что между ними находится пустое слово. Обратите внимание в тексте нашего примера, как и положено, после запятой следует пробел. При разборе текста на слова в качестве разделителей указаны символы пробела и запятой. По этой причине в массиве слов, полученном в результате разбора, имеются пустые слова. Если при разборе предложения на слова использовать в качестве разделителя только пробел, то пустые слова не появятся, но запятая будет являться частью некоторых слов. Как всегда, есть несколько способов справиться с проблемой. Один из них состоит в том, чтобы написать собственную реализацию этих функций, другой — в корректировке полученных результатов, третий — в использовании более мощного аппарата регулярных выражений, и о нем мы поговорим чуть позже. Динамические методы класса Операции, разрешенные над строками в С#, разнообразны. Методы этого класса позволяют выполнять вставку, удаление, замену, поиск вхождения подстроки в строку. Класс Рассмотрим наиболее характерные методы при работе со строками. Сводка методов, приведенная в таблице 14.2, дает достаточно полную картину широких возможностей, имеющихся при работе со строками в С#. Следует помнить, что класс Метод ∙ Описание Класс Объявление строк. Конструкторы класса Объекты этого класса объявляются с явным вызовом конструктора класса. Поскольку специальных констант этого типа не существует, то вызов конструктора для инициализации объекта просто необходим. Конструктор класса перегружен, и наряду с конструктором без параметров, создающим пустую строку, имеется набор конструкторов, которым можно передать две группы параметров. Первая группа позволяет задать строку или подстроку, значением которой будет инициализироваться создаваемый объект класса • • • Операции над строками Над строками этого класса определены практически те же операции с той же семантикой, что и над строками класса String: • присваивание (=); • две операции проверки эквивалентности (==) и (!=); • взятие индекса ([]). Операция конкатенации (+) не определена над строками класса StringBuilder, ее роль играет метод Со строкой этого класса можно работать как с массивом, но, в отличие от класса public void TestStringBuilder() { //Строки класса StringBuilder //операции над строками StringBuilder s1 =new StringBuilder("ABC"), s2=new StringBuilder("CDE"); StringBuilder s3 = new StringBuilder (); //s3=s1 + s 2; s3=s1.Append(s2); bool b1 = (s1==s3); char ch1 = s1 [0], ch2 = s2[0]; Console.WriteLine("s1={0}, s2={l}, b1={2}," + "ch1={3}, ch2={4}", s1,s2,b1,ch1,ch2); s2 = s1; b1 = (s1!=s2); ch2 = s2 [0]; Console.WriteLine("s1={0}, s2={1}, b1={2}," + "ch1={3 }, ch2={4 } ", s1,s2,b1,ch1,ch2); StringBuilder s = new StringBuilder("Zenon"); s [0] = ' L '; Console.WriteLine(s); }//TestStringBuilder Этот пример демонстрирует возможность выполнения над строками класса Основные методы У класса • • public StringBuilder Insert (int location,<объект>). Метод вставляет Строку, полученную из объекта, в позицию, указанную параметром location. Метод • • • За исключением метода Remove, все рассмотренные методы являются перегруженными. В их описании дана схема вызова метода, а не точный синтаксис перегруженных реализаций. Приведу примеры, чтобы продемонстрировать, как вызываются и как работают эти методы: //Методы Insert, Append, AppendFormat StringBuilder strbuild = new StringBuilder (); string str = "это это не"; strbuild.Append(str); strbuild.Append(true); strbuild.Insert(4,false); strbuild.Insert(0,"2*2=5 — "); Console.WriteLine(strbuild); string txt = "А это пшеница, которая в темном чулане хранится," +" в доме, который построил Джек!"; StringBuilder txtbuild = new StringBuilder(); int num =1; foreach(string sub in txt.Split (', ')) { txtbuild.AppendFormat(" {0}: {1} ", num++,sub); } str = txtbuild.ToString (); Console.WriteLine (str); В этом фрагменте кода конструируются две строки. Первая из них создается из строк и булевых значений Обратите внимание, что сконструированная вторая строка передается в обычную строку класса Обратите внимание, как выглядят результаты работы. Рис. 14.4. Емкость буфера Каждый экземпляр строки класса У класса • свойство • свойство • метод Приведу код, в котором проводятся различные эксперименты с емкостью буфера: //Емкость буфера int curvol1 = txtbuild.Capacity; int curvol2 = strbuild.Capacity; int maxvol1 = txtbuild.MaxCapacity; int maxvol2 = strbuild.MaxCapacity; Console.WriteLine("curvol1= {0}",curvol1); Console.WriteLine("curvol2= {0}",curvol2); Console.WriteLine("maxvol1= {0}",maxvol1); Console.WriteLine("maxvol2= {0}",maxvol2); int sure1 = txtbuild.EnsureCapacity (100); int sure2 = strbuild.EnsureCapacity (100); Console.WriteLine("sure1= {0}",sure1); Console.WriteLine("sure2= {0}",sure2); curvol2 = strbuild.Capacity; Console.WriteLine("curvol2= {0}",curvol2); //ошибка! попытка установить емкость меньше длины строки //strbuild.Capacity = 25; strbuild.Capacity = 256; //так можно! curvol2 = strbuild.Capacity; Console.WriteLine("curvol2= {0}",curvol2); //увеличим строку — емкость увеличится int len = txtbuild.Length; txtbuild.Append(txtbuild.ToString ()); curvol1 = txtbuild.Capacity; Console.WriteLine("curvol1= {0}",curvoll); //уменьшим строку txtbuild.Remove(len, len); curvol1 = txtbuild.Capacity; Console.WriteLine("curvol1= {0}",curvol1); В этом фрагменте кода анализируются и изменятся емкостные свойства буфера двух объектов. Демонстрируется, как меняется емкость при увеличении и уменьшении размера строки. Результаты работы этого фрагмента кода показаны на рис. 14.5. Рис. 14.5.
15. Регулярные выражения Регулярные выражения. Пространство RegularExpressions и его классы. Регулярные выражения и языки. Теория регулярных выражений. Практика применения регулярных выражений. Разбор текстов и поиск по образцу. Свойства и методы класса Regex и других классов, связанных с регулярными выражениями. Примеры применения регулярных выражений. Стандартный класс Немного теории Пусть Определим класс языков, задаваемых регулярными множествами. Регулярное множество определяется рекурсивно следующими правилами: • пустое множество, а также множество, содержащее пустое слово, и одноэлементные множества, содержащие символы алфавита, являются регулярными базисными множествами; • если множества Регулярные выражения представляют удобный способ задания регулярных множеств. Аналогично множествам, они определяются рекурсивно: • регулярные базисные выражения задаются символами и определяют соответствующие регулярные базисные множества, например, выражение • если По сути, регулярные выражения — это более простой и удобный способ записи регулярных множеств в виде обычной строки. Каждое регулярное множество, а, следовательно, и каждое регулярное выражение задает некоторый язык С точки зрения практика регулярное выражение задает образец поиска. После чего можно проверить, удовлетворяет ли заданная строка или ее подстрока данному образцу. В языках программирования синтаксис регулярного выражения существенно обогащается, что дает возможность более просто задавать сложные образцы поиска. Такие синтаксические надстройки, хотя и не меняют сути регулярных выражений, крайне полезны для практиков, избавляя программиста от ненужных сложностей. (В Net Framework эти усложнения, на мой взгляд, чрезмерны. Выигрывая в мощности языка, проигрываем в простоте записи его выражений.) Синтаксис регулярных выражений Регулярное выражение на C# задается строковой константой. Это может быть обычная или @-константа Чаще всего, следует использовать именно Синтаксис регулярного выражения простой формулой не описать, здесь используются набор разнообразных средств: • символы и escape-последовательности; • символы операций и символы, обозначающие специальные классы множеств; • имена групп и обратные ссылки; • символы утверждений и другие средства. Конечно, регулярное выражение может быть совсем простым, например, строка Повторяю, данная таблица не полна. В ней не отражены, например, такие категории, как подстановки, обратные ссылки, утверждения. Для приведенных категорий также не дан полный список возможных символов. Символ ∙ Интерпретация . ∙ Соответствует любому символу, за исключением символа конца строки ? ∙ Задает ноль или одно соответствие; например, Знакомство с классами пространства В данном пространстве расположено семейство из одного перечисления и восьми связанных между собой классов. Класс Это основной класс, всегда создаваемый при работе с регулярными выражениями. Объекты этого класса определяют регулярные выражения. Конструктор класса, как обычно, перегружен. В простейшем варианте ему передается в качестве параметра строка, задающая регулярное выражение. В других вариантах конструктора ему может быть передан объект, принадлежащий перечислению Рассмотрим четыре основных метода класса Regex. Метод Match запускает поиск соответствия. В качестве параметра методу передается строка поиска, где разыскивается первая подстрока, которая удовлетворяет образцу, заданному регулярным выражением. В качестве результата метод возвращает объект класса Match, описывающий результат поиска. При успешном поиске свойства объекта будут содержать информацию о найденной подстроке. Метод У алгоритма поиска есть важная особенность — разыскиваются непересекающиеся вхождения подстрок. Можно считать, что метод Метод Метод Классы Как уже говорилось, объекты этих классов создаются автоматически при вызове методов Класс Match является непосредственным наследником класса Group, который, в свою очередь, является наследником класса • свойства • свойство • свойство Классы Коллекция Давайте рассмотрим чуть более подробно, когда и как создаются группы в процессе поиска соответствия. Если внимательно проанализировать предыдущую таблицу, которая описывает символы, используемые в регулярных выражениях, в частности символы группирования, то можно понять несколько важных фактов, связанных с группами: • при обнаружении одной подстроки, удовлетворяющей условию поиска, создается не одна группа, а коллекция групп; • группа с индексом 0 содержит информацию о найденном соответствии; • число групп в коллекции зависит от числа круглых скобок в записи регулярного выражения. Каждая пара круглых скобок создает дополнительную группу, которая описывает ту часть подстроки, которая соответствует шаблону, заданному в круглых скобках; • группы могут быть индексированы, но могут быть и именованными, поскольку в круглых скобках разрешается указывать имя группы. В заключение отмечу, что создание именованных групп крайне полезно при разборе строк, содержащих разнородную информацию. Примеры разбора подобных текстов будут даны. Классы Коллекция Перечисление Объекты этого перечисления описывают опции, влияющие на то, как устанавливается соответствие. Обычно такой объект создается первым и передается конструктору объекта класса Класс При работе со сложными и большими текстами полезно предварительно скомпилировать используемые в процессе поиска регулярные выражения. В этом случае необходимо будет создать объект класса Примеры работы с регулярными выражениями Полагаю, что примеры дополнят краткое описание возможностей регулярных выражений и позволят лучше понять, как с ними работать. Начну с функции string FindMatch(string str, string strpat) { Regex pat = new Regex(strpat); Match match =pat.Match(str); string found = ""; if (match.Success) { found =match.Value; Console. WriteLine (" Строка = {0} \tОбpазец= {1} \ tНайдено={2}", str,strpat,found); } return(found); }//FindMatch В качестве входных аргументов функции передается строка Поскольку запись регулярных выражений — вещь, привычная не для всех программистов, я приведу достаточно много примеров: public void TestSinglePat () { //поиск по образцу первого вхождения string str,strpat,found; Console.WriteLine("Поиск по образцу"); //образец задает подстроку, начинающуюся с символа а, //далее идут буквы или цифры. str ="start"; strpat =@"a\w+"; found = FindMatch(str,strpat); str ="fab77cd efg"; found = FindMatch(str,strpat); //образец задает подстроку, начинающуюся с символа а, //заканчивающуюся f с возможными символами b и d в середине strpat = "a(b|d)*f"; str = "fabadddbdf"; found = FindMatch(str,strpat); //диапазоны и escape-символы strpat = M[X-Z]+"; str = "aXYb"; found = FindMatch(str,strpat); strpat = @"\u005BY\x5A"; str = "aXYZb"; found = FindMatch(str,strpat); }//TestSinglePat Некоторые комментарии к этой процедуре. Регулярные выражения задаются @-константами, описанными в лекции 14. Здесь они как нельзя кстати. В первом образце используется последовательность символов В следующем образце используется символ Последующие два образца демонстрируют использование диапазонов и escape-последовательностей для представления символов, заданных кодами (в Unicode и шестнадцатиричной кодировке). Взгляните на результаты, полученные при работе этой процедуры. Рис. 15.1. Пример "чет и нечет" Не всякий класс языков можно описать с помощью регулярных выражений. И даже тогда, когда такая возможность есть, могут потребоваться определенные усилия для корректной записи соответствующего регулярного выражения. Рассмотрим, например, язык (00|11) * ((01|10) (00|11) * (01|10) (00|11) * )* Дадим содержательное описание этого языка. Слова языка представляют возможно пустую последовательность из пар одинаковых символов. Далее может идти последовательность, начинающаяся и заканчивающаяся парами различающихся символов, между которыми может стоять произвольное число пар одинаковых символов. Такая группа может повторяться многократно. Регулярное выражение короче и точнее передает описываемую структуру слов языка Язык L2 описать теперь совсем просто. Его слова представляют собой единицу, окаймленную словами языка Прежде чем перейти к примеру распознавания слов языков void FindMatches(string str, string strpat) { Regex pat = new Regex(strpat); MatchCollection matchcol =pat.Matches(str); Console.WriteLine ("Строка = {0} \tОбразец= {1} ", str, strpat); Console.WriteLine("Число совпадений ={0}",matchcol.Count); foreach(Match match in matchcol) Console.WriteLine("Index = {0} Value = {1}, Length ={2}", match.Index,match.Value, match.Length); }//FindMatches Входные аргументы у процедуры те же, что и у функции В отличие от Вот процедура, в которой многократно вызывается public void TestMultiPat () { //поиск по образцу всех вхождений string str,strpat,found; Console.WriteLine("Распознавание языков: чет и нечет"); //четное число нулей и единиц strpat ="((00|11) * ((01|10) (00|11) * (01|10) (00|11) *)*)"; str = "0110111101101"; FindMatches(str, strpat); //четное число нулей и нечетное единиц tring strodd = strpat + "1" + strpat; FindMatches(str, strodd); }//TestMultiPat Коротко прокомментирую работу этой процедуры. Первые два примера связаны с распознаванием языков Рис. 15.2. Пример "око и рококо" Следующий образец в нашем примере позволяет прояснить некоторые особенности работы метода Console.WriteLine("око и рококо"); strpat="око"; str = "рококо"; FindMatches(str, strpat); strpat="123"; str= "0123451236123781239"; FindMatches(str, strpat); На рис. 15.3 показаны результаты поисков. Рис. 15.3. Пример "кок и кук" Этот пример на поиск множественных соответствий навеян словами песни Высоцкого, где говорится, что дикари не смогли распознать, где кок, а где Кук. Наше регулярное выражение также не распознает эти слова. Обратите внимание на точку в регулярном выражении, которая соответствует любому символу, за исключением символа конца строки. Все слова в строке поиска — кок, кук, кот и другие — будут удовлетворять шаблону, так что в результате поиска найдется множество соответствий. Console.WriteLine("кок и кук"); strpat="(т|к).(т|к)"; str="кок тот кук тут как кот"; FindMatches(str, strpat); Вот результаты работы этого фрагмента кода. Рис. 15.4. Пример "обратные ссылки" В этом примере рассматривается ранее упоминавшаяся, но не описанная возможность задания в регулярном выражении обратных ссылок. Можно ли описать с помощью регулярных выражений язык, в котором встречаются две подряд идущие одинаковые подстроки? Ответ на это вопрос отрицательный, поскольку грамматика такого языка должна быть контекстно-зависимой, и нужна память, чтобы хранить уже распознанные части строки. Аппарат регулярных выражений, предоставляемый классами пространства RegularExpression, тем не менее, позволяет решить эту задачу. Причина в том, что расширение стандартных регулярных выражений в Net Framework является не только синтаксическим. Содержательные расширения связаны с введением понятия группы, которой отводится память и дается имя. Это и дает возможность ссылаться на уже созданные группы, что и делает грамматику языка контекстно-зависимой. Ссылка на ранее полученную группу называется обратной ссылкой. Признаком обратной ссылки является пара символов Console.WriteLine("Ссылка назад — второе вхождение слова"); strpat = @"\s(? str = "I know know that, You know that!"; FindMatches(str, strpat); Рассмотрим более подробно регулярное выражение, заданное строкой Рис. 15.5. Пример "Дом Джека" Давайте вернемся к задаче разбора предложения на элементы. В классе string для этого имеется метод public void TestParsing() { string str,strpat; //разбор предложения — создание массива слов str = "А это пшеница, которая в темном чулане хранится," +" в доме, который построил Джек!"; strpat =" +|, "; Regex pat = new Regex(strpat); string[] words; words = pat.Split (str); int i=1; foreach(string word in words) Console.WriteLine("{0}: {1}",i++,word); } //TestParsing Регулярное выражение, заданное строкой Рис. 15.6. Регулярные выражения. Пример "Дом Джека" Пример "Атрибуты" Как уже говорилось, регулярные выражения особенно хороши при разборе сложных текстов. Примерами таковых могут быть различные справочники, различные текстовые базы данных, весьма популярные теперь XML-документы, разбором которых приходится заниматься. В качестве заключительного примера рассмотрим структурированный документ, строки которого содержат некоторые атрибуты, например, телефон, адрес и e-mail. Структуру документа можно задавать по-разному; будем предполагать, что каждый атрибут задается парой "имя: Значение" Наша задача состоит в том, чтобы выделить из строки соответствующие атрибуты. В таких ситуациях регулярное выражение удобно задавать в виде групп, где каждая группа соответствует одному атрибуту. Приведу начальный фрагмент кода очередной тестирующей процедуры, в котором описываются строки текста и образцы поиска: public void TestAttributes () { string s1 = "tel: (831-2) 94-20-55 string s2 = "Адрес: 117926, Москва, 5-й Донской проезд, стр. 10, кв. 7"; string s3 = "e-mail: [email protected] string s4 = s1+ s2 + s3; string s5 = s2 + s1 + s3; string pat1 = @"tel: \s(? string pat2 = @"Адрес: \s(? string pat3 = @"e-mail: \s(?[a-zA-Z.0]+)\s"; string compat=pat1+pat2+pat3; string tel="", addr = ""; em = ""; Строки Regex reg1 = new Regex(pat1); Match match1= reg1.Match(s4); Console.WriteLine("Value =" + match1.Value); tel= match1.Groups["tel"].Value; Console.WriteLine(tel); Regex reg2 = new Regex(pat2); Match match2= reg2.Match(s5); Console.WriteLine("Value =" + match2.Value); addr= match2.Groups["addr"].Value; Console.WriteLine(addr); Regex reg3 = new Regex(pat3); Match match3= reg3.Match(s5); Console.WriteLine("Value =" + match3.Value); em= match3.Groups["em"].Value; Console.WriteLine(em); Все выполняется нужным образом — создаются именованные группы, к ним можно получить доступ и извлечь найденный значения атрибутов. А теперь попробуем решить ту же задачу одним махом, используя составной шаблон Regex comreg = new Regex(compat); Match commatch= comreg.Match(s4); tel= commatch.Groups["tel"].Value; Console.WriteLine(tel); addr commatch.Groups["addr"].Value Console.WriteLine(addr); em= commatch.Groups["em"].Value; Console.WriteLine(em); }// TestAttributes И эта задача успешно решается. Взгляните на результаты разбора текста. Рис. 15.7. На этом и завершим рассмотрение регулярных выражений а также лекции, посвященные работе с текстами в С#.
16. Классы Две роли класса в ООП. Синтаксис описания класса. Поля и методы класса. Конструкторы и деструкторы. Статические поля и методы. Статические конструкторы. Поля только для чтения. Закрытые поля. Стратегии доступа к полям класса. Процедуры свойства. Индексаторы. Примеры. Объектно-ориентированное программирование и проектирование построено на классах. Любую программную систему, выстроенную в объектном стиле, можно рассматривать как совокупность классов, возможно, объединенных в проекты, пространства имен, решения, как это делается при программировании в Visual Studio.Net. Две роли классов У класса две различные роли: модуля и типа данных. Класс — это модуль, архитектурная единица построения программной системы. Модульность построения — основное свойство программных систем. В ООП программная система, строящаяся по модульному принципу, состоит из классов, являющихся основным видом модуля. Модуль может не представлять собой содержательную единицу — его размер и содержание определяется архитектурными соображениями, а не семантическими. Ничто не мешает построить монолитную систему, состоящую из одного модуля — она может решать ту же задачу, что и система, состоящая из многих модулей. Вторая роль класса не менее важна. Класс — это тип данных, задающий реализацию некоторой абстракции данных, характерной для задачи, в интересах которой создается программная система. С этих позиций классы — не просто кирпичики, из которых строится система. Каждый кирпичик теперь имеет важную содержательную начинку. Представьте себе современный дом, построенный из кирпичей, и дом будущего, где каждый кирпич выполняет определенную функцию: один следит за температурой, другой — за составом воздуха в доме. ОО-программная система напоминает дом будущего. Состав класса, его размер определяется не архитектурными соображениями, а той абстракцией данных, которую должен реализовать класс. Если вы создаете класс Объектно-ориентированная разработка программной системы основана на стиле, называемом В хорошо спроектированной ОО-системе каждый класс играет обе роли, так что каждый модуль нашей системы имеет вполне определенную смысловую нагрузку. Типичная ошибка — рассматривать класс только как архитектурную единицу, объединяя под обложкой класса разнородные поля и функции, после чего становится неясным, какой же тип данных задает этот класс. Синтаксис класса Ни одна из предыдущих лекций не обходилась без появления классов и обсуждения многих деталей, связанных с ними. Сейчас попробуем сделать некоторые уточнения, подвести итоги и с новых позиций взглянуть на уже знакомые вещи. Начнем с синтаксиса описания класса: [атрибуты][модификаторы]class имя_класса[: список_родителей] {тело_класса} Атрибутам будет посвящена отдельная лекция. Возможными модификаторами в объявлении класса могут быть модификаторы public class Rational {тело_класса} В теле класса могут быть объявлены: • константы-, • поля; • конструкторы и деструкторы; • методы, • события; • делегаты; • классы (структуры, интерфейсы, перечисления). О событиях и делегатах предстоит подробный разговор в последующих лекциях. Из синтаксиса следует, что классы могут быть вложенными. Такая ситуация — довольно редкая. Ее стоит использовать, когда некоторый класс носит вспомогательный характер, разрабатывается в интересах другого класса, и есть полная уверенность, что внутренний класс никому не понадобится, кроме класса, в который он вложен. Как уже упоминалось, внутренние классы обычно имеют модификатор доступа, отличный от Поля класса Поля класса синтаксически являются обычными переменными (объектами) языка. Их описание удовлетворяет обычным правилам объявления переменных, о чем подробно говорилось в лекции 5. Содержательно поля задают представление той самой абстракции данных, которую реализует класс. Поля характеризуют свойства объектов класса. Напомню, что, когда создается новый объект класса (в динамической памяти или в стеке), то этот объект представляет собой набор полей класса. Два объекта одного класса имеют один и тот же набор полей, но разнятся значениями, хранимыми в этих полях. Все объекты класса Каждое поле имеет модификатор доступа, принимающий одно из четырех значений: Методы класса Методы класса синтаксически являются обычными процедурами и функциями языка. Их описание удовлетворяет обычным правилам объявления процедур и функций, о чем подробно говорилось в лекции 9. Содержательно методы определяют ту самую абстракцию данных, которую реализует класс. Методы содержат описания операций, доступных над объектами класса. Два объекта одного класса имеют один и тот же набор методов. Каждый метод имеет модификатор доступа, принимающий одно из четырех значений: Если некоторые методы класса а должны быть доступны для вызовов в методах класса Методы, называемые свойствами (Properties), представляют специальную синтаксическую конструкцию, предназначенную для обеспечения эффективной работы со свойствами. При работе со свойствами объекта (полями) часто нужно решить, какой модификатор доступа использовать, чтобы реализовать нужную стратегию доступа к полю класса. Перечислю пять наиболее употребительных стратегий: • чтение, запись ( • чтение, запись при первом обращении ( • ТОЛЬКО чтение ( • ТОЛЬКО запись ( • ни чтения, ни записи ( Открытость свойств (атрибут Приведу вначале пример, а потом уточню синтаксис этих методов. Рассмотрим класс Person, у которого пять полей: public class Person { // поля (все закрыты) string fam="", status="", health=""; int age=0, salary=0; //методы — свойства /// /// стратегия: Read,Write-once (Чтение, запись при /// первом обращении) /// public string Fam { set {if (fam == "") fam = value;} get {return(fam);} } /// ///стратегия: Read-only(Только чтение) /// public string Status { get {return(status);} } /// /// стратегия: Read,Write (Чтение, запись) /// public int Age { set { age = value; if(age < 7)status ="ребенок"; else if(age <17)status ="школьник"; else if (age < 22)status = "студент"; else status = "служащий"; } get {return(age);} } /// /// стратегия: Write-only (Только запись) /// public int Salary { set {salary = value;} } } Рассмотрим теперь общий синтаксис методов-свойств. Пусть public void TestPersonProps () { Person pers1 = new Person(); pers1.Fam = "Петров"; pers1.Age = 21; pers1.Salary = 1000; Console.WriteLine ("Фам={0}, возраст={1}, статус={2}", pers1.Fam, pers1.Age, persl.Status); pers1.Fam = "Иванов"; pers1.Age += 1; Console.WriteLine ("Фам={0}, возраст={1}, статус={2}", pers1.Fam, pers1.Age, pers1.Status); }//TestPersonProps Заметьте, клиент работает с методами-свойствами так, словно они являются настоящими полями, вызывая их как в правой, так и в левой части оператора присваивания. Заметьте также, что с каждым полем можно работать только в полном соответствии с той стратегией, которую реализует данное свойство. Попытка изменения фамилии не принесет успеха, а изменение возраста приведет и к одновременному изменению статуса. На рис. 16.1 показаны результаты работы этой процедуры. Рис. 16.1. Индексаторы Свойства являются частным случаем метода класса с особым синтаксисом. Еще одним частным случаем является индексатор. Метод-индексатор является обобщением метода-свойства. Он обеспечивает доступ к закрытому полю, представляющему массив. Объекты класса индексируются по этому полю. Синтаксически объявление индексатора — такое же, как и в случае свойств, но методы Добавим в класс const int Child_Max = 20; //максимальное число детей Person[] children = new Person[Child_Max]; int count_children=0; //число детей public Person this[int i] //индексатор { get {if (i>= 0 && i< count_children)return(children[i]); else return(children[0]); } set { if (i==count_children && i< Child_Max) {children[i] = value; count children++; } } Имя у индексатора — Протестируем процесс добавления детей персоны и работу индексатора: public void TestPersonChildren () } Person pers1 = new Person (), pers2 = new Person(); pers1.Fam = "Петров"; persl.Age = 42; pers1.Salary = 10000; pers1[pers1.Count_children] = pers2; pers2.Fam ="Петров"; pers2.Age = 21; pers2.Salary = 1000 Person pers3= new Person("Петрова"); pers1[persl.Count_children] = pers3; pers3.Fam ="Петрова"; pers3.Age = 5; Console.WriteLine ("Фам={0}, возраст={1}, статус={2}, pers1.Fam, pers1.Age, pers1.Status); Console.WriteLine ("Сын={0}, возраст={1}, статус={2}, pers1[0].Fam, persl[0].Age, persl[0]. Status); Console.WriteLine ("Дочь={0}, возраст={1}, статус= {2}; pers1[1].Fam, pers1[1].Age, pers1[1].Status); } Заметьте, индексатор создает из объекта как бы массив объектов, индексированный по соответствующему полю, в данном случае по полю Рис. 16.2. Еще одним частным случаем являются методы, задающие над объектами-классами бинарную или унарную операцию. Введение в класс таких методов позволяет строить выражения, аналогичные арифметическим и булевым выражениям с обычно применяемыми знаками операций и сохранением приоритетов операций. Синтаксис задания таких методов и детали применения опишу чуть позже при проектировании класса рациональных чисел Статические поля и методы класса Ранее говорилось, что когда конструктор класса создает новый объект, то в памяти создается структура данных с полями, определяемыми классом. Уточним теперь это описание. Не все поля отражаются в структуре объекта. У класса могут быть поля, связанные не с объектами, а с самим классом. Эти поля объявляются как статические с модификатором Аналогично полям, у класса могут быть и статические методы, объявленные с модификатором Как вызываются статические поля и методы? Напомню, что всякий вызов метода в объектных вычислениях имеет вид Если же необходимо вызвать статический метод (поле), то целью должен быть сам класс. Можно полагать, что для каждого класса автоматически создается статический объект с именем класса, содержащий статические поля и обладающий статическими методами. Этот объект и его методы доступны и тогда, когда ни один другой динамический объект класса еще не создан. (Полагаю, что разумно обратиться к лекции 2, а именно, к разделу, описывающему точку большого взрыва, процесс вычислений в ОО-системах, вызовы статических методов.) Константы В классе могут быть объявлены константы. Синтаксис объявления приводился в лекции 5. Константы фактически являются статическими полями, доступными только для чтения, значения которых задаются при инициализации. Однако задавать модификатор Никаких проблем не возникает, когда речь идет о константах встроенных типов, таких, как Конструкторы класса Конструктор — неотъемлемый компонент класса. Нет классов без конструкторов. Конструктор представляет собой специальный метод класса, позволяющий создавать объекты класса. Одна из синтаксических особенностей этого метода в том, что его имя должно совпадать с именем класса. Если программист не определяет конструктор класса, то к классу автоматически добавляется конструктор по умолчанию — конструктор без аргументов. Заметьте, что если программист сам создает один или несколько конструкторов, то автоматического добавления конструктора без аргументов не происходит. Как и когда происходит создание объектов? Чаще всего, при объявлении сущности в момент ее инициализации. Давайте обратимся к нашему последнему примеру и рассмотрим создание трех объектов класса Person: Person pers1 = new Person (), pers2 = new Person(); Person pers3= new Person("Петрова"); Сущности • первым делом для сущности • затем в динамической памяти создается объект — структура данных с полями, определяемыми классом • если поля класса проинициализированы, как в нашем примере, то выполняется инициализация полей заданными значениями; • если вызван конструктор с аргументами, то начинает выполняться тело этого конструктора. Как правило, при этом происходит инициализация отдельных полей класса значениями, переданными конструктору. Так, поле • На заключительном этапе ссылка связывается с созданным объектом. Процесс создания объектов становится сложнее, когда речь идет об объектах, являющихся потомками некоторого класса. В этом случае, прежде чем создать сам объект, нужно вызвать конструктор, создающий родительский объект. Но об этом мы еще поговорим при изучении наследования. (Ключевое слово Зачем классу нужно несколько конструкторов? Дело в том, что, в зависимости от контекста и создаваемого объекта, может требоваться различная инициализация его полей. Перегрузка конструкторов и обеспечивает решение этой задачи. Немного экзотики, связанной с конструкторами. Конструктор может быть объявлен с атрибутом В классе можно объявить статический конструктор с атрибутом static Person() { Console.WriteLine("Выполняется статический конструктор!"); } В нашей тестирующей процедуре, работающей с объектами класса Подводя итоги, можно отметить, что объекты создаются динамически в процессе выполнения программы — для создания объекта всегда вызывается тот или иной конструктор класса. Деструкторы класса Если задача создания объектов полностью возлагается на программиста, то задача удаления объектов, после того, как они стали не нужными, в Visual Studio.Net снята с программиста и возложена на соответствующий инструментарий — сборщик мусора. В классическом варианте языка C++ деструктор так же необходим классу, как и конструктор. В языке C# у класса может быть деструктор, но он не занимается удалением объектов и не вызывается нормальным образом в ходе выполнения программы. Так же, как и статический конструктор, деструктор класса, если он есть, вызывается автоматически в процессе сборки мусора. Его роль — в освобождении ресурсов, например, файлов, открытых объектом. Деструктор C# фактически является финализатором (finalizer), с которыми мы еще встретимся при обсуждении исключительных ситуаций. Приведу формальное описание деструктора класса — Person() { //Код деструктора } Имя деструктора строится из имени класса с предшествующим ему символом ~ (тильда). Как и у статического конструктора, у деструктора не указывается модификатор доступа. Проектирование класса В заключение этой лекции займемся проектированием класса /// /// Класс Rational /// определяет новый тип данных — рациональные числа и /// основные операции над ними — сложение, умножение, /// вычитание и деление. Рациональное число задается парой /// целых чисел (m, n) и изображается обычно в виде дроби m/n. /// Число m называется числителем, n — знаменателем. Для /// каждого рационального числа существует множество его /// представлений, например, 1/2, 2/4, 3/6, 6/12 — задают /// одно и тоже рациональное число. Среди всех представлений /// можно выделить то, в котором числитель и знаменатель /// взаимно несократимы. Такой представитель будет храниться /// в полях класса. Операции над рациональными числами /// определяются естественным для математики образом /// public class Rational { // Описание тела класса Rational }//Rational Два целых числа — //Поля класса. Числитель и знаменатель рационального числа, int m,n; Инициализация полей конструктором по умолчанию никак не может нас устраивать, поскольку нулевой знаменатель — это нонсенс. Поэтому определим конструктор с аргументами, которому будут передаваться два целых: числитель и знаменатель создаваемого числа. Кажется, что это единственный разумный конструктор, который может понадобиться нашему классу. Однако чуть позже мы добавим в класс закрытый конструктор и статический конструктор, позволяющий создать константы нашего класса. Вот определение конструктора'. /// /// Конструктор класса. Создает рациональное число /// m/n, эквивалентное a/b, но со взаимно несократимыми /// числителем и знаменателем. Если Ь=0, то результатом /// является рациональное число 0 — пара (0,1). /// /// числитель /// знаменатель public Rational(int a, int b) { if (b==0) {m=0; n=1;} else { // приведение знака if(b<0) {b=-b; a=-a;} // приведение к несократимой дроби int d = nod(a,b); m=a/d; n=b/d; } } Как видите, конструктор класса может быть довольно сложным. В нем, как в нашем случае, может проверяться корректность задаваемых аргументов. Для рациональных чисел мы полагаем, что задание нулевого знаменателя означает задание рационального числа о, и это эквивалентно заданию пары (0, 1). В остальных случаях выполняется приведение заданной пары чисел к эквивалентному рациональному числу с несократимыми числителем и знаменателем. По ходу дела вызывается закрытый метод класса, вычисляющий значение Методы класса Если поля класса почти всегда закрываются, чтобы скрыть от пользователя представление данных класса, то методы класса всегда имеют открытую часть — те сервисы (службы), которые класс предоставляет своим клиентам и наследникам. Но не все методы открываются. Большая часть методов класса может быть закрытой, скрывая от клиентов детали реализации, необходимые для внутреннего использования. Заметьте, сокрытие представления и реализации делается не по соображениям утаивания того, как реализована система. Чаще всего, ничто не мешает клиентам ознакомиться с полным текстом класса. Сокрытие делается в интересах самих клиентов. При сопровождении программной системы изменения в ней неизбежны. Клиенты не почувствуют на себе негативные последствия изменений, если они делаются в закрытой части класса. Чем больше закрытая часть класса, тем меньше влияние изменений на клиентов класса. Закрытый метод НОД Метод, вычисляющий наибольший общий делитель пары чисел, понадобится не только конструктору класса, но и всем операциям над рациональными числами. Алгоритм нахождения общего делителя хорошо известен со времен Эвклида. Я приведу программный код метода без особых пояснений: /// /// Закрытый метод класса. /// Возвращает наибольший общий делитель чисел а, Ь /// /// первое число /// второе число, положительное /// int nod(int m, int n) { int p=0; m=Math.Abs(m); n =Math.Abs(n); if(n>m){p=m; m=n; n=p;} do { p = m%n; m=n; n=p; }while (n!=0); return(m); }//nod Почти любой класс содержит один или несколько методов, позволяющих выводить на печать данные о классе. Такой метод имеется и в классе Rational. Вот его текст: public void PrintRational(string name) { Console.WriteLine(" {0} = {1} / { 2}",name,m,n); } Метод печатает имя и значение рационального числа в форме m/n. В классе public void TestCreateRational() { Rational r1=new Rational(0,0), r2 = new Rational(1,1); Rational r3=new Rational(10,8), r4 = new Rational(2,6); Rational r5=new Rational(4,-12), r6 = new Rational (-12,-14); r1.PrintRational("r1: (0,0)"); r2.PrintRational("r2: (1,1)"); r3.PrintRational("r3: (10,8)"); r4.PrintRational("r4: (2,6)"); r5.PrintRational("r5: (4,-12)"); r6.PrintRational("r6: (-12,-14)"); } Она создает и печатает шесть рациональных чисел. Вот как выглядят результаты ее работы. Рис. 16.3. Операции над рациональными числами Определим над рациональными числами стандартный набор операций — сложение и вычитание, умножение и деление. Реализуем эти операции методами с именами Покажем вначале реализацию метода public Rational Plus(Rational a) { int u,v; u = m*a.n +n*a.m; v= n*a.n; return (new Rational(u, v)); }//Plus public static Rational operator +(Rational r1, Rational r2) { return (r1.Plus(r2)); } Метод Обратите внимание на то, как определяется операция класса. Именем соответствующего метода является сам знак операции, которому предшествует ключевое слово Рис. 16.4. В данном конкретном случае операция реализуется вызовом метода public void TestPlusRational() { Rational r1=new Rational(0,0), r2 = new Rational (1,1); Rational r3=new Rational(10,8), r4 = new Rational(2,6); Rational r5=new Rational(4,-12), r6 = new Rational (-12,-14); Rational r7,r8, r9,r10, r11,r12; r7 = r1.Plus(r2); r8 = r3.Plus(r4); r9 = r5.Plus(r6); r10 = r1+r2; r11 = r3+r4; r12 = r5 = r6 = r10 = r11; r1.PrintRational("r1:(0,0)"); r2.PrintRational("r2:(1,1)"); r3.PrintRational("r3:(10,8)"); r4.PrintRational("r4:(2,6)"); r5.PrintRational("r5:(4,-12); r6.PrintRational ("r6:(-12,-14)"); r7.PrintRational("r7:(r1+r2)" r8.PrintRational ("r8:(r3+r4)"); r9.PrintRational("9:(r5+r6)"); r10.PrintRational ("r10:(r1+r2)"); r11.PrintRational("r11:(r3+r4)"); r12.PrintRational("r12:(r5+r6+r10+r11)"); } Обратите внимание на вычисление Аналогичным образом определим остальные операции над рациональными числами: public Rational Minus(Rational a) { int u,v; u = m*a.n — n*a.m; v= n*a.n; return (new Rational(u, v)); }//Minus public static Rational operator — (Rational r1, Rational r2) { return (r1.Minus(r2)); } public Rational Mult(Rational a) { int u,v; u = m*a.m; v= n*a.n; return (new Rational(u, v)); }//Mult public static Rational operator *(Rational rl, Rational r2) { return (r1.Mult(r2)); } public Rational Divide(Rational a) { int u,v; u = m*a.n; v= n*a.m; return (new Rational(u, v)); }//Divide public static Rational operator /(Rational r1, Rational r2) { return (r1.Divide(r2)); } Вот тест, проверяющий работу этих операций'. public void TestOperRational() { Rational r1=new Rational(1,2), r2 = new Rational(1,3); Rational r3, r4, r5, r6; r3 = r1-r2; r4=r1*r2; r5=r1/r2; r6=r3+r4*r5; r1.PrintRational("r1: (1,2)"); r2.PrintRational("r2: (1,3)"); r3.PrintRational("r3: (r1-r2)"); r4.PrintRational("r4: (r1*r2)") г5.PrintRational("r5: (r1/r2)"); г6. PrintRational("r6: (r3+r4*r5)"); } Результаты работы этого теста показаны на рис. 16.5. Обратите внимание: при перегрузке операций сохраняется общепринятый приоритет операций. Поэтому при вычислении выражения Рис. 16.5. Константы класса Рассмотрим важную проблему определения констант в собственном классе. Определим две константы private Rational(int a, int b, string t) { m = a; n = b; } He забудем, что при перегрузке методов (в данном случае конструкторов) сигнатуры должны различаться, и поэтому пришлось ввести дополнительный аргумент //Константы класса 0 и 1 — Zero и One public static readonly Rational Zero, One; А теперь зададим статический конструктор, в котором определяются значения констант: static Rational() { Console.WriteLine("static constructor Rational"); Zero = new Rational(0, 1, "private"); One = new Rational (1, 1, "private"); }//Статический конструктор Как это все работает? Статический конструктор вызывается автоматически один раз до начала работы с объектами класса. Он и задаст значения статических полей public static bool operator ==(Rational r1, Rational r2) } return ((r1.m==r2.m) & & (r1.n==r2.n)); } public static bool operator!=(Rational r1, Rational r2) } return ((r1.m! =r2.m) || (r1.n!=r2.n)); } public static bool operator <(Rational r1, Rational r2) } return (r1.m*r2. n < r2.m* r1.n); } public static bool operator >(Rational r1, Rational r2) } return (r1.m * r2.n > r2.m* r1.n); } public static bool operator <(Rational rl, double r2) { return((double)r1.m / (double)rl.n < r2); } public static bool operator >(Rational rl, double r2) { return((double)r1.m / (double)r1.n > r2); } Наш последний пример демонстрирует работу с константами, булевыми и арифметическими выражениями над рациональными числами: public void TestRationalConst() { Rational r1 = new Rational(2,8), r2 =new Rational(2,5); Rational r3 = new Rational(4, 10), r4 = new Rational(3,7); Rational r5 = Rational.Zero, r6 = Rational.Zero; if ((r1!= Rational.Zero) && (r2 == r3)) r5 =(r3+Rational.One)*r4; r6 = Rational.One + Rational.One; r1.PrintRational ("r1: (2,8)"); r2.PrintRational ("r2: (2,5)"); r3.PrintRational ("r3: (4,10)"); r4.PrintRational ("r4: (3,7)"); r5.PrintRational ("r5: ((r3 +1)*r4)"); r6.PrintRational ("r6: (1 + 1)"); } Результаты работы этого примера показаны на рис. 16.6. Рис. 16.6.
17. Структуры и перечисления Понятие развернутого и ссылочного типа. Структуры — реализация развернутых классов. Синтаксис структур. Сравнение структур и классов. Встроенные структуры. Перечисление — частный случай класса. Особенности перечислений. Примеры. Рассмотрим объявление объекта класса Т х = new Т(); Напомню, как выполняется этот оператор. В памяти создается объект типа Определение 1. Класс Определение 2. Класс Для развернутого типа характерно то, что каждая сущность ни с кем не разделяет свою память; сущность жестко связывается со своим объектом. В этом случае сущность и объект можно и не различать, они становятся неделимым понятием. Для ссылочных типов ситуация иная — несколько сущностей могут ссылаться на один и тот же объект. Такие сущности разделяют память и являются разными именами одного объекта. Полезно понимать разницу между сущностью, заданной ссылкой, и объектом, на который в текущий момент указывает ссылка. Развернутые и ссылочные типы порождают две различные семантики присваивания — развернутое присваивание и ссылочное присваивание. Рассмотрим присваивание: Когда сущность Язык программирования должен позволять программисту в момент определения класса указать, к развернутому или ссылочному типу относится класс. К сожалению, язык C# не позволяет этого сделать напрямую — в нем у класса нет модификатора, позволяющего задать развернутый или ссылочный тип. Какие же средства языка позволяют частично решить эту важную задачу? В лекции 3, где рассматривалась система типов языка С#, отмечалось, что все типы языка делятся на ссылочные и значимые. Термин "значимый" является синонимом термина "развернутый". Беда только в том, что деление на значимые и ссылочные типы предопределено языком и не управляется программистом. Напомню, к значимым типам относятся все встроенные арифметические типы, булев тип, структуры, к ссылочным типам — массивы, строки, классы. Так можно ли в C# спроектировать свой собственный класс так, чтобы он относился к значимым типам? Ответ на это вопрос положительный, хотя и с рядом оговорок. Для того чтобы класс отнести к значимым типам, его нужно реализовать как структуру. Классы и структуры Структура — это частный случай класса. Исторически структуры используются в языках программирования раньше классов. В языках PL/1, С и Pascal они представляли собой только совокупность данных (полей класса), но не включали ни методов, ни событий. В языке C++ возможности структур были существенно расширены и они стали настоящими классами, хотя и с некоторыми ограничениями. В языке C# — наследнике C++ — сохранен именно такой подход к структурам. Чем следует руководствоваться, делая выбор между структурой и классом? Полагаю, можно пользоваться следующими правилами: • если необходимо отнести класс к развернутому типу, делайте его структурой; • если у класса число полей относительно невелико, а число возможных объектов относительно велико, делайте его структурой. В этом случае память объектам будет отводиться в стеке, не будут создаваться лишние ссылки, что позволит повысить эффективность работы; • в остальных случаях проектируйте настоящие классы. Поскольку на структуры накладываются дополнительные ограничения, то может возникнуть необходимость в компромиссе — согласиться с ограничениями и использовать структуру либо пожертвовать развернутостью и эффективностью и работать с настоящим классом. Стоит отметить: когда говорится, что все встроенные типы — Структуры Рассмотрим теперь более подробно вопросы описания структур, их синтаксиса, семантики и тех особенностей, что отличают их от классов. Синтаксис объявления структуры аналогичен синтаксису объявления класса: [атрибуты][модификаторы]struct имя_структуры[: список_интерфейсов] {тело_структуры} Какие изменения произошли в синтаксисе в сравнении с синтаксисом класса, описанным в лекции 16? Их немного. Перечислим их: • ключевое слово • список родителей, который для классов, наряду с именами интерфейсов, мог включать имя родительского класса, заменен списком интерфейсов. Для структур не может быть задан родитель (класс или структура). Заметьте, структура может наследовать интерфейсы; • для структур неприменимы модификаторы Все, что может быть вложено в тело класса, может быть вложено и в тело структуры, поля, методы, конструкторы и прочее, включая классы и интерфейсы. Аналогично классу, структура может иметь статические и не статические поля и методы, может иметь несколько конструкторов, в том числе статические и закрытые конструкторы. Для структур можно создавать собственные константы, используя поля с атрибутом Перечислим ограничения, накладываемые на структуры. • Самое серьезное ограничение связано с ограничением наследования. У структуры не может быть наследников. У структуры не может быть задан родительский класс или родительская структура. Конечно, всякая структура, как и любой класс в С#, является наследником класса O • Второе серьезное ограничение связано с процессом создания объектов. Пусть • Если при объявлении класса его поля можно инициализировать, что найдет отражение при работе конструктора класса, то поля структуры не могут быть инициализированы. • Конструктор по умолчанию у структур имеется, при его вызове поля инициализируются значениями по умолчанию. Этот конструктор нельзя заменить, создав собственный конструктор без аргументов. • В конструкторе нельзя вызывать методы класса. Поля структуры должны быть проинициализированы до вызова методов. Класс Вернемся к классу public struct Rational { public Rational(int a, int b) { if(b==0) {m=0; n=1;} else { //приведение знака if(b<0) {b=-b; a=-a;} //приведение к несократимой дроби int р = 1, m1=a, n1 =b; m1=Math.Abs(m1); nl =Math.Abs(n1); if(n1>ml){p=m1; m1=n1; n1=p;} do { p = m1%n1; m1=n1; n1=p; }while (n1!=0); p=m1; m=a/p; n=b/p; } }//Конструктор // поля и методы класса } Все остальное остается без изменения. Приведу пример работы с рациональными числами, представленными структурой: public void TwoSemantics() { Rational r1 = new Rational(1,3), r2 = new Rational (3,5); Rational r3, r4; r3 = r1+r2; r4 = r3; if(r3 >1) r3 = r1+r3 + Rational.One; else r3 = r2+r3 — Rational.One; r3.PrintRational("r3"); r4.PrintRational("r4"); } В этом примере используются константы, работает статический конструктор, закрытый конструктор, перегруженные операции сравнения, арифметические выражения над рациональными числами. В результате вычислений Встроенные структуры Как уже говорилось, все значимые типы языка реализованы структурами. В библиотеке FCL имеются и другие встроенные структуры. Рассмотрим в качестве примера структуры Между четырьмя структурами определены взаимные преобразования: точки можно преобразовать в размеры и наоборот, сложение и вычитание определено над точками и размерами, но не над точками, плавающий тип которых разными способами можно привести к целому. Ряд операций над этими структурами продемонстрирован в следующем примере: public void TestPointAndSize() { Point pt1 = new Point(3,5), pt2 = new Point(7,10), pt3; PointF pt4 = new PointF(4.55f,6.75f); Size sz1 = new Size(10,20), sz2; SizeF sz3 = new SizeF(10.3f, 20.7f); pt3 = Point.Round(pt4); sz2 = new Size (pt1); Console.WriteLine ("pt1: " + pt1); Console.WriteLine ("sz2 =new Size (pt1): " + sz2); Console.WriteLine ("pt4: " + pt4); Console.WriteLine("pt3 =Point.Round(pt4): " + pt3); pt1.Offset (5,7); Console.WriteLine ("pt1.Offset(5,7): + pt1); Console.WriteLine ("pt2: " + pt2); pt2 = pt2 + sz2; Console.WriteLine ("pt2= pt2 + sz2: " + pt2) }//TestPointAndSize Результаты его выполнения показаны на рис. 17.1 Рис. 17.1. Отметим, что метод Tostring, определенный для этих структур, выдает строку со значениями полей в приемлемой для восприятия форме. Еще раз о двух семантиках присваивания В заключение разговора о ссылочных и развернутых типах построим класс public void TestTwoSemantics() { Console.WriteLine("Структуры: присваивание развернутого типа!"); Point pt1 = new Point(3,5), pt2; pt2 = pt1; Console.WriteLine ("pt1: " + pt1); Console.WriteLine ("pt2=pt1: " + pt2); pt1.X +=10; Console.WriteLine ("pt1.X =pt1.X +10: " + pt1); Console.WriteLine ("pt2: " + pt2); Console.WriteLine("Классы: присваивание ссылочного типа!"); CPoint cpt1 = new CPoint(3,5), cpt2; cpt2 = cpt1; Console.WriteLine ("cpt1: " + cpt1); Console.WriteLine ("cpt2=cpt1: " + cpt2); cpt1.X +=10; Console.WriteLine ("cptl.X =cpt1.X +10: " + cpt1); Console.WriteLine ("cpt2: " + cpt2); } Результаты вычислений показаны на рис. 17.2. Рис. 17.2. Действия над объектами Перечисления [атрибуты][модификаторы] enum имя_перечисления[: базовый класс] {список_возможных_значений} Описание атрибутов отложим на последующие лекции. Модификаторами могут быть четыре известных модификатора доступа и модификатор Дело в том, что значения, заданные списком, проецируются на плотное подмножество базового класса. Реально значения объектов перечисления в памяти задаются значениями базового класса, так же, как значения класса Приведу примеры объявлений классов-перечислений: public enum Profession{teacher, engineer, businessman}; public enum MyColors {red, blue, yellow, black, white}; public enum TwoColors {black, white}; public enum Rainbow {красный, оранжевый, желтый, зеленый, голубой, синий, фиолетовый}; public enum Sex: byte {man=1, woman}; public enum Days: long {Sun,Mon,Tue,Wed,Thu, Fri, Sat}; Вот несколько моментов, на которые следует обратить внимание при объявлении перечислений: • как и другие классы, перечисления могут быть объявлены непосредственно в пространстве имен проекта или могут быть вложены в описание класса. Последний вариант часто применяется, когда перечисление используется в одном классе и имеет атрибут доступа • константы разных перечислений могут совпадать, как в перечислениях • константы могут задаваться словами русского языка, как в перечислении • разрешается задавать базовый класс перечисления. Для перечисления • разрешается задавать не только базовый класс, но и указывать начальный элемент подмножества, на которое проецируется множество значений перечисления. Для перечисления Рассмотрим теперь пример работы с объектами — экземплярами различных перечислений: public void TestEnum() { //MyColors colorl = new MyColors(MyColors.blue); MyColors colorl= MyColors.white; TwoColors color2; color2 = TwoColors.white; //if (color1!= color2) color2 = color1; if(color1.ToString ()!= color2.ToString()) Console.WriteLine ("Цвета разные: {0}, {1}", color1, color2); else Console.WriteLine("Цвета одинаковые: {0}, {1}",color1, color2); Rainbow color3; color3 = (Rainbow)3; if (color3!= Rainbow.красный)color3 =Rainbow.красный; int num = (int)color3; Sex who = Sex.man; Days first_work_day = (Days) (long)l; Console.WriteLine ("color1={0}, color2={1}, color3={2}",color1, color2, color3); Console.WriteLine ("who={0}, first_work_day={1}", who,first_work_day); } Данный пример иллюстрирует следующие особенности работы с объектами перечислений-. • объекты перечислений нельзя создавать в объектном стиле с использованием операции new, поскольку перечисления не имеют конструкторов; • объекты можно объявлять с явной инициализацией, как • объекту можно присвоить значение, которое задается константой перечисления, уточненной именем перечисления, как для • нельзя сравнивать объекты разных перечислений, например • существуют явные взаимно обратные преобразования констант базового типа и констант перечисления; • Метод Персоны и профессии Рассмотрим еще один пример работы с перечислениями, приближенный к реальности. Добавим в класс Profession prof; public Profession Prof { get {return (prof);} set {prof = value;} } Добавим еще в класс public void Analysis() { switch (prof) { case Profession.businessman: Console.WriteLine ("профессия: бизнесмен"); break; case Profession.teacher: Console.WriteLine ("профессия: учитель"); break; case Profession.engineer: Console.WriteLine ("профессия: инженер"); break; default: Console.WriteLine ("профессия: неизвестна"); break; } } Приведу простой тестирующий пример работы с объектом Person и его профессией: public void TestProfession () { Person pers1 = new Person ("Петров"); pers1.Prof = Profession.teacher; pers1.Analysis (); } Результаты работы с объектами перечислений, полученные при вызове тестов Рис. 17.3.
18. Отношения между классами. Клиенты и наследники Классы. Отношения между классами. Отношение клиенты — поставщики. Отношение наследования. Единичное наследование. Родители и наследники. Предки и потомки. Что наследуют потомки. Что могут изменить потомки. Одностороннее присваивание. Контроль типов и связывание — статическое и динамическое. Полиморфизм. Проектирование классов. Абстрактные классы. Классы поведения. Каждый класс, как не раз отмечалось, играет две роли: он является модулем — архитектурной единицей, и он имеет содержательный смысл, определяя некоторый тип данных. Но классы программной системы — это ансамбль, в котором классы, играя свои роли, не являются независимыми — все они находятся в определенных отношениях друг с другом. Два основных типа отношений между классами определены в ОО-системах. Первое отношение "клиенты и поставщики", называется часто клиентским отношением или отношением вложенности (встраивания). Второе отношение "родители и наследники" называется отношением наследования. Определение 1. Классы Оба отношения — наследования и вложенности — являются транзитивными. Если Определения 1 и 2 задают прямых или непосредственных клиентов и поставщиков, прямых родителей и наследников. Вследствие транзитивности необходимо ввести понятие уровня. Прямые клиенты и поставщики, прямые родители и наследники относятся к соответствующему уровню 1 (клиенты уровня 1, поставщики уровня 1 и так далее). Затем следует рекурсивное определение: прямой клиент клиента уровня к относится к уровню Для отношения наследования используется терминология, заимствованная из естественного языка. Прямые классы-наследники часто называются Замечу, что цепочки вложенности и наследования могут быть достаточно длинными. На практике вполне могут встречаться цепочки длины 10. Например, библиотечные классы, составляющие систему Microsoft Office, полностью построены на отношении вложенности. При программной работе с объектами Word можно начать с объекта, задающего приложение Word, и добраться до объекта, задающего отдельный символ в некотором слове некоторого предложения одного из открытых документов Word. Для выбора нужного объекта можно задать такую цепочку: приложение Word — коллекция документов — документ — область документа — коллекция абзацев — абзац — коллекция предложений — предложение — коллекция слов — слово — коллекция символов — символ. В этой цепочке каждому понятию соответствует класс библиотеки Microsoft Office, где каждая пара соседствующих классов связана отношением "поставщик-клиент". Классы библиотеки FCL связаны как отношением вложенности, так и отношением наследования. Длинные цепочки наследования достаточно характерны для классов этой библиотеки. Отношения "является" и "имеет" При проектировании классов часто возникает вопрос, какое же отношение между классами нужно построить. Рассмотрим совсем простой пример двух классов — В случае автомобилей, персон и владельцев авто также понятно, что владелец "имеет" автомобиль и "является" персоной. Поэтому класс Person of Отношение вложенности Рассмотрим два класса public class ClassA { public ClassA(string f1, int f2) { fieldA1 = f1; fieldA2 = f2; } public string fieldA1; public int fieldA2; public void MethodA() { Console.WriteLine ("Это класс A"); Console.WriteLine ("поле1 = {0}, поле2 = {1}", fieldA1, fieldA2); } public static void StatMethodA() { string s1 = "Статический метод класса A"; string s2 = "Помните: 2*2 = 4"; Console.WriteLine(s1 + " ***** " + s2); } } Построим теперь класс public class ClassB { public ClassB(string f1A, int f2A, string f1B, int f2B) { inner = new ClassA(f1A, f2A); fieldB1 = f1B; fieldB2 = f2B; } ClassA inner; public string fieldB1; public int fieldB2; public void MethodBl() { inner.MethodA (); Console.WriteLine("Это класс В"); Console.WriteLine ("поле1 = {0}, поле2 = {1}", fieldB1, fieldB2); } } Обратите внимание: конструктор клиента (класса После того как конструктор создал поле — объект поставщика — методы класса могут использовать этот объект, вызывая доступные клиенту методы и поля класса поставщика. Метод класса Расширение определения клиента класса До сих пор мы говорили, что клиент содержит поле, представляющее объект класса поставщика. Это частая, но не единственная ситуация, когда класс является клиентом другого класса. Возможна ситуация, когда метод клиентского класса локально создает объект поставщика, вызывает его методы в собственных целях, но по завершении метода локальный объект заканчивает свою жизнь. Еще одна возможная ситуация — когда объекты поставщика вообще не создаются ни конструктором, ни методами класса клиента, но клиент вызывает статические методы класса поставщика. Оба эти варианта демонстрируют следующие два метода класса public void MethodB2() { ClassA loc = new ClassA("локальный объект A",11); loc.MethodA (); } public void MethodB3() { ClassA.StatMethodA(); } Дадим теперь расширенное определение клиента. Определение 3. Класс Отношения между клиентами и поставщиками Что могут делать клиенты и что могут делать поставщики? Класс-поставщик создает свойства (поля) и сервисы (методы), предоставляемые своим клиентам. Клиенты создают объекты поставщика. Вызывая доступные им методы и поля объектов, они управляют работой созданных объектов поставщика. Клиенты не могут ни повлиять на поведение методов поставщика, ни изменить состав предоставляемых им полей и методов, они не могут вызывать закрытые поставщиком поля и методы класса. Класс-поставщик интересен клиентам своей открытой частью, составляющей интерфейс класса. Но большая часть класса может быть закрыта для клиентов — им незачем вникать в детали представления и в детали реализации. Сокрытие информации вовсе не означает, что разработчики класса не должны быть знакомы с тем, как все реализовано, хотя иногда и такая цель преследуется. В общем случае сокрытие означает, что классы-клиенты строят свою реализацию, основываясь только на интерфейсной части класса-поставщика. Поставщик закрывает поля и часть методов класса от клиентов, задавая для них атрибут доступа В заключение построим тест, проверяющий работу с объектами классов а и в; public void TestClientSupplier () { ClassB objB = new ClassB("AA",22, "BB",33); objB.MethodB1 (); objВ.MethodB2(); objВ.MethodB3 (); } Результаты работы этого теста показаны на рис. 18.1. Рис. 18.1. Сам себе клиент Зададимся вопросом, может ли класс быть сам себе клиентом, другими словами, может ли поле класса быть объектом описываемого класса? Другой, не менее интересный вопрос — могут ли два класса быть одновременно клиентами и поставщиками друг для друга? Ответы на оба вопросы положительны, и подобные ситуации типичны и не являются какой-либо экзотикой. Первая ситуация характерна для динамических структур данных. Элемент односвязного списка имеет поле, представляющее элемент односвязного списка; элемент двусвязного списка имеет два таких поля; узел двоичного дерева имеет два поля, представляющих узлы двоичного дерева. Эта ситуация характерна не только для рекурсивно определяемых структур данных. Вот еще один типичный пример. В классе Не менее часто встречается ситуация, когда классы имеют поля, взаимно ссылающиеся друг на друга. Типичным примером могут служить классы Заметьте, классы устроены довольно просто — их тексты понятны, отношения между классами очевидны. А вот динамический мир объектов этих классов может быть довольно сложным, отношения между объектами могут быть запутанными; для объектов характерны не только любовные треугольники, но и куда более сложные фигуры. Наследование Мощь ООП основана на наследовании. Когда построен полезный класс, то он может многократно использоваться. Повторное использование — это одна из главных целей ООП. Но и для хороших классов неизбежно наступает момент, когда необходимо расширить возможности класса, придать ему новую функциональность, изменить интерфейс. Всякая попытка изменять сам работающий класс чревата большими неприятностями — могут перестать работать прекрасно работавшие программы, многим клиентам класса вовсе не нужен новый интерфейс и новые возможности. Здесь-то и приходит на выручку наследование. Существующий класс не меняется, но создается его потомок, продолжающий дело отца, только уже на новом уровне. Класс-потомок наследует все возможности родительского класса — все поля и все методы, открытую и закрытую часть класса. Правда, не ко всем полям и методам класса возможен прямой доступ потомка. Поля и методы родительского класса, снабженные атрибутом Рассмотрим класс, названный public class Found { //поля protected string name; protected int credit; public Found() { } public Found(string name, int sum) { this.name = name; credit = sum; } public virtual void VirtMethodO { Console.WriteLine ("Отец: " + this.ToString()); } public override string ToString () { return(String.Format("поля: name = {0}, credit = {1}", name, credit)); } public void NonVirtMethod() { Console.WriteLine ("Мать: " + this.ToString()); } public void Analysis() { Console.WriteLine ("Простой анализ"); } public void Work() } VirtMethod(); NonVirtMethod(); Analysis (); } } Заметьте, класс Класс Создадим теперь класс public class Derived: Found } } Тело класса Вот пример работы с объектами родительского и производного класса: public void TestFoundDerived() { Found bs = new Found ("father", 777); Console.WriteLine("Объект bs вызывает методы базового класса"); bs.VirtMethod(); bs.NonVirtMethod (); bs.Analysis(); bs.Work(); Derived der = new Derived (); Console.WriteLine("Объект der вызывает методы класса потомка"); der.VirtMethod(); der.NonVirtMethod(); der.Analysis(); der.Work(); } Результаты работы этой процедуры показаны на рис. 18.2. Рис. 18.2. В чем отличие работы объектов Добавление полей потомком Ничего не делающий самостоятельно потомок не эффективен, от него мало проку. Что же может делать потомок? Прежде всего, он может добавить новые свойства — поля класса. Заметьте, потомок не может ни отменить, ни изменить модификаторы или типы полей, наследованных от родителя — он может только добавить собственные поля. Модифицируем наш класс Derived. Пусть он добавляет новое поле класса, закрытое для клиентов этого класса, но открытое для его потомков: protected int debet; Напомню, хорошей стратегией является стратегия "ничего не скрывать от потомков". Какой родитель знает, что именно из сделанного им может понадобиться потомкам? Конструкторы родителей и потомков Каждый класс должен позаботиться о создании собственных конструкторов. Он не может в этом вопросе полагаться на родителя, поскольку, как правило, добавляет собственные поля, о которых родитель ничего не может знать. Конечно, если не задать конструкторов класса, то будет добавлен конструктор по умолчанию, инициализирующий все поля значениями по умолчанию, как это мы видели в предыдущем примере. Но это редкая ситуация. Чаще всего, класс создает собственные конструкторы и, как правило, не один, задавая разные варианты инициализации полей. При создании конструкторов классов потомков есть одна важная особенность. Всякий конструктор создает объект класса — структуру, содержащую поля класса. Но потомок, прежде чем создать собственный объект, вызывает конструктор родителя, создавая родительский объект, который затем будет дополнен полями потомка. Ввиду транзитивности этого процесса, конструктор родителя вызывает конструктор своего родителя, и этот процесс продолжается, пока первым делом не будет создан объект прародителя. Вызов конструктора родителя происходит не в теле конструктора, а в заголовке, пока еще не создан объект класса. Для вызова конструктора используется ключевое слово base, именующее родительский класс. Как это делается, покажу на примере конструкторов класса Derived: public Derived () {} public Derived(string name, int cred, int deb):base (name,cred) { debet = deb; } Для конструктора без аргументов вызов аналогичного конструктора родителя подразумевается по умолчанию. Для конструкторов с аргументами вызов конструктора с аргументами родительского класса должен быть явным. Этот вызов синтаксически следует сразу за списком аргументов конструктора, будучи отделен от этого списка символом двоеточия. Конструктору потомка передаются все аргументы, необходимые для инициализации полей, часть из которых передаются конструктору родителя для инициализации родительских полей. Итак, вызов конструктора — потомка приводит к цепочке вызовов конструкторов — предков, заканчивающейся вызовом конструктора прародителя. Затем в обратном порядке создаются объекты, начиная с объекта прародителя, и выполняются тела соответствующих конструкторов, инициализирующие поля и выполняющие другую работу этих конструкторов. Последним создается объект потомка и выполняется тело конструктора потомка. Добавление методов и изменение методов родителя Потомок может создать новый собственный метод с именем, отличным от имен наследуемых методов. В этом случае никаких особенностей нет. Вот пример такого метода, создаваемого в классе public void DerivedMethod() { Console.WriteLine ("Это метод класса Derived"); } В отличие от неизменяемых полей классов — предков, класс — потомок может изменять наследуемые им методы. Если потомок создает метод с именем, совпадающим с именем метода предков, то возможны три ситуации: • перегрузка метода. Она возникает, когда сигнатура создаваемого метода отличается от сигнатуры наследуемых методов предков. В этом случае в классе потомка будет несколько перегруженных методов с одним именем, и вызов нужного метода определяется обычными правилами перегрузки методов; • переопределение метода. Метод родителя в этом случае должен иметь модификатор • скрытие метода. Если родительский метод не является виртуальным или абстрактным, то потомок может создать новый метод с тем же именем и той же сигнатурой, скрыв родительский метод в данном контексте. При вызове метода предпочтение будет отдаваться методу потомка, а имя наследуемого метода будет скрыто. Это не означает, что оно становится недоступным. Скрытый родительский метод всегда может быть вызван, если при вызове уточнить ключевым словом base имя метода. Метод потомка, скрывающий метод родителя, следует сопровождать модификатором new, указывающим на новый метод. Если этот модификатор опущен, но из контекста ясно, что речь идет о новом методе, то выдается предупреждающее сообщение при компиляции проекта. Вернемся к нашему примеру. Класс new public void Analysis() { base.Analysis (); Console.WriteLine("Сложный анализ"); } Если модификатор new опустить, он добавится по умолчанию с выдачей предупреждающего сообщения о скрытии метода родителя. Как компилятор узнает, что в этой ситуации речь идет о новом методе? Причины понятны. С одной стороны, родительский метод не имеет модификаторов Заметьте, потомок строит свой анализ на основе метода, наследованного от родителя, вызывая первым делом скрытый родительский метод. Рассмотрим случай, когда потомок добавляет перегруженный метод. Вот пример, когда потомок класса public void Analysis(int level) { base.Analysis (); Console.WriteLine ("Анализ глубины {0}", level); } Большой ошибки не будет, если указать модификатор Статический контроль типов и динамическое связывание Рассмотрим семейство классов x1.M(arg1, arg2… argN) Контролем типов называется проверка каждого вызова, удостоверяющая, что: • в классе • список фактических аргументов в точке вызова соответствует по числу и типам списку формальных аргументов метода Язык С#, как и большинство других языков программирования, позволяет выполнить эту проверку еще на этапе компиляции и в случае нарушений выдать сообщение об ошибке периода компиляции. Контроль типов, выполняемый на этапе компиляции, называется статическим контролем типов. Некоторые языки, например Smalltalk, производят этот контроль динамически — непосредственно перед выполнением метода. Понятно, что ошибки, обнаруживаемые при динамическом контроле типов, трудно исправимы и потому приводят к более тяжелым последствиям. В таких случаях остается уповать на то, что система тщательно отлажена, иначе непонятно, что будет делать конечный пользователь, получивший сообщение о том, что вызываемого метода вообще нет в классе данного объекта. Перейдем к рассмотрению связывания. Напомним, что в рассматриваемом семействе классов метод При статическом связывании метод выбирается из класса сущности, при динамическом — из класса объекта, связанного с сущностью. Понятно, что на этапе компиляции возможно только статическое связывание, поскольку только в период выполнения можно определить, с объектом какого класса связана данная сущность. Это может быть класс любого из потомков класса сущности. Какой же из видов связывания следует применять? Статическое связывание более эффективно в реализации, поскольку может быть сделано на этапе компиляции, так что при выполнении не потребуется никаких проверок. Динамическое связывание требует накладных расходов в период выполнения. Однако во многих случаях преимущества динамического связывания столь значительны, что о затратах не стоит и беспокоиться. Уже достаточно давно разработан эффективный механизм реализации динамического связывания. Еще на этапе компиляции подготавливается так называемая таблица виртуальных методов, содержащая их адреса. Связывание объекта В языке C# принята следующая стратегия связывания. По умолчанию предполагается статическое связывание. Для того чтобы выполнялось динамическое связывание, метод родительского класса должен снабжаться модификатором Три механизма, обеспечивающие полиморфизм Под полиморфизмом в ООП понимают способность одного и того же программного текста В основе полиморфизма, характерного для семейства классов, лежат три механизма: • одностороннее присваивание объектов внутри семейства классов; сущность, базовым классом которой является класс предка, можно связать с объектом любого из потомков. Другими словами, для введенной нами последовательности объектов • переопределение потомком метода, наследованного от родителя. Благодаря переопределению, в семействе классов существует совокупность полиморфных методов с одним именем и сигнатурой; • динамическое связывание, позволяющее в момент выполнения вызывать метод, который принадлежит целевому объекту. В совокупности это и называется полиморфизмом семейства классов. Целевую сущность часто называют полиморфной сущностью, вызываемый метод — Вернемся к нашему примеру с классами public override void VirtMethod() { Console.WriteLine("Сын: " + this.ToString ()); public override string ToString () { return(String.Format("поля: name = {0}, credit = {1},debet ={2}",name, credit, debet)); } Потомок класса public ChildDerived(string name, int cred, int deb):base (name,cred, deb) { } Нет и переопределения метода public override void VirtMethod() { Console.WriteLine("внук: " + this.ToString ()); } В классе public void Work() { VirtMethod(); NonVirtMethod(); Analysis (); } При компиляции метода work будет обнаружено, что вызываемый метод Для не виртуальных методов public void NonVirtMethod() { Console.WriteLine ("Мать: "+ this.ToString ()); } в процессе своей работы вызывает виртуальный метод Что же касается метода Хочу обратить внимание на важный принципиальный момент. Вполне понятно, когда потомки вызывают методы родительского класса. Потомкам все известно о своих предках. Но благодаря полиморфизму методы родительского класса, в свою очередь, могут вызывать методы своих потомков, которых они совсем не знают и которые обычно и не написаны в момент создания родительского класса. Достигается это за счет того, что между родителями и потомками заключается жесткий контракт. Потомок, переопределяющий виртуальный метод, сохраняет сигнатуру метода, сохраняет атрибуты доступа, изменяя реализацию метода, но не форму его вызова. Класс Пример работы с полиморфным семейством классов Классы семейства с полиморфными методами уже созданы. Давайте теперь в клиентском классе Testing напишем метод, создающий объекты наших классов и вызывающий методы классов для объектов семейства: public void TestFoundDerivedReal () { Found bs = new Found ("father", 777); Console.WriteLine("Объект bs вызывает методы класса Found"); bs.VirtMethod(); bs.NonVirtMethod (); bs.Analysis(); bs.Work(); Derived der = new Derived("child", 888, 555); Console.WriteLine("Объект der вызывает методы класса Derived"); der.DerivedMethod(); der.VirtMethod(); der.NonVirtMethod (); der.Analysis(); der.Work(); ChildDerived chider = new ChildDerived("grandchild", 999, 444); Console.WriteLine("Объект chider вызывает методы ChildDerived"); chider.VirtMethod(); chider.NonVirtMethod(); chider.Analysis(5); chider.Work(); } Результаты работы этого метода изображены на рис. 18.3. Рис. 18.3. В последующих лекциях нам неоднократно встретятся более содержательные семейства классов с полиморфизмом, так что мы сумеем еще оценить мощь этого механизма ООП. Абстрактные классы С наследованием тесно связан еще один важный механизм проектирования семейства классов — механизм абстрактных классов. Начну с определений. Класс называется Метод называется Объявление абстрактных методов и абстрактных классов должно сопровождаться модификатором Абстрактные классы являются одним из важнейших инструментов объектно-ориентированного проектирования классов. К сожалению, я не могу входить в детали рассмотрения этой важной темы и ограничусь лишь рассмотрением самой идеи применения абстрактного класса. В основе любого класса лежит абстракция данных. Абстрактный класс описывает эту абстракцию, не входя в детали реализации, ограничиваясь описанием тех операций, которые можно выполнять над данными класса. Так, проектирование абстрактного класса Вот описание полностью абстрактного класса public abstract class Stack { public Stack() { } /// /// втолкнуть элемент item в стек /// /// public abstract void put(int item); /// /// удалить элемент в вершине стека /// public abstract void remove(); /// /// прочитать элемент в вершине стека /// public abstract int item(); /// /// определить, пуст ли стек /// /// public abstract bool IsEmpty(); } Описание класса содержит только сигнатуры методов класса и их спецификацию, заданную тегами public class Linkable { public Linkable() { } public int info; public Linkable next; } В нем — два поля и конструктор по умолчанию. Построим теперь класс public class Liststack: Stack { public Liststack () { top = new Linkable (); } Linkable top; /// /// втолкнуть элемент item в стек /// /// public override void put (int item) { Linkable newitem = new Linkable(); newitem.info = item; newitem.next = top; top = newitem; } /// /// удалить элемент в вершине стека /// public override void remove() { top = top.next; } /// /// прочитать элемент в вершине стека /// public override int item() { return(top.info); } /// /// определить, пуст ли стек /// /// public override bool IsEmpty() { return(top.next == null); } } Класс имеет одно поле Приведу пример работы со стеком: public void TestStack() { ListStack stack = new ListStack(); stack.put (7); stack.put (9); Console.WriteLine(stack.item()); stack.remove(); Console.WriteLine(stack.item()); stack.put (11); stack.put (13); Console.WriteLine(stack.item()); stack.remove(); Console.WriteLine(stack.item()); if(!stack.IsEmpty()) stack.remove(); Console.WriteLine(stack.item()); } В результате работы этого теста будет напечатана следующая последовательность целых: 9, 7, 13, и, 7. Классы без потомков Экзотическим, но иногда полезным видом классов являются классы, для которых запрещается строить классы-потомки путем наследования. При создании такого класса нет необходимости в выполнении над классом каких-либо болезненных операций. Вполне достаточно приписать классу модификатор sealed — он и запрещает построение потомков.
19. Интерфейсы. Множественное наследование Интерфейсы как частный случай класса. Множественное наследование. Проблемы. Множественное наследование интерфейсов. Встроенные интерфейсы. Интерфейсы IComparable, ICIoneable, ISerializable. Поверхностное и глубокое клонирование и сериализация. Сохранение и обмен данными. Слово "интерфейс" многозначное и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом interface. В таком понимании интерфейс — это частный случай класса. Введение в язык частных случаев усложняет его и свидетельствует о некоторых изъянах, для преодоления которых и вводятся частные случаи. Например, введение структур в язык C# позволило определять классы как развернутые типы. Конечно, проще было бы ввести в объявление класса соответствующий модификатор, позволяющий любой класс объявлять развернутым. Но этого сделано не было, а, следуя традиции языка C++, были введены структуры как частный случай классов. Подробнее о развернутых и ссылочных типах см. лекцию 17. Интерфейсы позволяют частично справиться с таким существенным недостатком языка, как отсутствие множественного наследования классов. Хотя реализация множественного наследования встречается с рядом проблем, его отсутствие существенно снижает выразительную мощь языка. В языке C# полного множественного наследования классов нет. Чтобы частично сгладить этот пробел, допускается множественное наследование интерфейсов. Обеспечить возможность классу иметь несколько родителей — один полноценный класс, а остальные в виде интерфейсов, — в этом и состоит основное назначение интерфейсов. Отметим одно важное назначение интерфейсов. Интерфейс позволяет описывать некоторые желательные свойства, которыми могут обладать объекты разных классов. В библиотеке FCL имеется большое число подобных интерфейсов, с некоторыми из них мы познакомимся в этой лекции. Все классы, допускающие сравнение своих объектов, обычно наследуют интерфейс Две стратегии реализации интерфейса Давайте опишем некоторый интерфейс, задающий дополнительные свойства объектов класса: public interface IProps { void Prop1(string s); void Prop2 (string name, int val); } У этого интерфейса два метода, которые и должны будут реализовать все классы — наследники интерфейса. Заметьте, у методов нет модификаторов доступа. Класс, наследующий интерфейс и реализующий его методы, может реализовать их явно, объявляя соответствующие методы класса открытыми. Вот пример: public class Clain: IProps { public Clain() {} public void Propl(string s) { Console.WriteLine(s); } public void Prop2(string name, int val) { Console.WriteLine("name = 10), val ={1}", name, val); } }//Clain Класс реализует методы интерфейса, делая их открытыми для клиентов класса и наследников. Другая стратегия реализации состоит в том, чтобы все или некоторые методы интерфейса сделать закрытыми. Для реализации этой стратегии класс, наследующий интерфейс, объявляет методы без модификатора доступа, что по умолчанию соответствует модификатору public class ClainP: IProps { public ClainP(){ } void IProps.Propl(string s) { Console.WriteLine (s); } void IProps.Prop2(string name, int val) { Console.WriteLine("name = {0}, val ={1}", name, val); } }//class ClainP Класс • • В чем главное достоинство обертывания? Оно позволяет переименовывать методы интерфейса. Метод интерфейса со своим именем закрывается, а потом открывается под тем именем, которое класс выбрал для него. Вот пример обертывания закрытых методов в классе public void MyPropl(string s) { ((IProps)this).Prop1 (s); } public void MyProp2(string s, int x) { ((IProps)this).Prop2(s, x); } Как видите, методы переименованы и получили другие имена, под которыми они и будут известны клиентам класса. В обертке для вызова закрытого метода пришлось использовать кастинг, приведя объект Преобразование к классу интерфейса Создать объект класса интерфейса обычным путем с использованием конструктора и операции new нельзя. Тем не менее, можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса — даже если они закрыты в классе, для интерфейсных объектов они являются открытыми. Приведу соответствующий пример, в котором идет работа как с объектами классов public void TestClainlProps() { Console.WriteLine("Объект класса Clain вызывает открытые методы!"); Clain clain = new Clain (); clain.Prop1(" свойство 1 объекта"); clain.Prop2("Владимир", 44); Console.WriteLine("Объект класса IProps вызывает открытые методы!"); IProps ip = (IProps)clain; ip.Prop1("интерфейс: свойство"); ip.Prop2 ("интерфейс: свойство",77); Console.WriteLine("Объект класса ClainP вызывает открытые методы!"); ClainP clainp = new ClainP (); clainp.MyProp1(" свойство 1 объекта"); clainp.MyProp2("Владимир", 44); Console.WriteLine("Объект класса IProps вызывает закрытые методы!"); IProps ipp = (IProps)clainp; ipp.Prop1("интерфейс: свойство"); ipp.Prop2 ("интерфейс: свойство",77); } Этот пример демонстрирует работу с классом, где все наследуемые методы интерфейса открыты, и с классом, закрывающим наследуемые методы интерфейса. Показано, как обертывание и кастинг позволяют добраться до закрытых методов класса. Результаты выполнения этой тестирующей процедуры приведены на рис. 19.1. Рис. 19.1. Проблемы множественного наследования При множественном наследовании классов возникает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы — коллизию имен и наследование от общего предка. Коллизия имен Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Сразу же заметим, что если имена методов совпадают, но сигнатуры разные, то это не приводит к конфликтам — при реализации у класса наследника просто появляются перегруженные методы. Но что следует делать классу-наследнику в тех случаях, когда сигнатуры методов совпадают? И здесь возможны лее стратегии — склеивание методов и переименование. Стратегия склеивания применяется тогда, когда класс — наследник интерфейсов — полагает, что разные интерфейсы задают один и тот же метод, единая реализация которого и должна быть обеспечена наследником. В этом случае наследник строит единственную общедоступную реализацию, соответствующую методам всех интерфейсов, которые имеют единую сигнатуру. Другая стратегия исходит из того, что, несмотря на единую сигнатуру, методы разных интерфейсов должны быть реализованы по-разному. В этом случае необходимо переименовать конфликтующие методы. Конечно, переименование можно сделать в самих интерфейсах, но это неправильный путь: наследники не должны требовать изменений своих родителей — они сами должны меняться. Переименование методов интерфейсов иногда невозможно чисто технически, если интерфейсы являются встроенными или поставляются сторонними фирмами. К счастью, мы знаем, как производить переименование метода интерфейса в самом классе наследника. Напомню, для этого достаточно реализовать методы разных интерфейсов как закрытые, а затем открыть их с переименованием. Итак, коллизия имен при множественном наследовании интерфейсов хотя и возможна, но легко разрешается. Разработчик класса может выбрать любую из двух возможных стратегий, наиболее подходящую для данного конкретного случая. Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса — наследника этих интерфейсов, применяющего разные стратегии реализации для конфликтующих методов. public interface IProps { void Prop1(string s); void Prop2 (string name, int val); void Ргор3 (); } public interface IPropsOne { void Prop1(string s); void Prop2 (int val); void Ргор3 (); } У двух интерфейсов — по три метода с совпадающими именами, сигнатуры двух методов совпадают, а в одном случае различаются. Вот класс, наследующий оба интерфейса: public class ClainTwo: IProps,IPropsOne { /// /// склеивание методов двух интерфейсов /// /// public void Prop1 (string s) { Console.WriteLine (s); } /// /// перегрузка методов двух интерфейсов /// /// /// public void Prop2(string s, int x) { Console.WriteLine(s +"; " + x); } public void Prop2 (int x) { Console.WriteLine(x); } /// /// переименование методов двух интерфейсов /// void IProps.Ргор3() { Console.WriteLine("Свойство 3 интерфейса 1"); } void IPropsOne.Ргор3() { Console.WriteLine("Свойство 3 интерфейса 2"); } public void Prop3FromInterface 1() { ((IProps)this). Ргор3(); } public void Prop3FromInterface2() { ((IPropsOne)this). Ргор3(); } } Для первого из методов с совпадающей сигнатурой выбрана стратегия склеивания, так что в классе есть только один метод, реализующий методы двух интерфейсов. Методы с разной сигнатурой реализованы двумя перегруженными методами класса. Для следующей пары методов с совпадающей сигнатурой выбрана стратегия переименования. Методы интерфейсов реализованы как закрытые методы, а затем в классе объявлены два новых метода с разными именами, являющиеся обертками закрытых методов класса. Приведу пример работы с объектами класса и интерфейсными объектами: public void TestCliTwoInterfaces() { Console.WriteLine("Объект ClainTwo вызывает методы двух интерфейсов!"); Cli.ClainTwo claintwo = new Cli.ClainTwo (); claintwo.Prop1("Склейка свойства двух интерфейсов"); claintwo.Ргор2("перегрузка::: ",99); claintwo.Prop2(9999); claintwo.Prop3FromInterface1 (); claintwo.Prop3FromInterface2 (); Console.WriteLine("Интерфейсный объект вызывает методы 1-го интерфейса!"); Cli.IProps ip1 = (Cli.IProps)claintwo; ip1.Prop1("интерфейс IProps: свойство 1"); ip2.Prop2("интерфейс 1", 88); ip1.Ргор3 (); Console.WriteLine("Интерфейсный объект вызывает методы 2-го интерфейса!"); Cli.IPropsOne ip2 = (Cli.IPropsOne)claintwo; ip2.Propl("интерфейс IPropsOne: свойство1"); ip2.Prop2 (7777); ip2.Ргор3 (); } Результаты работы тестирующей процедуры показаны на рис. 19.2. Рис. 19.2. Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс Для интерфейсов сама ситуация дублирующего наследования маловероятна, но возможна, поскольку интерфейс, как и любой класс, может быть наследником другого интерфейса. Поскольку у интерфейсов наследуются только сигнатуры, а не реализации, как в случае классов, то проблема дублирующего наследования сводится к проблеме коллизии имен. По-видимому, естественным решением этой проблемы в данной ситуации является склеивание, когда методам, пришедшим разными путями от одного родителя, будет соответствовать единая реализация. Начнем наш пример с наследования интерфейсов: public interface IParent { void ParentMethod(); } public interface ISonl: IParent { void SonlMethod(); } public interface ISon2:IParent { void Son2Method(); } Два сыновних интерфейса наследуют метод своего родителя. А теперь рассмотрим класс, наследующий оба интерфейса-. public class Pars: ISon1, ISon2 { public void ParentMethod() { Console.WriteLine("Это метод родителя!"); } public void Son1Method() { Console.WriteLine("Это метод старшего сына!"); } public void Son2Method() { Console.WriteLine("Это метод младшего сына!"); } }//class Pars Класс обязан реализовать метод public void TestlParsons () { Console.WriteLine("Объект класса вызывает методы трех интерфейсов!"); Cli. Pars ct = new Cli.Pars(); ct.ParentMethod(); ct.Son1Method(); ct.Son2Method(); Console.WriteLine("Интерфейсный объект 1 вызывает свои методы!"); Cli.IParent ip = (IParent)ct; ip.ParentMethod(); Console.WriteLine("Интерфейсный объект 2 вызывает свои методы!"); Cli.ISonl ipl = (ISon1)ct; ip1.ParentMethod (); ip1.SonlMethod(); Console.WriteLine("Интерфейсный объект 3 вызывает свои методы!"); Cli.ISon2 ip2 = (ISon2)ct; ip2.ParentMethod (); ip2.Son2Method(); } Результаты работы тестирующей процедуры показаны на рис. 19.3. Рис. 19.3. Встроенные интерфейсы Рассмотрим несколько встроенных интерфейсов, являющихся частью библиотеки FCL. Они используются многими классами-библиотеками так же, как и классами, создаваемыми пользователем. Упорядоченность объектов и интерфейс Часто, когда создается класс, желательно задать отношение порядка на его объектах. Такой класс следует объявить наследником интерфейса Как правило, в классе вначале определяют метод Давайте введем отношение порядка на классе public class Person: IComparable { public int CompareTo(object pers) { const string s = "Сравниваемый объект не принадлежит классу Person"; Person р = pers as Person; if (!p.Equals(null)) return (fam.CompareTo(p.fam)); throw new ArgumentException (s); } // другие компоненты класса } Поскольку аргумент метода должен иметь универсальный тип При приведении типов часто используются операции Заметьте также, что при проверке на значение Отношение порядка на объектах класса Конечно, сравнение персон может выполняться по разным критериям: возрасту, росту, зарплате. Общий подход к сравнению персон будет рассмотрен в следующей лекции 20. Введем теперь в нашем классе Person перегрузку операций отношения: public static bool operator <(Person p1, Person p2) { return (pi.CompareTo(p2) < 0); } public static bool operator >(Person p1, Person p2) { return (pi.CompareTo(p2) > 0); } public static bool operator <=(Person p1, Person p2) { return (pi.CompareTo(p2) <= 0); } public static bool operator >= (Person p1, Person p2) { return (pi.CompareTo(p2) >=0); } public static bool operator == (Person p1, Person p2) { return (pi.CompareTo(p2) == 0); } public static bool operator!= (Person p1, Person p2) { return (p1.CompareTo(p2)!= 0); } Как обычно, приведу тестовый пример, проверяющий работу с введенными методами: public void TestCompare() { Person poet1 = new Person("Пушкин"); Person poet2 = new Person("Лермонтов"); Person poet3 = new Person("Пастернак"); Person poet4 = new Person("Мандельштам"); Person poet5 = new Person("Ахматова"); Person poet6 = new Person("Цветаева"); Console.WriteLine("{0} > {1} = {2}", poet1.Fam, poet2.Fam, (poet1 > poet2)); Console.WriteLine("{0} >= {1} = {2}", poet3.Fam, poet4.Fam, (poet3 >= poet4)); Console.WriteLine("{0}!= {1} = {2}", poet5.Fam, poet6.Fam, (poet5!= poet6)); } Вот результаты работы этого теста. Рис. 19.4. Конечно, заданный нами порядок не имеет никакого отношения к поэтическому дару, а лишь говорит об относительном расположении фамилий поэтов в словарях. Клонирование и интерфейс Глубокое клонирование требует рекурсивной процедуры обхода существующей структуры объектов, тщательно отработанной во избежание проблемы зацикливания. В общем случае, когда есть несколько классов, являющихся взаимными клиентами, глубокое клонирование требует наличия в каждом классе рекурсивной процедуры. Эти процедуры взаимно вызывают друг друга. Я не буду в этой лекции приводить примеры, хотя среди проектов, поддерживающих книгу, есть и проект, реализующий глубокое клонирование, где объекты разных классов связаны взаимными ссылками. Поверхностное клонирование можно выполнить достаточно просто. Наиболее простой путь — клонирование путем вызова метода Давайте обеспечим эту возможность для класса public Person StandartClone() { Person p = (Person)this.MemberwiseClone (); return(p); } Теперь клиенты класса могут легко создавать поверхностные клоны. Вот пример: public void TestStandartClone() { Person mother = new Person("Петрова Анна"); Person daughter = new Person ("Петрова Ольга"); Person son = new Person("Петров Игорь"); mother[0] = daughter; mother[1] = son; Person mother_clone = mother.StandartClone(); Console.WriteLine("Дети матери: {0}",mother.Fam); Console.WriteLine (mother[0].Fam); Console.WriteLine (mother[1].Fam); Console.WriteLine("Дети клона: {0}",mother_clone.Fam); Console.WriteLine (mother_clone[0].Fam); Console.WriteLine (mother_clone[1].Fam); } При создании клона будет создана копия только одного объекта Рис. 19.5. Если стандартное поверхностное клонирование нас не устраивает, то класс можно объявить наследником интерфейса Давайте расширим наш класс, придав ему родительский интерфейс public object Clone() { Person р = new Person(this.fam + "_clone"); //копирование полей p.age = this.age; p.children = this.children; p.count_children = this.count_children; p.health = this.health; p.salary = this.salary; p.status = this.status; return (p); } Эта реализация является слегка модифицированной версией стандартного поверхностного клонирования. Я добавил несколько строчек в тестирующую процедуру для проверки работы этой версии клона: Person mother_clone2 = (Person)mother.Clone(); Console.WriteLine("Дети клона_2: {0}",mother_clone2.Fam); Console.WriteLine (mother_clone2[0].Fam); Console.WriteLine (mother_clone2[1].Fam); Все работает должным образом. Сериализация объектов При работе с программной системой зачастую возникает необходимость в сериализации объектов. Под Сериализация позволяет запомнить рубежные состояния системы объектов с возможностью последующего возвращения к этим состояниям. Она необходима, когда завершение сеанса работы не означает завершение вычислений. В этом случае очередной сеанс работы начинается с восстановления состояния, сохраненного в конце предыдущего сеанса работы. Альтернативой сериализации является работа с обычной файловой системой, с базами данных и другими хранилищами данных. Поскольку механизмы сериализации, предоставляемые языком С#, эффективно поддерживаются. Net Framework, то при необходимости сохранения данных значительно проще и эффективнее пользоваться сериализацией, чем самому организовывать их хранение и восстановление. Еще одно важное применение сериализации — это обмен данными удаленных систем. При удаленном обмене данными предпочтительнее формат xml из-за открытого стандарта передачи данных в Интернете по soap-протоколу, из-за открытого стандарта на структуру xml-документов. Обмен становится достаточно простым даже для систем, построенных на разных платформах и в разных средах разработки. Так же, как и клонирование, сериализация может быть поверхностной, когда сериализуется на одном шаге единственный объект, и глубокой, когда, начиная с корневого объекта, сериализуется совокупность объектов, связанных взаимными ссылками (граф объектов). Глубокую сериализацию, часто обязательную, самому организовать непросто, так как она требует, как правило, рекурсивного обхода структуры объектов. Если класс объявить с атрибутом [Serializable], то в него встраивается стандартный механизм сериализации, поддерживающий, что крайне приятно, глубокую сериализацию. Если по каким-либо причинам стандартная сериализация нас не устраивает, то класс следует объявить наследником интерфейса Класс с атрибутом сериализации Класс, объекты которого предполагается сериализовать стандартным образом, должен при объявлении сопровождаться атрибутом [Serializable]. Стандартная сериализация предполагает два способа сохранения объекта: в виде бинарного потока символов и в виде xml-документа. В бинарном потоке сохраняются все поля объекта, как открытые, так и закрытые. Процессом этим можно управлять, помечая некоторые поля класса атрибутом [Nonserialized] — эти поля сохраняться не будут: [Serializable] public class Test { public string name; [NonSerialized] int id; int age; //другие поля и методы класса } В класс Для запуска механизма необходимо создать объект, называемый форматером и выполняющий сериализацию и десериализацию данных с подходящим их форматированием. Библиотека FCL предоставляет два класса форматеров. Бинарный форматер, направляющий данные в бинарный поток, принадлежит классу System.Runtime.Serialization.Formatters.Binary Давайте разберемся, как устроен этот класс. Он является наследником двух интерфейсов: В пространстве имен библиотеки FCL: System.Runtime.Serialization.Formatters.Soap находится класс Из новых средств, еще не рассматривавшихся в наших лекциях, для организации сериализации понадобятся файлы. Пространство имен ю библиотеки FCL предоставляет классы, поддерживающие ввод-вывод данных. В частности, в этом пространстве есть абстрактный класс В качестве примера промоделируем сказку Пушкина "О рыбаке и рыбке". Как вы помните, жадная старуха богатела, богатела, но после очередного желания оказалась у разбитого корыта, вернувшись в начальное состояние. Сериализация позволит нам запомнить начальное состояние, меняющееся по мере выполнения рыбкой первых пожеланий рыбака и его старухи. Десериализация вернет все в начальное состояние. Опишу класс, задающий героев пушкинской сказки: [Serializable] public class Personage { public Personage(string name, int age) { this.name = name; this.age = age; } //поля класса static int wishes; public string name, status, wealth; int age; public Personage couple; //методы класса } Герои сказки — объекты этого класса обладают свойствами, задающими имя, возраст, статус, имущество и супруга. Имя и возраст задаются в конструкторе класса, а остальные свойства задаются в следующем методе: public void marry (Personage couple) { this.couple = couple; couple.couple = this; this.status ="крестьянин"; this.wealth ="рыбацкая сеть"; this.couple.status = "крестьянка"; this.couple.wealth = "корыто"; SaveState (); } Предусловие метода предполагает, что метод вызывается один раз главным героем (рыбаком). В методе устанавливаются взаимные ссылки между героями сказки, их начальное состояние. Завершается метод сохранением состояния объектов, выполняемого при вызове метода void SaveState () { BinaryFormatter bf = new BinaryFormatter (); FileStream fs = new FileStream ("State.bin",FileMode.Create, FileAccess.Write); bf.Serialize(fs,this); fs.Close (); } Здесь и выполняется сериализация графа объектов. Как видите, все просто. Вначале создается форматер — объект Нам понадобится еще метод, описывающий жизнь героев сказки: public Personage AskGoldFish() { Personage fisher = this; if (fisher.name == "рыбак") { wishes++; switch (wishes) { case 1: ChangeStateOne();break; case 2: ChangeStateTwo();break; case 3: ChangeStateThree();break; default: BackState(ref fisher);break; } } return(fisher); }//AskGoldFish Метод реализует анализ желаний героини сказки. Первые три желания исполняются, и состояние героев меняется: void ChangeStateOne () { this.status = "муж дворянки"; this.couple.status = "дворянка"; this.couple.wealth = "имение"; } void ChangeStateTwo () { this.status = "муж боярыни"; this.couple.status = "боярыня"; this.couple.wealth = "много поместий"; } void ChangeStateThree() { this.status = "муж государыни"; this.couple.status = "государыня"; this.couple.wealth = "страна"; } Начиная с четвертого желания, все возвращается в начальное состояние — выполняется десериализация графа объектов: void BackState(ref Personage fisher) { BinaryFormatter bf = new BinaryFormatter (); FileStream fs = new FileStream ("State.bin",FileMode.Open, FileAccess.Read); fisher = (Personage)bf.Deserialize(fs); fs.Close(); } Обратите внимание, что у метода есть аргумент, передаваемый по ссылке. Этот аргумент получает значение — ссылается на объект, создаваемый методом public void About() { Console.WriteLine("имя = {0}, возраст = {1},"+ "статус = {2}, состояние ={3}",name,age,status, wealth); Console.WriteLine("имя = {0}, возраст = {1}," + "статус = {2}, состояние ={3}", this.couple.name, this.couple.age,this.couple.status, this.couple.wealth); } Для завершения сказки нам нужно в клиентском классе создать ее героев: public void TestGoldFish() { Personage fisher = new Personage("рыбак", 70); Personage wife = new Personage("старуха", 70); fisher.marry(wife); Console.WriteLine("До золотой рыбки"); fisher.About(); fisher = fisher.AskGoldFish(); Console.WriteLine("Первое желание"); fisher.About(); fisher = fisher.AskGoldFish(); Console.WriteLine("Второе желание"); fisher.About(); fisher = fisher.AskGoldFish(); Console.WriteLine("Третье желание"); fisher.About(); fisher = fisher.AskGoldFish(); Console.WriteLine("Еще хочу"); fisher.About(); fisher = fisher.AskGoldFish(); Console.WriteLine("Хочу, но уже поздно"); fisher.About(); } На рис. 19.6 показаны результаты исполнения сказки. Рис. 19.6. Что изменится, если перейти к сохранению данных в xml-формате? немногое. Нужно лишь заменить объявление форматера: void SaveStateXML() { SoapFormatter sf = new SoapFormatter(); FileStream fs = new FileStream ("State.xml",FileMode.Create, FileAccess.Write); sf.Serialize(fs,this); fs.Close (); } void BackStateXML(ref Personage fisher) { SoapFormatter sf = new SoapFormatter(); FileStream fs = new FileStream ("State.xml",FileMode.Open, FileAccess.Read); fisher = (Personage)sf.Deserialize(fs); fs.Close (); } Клиент, работающий с объектами класса, этих изменений и не почувствует. Результаты вычислений останутся теми же, что и в предыдущем случае. Правда, файл, сохраняющий данные, теперь выглядит совсем по-другому. Это обычный xml-документ, который мог быть создан в любом из приложений. Вот как выглядит этот документ, открытый в браузере Internet Explorer. Рис. 19.7. Интерфейс При необходимости можно самому управлять процессом сериализации. В этом случае наш класс должен быть наследником интерфейса Конечно, возможность управлять сохранением и восстановлением данных дает большую гибкость и позволяет, в конечном счете, уменьшить размер файла, хранящего данные, что может быть крайне важно, особенно если речь идет об обмене данными с удаленным приложением. Если речь идет о поверхностной сериализации, то атрибут Рассмотрим, как устроен метод GetObjectData (Serializedlnfo info, StreamingContext context) Поскольку самому вызывать этот метод не приходится — он вызывается автоматически методом info.AddValue("name",name); infо. AddValue("age", age); Поскольку имена полей уникальны, то их разумно использовать в качестве ключей. Если поле son класса Father является объектом класса child и этот класс сериализуем, то для сохранения объекта son следует вызвать метод: son.GetObjectData(info, context) Если не возникает циклов, причиной которых являются взаимные ссылки, то особых сложностей с сериализацией и десериализацией не возникает. Взаимные ссылки осложняют картину и требуют индивидуального подхода к решению. На последующем примере мы покажем, как можно справиться с этой проблемой в конкретном случае. Перейдем теперь к рассмотрению специального конструктора класса. Он может быть объявлен с атрибутом доступа private, но лучше, как и во многих других случаях, применять атрибут protected, что позволит использовать этот конструктор потомками класса, осуществляющими собственную сериализацию. У конструктора те же аргументы, что и у метода name = infо. GetString("name"); age = infо. GetInt32("age"); Восстановление поля son, являющегося ссылочным типом, выполняется вызовом его специального конструктора: son = new Child(info, context); А теперь вернемся к нашему примеру со стариком, старухой и золотой рыбкой. Заменим стандартную сериализацию собственной. Для этого, оставив атрибут сериализации у класса [Serializable] public class Personage: ISerializable {…} Добавим в наш класс специальный метод, вызываемый при сериализации — метод сохранения данных: //Специальный метод сериализации public void GetObjectData(Serializationlnfо info, StreamingContext context) { info.AddValue("name",name); infо. AddValue("age", age); infо.AddValue("status",status); infо.AddValue("wealth", wealth); info.AddValue("couplename",couple.name); info.AddValue("coupleage", couple.age); infо.AddValue("couplestatus",couple.status); infо. AddValue("couplewealth", couple.wealth); } В трех первых строках сохраняются значимые поля объекта и тут все ясно. Но вот запомнить поле, хранящее объект couple класса Personage, напрямую не удается. Попытка рекурсивного вызова couple.GetobjectData(info,context); привела бы к зацикливанию, если бы раньше из-за повторяющегося ключа не возникала исключительная ситуация в момент записи поля name объекта couple. Поэтому приходится явно сохранять поля этого объекта уже с другими ключами. Понятно, что с ростом сложности структуры графа объектов задача существенно осложняется. Добавим в наш класс специальный конструктор, вызываемый при десериализации — конструктор восстановления состояния: //Специальный конструктор сериализации protected Personage (Serializationlnfo info, StreamingContext context) { name = infо. GetString("name"); age = infо. Getlnt32("age"); status = infо. GetString("status"); wealth = infо. GetString("wealth"); couple = new Personage(infо. GetString("couplename"), infо. Getlnt32("coupleage")); couple.status = infо. GetString("couplestatus"); couple.wealth = infо. GetString("couplewealth"); this.couple = couple; couple.couple = this; } Опять первые строки восстановления значимых полей объекта прозрачно ясны. А с полем Кроме введения конструктора класса и метода Мораль: должны быть веские основания для отказа от стандартно реализованной сериализации. Повторюсь, такими основаниями могут служить необходимость в уменьшении объема файла, хранящего данные, и в сокращении времени передачи данных. Когда в нашем примере вводилось собственное управление сериализацией, то не ставилась цель минимизации объема хранимых данных, в обоих случаях сохранялись одни и те же данные. Тем не менее представляет интерес взглянуть на таблицу, хранящую объемы создаваемых файлов. Формат ∙ Сериализация ∙ Размер файла Бинарный поток ∙ Стандартная ∙ 355 байтов Бинарный поток ∙ Управляемая ∙ 355 байтов XML-документ ∙ Стандартная ∙ 1,14 Кб. XML-документ ∙ Управляемая ∙ 974 байта Преимуществами XML-документа являются его читабельность и хорошо развитые средства разбора, но зато бинарное представление выигрывает в объеме и скорости передачи тех же данных.
20. Функциональный тип в С#. Делегаты Новое слово для старого понятия. Функциональный тип. Функции высших порядков. Вычисление интеграла и сортировка. Два способа взаимодействия частей при построении сложных систем. Функции обратного вызова. Наследование и функциональные типы. Сравнение двух подходов. Класс Delegate. Методы и свойства класса. Операции над делегатами. Комбинирование делегатов. Список вызовов. Слово делегат (delegate) используется в C# для обозначения хорошо известного понятия. Делегат задает определение функционального типа (класса) данных. Экземплярами класса являются функции. Описание делегата в языке C# представляет собой описание еще одного частного случая класса. Каждый делегат описывает множество функций с заданной сигнатурой. Каждая функция (метод), сигнатура которого совпадает с сигнатурой делегата, может рассматриваться как экземпляр класса, заданного делегатом. Синтаксис объявления делегата имеет следующий вид: [<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>); Этим объявлением класса задается функциональный тип — множество функций с заданной сигнатурой, у которых аргументы определяются списком, заданным в объявлении делегата, и тип возвращаемого значения определяется типом результата делегата. Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности: • непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов; • внутри другого класса, наряду с объявлениями методов и свойств. Такое объявление рассматривается как объявление вложенного класса. Так же, как и интерфейсы С#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, должны объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Если контракт выполняется, то можно создать экземпляры делегата, присвоив им в качестве значений функции, удовлетворяющие контракту. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра — Начнем примеры этой лекции с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс: namespace Delegates { //объявление классов — делегатов delegate void Proc(ref int x); delegate void MesToPers(string s); class OwnDel { public delegate int Fun1(int x); int Plus1(int x){return(x+100);}//Plus1 int Minus1(int x){return(x-100);}//Minus1 void Plus(ref int x){x+= 100;} void Minus(ref int x){x-=100;} //поля класса public Proc p1; public Fun1 f1; char sign; //конструктор public OwnDel(char sign) { this.sign = sign; if (sign == '+') {p1 = new Proc(Plus);f1 = new Fun1(Plus1);} else {p1 = new Proc(Minus);f1 = new Fun1(Minus1);} } }//class OwnDel Прокомментирую этот текст. • Первым делом объявлены три функциональных класса — три делегата: • В классе • Поля • В конструкторе класса поля Заметьте, экземпляры делегатов можно рассматривать как ссылки (указатели на функции), а методы тех или иных классов с соответствующей сигнатурой можно рассматривать как объекты, хранимые в динамической памяти. В определенный момент происходит связывание ссылки и объекта (в этой роли выступают не обычные объекты, имеющие поля, а методы, задающие код). Взгляд на делегата как на указатель функции характерен для программистов, привыкших к C++. Приведу теперь процедуру, тестирующую работу созданного класса: public void TestOwnDel() { int account = 1000, account1=0; OwnDel oda = new OwnDel('+'); Console.WriteLine("account = {0}, account1 = {1}", account, account1); oda.pl(ref account); account1=oda.f1 (account); Console.WriteLine("account = {0}, accountl = {1}", account, account1); } Клиент класса В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в самом классе. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим многократно встречавшийся класс class Person { //конструкторы public Person(){name =""; id=0; salary=0.0;} public Person(string name){this.name = name;} public Person (string name, int id, double salary) {this.name = name; this.id=id; this.salary = salary;} public Person (Person pers) {this.name = pers.name; this.id = pers.id; this.salary = pers.salary;} //методы public void ToPerson(string mes) { this.message = mes; Console.WriteLine("{0}, {l}",name, message); } //свойства private string name; private int id; private double salary; private string message; //доступ к свойствам public string Name {get {return(name);} set {name = value;}} public double Salary {get {return(salary);} set {salary = value;}} public int Id {get {return(id);} set {id = value;}} }//class Person Класс Person man2 = new Person("Владимир"); MesToPers mestopers = new MesToPers(man2.ToPerson); mestopers("пора работать!"); Обратите внимание, что поскольку метод Последние три строки были добавлены в вышеприведенную тестирующую процедуру. Взгляните на результаты ее работы. Рис. 20.1. Одно из наиболее важных применений делегатов связано с функциями высших порядков. Вычисление интеграла Давайте более подробно рассмотрим ситуацию с функциями высшего порядка на примере задачи вычисления определенного интеграла с заданной точностью. С этой целью создадим класс, в котором будет описан делегат, определяющий контракт, коему должны удовлетворять подынтегральные функции. В этом же классе определим метод, вычисляющий интеграл. По сути самой задачи этот метод представляет собой функцию высшего порядка. Приведу программный код, описывающий класс: public class HighOrderIntegral { //delegate public delegate double SublntegralFun(double x); public double Evallntegral(double a, double b, double eps,SublntegralFun sif) { int n=4; double I0=0, I1 = I(a, b, n,sif); for(n=8; n < Math.Pow(2.0,15.0); n*=2) { I0 =I1; I1=I(a,b,n,sif); if(Math.Abs(I1-10) } if(Math.Abs(I1–10)< eps) Console.WriteLine("Требуемая точность достигнута! "+ " eps = {0}, достигнутая точность ={1}, n= {2}", eps,Math.Abs(11–10), n); else Console.WriteLine("Требуемая точность не достигнута! "+ " eps = {0}, достигнутая точность ={1}, n= {2}", eps,Math.Abs(I1–I0), n); return(I1); } private double I(double a, double b, int n, SublntegralFun sif) { //Вычисляет частную сумму по методу трапеций double х = a, sum = sif(x)/2, dx = (b-a)/n; for (int i= 2; i <= n; i++) { x += dx; sum += sif (x); } x = b; sum += sif(x)/2; return(sum*dx); } }//class HighOrderIntegral Прокомментирую этот текст: • Класс • Метод • Для вычисления интеграла применяется классическая схема. Интервал интегрирования разбивается на • Вычисление частичной суммы интеграла по методу трапеций реализовано закрытой процедурой • Впоследствии класс может быть расширен, и помимо вычисления интеграла он может вычислять и другие характеристики функций. Чтобы продемонстрировать работу с классом class functions { //подынтегральные функции static public double sif1(double x) { int k = 1; int b = 2; return (double)(k*x +b); } static public double sif2(double x) { double a = 1.0; double b = 2.0; double c= 3.0; return (double)(a*x*x +b*x +c); } }//class functions А теперь рассмотрим метод класса клиента, выполняющий создание нужных объектов и тестирующий их работу: public void TestEvalIntegrals () { double myint1=0.0; HighOrderIntegral.SubIntegralFun hoisifl = new HighOrderIntegral.SubIntegralFun(functions.sif1); HighOrderIntegral hoi = new HighOrderIntegral(); myint1 = hoi.EvalIntegral(2,3,0.le-5,hoisifl); Console.WriteLine("myIntegral1 = {0}",myint1); HighOrderIntegral.SubIntegralFun hoisif2 = new HighOrderIntegral.SublntegralFun(functions.sif2); myint1= hoi.Evaiintegral(2,3,0.1e-5,hoisif2); Console.WriteLine("myIntegral2 = {0}",myint1); }//Evallntegrals Здесь создаются два экземпляра делегата и объект класса Рис. 20.2. Построение программных систем методом "раскрутки". Функции обратного вызова Рис. 20.3. Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время они были созданы методом раскрутки. Это позволило написать на 95 % на языке С транслятор с языка С и операционную систему. Благодаря этому, обеспечивался легкий перенос транслятора и операционной системы на компьютеры с разной системой команд. Замечу, что в те времена мир компьютеров отличался куда большим разнообразием, чем в нынешнее время. Для переноса системы на новый тип компьютера достаточно было написать ядро системы в соответствии с машинным кодом данного компьютера, далее работала раскрутка. При построении систем методом раскрутки возникает одна проблема. Понятно, что функциям внешнего слоя известно все о внутренних слоях и они без труда могут вызывать функции внутренних слоев. Но как быть, если функциям внутреннего слоя необходимо вызывать функции внешних, еще не написанных и, возможно, еще не спроектированных слоев? Возможна ли симметрия вызовов? На первый взгляд, это кажется невозможным. Но программисты придумали, по крайней мере, два способа решения этой проблемы. Оба они используют контракты. Один основан на функциях обратного вызова, другой — на наследовании и полиморфизме. Мы разберем оба способа, но начнем с функций обратного вызова. Пусть F — функция высшего порядка с параметром Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс Многие из функций операционной системы Windows, входящие в состав Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением синтаксиса объявления конструктора объекта public Timer(TimerCallback callback,object state, int dueTime, int period); Первым параметром конструктора является функция обратного вызова public delegate void TimerCallback(object state); Наследование и полиморфизм — альтернатива обратному вызову Сегодня многие программные системы проектируются и разрабатываются не в функциональном, а в объектно-ориентированном стиле. Такая система представляет собой одно или несколько семейств интерфейсов и классов, связанных отношением наследования. Классы-потомки наследуют методы своих родителей, могут их переопределять и добавлять новые методы. Переопределив метод родителя, потомки без труда могут вызывать как собственный метод, так и метод родителя; все незакрытые методы родителя им известны и доступны. Но может ли родитель вызывать методы, определенные потомком, учитывая, что в момент создания родительского метода потомок не только не создан, но еще, скорее всего, и не спроектирован? Тем не менее, ответ на этот вопрос положителен. Достигается такая возможность опять-таки благодаря контрактам, заключаемым при реализации полиморфизма. О полиморфизме говорилось достаточно много в предыдущих лекциях. Тем не менее, позволю напомнить суть дела. Родитель может объявить свой метод виртуальным, в этом случае в контракте на метод потомку разрешается переопределить реализацию, но он не имеет права изменять сигнатуру виртуального метода. Когда некоторый метод родителя Идея примера такова. Вначале построим родительский класс, метод которого будет вычислять интеграл от некоторой подынтегральной функции, заданной виртуальным методом класса. Далее построим класс-потомок, наследующий родительский метод вычисления интеграла и переопределяющий виртуальный метод, в котором потомок задаст собственную подынтегральную функцию. При такой технологии, всякий раз, когда нужно вычислить интеграл, нужно создать класс-потомок, в котором переопределяется виртуальный метод. Приведу пример кода, следующего этой схеме: class FIntegral { //базовый класс, в котором определен метод вычисления //интеграла и виртуальный метод, задающий базовую //подынтегральную функцию public double Evaluatelntegral(double a, double b, double eps) { int n=4; double I0 = 0, I1 = I (a, b, n); for(n=8; n < Math.Pow(2.0,15.0); n*=2) { I0 =I1; I1=1(a,b,n); if(Math.Abs(I1-I0) } if(Math.Abs(I1–10)< eps) Console.WriteLine("Требуемая точность достигнута! "+ " eps = {0}, достигнутая точность ={1}, n= {2}", eps,Math.Abs(I1–I0), n); else Console.WriteLine("Требуемая точность не достигнута! "+ " eps = {0}, достигнутая точность ={1}, n= {2}", eps,Math.Abs(I1–I0), n); return (I1); } private double I(double a, double b, int n) { //Вычисляет частную сумму по методу трапеций double х = a, sum = sif(x)/2, dx = (b-a)/п; for (int i= 2; i <= n; i++) { x += dx; sum += sif(x); } x = b; sum += sif(x)/2; return(sum*dx); } protected virtual double sif(double x) {return(1.0);} }//FIntegral Этот код большей частью знаком. В отличие от класса Для вычисления интеграла от реальной функции единственное, что теперь нужно сделать — это задать класс-потомок, переопределяющий виртуальный метод. Вот пример такого класса: class FIntegralSon: FIntegral { protected override double sif(double x) { double a = 1.0; double b = 2.0; double c= 3.0; return (double)(a*x*x +b*x +c); } }//FIntegralSon Принципиально задача решена. Осталось только написать фрагмент кода, запускающий вычисления. Он оформлен в виде следующей процедуры: public void TestPolymorphIntegral() { FIntegral integral1 = new FIntegral (); FIntegralSon integral2 = new FIntegralSon(); double res1 = integral1.Evaluatelntegral(2.0,3.0,0.le-5); double res2 = integral2.Evaluatelntegral(2.0,3.0,0.le-5); Console.WriteLine("Father = {0}, Son = {1}", resl,res2); }//PolymorphIntegral Взгляните на результаты вычислений. Рис. 20.4. Делегаты как свойства В наших примерах рассматривалась ситуация, при которой в некотором классе объявлялись функции, удовлетворяющие контракту с делегатом, но создание экземпляров делегата и их инициирование функциями класса выполнялось в другом месте, там, где предполагалось вызывать соответствующие функции. Чаще всего, создание экземпляров удобнее возложить на класс, создающий требуемые функции. Более того, в этом классе делегат можно объявить как свойство класса, что позволяет "убить двух зайцев". Во-первых, с пользователей класса снимается забота создания делегатов, что требует некоторой квалификации, которой у пользователя может и не быть. Во-вторых, делегаты создаются динамически, в тот момент, когда они требуются. Это важно как при работе с функциями высших порядков, когда реализаций, например, подынтегральных функций, достаточно много, так и при работе с событиями класса, в основе которых лежат делегаты. Рассмотрим пример, демонстрирующий и поясняющий эту возможность при работе с функциями высших порядков. Идея примера такова. Спроектируем два класса: • класс объектов • класс В этом классе будут определены операции над объектами. Среди операций нас, прежде всего, будет интересовать сортировка объектов, реализованная в виде функции высших порядков. Функциональный параметр будет задавать класс функций сравнения объектов, реализации которых находятся в классе Теперь, когда задача ясна, приступим к ее реализации. Класс Person уже появлялся в наших примерах, поэтому он просто дополнен до нужной функциональности. Добавим методы сравнения двух объектов //методы сравнения private static int CompareName(Person obj1, Person obj2) { return(string.Compare(obj1.name,obj 2.name)); } private static int Compareld(Person obj1, Person obj2) { if(obj1.id > obj2.id) return(1); else return(-1); } private static int CompareSalary(Person obj1, Person obj2) { if(obj1.salary > obj2.salary) return(1); else if(obj1.salary < obj2.salary)return(-1); else return(0); } private static int CompareSalaryName(Person obj1, Person obj2) { if(obj1.salary > obj2.salary) return(1); else if(obj1.salary < obj2.salary)return (-1); else return(string.Compare(obj1.name,obj2.name)); } Заметьте, методы закрыты и, следовательно, недоступны извне. Их четыре, но могло бы быть и больше, при возрастании сложности объекта растет число таких методов. Все методы имеют одну и ту же сигнатуру и удовлетворяют контракту, заданному делегатом, который будет описан чуть позже. Для каждого метода необходимо построить экземпляр делегата, который будет задавать ссылку на метод. Поскольку не все экземпляры нужны одновременно, то хотелось бы строить их динамически, в тот момент, когда они понадобятся. Это можно сделать, причем непосредственно в классе Закрытые методы будем рассматривать как закрытые свойства и для каждого из них введем статическую процедуру-свойство, возвращающую в качестве результата экземпляр делегата со ссылкой на метод. Проще написать, чем объяснить на словах: //делегаты как свойства public static Persons.CompareItems SortByName { get {return(new Persons.CompareItems(CompareName));} } public static Persons.CompareItems SortById } get {return(new Persons.CompareItems(CompareId)); } public static Persons.CompareItems SortBySalary { get {return(new Persons.CompareItems(CompareSalary));} } public static Persons.CompareItems SortBySalaryName { get {return(new Persons.CompareItems(CompareSalaryName));} } Всякий раз, когда будет запрошено, например, свойство Класс class Persons { //контейнер объектов Person //делегат public delegate int CompareItems(Person obj1, Person obj2); private int freeItem = 0; const int n = 100; private Person[]persons = new Person[n]; } В классе определен функциональный класс — делегат Контейнер объектов реализован простейшим образом в виде массива объектов. Переменная //индексатор public Person this[int num] { get { return(persons[num-1]); } set { persons[num-1] = value; } } Добавим классический для контейнеров набор методов — добавление нового элемента, загрузка элементов из базы данных и печать элементов: public void AddPerson(Person pers) { if (freeItem < n) { Person p = new Person(pers); persons[freeItem++]= p; } else Console.WriteLine("He могу добавить Person"); } public void LoadPersons() { //реально загрузка должна идти из базы данных AddPerson(new Person("Соколов",123, 750.0)); AddPerson(new Person("Синицын",128, 850.0)); AddPerson(new Person("Воробьев",223, 750.0)); AddPerson(new Person("Орлов",129, 800.0)); AddPerson(new Person("Соколов", 133, 1750.0)); AddPerson(new Person("Орлов",119, 750.0)); }//LoadPersons public void PrintPersons() { for (int i =0; i { Console.WriteLine("{0,10} {1,5} {2,5}", persons[i].Name, persons[i].Id, persons[i].Salary); } }//PrintPersons Конечно, метод //сортировка public void SimpleSortPerson(CompareItems compare) { Person temp = new Person(); for(int i = 1; i for(int j = freeItem -1; j>=i; j-) if (compare(persons[j],persons[j —1])==-1) { temp = persons[j-1]; persons[j — 1]=persons[j]; persons[j] = temp; } }//SimpleSortObject }//Persons Единственный аргумент метода public void TestSortPersons () { Persons persons = new Persons (); persons.LoadPersons(); Console.WriteLine (" Сортировка по имени: "); persons.SimpleSortPerson(Person.SortByName); persons.PrintPersons (); Console.WriteLine (" Сортировка по идентификатору: "); persons.SimpleSortPerson(Person.SortByld); persons.PrintPersons (); Console.WriteLine (" Сортировка по зарплате: "); persons.SimpleSortPerson(Person.SortBySalary); persons.PrintPersons (); Console.WriteLine (" Сортировка по зарплате и имени: "); persons.SimpleSortPerson(Person.SortBySalaryName); persons.PrintPersons (); }//SortPersons Результаты работы сортировки данных изображены на рис. 20.5. Рис. 20.5. Операции над делегатами. Класс Давайте просуммируем то, что уже известно о функциональном типе данных. Ключевое слово delegate позволяет задать определение функционального типа (класса), фиксирующее контракт, которому должны удовлетворять все функции, принадлежащие классу. Функциональный класс можно рассматривать как ссылочный тип, экземпляры которого являются ссылками на функции. Заметьте, ссылки на функции — это безопасные по типу указатели, которые ссылаются на функции с жестко фиксированной сигнатурой, заданной делегатом. Следует также понимать, что это не простая ссылка на функцию. В том случае, когда экземпляр делегата инициирован динамическим методом, то экземпляр хранит ссылку на метод и на объект X, вызвавший этот метод. Вместе с тем, объявление функционального типа не укладывается в синтаксис, привычный для С#. Хотелось бы писать, как принято: Delegate FType = new Delegate(<определение типа>) Но так объявлять переменные этого класса нельзя, и стоит понять, почему. Есть ли вообще класс Delegate? Ответ положителен — есть такой класс. При определении функционального типа, например: public delegate int FType(int X); переменная public abstract class Delegate: ICloneable, ISerializable Для абстрактных классов реализация не определена, и это означает, что нельзя создавать экземпляры класса. Класс Delegate служит базовым классом для классов — наследников. Но создавать наследников могут только компиляторы и системные программы — этого нельзя сделать в программе на С#. Именно поэтому введено ключевое слово delegate, которое косвенно позволяет работать с классом Delegate, создавая уже не абстрактный, а реальный класс. Заметьте, при этом все динамические и статические методы класса Delegate становятся доступными программисту. Трудно, кажется, придумать, что можно делать с делегатами. Однако, у них есть одно замечательное свойство — их можно комбинировать. Представьте себе, что существует список работ, которые нужно выполнять, в зависимости от обстоятельств, в разных комбинациях. Если функции, выполняющие отдельные работы, принадлежат одному классу, то для решения задачи можно использовать делегатов и использовать технику их комбинирования. Замечу, что возможность комбинирования делегатов появилась, в первую очередь, для поддержания работы с событиями. Когда возникает некоторое событие, то сообщение о нем посылается разным объектам, каждый из которых по-своему обрабатывает событие. Реализуется эта возможность на основе комбинирования делегатов. В чем суть комбинирования делегатов? Она прозрачна. К экземпляру делегату разрешается поочередно присоединять другие экземпляры делегата того же типа. Поскольку каждый экземпляр хранит ссылку на функцию, то в результате создается список ссылок. Этот список называется Понятно, что, если есть операция присоединения делегатов, то должна быть и обратная операция, позволяющая удалять делегатов из списка. Рассмотрим основные методы и свойства класса Delegate. Начнем с двух статических методов — Combine(del1, del2) Remove(del1, del2) Аргументы Класс Delegate относится к неизменяемым классам, поэтому оба метода возвращают ссылку на нового делегата. Возвращаемая ссылка принадлежит родительскому классу Delegate, поэтому ее необходимо явно преобразовать к нужному типу, которому принадлежат del1 = ( Метод Два динамических свойства У класса Delegate, помимо методов, наследуемых от класса object, есть еще несколько методов, но мы на них останавливаться не будем, они используются не столь часто. Операции "+" и "-" Наряду с методами, над делегатами определены и две операции: "+" и "-", которые являются более простой формой записи добавления делегатов в список вызовов и удаления из списка. Операции заменяют собой методы del1 +=del2; del1 — =del2; Как видите, запись становится проще, исчезает необходимость в задании явного приведения к типу. Ограничения на Пример "Комбинирование делегатов" Рассмотрим следующую ситуацию. Пусть есть городские службы: милиция, скорая помощь, пожарные. Каждая из служб по-своему реагируют на события, происходящие в городе. Построим примитивную модель жизни города, в которой случаются события и сообщения о них посылаются службам. В последующей лекции эта модель будет развита. Сейчас она носит формальный характер, демонстрируя, главным образом, работу с делегатами, заодно поясняя ситуации, в которых разумно комбинирование делегатов. Начнем с построения класса с именем class Combination { private static void policeman (string mes) { //анализ сообщения if(mes =="Пожар!") Console.WriteLine(mes + " Милиция ищет виновных!"); else Console.WriteLine(mes +" Милиция здесь!"); } private static void ambulanceman(string mes) { if(mes =="Пожар!") Console.WriteLine(mes + " Скорая спасает пострадавших!"); else Console.WriteLine(mes + " Скорая помощь здесь!"); } private static void fireman(string mes) { if(mes =="Пожар!") Console.WriteLine(mes + " Пожарные тушат пожар!"); else Console.WriteLine(mes + " Пожарные здесь!"); } } Как видите, все три функции имеют не только одинаковую сигнатуру, но и устроены одинаково. Они анализируют приходящее к ним сообщение, переданное через параметр public static MesToPers Policeman { get {return (new MesToPers(policeman));} } public static MesToPers Fireman { get {return (new MesToPers(fireman));} } public static MesToPers Ambulanceman ( get (return (new MesToPers(ambulanceman));} } Три статических открытых свойства — Службы у нас есть, покажем, как с ними можно работать. С этой целью добавим в класс public void TestSomeServices() { MesToPers Comb; Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman, Combination.Policeman); Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman); Comb("Пожар!"); Вначале объявляется без инициализации функциональная переменная Давайте теперь начнем поочередно отключать делегатов, вызывая затем Comb = (MesToPers)Delegate.Remove (Comb,Combination.Policeman); //Такое возможно: попытка отключить не существующий элемент Comb = (MesToPers)Delegate.Remove (Comb,Combination.Policeman); Comb ("Через 30 минут!"); Comb = (MesToPers)Delegate.Remove(Comb,Combination.Ambulanceman); Comb("Через час! "); Comb = (MesToPers)Delegate.Remove(Comb,Combination.Fireman); //Comb("Через два часа!"); // Comb не определен В этом фрагменте поочередно отключаются разные службы — милиция, скорая помощь, пожарные, и каждый раз вызывается Покажем теперь, что ту же работу можно выполнить, используя не методы, а операции: //операции + и - Comb = Combination.Ambulanceman; Console.WriteLine(Comb.Method.Name); Comb+= Combination.Fireman; Comb+= Combination.Policeman; Соmb("День города!"); Comb — = Combination.Ambulanceman; Comb — = Combination.Fireman; Comb("На следующий день!"); }//TestSomeServices Обратите внимание, здесь демонстрируется вызов свойства Рис. 20.6. Пример "Плохая служба" Как быть, если в списке вызовов есть "плохой" экземпляр, при вызове которого возникает ошибка, приводящая к выбрасыванию исключительной ситуации? Тогда стоящие за ним в очереди экземпляры не будут вызваны, хотя они вполне могли бы выполнить свою часть работы. В этом случае полезно использовать метод Добавим в класс //метод, вызывающий исключительную ситуацию public static void BadService(string mes) { int i =7, j=5, k=0; Console.WriteLine("Bad Service: Zero Divide"); j = i / k; } Создадим процедуру, в которой в списке вызовов есть хорошие и плохие кандидаты. Эта процедура использует управление исключительными ситуациями, о которых подробнее будет рассказано в последующих лекциях. public void TestBadJob() { MesToPers Comb; Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman, Combination.Policeman); Comb = (MesToPers)Delegate.Combine(Comb, new MesToPers(Combination.BadService)); Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman); foreach(MesToPers currentJob in Comb.GetinvocationList()) { try { currentJob("Пожар!"); } catch (Exception e) { Console.WriteLine(e.Message); Console.WriteLine(currentJob.Method.Name); } } }//BadJob Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация "деление на ноль". Универсальный обработчик Рис. 20.7. Разговор о делегатах еще не закончен. Он будет продолжен в следующей лекции, в которой рассмотрим классы с событиями. События основываются на делегатах.
21. События Классы с событиями. Общий взгляд. Класс Sender и классы Receivers. Класс Sender. Как объявляются события? Делегаты и события. Классы с событиями, допускаемые. Net Framework. Класс EventArgs и его потомки. Входные и выходные аргументы события. Класс Receiver. Обработчик события. Встраивание объекта Sender. Связывание обработчика с событием. Отключение обработчика. Взаимодействие объектов sender и receiver. События — поля или процедуры-свойства? Динамическое связывание событий с их обработчиками. Каждый объект является экземпляром некоторого класса. Класс задает свойства и поведение своих экземпляров. Методы класса определяют поведение объектов, свойства — их состояние. Все объекты обладают одними и теми же методами и, следовательно, ведут себя одинаково. Можно полагать, что методы задают врожденное поведение объектов. Этого нельзя сказать о свойствах — значения свойств объектов различны, так что экземпляры одного класса находятся в разных состояниях. Объекты класса "человек" могут иметь разные свойства: один — высокий, другой — низкий, один — сильный, другой — умный. Но методы у них одни: есть и спать, ходить и бегать. Как сделать поведение объектов специфическим? Как добавить им поведение, характерное для данного объекта? Один из наиболее известных путей — это наследование. Можно создать класс-наследник, у которого, наряду с унаследованным родительским поведением, будут и собственные методы. Например, наследником класса "человек" может быть класс "человек_образованный", обладающий методами: читать и писать, считать и программировать. Есть еще один механизм, позволяющий объектам вести себя по-разному в одних и тех же обстоятельствах. Это механизм событий, рассмотрением которого мы сейчас и займемся. Класс, помимо свойств и методов, может иметь события. Содержательно, событием является некоторое специальное состояние, в котором может оказаться объект класса. Так, для объектов класса "человек" событием может быть рождение или смерть, свадьба или развод. О событиях в мире программных объектов чаще всего говорят в связи с интерфейсными объектами, у которых события возникают по причине действий пользователя. Так, командная кнопка может быть нажата — событие Интерфейсные и многие другие программные объекты обладают стандартным набором предопределенных событий. В конце этой лекции мы поговорим немного об особенностях работы с событиями таких объектов. Сейчас же наше внимание будет сосредоточено на классах, создаваемых программистом. Давайте разберемся, как для таких классов создаются и обрабатываются события. Класс, решивший иметь события, должен уметь, по крайней мере, три вещи: • объявить событие в классе; • зажечь в нужный момент событие, передав обработчику необходимые для обработки аргументы. (Под зажиганием или включением события понимается некоторый механизм, позволяющий объекту уведомить клиентов класса, что у него произошло событие.); • проанализировать, при необходимости, результаты события, используя значения выходных аргументов события, возвращенные обработчиком. Заметьте, что, зажигая событие, класс посылает сообщение получателям события — объектам некоторых других классов. Будем называть класс, зажигающий событие, классом — Рис. 21.1. Класс При проектировании класса с событиями, возможно, самое трудное — содержательная сторона дела. Какими событиями должен обладать класс, в каких методах и в какой момент зажигать то или иное событие? Содержательную сторону будем пояснять на содержательных примерах. А сейчас рассмотрим технический вопрос: как объявляются события средствами языка С#? Прежде всего, уточним, что такое событие с программистской точки зрения. Начнем не с самого события, а с его обработчика. Обработчик события — это обычная процедура с аргументами. Понятно, что сообщение, посылаемое при зажигании события, является аналогом вызова процедуры. Поскольку сигнатура посылаемого сообщения должна соответствовать сигнатуре принимаемого сообщения, то объявление события синтаксически должно задавать сигнатуру процедуры. Делегаты и события Наверное, вы уже заметили, что схема работы с событиями вполне укладывается в механизм, определяемый делегатами. В C# каждое событие определяется делегатом, описывающим сигнатуру сообщения. Объявление события — это двухэтапный процесс: • Вначале объявляется делегат — функциональный класс, задающий сигнатуру. Как отмечалось при рассмотрении делегатов, объявление делегата может быть помещено в некоторый класс, например, класс • Если делегат определен, то в классе Sender, создающем события, достаточно объявить событие как экземпляр соответствующего делегата. Это делается точно так же, как и при объявлении функциональных экземпляров делегата. Исключением является добавление служебного слова event. Формальный синтаксис объявления таков: [атрибуты] [модификаторы]event [тип, заданный делегатом] [имя события] Есть еще одна форма объявления, но о ней чуть позже. Чаще всего, атрибуты не задаются, а модификатором является модификатор доступа — public. Приведу пример объявления делегата и события, представляющего экземпляр этого делегата: namespace Events { public delegate void FireEventHandler(object Sender, int time, int build); public class TownWithEvents { public event FireEventHandler FireEvent; …. }//TownWithEvents …. }//namespace Events Здесь делегат Как зажигаются события Причины возникновения события могут быть разными. Поэтому вполне вероятно, что одно и то же событие будет зажигаться в разных методах класса в тот момент, когда возникнет одна из причин появления события. Поскольку действия по включению могут повторяться, полезно в состав методов класса добавить защищенную процедуру, включающую событие. Даже если событие зажигается только в одной точке, написание такой процедуры считается признаком хорошего стиля. Этой процедуре обычно дается имя, начинающееся со слова protected virtual void OnFire(int time, int build) { if (FireEvent!=null) FireEvent(this,time, build); } Хочу обратить внимание: те, кто принимает сообщение о событии, должны заранее присоединить обработчики событий к объекту Заметьте также, что процедура On объявляется, как правило, с модификаторами Это позволяет потомкам класса переопределить ее, когда, например, изменяется набор аргументов события. Последний шаг, который необходимо выполнить в классе Классы Объекты класса Понятно, что класс receiver должен: • иметь обработчик события — процедуру, согласованную по сигнатуре с функциональным типом делегата, который задает событие; • иметь ссылку на объект, создающий событие, чтобы получить доступ к этому событию — event-объекту; • уметь присоединить обработчик события к event-объекту. Это можно реализовать по-разному, но технологично это делать непосредственно в конструкторе класса, так что когда создается объект, получающий сообщение, он изначально готов принимать и обрабатывать сообщения о событиях. Вот пример, демонстрирующий возможное решение проблем: public class FireMen { private TownWithEvents MyNativeTown; public FireMen(TownWithEvents TWE) { this.MyNativeTown=TWE; MyNativeTown.FireEvent += new FireEventHandler(FireHandler); } private void FireHandler(object Sender, int time, int build) { Console.WriteLine("Fire at day {0}, in build {1}!", time, build); } public void GoOutO { MyNativeTown.FireEvent — = new FireEventHandler(FireHandler); } }//FireMan В классе Классы с событиями, допустимые в каркасе. Net Framework Если создавать повторно используемые компоненты с событиями, работающие не только в проекте С#, то необходимо удовлетворять некоторым ограничениям. Эти требования предъявляются к делегату; они носят, скорее, синтаксический характер, не ограничивая существа дела. Перечислю эти ограничения: • делегат, задающий тип события, должен иметь фиксированную сигнатуру из двух аргументов: delegate <Имя_делегата> (object sender, <Тип_аргументов> args); • первый аргумент задает объект Если обработчику никаких дополнительных аргументов не передается, то следует просто указать класс • рекомендуемое имя делегата — составное, начинающееся именем события, после которого следует слово Пример "Списки с событиями" В этом примере строится класс L Начнем с объявления делегата: // Объявление делегата public delegate void ChangedEventHandler(object sender, ChangedEventArgs args); Здесь объявлен делегат public class ChangedEventArgs: EventArgs { private object item; private bool permit; public object Item { get {return(item);} set { item = value;} } public bool Permit { get {return(permit);} set { permit = value;} } }//class ChangedEventArgs У класса два закрытых свойства, доступ к которым осуществляется через процедуры-свойства В модели, которую мы рассматриваем, предполагается, что обработчик события, получив уведомление об изменении элемента, анализирует ситуацию и может разрешить или не разрешить изменение, например, если значение элемента больше некоторого предельного значения. Правильно ли, что обработчик события, а не сам класс, создающий событие, принимает решение о допуске изменения элемента списка? Все зависит от контекста. В прошлые времена молодые могли объявить о своей помолвке, но требовалось разрешение родителей на брак. Времена изменились — теперь на брак родительского благословения не требуется. Но в программистском мире ситуации, требующие внешнего разрешения, встречаются довольно часто. Класс Рассмотрим теперь, как устроен в нашем примере класс, создающий события. Начнем со свойств класса: // Класс, создающий событие. Потомок класса ArrayList. public class ListWithChangedEvent: ArrayList { //Свойства класса: событие и его аргументы //Событие Changed, зажигаемое при всех изменениях //элементов списка. public event ChangedEventHandler Changed; //Аргументы события private ChangedEventArgs evargs = new ChangedEventArgs(); Первое свойство описывает событие Хороший стиль требует задания в классе процедуры //Методы класса: процедура On и переопределяемые методы. //Процедура On, включающая событие protected virtual void OnChanged(ChangedEventArgs args) { if (Changed!= null) Changed (this, args); } Процедура Наш класс, являясь наследником класса • метод • индексатор • метод //Переопределяемые методы, вызывающие событие Changed //Добавление нового элемента //при получении разрешения у обработчиков события public override int Add (object value) { int i=0; evargs.Item = value; OnChanged(evargs); if (evargs.Permit) i = base.Add(value); else Console.WriteLine("Добавление элемента запрещено." + "Значение = {0}", value); return i; } public override void Clear() { evargs.Item=0; OnChanged(evargs); base.Clear(); } public override object this[int index] { set { evargs.Item = value; OnChanged(evargs); if (evargs.Permit) base[index] = value; else Console.WriteLine("Замена элемента запрещена." + " Значение = {0}", value); } get{return(base[index]);} } Обратите внимание на схему включения события, например, в процедуре Классы Мы построим два класса, объекты которых способны получать и обрабатывать событие class EventReceiver1 { private ListWithChangedEvent List; public EventReceiveri(ListWithChangedEvent list) { List = list; // Присоединяет обработчик к событию. OnConnect (); } //Обработчик события — выдает сообщение. //Разрешает добавление элементов, меньших 10. private void ListChanged(object sender, ChangedEventArgs args) { Console.WriteLine("EventReceiveri: Сообщаю об изменениях: " + "Item ={0}", args.Item); args.Permit = ((int)args.Item < 10); } public void OnConnect () { //Присоединяет обработчик к событию List.Changed += new ChangedEventHandler(ListChanged); } public void OffConnectO { { //Отсоединяет обработчик от события и удаляет список List.Changed — = new ChangedEventHandler(ListChanged); List = null; } }//class EventReceiver1 Дам краткие комментарии. • Среди закрытых свойств класса есть ссылка • Конструктору класса передается фактический объект, который и будет присоединен к • Класс содержит метод • Обработчик события, анализируя переданный ему входной аргумент события Класс class Receiver2 { private ListWithChangedEvent List; public Receiver2(ListWithChangedEvent list) { List = list; // Присоединяет обработчик к событию. OnConnect (); } // Обработчик события — выдает сообщение. //Разрешает добавление элементов, меньших 20. private void ListChanged(object sender, ChangedEventArgs args) { Console.WriteLine("Receiver2: Сообщаю об изменениях:" + " Объект класса {0}: " + "Item ={1}", sender.GetType(), args.Item); args.Permit = ((int)args.Item < 20); } public void OnConnect () { //Присоединяет обработчик к событию List.Changed += new ChangedEventHandler(ListChanged); //Заметьте, допустимо только присоединение (+=), //но не замена (=) //List.Changed = new ChangedEventHandler(ListChanged); } public void OffConnect() { //Отсоединяет обработчик от события и удаляет список List.Changed — = new ChangedEventHandler(ListChanged); List = null; } }//class Receiver2 Классы созданы, теперь осталось создать объекты и заставить их взаимодействовать, чтобы одни создавали события, а другие их обрабатывали. Эту часть работы будет выполнять тестирующая процедура класса public void TestChangeList () { //Создаются два объекта, вырабатывающие события ListWithChangedEvent list = new ListWithChangedEvent (); ListWithChangedEvent list1 = new ListWithChangedEvent(); //Создаются три объекта двух классов EventReceiver1 и //Receiver2, способные обрабатывать события класса //ListWithChangedEvent EventReceiver1 Receiver1 = new EventReceiver1(list); Receiver2 Receiver21 = new Receiver2 (list); Receiver2 Receiver22 = new Receiver2(listl); Random rnd = new Random(); //Работа с объектами, приводящая к появлению событий list.Add(rnd.Next(20)); list.Add(rnd.Next(2 0)); list[1] =17; int val = (int)list[0] + (int)list[1];list.Add(val); list.Clear(); list1.Add(10); list1[0] = 25; list1.Clear(); //Отсоединение обработчика событий Receiver1.OffConnect(); list.Add(21); list.Clear (); } В заключение взгляните на результаты работы этой процедуры. Рис. 21.2. Объекты, создающие события, ничего не знают об объектах, обрабатывающих эти события. Объекты, обрабатывающие события, ничего не знают друг о друге, независимо выполняя свою работу. В такой модели могут возникать определенные проблемы. Рассмотрим некоторые из них. Игнорирование коллег Задумывались ли вы, какую роль играет ключевое слово event, появляющееся при объявлении события? Событие, объявленное в классе, представляет экземпляр делегата. В предыдущей лекции, когда речь шла о делегатах, их экземпляры объявлялись без всяких дополнительных ключевых слов. Слово "event" играет важную роль, позволяя решить проблему, названную нами С этим как-то нужно бороться. Ключевое слово "event" дает указание компилятору создать для события закрытое поле, доступ к которому можно получить только через два автоматически создаваемых для события метода: Переопределение значений аргументов события Обработчику события, как правило, передаются входные и выходные аргументы, характеризующие событие. Они необходимы, чтобы обработчик мог нужным образом обработать событие. Но работа с аргументами требует аккуратного с ними обращения. Могут возникать проблемы, связанные с тем, что обработчик может переопределить значения аргументов в процессе своей работы. Приведенный выше пример "Работа со списками" демонстрирует не самый лучший способ определения аргументов, провоцирующий классы • В классе public object Item { get {return(item);} //set { item = value;} } public ChangedEventArgs(object item) { this.item = item; } • В методы класса public override int Add(object value) { int i=0; ChangedEventArgs evargs = new ChangedEventArgs(value); //evargs.Item = value; OnChanged(evargs); if (evargs.Permit) i = base.Add(value); else Console.WriteLine("Добавление элемента запрещено." + "Значение = {0}", value); return i; } public override void Clear() { ChangedEventArgs evargs = new ChangedEventArgs(0); //evargs.Item=0; OnChanged(evargs); base.Clear(); } public override object this[int index] { set { ChangedEventArgs evargs = new ChangedEventArgs(value); //evargs.Item = value; OnChanged(evargs); if (evargs.Permit) base[index] = value; else Console.WriteLine("Замена элемента запрещена." + " Значение = {0}", value); } get {return(base[index]);} } Таким образом, обработчикам можно запретить изменение входных аргументов события. Но есть еще выходные аргументы события, значения которых определяются в обработчике; в нашем примере это аргумент И здесь возникает коллизия интересов — каждый обработчик по своему может формировать значения выходных аргументов, не обращая внимания на результаты работы предыдущих обработчиков. Преимуществом в таких ситуациях обладает последний работающий обработчик события. Эта проблема остается открытой, в языке C# здесь "дыра" — нет специальных средств, позволяющих избежать или, по крайней мере, предупредить о возникновении подобной ситуации. Вся ответственность лежит на программисте, который может выбрать некоторую стратегию решения проблемы, отдавая, например, предпочтение решению одного из обработчиков или вырабатывая итоговое решение, учитывающее все частные решения. Итак, если событие имеет аргументы, то все входные аргументы должны быть закрыты для обработчиков события. Если обработчиков несколько, то лучше или не использовать выходных аргументов, или аккуратно запрограммировать логику обработчиков, которая учитывает решения, полученные коллегами — ранее отработавшими обработчиками события. Как было сказано, каждое событие класса представляется полем этого класса. Если у класса много объявленных событий, а реально возникает лишь малая часть из них, то предпочтительнее динамический подход, когда память отводится только фактически возникшим событиям. Это несколько замедляет время выполнения, но экономит память. Решение зависит от того, что в данном контексте важнее — память или время. Для реализации динамического подхода в языке предусмотрена возможность задания пользовательских методов public event <Имя Делегата> <Имя события> { add {…} remove {… } } Оба метода должны быть реализованы, при этом для хранения делегатов используется некоторое хранилище. Именно так реализованы классы для большинства интерфейсных объектов, использующие хэш-таблицы для хранения делегатов. Давайте построим небольшой пример, демонстрирующий такой способ объявления и работы с событиями. Вначале построим класс с несколькими событиями: class ManyEvents { //хэш таблица для хранения делегатов Hashtable DStore = new Hashtable(); public event EventHandier Evl { add { DStore["Evl"]= (EventHandier)DStore["Evl"]+ value; } remove { DStore["Evl"]= (EventHandier)DStore["Evl"] - value; } } public event EventHandier Ev2 { add { DStore["Ev2"]= (EventHandier)DStore["Ev2"]+ value; } remove { DStore["Ev2"]= (EventHandier)DStore["Ev2"] - value; } } public event EventHandier Ev3 { add { DStore["Ev3"]= (EventHandier)DStore["Ev3"]+ value; } remove { DStore["Ev3"]= (EventHandier)DStore["Ev3"] - value; } } public event EventHandier Ev4 { add { DStore["Ev4"]= (EventHandler)DStore["Ev4"] - value; } remove } DStore["Ev4"]= (EventHandler)DStore["Ev4"] - value; } } public void SimulateEvs() { EventHandler ev = (EventHandler) DStore["Ev1"]; if(ev!= null) ev(this, null); ev = (EventHandler) DStore["Ev3"]; if(ev!= null) ev(this, null); } }//class ManyEvents В нашем классе созданы четыре события и хэш-таблица DStore для их хранения. Все события принадлежат встроенному классу Рассмотрим теперь класс class ReceiverEvs { private ManyEvents manyEvs; public ReceiverEvs(ManyEvents manyEvs) { this.manyEvs = manyEvs; OnConnect (); } public void OnConnect () { manyEvs.Ev1 += new EventHandler(H1); manyEvs.Ev2 += new EventHandler(H2); manyEvs.Ev3 += new EventHandler (H3); manyEvs.Ev4 += new EventHandler(H4); } public void HI (object s, EventArgs e) { Console.WriteLine("Событие Ev1"); } public void H2 (object s, EventArgs e) { Console.WriteLine("Событие Ev2"); } public void H3 (object s, EventArgs e) { Console.WriteLine("Событие Ev3"); } public void H4 (object s, EventArgs e) { Console.WriteLine("Событие Ev4"); } }//class ReceiverEvs Тестирующая процедура состоит из нескольких строчек, в которых создаются нужные объекты и запускается метод public void TestManyEvents() { ManyEvents me = new ManyEvents (); ReceiverEvs revs = new ReceiverEvs(me); me.SimulateEvs(); } Все работает предусмотренным образом. Проект "Город и его службы" Завершить лекцию о событиях хочется содержательным учебным проектом, в котором моделируется жизнь города, происходящие в нем события и реакция на них городских служб. Наша главная цель в данном проекте — еще раз показать, как возникающее событие, в данном случае — пожар в одном из домов города, обрабатывается по-разному городскими службами — пожарными, милицией, скорой помощью. Конечно, все упрощено, в реальном городе событиями являются не только пожары и преступления, но и более приятные ситуации: день города, открытие фестивалей и выставок, строительство новых театров и институтов. Начнем с описания класса, задающего наш город. Этот класс уже появлялся и в этой, и в предыдущей лекции, здесь его описание будет расширено. Начнем со свойств класса: public class NewTown { //свойства private int build, BuildingNumber; //дом и число домов в городе private int day, days; //Текущий день года //городские службы private Police policeman; private Ambulance ambulanceman; private FireDetect fireman; //события в городе public event FireEventHandler Fire; //моделирование случайных событий private Random rnd = new Random(); //вероятность пожара в доме в текущий день: р= m/n private int m = 3, n= 10000; В нашем городе есть дома; есть время, текущее день за днем; городские службы; событие "пожар", которое, к сожалению, может случайно с заданной вероятностью возникать каждый день в каждом доме. Рассмотрим конструктор объектов нашего класса: //конструктор класса public NewTown(int TownSize, int Days) { BuildingNumber = rnd.Next(TownSize); days = Days; policeman = new Police (this); ambulanceman= new Ambulance (this); fireman= new FireDetect (this); policeman.On (); ambulanceman.On (); fireman.On (); } При создании объектов этого класса задается размер города — число его домов и период времени, в течение которого будет моделироваться жизнь города. При создании объекта создаются его службы — объекты соответствующих классов В соответствии с ранее описанной технологией определим метод protected virtual void OnFire(FireEventArgs e) { if(Fire!= null) Fire(this, e); } Где и когда будет включаться событие public void LifeOurTown () { for(day = 1; day<=days; day++) for(build =1; build <= BuildingNumber; build++) { if(rnd.Next(n) <=m) //загорелся дом { //аргументы события FireEventArgs е = new FireEventArgs(build, day, true); OnFire(e); if(e.Permit) Console.WriteLine("Пожар потушен!" + " Ситуация нормализована."); else Console.WriteLine("Пожар продолжается." + " Требуются дополнительные средства!"); } } } Рассмотрим теперь классы Receiver, обрабатывающие событие public abstract class Receiver { private NewTown town; public Receiver(NewTown town) {this.town = town;} public void On() { town.Fire += new FireEventHandler(It_is_Fire); } public void Off() { town.Fire — = new FireEventHandler(It_is_Fire); town = null; } public abstract void It_is_Fire(object sender, FireEventArgs e); }//class Receiver Для классов потомков абстрактный метод public class Police: Receiver { public Police (NewTown town): base(town){} public override void It_is_Fire(object sender, FireEventArgs e) { Console.WriteLine("Пожар в доме {0}. День {1}-й. " + " Милиция ищет виновных!", е. Build,е. Day); е. Permit &= true; } }// class Police public class FireDetect: Receiver { public FireDetect (NewTown town): base(town){} public override void It_is_Fire(object sender, FireEventArgs e) { Console.WriteLine("Пожар в доме {0}. День {1}-й."+ " Пожарные тушат пожар!", е. Build,е. Day); Random rnd = new Random(e.Build); if(rnd.Next(10) >5) e. Permit &= false; else e.Permit &=true; } }// class FireDetect public class Ambulance: Receiver { public Ambulance(NewTown town): base(town){} public override void It_is_Fire(object sender, FireEventArgs e) { Console.WriteLine("Пожар в доме {0}. День {1}-й."+ " Скорая спасает пострадавших!", е. Build,е. Day); е. Permit &= true; } }// class Ambulance Для каждого потомка задан конструктор, вызывающий базовый метод родителя. Каждый потомок по-своему определяет обработчика события Для полноты картины необходимо показать, как выглядит класс, задающий аргументы события, который, как и положено, является потомком класса public class FireEventArgs: EventArgs { private int build; private int day; private bool permit; public int Build { get{ return(build);} ///set{ build = value;} } public int Day { get{ return(day);} ///set{ day = value;} } public bool Permit { get{ return(permit);} set{ permit = value;} } public FireEventArgs(int build, int day, bool permit) { this.build = build; this.day = day; this.permit = permit; } }//class FireEventArgs Входные параметры события — Для завершения проекта нам осталось определить тестирующую процедуру в классе public void TestLifeTown() { NewTown sometown = new NewTown(100,100); sometown.LifeOurTown (); } Результаты ее работы зависят от случайностей. Вот как выглядит один из экспериментов: Рис. 21.3.
22. Универсальность. Классы с родовыми параметрами Наследование и универсальность — взаимно дополняющие базовые механизмы создания семейства классов. Родовые параметры универсального класса. Синтаксис универсального класса. Родовое порождение экземпляров универсального класса. Методы с родовыми параметрами. Ограниченная универсальность — ограничения, накладываемые на родовые параметры. Виды ограничений. Ограничение универсальности — это свобода действий. Примеры. Родовые параметры и частные случаи классов: структуры, интерфейсы, делегаты. Универсальность и Framework.Net. Наследование и универсальность Необходимость в универсализации возникает с первых шагов программирования. Одна из первых процедур, появляющихся при обучении программированию — это процедура public void Swap(ref T x1, ref T x2) { T temp; temp = x1; x1 = x2; x2 = temp; } Если тип В типизированных языках, не обладающих механизмом универсализации, выхода практически нет — приходится писать многочисленные копии Swap. До недавнего времени Framework.Net и соответственно язык C# не поддерживали универсальность. Так что те, кто работает с языком С#, входящим в состав Visual Studio 2003 и ранних версий, должны смириться с отсутствием универсальных классов. Но в новой версии Visual Studio 2005, носящей кодовое имя Whidbey, проблема решена, и программисты получили наконец долгожданный механизм универсальности. Я использую в примерах этой лекции бета-версию Whidbey. Замечу, что хотя меня прежде всего интересовала реализация универсальности, но и общее впечатление от Whidbey самое благоприятное. Для достижения универсальности процедуры Swap следует рассматривать тип Под Синтаксис универсального класса Объявить класс C# универсальным просто: для этого достаточно указать в объявлении класса, какие из используемых им типов являются параметрами. Список типовых параметров класса, заключенный в угловые скобки, добавляется к имени класса: class MyClass Как и всякие формальные параметры, В C# универсальными могут быть как классы, так и все их частные случаи — интерфейсы, структуры, делегаты, события. Класс с универсальными методами Специальным частным случаем универсального класса является класс, не объявляющий сам параметров, но разрешающий делать это своим методам. Давайте начнем рассмотрение универсальности с этого частного случая. Вот как выглядит класс, содержащий универсальный метод class Change { static public void Swap { T temp; temp = x1; x1 = x2; x2 = temp; } } Как видите, сам класс в данном случае не имеет родовых параметров, но зато универсальным является статический метод класса Рассмотрим тестирующую процедуру из традиционного для наших примеров класса public void TestSwap() { int x1 = 5, x2 = 7; Console.WriteLine("до обмена: x1={0}, x2={1}",x1, x2); Change.Swap Console.WriteLine("после обмена: xl={0}, x2={1}", x1, x2); string s1 = "Савл", s2 = "Павел"; Console.WriteLine("до обмена: s1={0}, s2={1}", s1, s2); Change.Swap Console.WriteLine("после обмена: s1={0}, s2={1}", s1, s2); Person pers1 = new Person("Савлов", 25, 1500); Person pers2 = new Person("Павлов", 35, 2100); Console.WriteLine("до обмена: "); pers1.PrintPerson (); pers2.PrintPerson (); Change.Swap Console.WriteLine("после обмена: "); pers1.PrintPerson (); pers2.PrintPerson (); } Обратите внимание на строки, осуществляющие вызов метода: Change.Swap Change.Swap Change.Swap В момент вызова метода передаются фактические аргументы и фактические типы. В данном примере в качестве фактических типов использовались встроенные типы Еще раз напомню, что все эти примеры построены в Whidbey, и вот как выглядят внешний вид среды разработки и окно с результаты работы этой процедуры. Рис. 22.1. В этом примере использовался класс class Person { public Person(string name, int age, double salary) { this.name = name; this.age = age; this.salary = salary; } public string name; public int age; public double salary; public void PrintPerson() { Console.WriteLine("name= {0}, age = {1}, salary ={2}", name, age, salary); } } Два основных механизма объектной технологии Наследование и универсальность являются двумя основными механизмами, обеспечивающими мощность объектной технологии разработки. Наследование позволяет специализировать операции класса, уточнить, как должны выполняться операции. Универсализация позволяет специализировать данные, уточнить, над какими данными выполняются операции. Эти механизмы взаимно дополняют друг друга. Универсальность можно ограничить (об этом подробнее будет сказано ниже), указав, что тип, задаваемый родовым параметром, обязан быть наследником некоторого класса и/или ряда интерфейсов. С другой стороны, когда формальный тип Эти механизмы в совокупности обеспечивают бесшовный процесс разработки программных систем, начиная с этапов спецификации и проектирования системы и заканчивая этапами реализации и сопровождения. На этапе задания спецификаций появляются абстрактные, универсальные классы, которые в ходе разработки становятся вполне конкретными классами с конкретными типами данных. Механизмы наследования и универсализации позволяют существенно сократить объем кода, описывающего программную систему, поскольку потомки не повторяют наследуемый код своих родителей, а единый код универсального класса используется при каждой конкретизации типов данных. На рис. 22.2 показан схематически процесс разработки программной системы. Рис. 22.2.1. Рис. 22.2.2. Рис. 22.2.3. На этапе спецификации, как правило, создается абстрактный, универсальный класс, где задана только сигнатура методов, но не их реализация; где определены имена типов, но не их конкретизация. Здесь же, используя возможности тегов класса, формально или неформально задаются спецификации, описывающие семантику методов класса. Далее в ходе разработки, благодаря механизму наследования, появляются потомки абстрактного класса, каждый из которых задает реализацию методов. На следующем этапе, благодаря механизму универсализации, появляются экземпляры универсального класса, каждый из которых выполняет операции класса над данными соответствующих типов. Для наполнения этой схемы реальным содержанием давайте рассмотрим некоторый пример с прохождением всех трех этапов. Стек. От абстрактного, универсального класса к конкретным версиям Возьмем классическую задачу определения стека. Следуя схеме, определим абстрактный универсальный класс, описывающий всевозможные представления стеков: /// /// Абстрактный класс GenStack /// доступом LIFO: /// Функции: /// конструктор new: — > GenStack /// запросы: /// item: GenStack — > T /// empty: GenStack — > Boolean /// процедуры: /// put: GenStack*T — > GenStack /// remove: GenStack — > GenStack /// ксиомы: /// remove(put (s, x)) = s /// item(put(s,x)) = x /// empty(new)= true /// empty(put(s,x)) = false /// abstract public class GenStack { /// /// require: not empty (); /// /// abstract public T item(); /// /// require: not empty(); /// ensure: удален элемент вершины(последний пришедший) /// abstract public void remove(); /// /// require: true; ensure: elem находится в вершине стека /// /// abstract public void put(T t); /// /// require: true; /// /// abstract public bool empty(); }// class GenStack В приведенном примере программного текста чуть-чуть. Это объявление абстрактного универсального класса: abstract public class GenStack и четыре строки с объявлением сигнатуры его методов. Основной текст задает описание спецификации класса и его методов. Заметьте, здесь спецификации заданы достаточно формально с использованием аксиом, характеризующих смысл операций, которые выполняются над стеком. Не хочется вдаваться в математические подробности, отмечу лишь, что, если задать последовательность операций над стеком, то аксиомы позволяют точно определить состояние стека в результате выполнения этих операций. Как неоднократно отмечалось с первых лекций курса, XML-отчет, построенный по этому проекту, будет содержать в читаемой форме все спецификации нашего класса. Отмечу еще, что все потомки класса должны удовлетворять этим спецификациям, хотя могут добавлять и собственные ограничения. Наш класс является универсальным — стек может хранить элементы любого типа, и конкретизация типа будет производиться в момент создания экземпляра стека. Наш класс является абстрактным — не задана ни реализация методов, ни то, как стек будет представлен. Эти вопросы будут решать потомки класса. Перейдем теперь ко второму этапу и построим потомков класса, каждый из которых задает некоторое представление стека и соответствующую этому представлению реализацию методов. Из всех возможных представлений ограничимся двумя. В первом из них стек будет представлен линейной односвязной списковой структурой. Во втором — он строится на массиве фиксированного размера, задавая стек ограниченной емкости. Вот как выглядит первый потомок абстрактного класса: /// /// Стек, построенный на односвязных элементах списка GenLinkable /// public class OneLinkStack { public OneLinkStack() { last = null; } GenLinkable public override Т item() { return (last.Item); }//item public override bool empty() { return (last == null); }//empty public override void put (T elem) { GenLinkable newitem.Item = elem; newitem.Next = last; last = newitem; }//put public override void remove() { last = last.Next; }//remove } //class OneLinkStack Посмотрите, что происходит при наследовании от универсального класса. Во-первых, сам потомок также является универсальным классом-. public class OneLinkStack Во-вторых, если потомок является клиентом некоторого класса, то и этот класс, возможно, также должен быть универсальным, как в нашем случае происходит с классом GenLinkabie GenLinkable В-третьих, тип т встречается в тексте потомка всюду, где речь идет о типе элементов, добавляемых в стек, как, например: public override void put (T elem) По ходу дела нам понадобился класс, задающий представление элементов стека в списковом представлении. Объявим его: public class GenLinkable { public T Item; public GenLinkable public GenLinkable() { Item = default(T); Next = null; } } Класс устроен достаточно просто, у него два поля-, одно для хранения элементов, помещаемых в стек и имеющее тип Второй потомок абстрактного класса реализует стек по-другому, используя представление в виде массива. Потомок задает стек ограниченной емкости. Емкостью стека можно управлять в момент его создания. В ряде ситуаций использование такого стека предпочтительнее по соображениям эффективности, поскольку не требует динамического создания элементов. Приведу текст этого класса уже без дополнительных комментариев: public class ArrayUpStack { int SizeOfStack; T[] stack; int top; /// /// конструктор /// /// paзмер стека public ArrayUpStack(int size) { SizeOfStack = size; stack = new T [SizeOfStack]; top = 0; } /// /// require: (top < SizeOfStack) /// /// элемент, помещаемый в стек public override void put (T x) { stack[top] = x; top++; } public override void remove() { top-; } public override T item() { return (stack[top-1]); } public override bool empty() { return (top == 0); } }//class ArrayUpStack Созданные в результате наследования классы-потомки перестали быть абстрактными, но все еще остаются универсальными. На третьем этапе порождаются конкретные экземпляры потомков — универсальных классов, в этот момент и происходит конкретизация типов, и два экземпляра одного универсального класса могут работать с данными различных типов. Этот процесс создания экземпляров с подстановкой конкретных типов называют public void TestStacks() { OneLinkStack OneLinkStack ArrayUpStack stack1.put (11); stackl.put (22); int x1 = stackl.item(), x2 = stackl.item(); if ((x1 == x2) && (xl == 22)) Console.WriteLine("OK!"); stack1.remove(); x2 = stack1.item(); if ((x1!= x2) && (x2 == 11)) Console.WriteLine("OK!"); stack1.remove(); x2 = (stack1.empty())? 77: stackl.item(); if ((x1!= x2) && (x2 == 77)) Console.WriteLine("OK!"); stack2.put("first"); stack2.put("second"); stack2.remove(); string s = stack2.item(); if (!stack2.empty()) Console.WriteLine(s); stack3.put(3.33); stack3.put(Math.Sqrt(Math.PI)); double res = stack3.item(); stack3.remove(); res += stack3.item(); Console.WriteLine("res= {0}", res); } В трех первых строках этой процедуры порождаются три экземпляра стеков. Все они имеют общего родителя — абстрактный универсальный класс Рис. 22.3. Дополним наше рассмотрение еще одним примером работы с вариацией стеков, в том числе хранящим Объекты класса Person; public void TestPerson() { OneLinkStack OneLinkStack ArrayUpStack ArrayUpStack stack2.put("Петров"); stack2.put("Васильев"); stack2.put("Шустов"); stack1.put (27); stack1.put (45); stack1.put (53); stack3.put (21550.5); stack3.put (12345.7); stack3.put (32 458.8); stack4.put(new Person(stack2.item(), stack1.item(), stack3.item())); stack1.remove(); stack2.remove(); stack3.remove(); stack4.put(new Person(stack2.item(), stack1.item(), stack3.item())); stack1.remove(); stack2.remove(); stack3.remove(); stack4.put(new Person(stack2.item(), stack1.item(), stack3.item())); Person pers = stack4.item(); pers.PrintPerson (); stack4.remove(); pers = stack4.item(); pers.PrintPerson(); stack4.remove(); pers = stack4.item(); pers.PrintPerson(); stack4.remove(); if (stack4.empty()) Console.WriteLine("OK!"); } Результаты работы этой процедуры приведены на рис. 22.4. Рис. 22.4. Ограниченная универсальность Хорошо, когда есть свобода. Еще лучше, когда свобода ограничена. Аналогичная ситуация имеет место и с универсальностью. Универсальность следует ограничивать. На типы универсального класса, являющиеся его параметрами, следует накладывать ограничения. Звучит парадоксально, но, наложив ограничения на типы, программист получает гораздо большую свободу в работе с объектами этих типов. Если немного подумать, то это совершенно естественная ситуация. Когда имеет место неограниченная универсальность, над объектами типов можно выполнять только те операции, которые допускают все типы, — в C# это эквивалентно операциям, разрешенным над объектами типа object, прародителя всех типов. В нашем предыдущем примере, где речь шла о свопинге, над объектами выполнялась единственная операция присваивания. Поскольку присваивание внутри одного типа разрешено для всех типов, то неограниченная универсальность приемлема в такой ситуации. Но что произойдет, если попытаться выполнить сложение элементов, сравнение их или даже простую проверку элементов на равенство? Немедленно возникнет ошибка еще на этапе компиляции. Эти операции не разрешены для всех типов, поэтому в случае компиляции такого проекта ошибка могла бы возникнуть на этапе выполнения, когда вместо формального типа появился бы тип конкретный, не допускающий подобную операцию. Нельзя ради универсальности пожертвовать одним из важнейших механизмов C# и Framework.Net — безопасностью типов, поддерживаемой статическим контролем типов. Именно поэтому неограниченная универсальность существенно ограничена. Ее ограничивает статический контроль типов. Бывают, разумеется, ситуации, когда необходимо на типы наложить ограничения, позволяющие ослабить границы статического контроля. На практике универсальность почти всегда ограничивается, что, повторяю, дает большую свободу программисту. В языке C# допускаются три вида ограничений, накладываемых на родовые параметры. • • • Возникает законный вопрос: насколько полна предлагаемая система ограничений? Конечно, речь идет о практической полноте, а не о математически строгих определениях. С позиций практики систему хотелось бы дополнить, в первую очередь, введением ограничений операций, указывающим допустимые знаки операций в выражениях над объектами соответствующего типа. Хотелось бы, например, указать, что к объектам типа т применима операция сложения + или операция сравнения <. Позже я покажу, как можно справиться с этой проблемой, но предлагаемое решение довольно сложно. Наличие ограничения операций намного элегантнее решало бы эту проблему. Синтаксис ограничений Уточним некоторые синтаксические правила записи ограничений. Если задан универсальный класс с типовыми параметрами public class Father { } public ckass Base } public void M1() { } public void M2() { } } public class Child where T1:Base,IEnumerable where T2:struct,IComparable { } Класс Список с возможностью поиска элементов по ключу Ключевые идеи ограниченной универсальности, надеюсь, понятны. Давайте теперь рассмотрим пример построения подобного класса, где можно будет увидеть все детали. Возьмем классическую и саму по себе интересную задачу построения списка с курсором. Как и всякий контейнер данных, список следует сделать универсальным, допускающим хранение данных разного типа. С другой стороны, мы не хотим, чтобы в одном списке происходило смешение типов, — уж если там хранятся персоны, то чисел Чтобы не затемнять ситуацию сложностью списка, рассмотрим достаточно простой односвязный список с курсором. Элементы этого списка будут принадлежать классу class Node { public Node() { next = null; key = default(K); item = default(T); } public К key; public T item; public Node } Класс Рассмотрим теперь организацию односвязного списка. Начнем с того, как устроены его данные: public class OneLinkList { Node } Являясь клиентом универсального класса public void start () { cursor = first; } public void finish () { while (cursor.next!= null) cursor = cursor.next; } public void forth() { if (cursor.next!= null) cursor = cursor.next; } Операция Основной операцией является операция добавления элемента с ключом в список. Возможны различные ее вариации, из которых рассмотрим только одну — новый элемент добавляется за текущим, отмеченным курсором. Вот текст этого метода: public void add(К key, Т item) { Node if (first == null) { first = newnode; cursor = newnode; newnode.key = key; newnode.item = item; } else { newnode.next = cursor.next; cursor.next = newnode; newnode.key = key; newnode.item = item; } } Заметьте, аргументы метода имеют соответствующие родовые параметры, чем и обеспечивается универсальный характер списка. При добавлении элемента в список различаются два случая — добавление первого элемента и всех остальных. Рассмотрим теперь операцию поиска элемента по ключу, реализация которой потребовала ограничения универсальности типа ключа к; public bool findstart(К key) { Node while (temp!= null) { if (temp.key.CompareTo(key) == 0) {cursor=temp; return(true);} temp= temp.next; } return (false); } Искомые элементы разыскиваются во всем списке. Если элемент найден, то курсор устанавливается на найденном элементе и метод возвращает значение Два метода класса являются запросами, позволяющими извлечь ключ и элемент списка, который отмечен курсором: public К Key () { return (cursor.key); } public T Item() { return(cursor.item); } Давайте рассмотрим теперь тестирующую процедуру — клиента нашего списка, демонстрирующую работу со списками, в которых элементы и ключи имеют разные типы: public void TestConstraint () { OneLinkList list1.add(33, "thirty three"); list1.add (22, "twenty two"); if (list1.findstart(33)) Console.WriteLine ("33 — найдено!"); else Console.WriteLine("33 — не найдено!"); if (list1.findstart(22)) Console.WriteLine ("22 — найдено!"); else Console.WriteLine("22 — не найдено!"); if (list1.findstart(44)) Console.WriteLine ("44 — найдено!"); else Console.WriteLine("44 — не найдено!"); Person pers1 = new Person("Савлов", 25, 1500); Person pers2 = new Person("Павлов", 35, 2100); OneLinkList < string, Person>(); list2.add("Савл", pers1); list2.add("Павел", pers2); if (list2.findstart("Павел")) Console.WriteLine ("Павел — найдено!"); else Console.WriteLine("Павел — не найдено!"); if (list2.findstart("Савл")) Console.WriteLine ("Савл — найдено!"); else Console.WriteLine("Савл — не найдено!"); if (list2.findstart("Иоанн")) Console.WriteLine ("Иоанн — найдено!"); else Console.WriteLine("Иоанн — не найдено!"); Person pers3 = new Person("Иванов", 33, 3000); list2.add("Иоанн", pers3); list2.start (); Person pers = list2.Item(); pers.PrintPerson(); list2.findstart("Иоанн"); pers = list2.Item(); pers.PrintPerson(); } Рис. 22.5. Обратите внимание на строки, где создаются два списка: OneLinkList OneLinkList У списка Как справиться с арифметикой Представьте себе, что мы хотим иметь специализированный вариант нашего списка, элементы которого допускали бы операцию сложения и одно из полей которого сохраняло бы сумму всех элементов, добавленных в список. Как задать соответствующее ограничение на класс? Как уже говорилось, наличие ограничения операции, где можно было бы указать, что над элементами определена операция Вот один из возможных выходов, предлагаемых в такой ситуации. Стратегия следующая: определим абстрактный универсальный класс public abstract class Calc { public abstract T Add(T a, T b); public abstract T Sub(T a, T b); public abstract T Mult(T a, T b); public abstract T Div(T a, T b); } Наш абстрактный универсальный класс определяет четыре арифметические операции. Давайте построим трех его конкретизированных потомков: public class IntCalc: Calc { public override int Add(int a, int b) { return (a + b);} public override int Sub (int a, int b) { return (a — b);} public override int Mult(int a, int b) { return (a * b);} public override int Div(int a, int b) { return (a / b); } } public class DoubleCalc: Calc { public override double Add(double a, double b) {return (a + b);} public override double Sub(double a, double b) {return (a — b);} public override double Mult(double a, double b) {return (a * b);} public override double Div(double a, double b) {return (a / b);} } public class StringCalc: Calc { public override string Add(string a, string b) {return (a + b);} public override string Sub(string a, string b) {return (a);} public override string Mult(string a, string b) {return (a);} public override string Div (string a, string b) {return (a);} } Здесь определяются три разных калькулятора: один — над целочисленными данными, другой — над данными с плавающей точкой, третий — над строковыми данными. В последнем случае определена, по сути, только операция сложения строк (конкатенации). Теперь нам нужно ввести изменения в ранее созданный класс В полном соответствии с этим принципом построим класс public class SumList IComparable { Calc T s um; public SumList(Calc { this.calc = calc; sum = default(T); } public new void add(K key, T item) { Node if (first == null) { first = newnode; cursor = newnode; newnode.key = key; newnode.item = item; sum = calc.Add(sum, item); } else { newnode.next = cursor.next; cursor.next = newnode; newnode.key = key; newnode.item = item; sum = calc.Add(sum, item); } } public T Sum() {return (sum); } }//SumList У класса добавилось поле sum, задающее сумму хранимых элементов, и поле Некоторые изменения в уже существующем проекте пришлось-таки сделать, изменив статус доступа у полей. А все потому, что в целях экономии текста кода я не стал закрывать поля и вводить, как положено, открытые процедуры-свойства для закрытых полей. Проведем теперь эксперименты с новыми вариантами списков, допускающих суммирование элементов: public void TestSum() { SumList new SumList list1.add("Петр", 33); list1.add("Павел", 44); Console.WriteLine("sum= {0}", list1.Sum()); SumList new SumList list2.add("Петр", 33.33); list2.add("Павел", 44.44); Console.WriteLine("sum= {0}", list2.Sum()); SumList new SumList list3.add("Мама", " Мама мыла "); list3.add("Маша", "Машу мылом!"); Console.WriteLine("sum= {0}", list3.Sum ()); } Обратите внимание на создание списков: SumList new SumList SumList new SumList SumList new SumList Как видите, конструктору объекта передается калькулятор, согласованный с типами данных, которые хранятся в списке. Результаты вычислений, полученных при работе с этими списками, приведены на рис. 22.6. Рис. 22.6. Родовое порождение класса. Предложение До сих пор рассматривалась ситуация родового порождения экземпляров универсального класса. Фактические типы задавались в момент создания экземпляра. Это наглядно показывает преимущества применяемой технологии, поскольку очевидно, что не создается дублирующий код для каждого класса, порожденного универсальным классом. И все-таки остается естественный вопрос: можно ли породить класс из универсального класса путем подстановки фактических параметров, а потом спокойно использовать этот класс обычным образом? Такая вещь возможна. Это можно сделать не совсем обычным путем — не в программном коде, а в предложении Давайте вернемся к универсальному классу using IntStack = Generic.OneLinkStack Вот тест, в котором создаются несколько объектов этого класса: public void TestlntStack () { IntStack stack1 = new IntStack (); IntStack stack2 = new IntStack (); IntStack stack3 = new IntStack (); stack2.put (11); stackl.put (22); int x1 = stack1.item(), x2 = stack1.item(); if ((x1 == x2) && (xl == 22)) Console.WriteLine("OK!"); stack1.remove(); x2 = stack1.item(); if ((x1!= x2) && (x2 == 11)) Console.WriteLine("OK!"); stack1.remove(); x2 = (stack1.empty())? 77: stack1.item(); if ((x1!= x2) && (x2 == 77)) Console.WriteLine("OK!"); stack2.put (55); stack2.put (66); stack2.remove(); int s = stack2.item(); if (!stack2.empty()) Console.WriteLine(s); stack3.put (33 3); stack3.put((int)Math.Sqrt(Math.PI)); int res = stack3.item(); stack3.remove(); res += stack3.item(); Console.WriteLine("res= {0}", res); } Все работает заданным образом, можете поверить. Универсальность и специальные случаи классов Универсальность — это механизм, воздействующий на все элементы языка. Поэтому он применим ко всем частным случаям классов C#. Универсальные структуры Так же, как и обычный класс, структура может иметь родовые параметры. Синтаксис объявления, ограниченная универсальность, другие детали универсальности естественным образом распространяются на структуры. Вот типичный пример: public struct Point { Т х, у;//координаты точки, тип которых задан параметром // другие свойства и методы структуры } Универсальные интерфейсы Интерфейсы чаще всего следует делать универсальными, предоставляя большую гибкость для позднейших этапов создания системы. Возможно, вы заметили применение в наших примерах универсальных интерфейсов библиотеки FCL — Универсальные делегаты Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в универсальном классе и использует в своем объявлении параметры универсального класса. Давайте рассмотрим ситуацию с делегатами более подробно. Вот объявление универсального класса, не очень удачно названного class Delegate { public delegate T Del(T a, T b); } Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется классом Добавим в класс функцию высшего порядка public T FunAr(T[] arr, T a0, Del f) { T temp = a 0; for (int i =0; i { temp = f(temp, arr[i]); } return (temp); } Эта универсальная функция с успехом может применяться для вычисления сумм, произведения, минимума и других подобных характеристик массива. Рассмотрим теперь клиентский класс public int max2(int a, int b) { return (a > b)? a: b; } public double min2(double a, double b) { return (a < b)? a: b; } public string sum2(string a, string b) { return a + b; } public float prod2(float a, float b) { return a * b; } Хотя все функции имеют разные типы, все они соответствуют определению класса public void TestFun() int[] ar1 = { 3, 5, 7, 9 }; doublet] ar2 = { 3.5, 5.7, 7.9 }; string[] агЗ = { "Мама", "мыла", "Машу", "мылом." }; float[] ar4 = { 5f, 7f, 9f, 11f }; Delegate Delegate del1= this.max2; int max = d1.FunAr(ar1, ar1[0], del1); Console.WriteLine("max= {0}", max); Delegate Delegate del2 = this.min2; double min = d2.FunAr(ar2, ar2[0], del2); Console.WriteLine("min= {0}", min); Delegate Delegate del3 = this.sum2; string sum = d3.FunAr(ar3, del3); Console.WriteLine("concat= {0}", sum); Delegate Delegate del4 = this.prod2; float prod = d4.FunAr(ar4, If, del4); Console.WriteLine("prod= {0}", prod); } Обратите внимание на объявление экземпляра делегата: Delegate В момент объявления задается фактический тип, и сигнатура экземпляра становится конкретизированной. Теперь экземпляр можно создать и связать с конкретной функцией. В C# 2.0 это делается проще и естественнее, чем ранее, — непосредственным присваиванием: del1= this.max2; При выполнении этого присваивания производятся довольно сложные действия — проверяется соответствие сигнатуры функции в правой части и экземпляра делегата, в случае успеха создается новый экземпляр делегата, который и связывается с функцией. Покажем, что и сам функциональный тип-делегат можно объявлять с родовыми параметрами. Вот пример такого объявления: public delegate T FunTwoArg Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом: FunTwoArg myde1 = max 2; max = mydel(17, 21); Console.WriteLine("max= {0}", max); Вот как выглядят результаты работы тестового примера: Рис. 22.7. Универсальные делегаты с успехом используются при определении событий. В частности, класс public void delegate EventHandler Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра Универсальность принадлежит к основным механизмам языка. Ее введение в язык C# не могло не сказаться на всех его основных свойствах. Как уже говорилось, классы и все частные случаи стали обладать этим свойством. Введение универсальности не должно было ухудшить уже достигнутые свойства языка — статический контроль типов, динамическое связывание и полиморфизм. Не должна была пострадать и эффективность выполнения программ, использующих универсальные классы. Решение этих задач потребовало введения универсальности не только в язык С#, но и поддержки на уровне каркаса Framework.Net и языка IL, включающем теперь параметризованные типы. Универсальный класс C# не является шаблоном, на основе которого строится конкретизированный класс, компилируемый далее в класс (тип) IL. Компилятору языка C# нет необходимости создавать классы для каждой конкретизации типов универсального класса. Вместо этого происходит компиляция универсального класса C# в параметризованный тип IL. Когда же CLR занимается исполнением управляемого кода, то вся необходимая информация о конкретных типах извлекается из метаданных, сопровождающих объекты. При этом дублирования кода не происходит и на уровне JIT-компиляторов, которые, однажды сгенерировав код для конкретного типа, сохраняют ссылку на этот участок кода и передают ее, когда такой код понадобится вторично. Это справедливо как для ссылочных, так и значимых типов. Естественно, что универсальность потребовала введения в библиотеку FCL соответствующих классов, интерфейсов, делегатов и методов классов, обладающих этим свойством. Так, например, в класс System.Array добавлен ряд универсальных статических методов. Вот один из них: public static int BinarySearch В таблице 22.1 показаны некоторые универсальные классы и интерфейсы библиотеки FCL 2.0 из пространства имен
23. Отладка и обработка исключительных ситуаций Корректность и устойчивость. Спецификация системы. Корректность и устойчивость программных систем. Исключительные ситуации. Обработка исключительных ситуаций. Жизненный цикл программной системы. Три закона программотехники. Отладка. Создание надежного кода. Искусство отладки. Отладка и инструментальная среда Visual Studio.Net. Корректность и устойчивость — два основных качества программной системы, без которых все остальные ее достоинства не имеют особого смысла. Понятие корректности программной системы имеет смысл только тогда, когда задана ее спецификация. В зависимости оттого, как формализуется спецификация, уточняется понятие корректности. В лекции 9 введено строгое понятие корректности метода по отношению к его спецификациям, заданным в виде предусловия и постусловия метода. Во время работы системы могут возникать ситуации, выходящие за пределы, предусмотренные спецификацией. Такие ситуации называются исключительными. Почему так трудно создавать корректные и устойчивые программные системы? Все дело в сложности разрабатываемых систем. Когда в 60-х годах прошлого века фирмой IBM создавалась операционная система OS-36C), то на ее создание потребовалось 5000 человеко-лет, и проект по сложности сравнивался с проектом высадки первого человека на Луну. Сложность нынешних сетевых операционных систем, систем управления хранилищами данных, прикладных систем программирования на порядки превосходит сложность OS-360, так что, несмотря на прогресс, достигнутый в области технологии программирования, проблемы, стоящие перед разработчиками, не стали проще. Жизненный цикл программной системы Под "жизненным циклом" понимается период от замысла программного продукта до его "кончины". Обычно рассматриваются следующие фазы этого процесса: Проектирование <-> Разработка <-> Развертывание и Сопровождение Все это называется циклом, поскольку после каждой фазы возможен возврат к предыдущим этапам. В объектной технологии этот процесс является бесшовным, все этапы которого тесно переплетены. Не следует рассматривать его как однонаправленный — от проектирования к сопровождению. Чаще всего, ситуация обратная: уже существующая реализация системы, прошедшая сопровождение, и существующие библиотеки компонентов оказывают решающее влияние на то, какой будет новая система, каковы будут ее спецификации. Вот некоторые типовые правила, характерные для процесса разработки ПО: • Уделяйте этапу проектирования самое пристальное внимание. Успех дела во многом определяется первым этапом. Нет смысла торопиться с переходом на последующие этапы, пока не составлены ясные и четкие спецификации. Ошибки этого этапа — самые дорогие и трудно исправляемые. • Помните о тех, для кого разрабатывается программный продукт. Идите "в люди", чтобы понять, что нужно делать. Вместе с тем, не следует полностью полагаться на пользователей — их опыт консервативен, новые идеи могут часто приходить от разработчиков, а не от пользователей. • Разработка не начинается "с нуля". Только используя уже готовые компоненты, можно своевременно создать новую систему. Работая над проектом, думайте о будущем, создавайте компоненты, допускающие их повторное использование в других проектах. • Создавайте как можно раньше прототип своей системы и передавайте его пользователям в опытную эксплуатацию. Это поможет устранить множество недостатков и ошибок в заключительной версии программного продукта. • Какие бы хорошие спецификации не были написаны, какими бы хорошими технологиями и инструментами не пользовались разработчики, какими бы профессионалами они ни были — этого еще не достаточно для успеха дела. Необходимым условием является управление проектом, наличие специальных средств управления. Но и этого не достаточно. Третьим важным фактором является существование команды. Коллектив разработчиков должен представлять собой единый коллектив. Умение работать в команде так же важно, как и профессиональные навыки разработчика. Три закона программотехники Этот закон отражает сложность нетривиальных систем. Разработчик всегда должен быть готов к тому, что в работающей системе имеются ситуации, в которых система работает не в точном соответствии со своей спецификацией, так что от него может требоваться очередное изменение либо системы, либо ее спецификации. Не бывает некорректных систем. Каждая появляющаяся ошибка при эксплуатации системы — это следствие незнания спецификации системы. Есть два объяснения справедливости второго закона. Несерьезное объяснение состоит в том, что любая система, что бы она ни делала, при любом постусловии корректна по отношению к предусловию Более поучительна реальная ситуация, подтверждающая второй закон и рассказанная мне в былые годы Виталием Кауфманом — специалистом по тестированию трансляторов. В одной серьезной организации была разработана серьезная прикладная система, имеющая для них большое значение. К сожалению, при ее эксплуатации сплошь и рядом возникали ошибки, из-за которых организация вынуждена была отказаться от использования системы. Разработчики обратились к нему за помощью. Он, исследуя систему, не внес в нее ни строчки кода. Единственное, что он сделал, это описал точную спецификацию системы, благодаря чему стала возможной нормальная эксплуатация. Обратите внимание на философию, характерную для этих законов: при возникновении ошибки разработчик и пользователь должны винить себя, а не кивать друг на друга. Так что часто встречающиеся фразы "Ох уж эта фирма Чейтософт — вечно у них ошибки!" характеризует, мягко говоря, непрофессионализм говорящего. Если Неквалифицированный пользователь в любом контексте всегда способен выбрать наименее подходящее действие, явно не удовлетворяющее спецификации, которая ориентирована на "разумное" поведение пользователей. Полезным практическим следствием этого закона является привлечение к этапу тестирования системы неквалифицированного пользователя — "человека с улицы". Отладка Что должно делать для создания корректного и устойчивого программного продукта? Как минимум, необходимо: • создать надежный код, корректность которого предусматривается с самого начала; • отладить этот код; • предусмотреть в нем обработку исключительных ситуаций. Создание надежного кода Большинство вопросов, затрагиваемых в этой лекции, в том числе и проблемы создания надежного кода, заслуживают отдельного и глубокого рассмотрения. К сожалению, придется ограничиться лишь высказыванием ряда тезисов. Для повышения надежности нужно уменьшить сложность системы, и главное в этом процессе — это повторное использование. В идеале большая часть системы должна быть собрана из уже готовых компонентов. Объектная технология проектирования вносит свой вклад в повышение надежности кода. Наследование и универсализация позволяют, не изменяя уже существующие классы, создать новые классы, новые типы данных, придающие проектируемой системе новые свойства при минимальных добавлениях нового кода. Статический контроль типов позволяет выявить многие ошибки еще на этапе компиляции. Динамическое связывание и полиморфизм позволяют автоматически включать объекты классов-потомков в уже существующие схемы работы — методы родителя могут вызывать методы потомков, ничего не зная о появлении этих новых потомков. Автоматическая сборка мусора позволяет снять с разработчика обязанности управления освобождением памяти и предотвратить появление крайне неприятных и опасных ошибок, связанных с некорректным удалением объектов. Крайне важную роль в создании надежного кода играют спецификации методов класса, класса в целом, системы классов. Спецификации являются частью документации, встроенной в проект, и вообще важной его частью. Их существование облегчает не только создание корректного кода, соответствующего спецификации, но и создание системы тестов, проверяющих корректность кода. Нужно сказать, что существуют специальные инструментальные средства, поддерживающие автоматическое создание тестов на основе спецификаций. Незаменима роль спецификаций на этапе сопровождения и повторного использования компонентов. Невозможно повторно использовать компонент, если у него нет ясной и полной спецификации. Искусство отладки Нужно стараться создавать надежный код. Но без отладки пока обойтись невозможно. Роль тестеров в современном процессе разработки ПО велика. Отладка — это некоторый детективный процесс. Программа, в которую внесены изменения, подозревается в том, что она работает некорректно. Презумпция невиновности здесь не применима. Если удается предъявить тест, на котором программа дает неверный результат, то доказано, что подозрения верны. Втайне мы всегда надеемся, что программа работает правильно. Но цель тестирования другая — попытаться опровергнуть это предположение. Отладка может доказать некорректность программы, но она не может доказать ее правильность. Отладка не гарантирует корректности программы, даже если все тесты пройдены успешно. Искусное тестирование создает высокую степень уверенности в корректности программы. Часть ошибок программы ловится автоматически еще на этапе компиляции. Сюда относятся все синтаксические ошибки, ошибки несоответствия типов и некоторые другие. Это простые ошибки и их исправление, как правило, не вызывает трудностей. В отладке нуждается синтаксически корректная программа, результаты вычислений которой получены, но не соответствуют требуемым спецификациям. Чаще всего еще не отлаженная программа на одних исходных данных работает правильно, на других — дает ошибочный результат. Искусство отладки состоит в том, чтобы обнаружить все ситуации, в которых работа программы приводит к ошибочным вычислениям. Как и во всякой детективной деятельности, в ходе отладки необходим сбор улик, для чего применяется две группы средств. Первая позволяет контролировать ход вычислительного процесса: порядок следования операторов в методах, порядок вызова самих методов, условия окончания циклов, правильность переходов. Вторая отслеживает изменение состояния вычислительного процесса (значения свойств объектов) в процессе выполнения. Есть и другая классификация. Средства, используемые при отладке, можно разделить на инструментарий, предоставляемый средой разработки Visual Studio.Net, и программные средства, предоставляемые языком и специальными классами библиотеки FCL. Начнем рассмотрение с программных средств. Отладочная печать и условная компиляция Одним из основных средств отладки является отладочная печать, позволяющая получить данные о ходе и состоянии процесса вычислений. Обычно разрабатываются специальные отладочные методы, вызываемые в критических точках программы — на входе и выходе программных модулей, на входе и выходе циклов и так далее. Искусство отладки в том и состоит, чтобы получить нужную информацию о прячущихся ошибках, проявляющихся, возможно, только в редких ситуациях. Хотелось бы иметь легкий механизм управления отладочными методами, позволяющий включать при необходимости те или иные инструменты. Для этого можно воспользоваться механизмом условной компиляции, встроенным в язык С#. Этот механизм состоит из двух частей. К проекту, точнее, к конфигурации проекта можно добавить специальные константы условной компиляции. Вызов отладочного метода может быть сделан условным. Если соответствующая константа компиляции определена, то происходит компиляция вызова метода и он будет вызываться при выполнении проекта. Если же константа не определена (выключена), то вызов метода даже не будет компилироваться и никаких динамических проверок — вызывать метод или нет — делаться не будет. Как задавать константы компиляции? Напомню, что проекты в Visual Studio существуют в нескольких конфигурациях. В ходе работы с проектом можно легко переключаться с одной конфигурации на другую, после чего она становится активной, можно изменять настройки конфигурации, можно создать собственные конфигурации проекта. По умолчанию проект создается в двух конфигурациях — В лекции 2 рассказывалось, как добраться до страницы свойств проекта. Взгляните еще раз на рис. 2.3 этой лекции, где показана страница свойств, и обратите внимание на первую строчку, содержащую список констант условной компиляции активной конфигурации (в данном случае — Debug). К этому списку можно добавлять собственные константы. Можно также задавать константы условной компиляции в начале модуля проекта вперемешку с предложениями using. Предложение define позволяет определить новую константу: #define COMPLEX Как используются константы условной компиляции? В языке C++, где имеется подобный механизм, определен специальный препроцессорный I [Conditional ("COMPLEX")] public void ComplexMethod () {…} Если константа условной компиляции На методы, для которых возможно задание атрибута • функцией, возвращающей значение; • методом интерфейса; • методом с модификатором override. Возможно его задание для virtual-метода. В этом случае атрибут наследуется методами потомков. Атрибут Приведу пример работы с отладочными методами. Рассмотрим класс, в котором определены три метода, используемые при отладке: public class DebugPrint { [Conditional("DEBUG")] static public void PrintEntry(string name) { Console.WriteLine("Начал работать метод " + name); } [Conditional("DEBUG")] static public void PrintExit(string name) { Console.WriteLine("Закончил работать метод " + name); } [Conditional("DEBUG")] static public void PrintObject(object obj, string name) { Console.WriteLine("Объект {0}: {1}", name, obj.ToString ()); } } В классе int state = 1; и группа методов: public void TestDebugPrint () { DebugPrint.PrintEntry("Testing.TestDebugPrint"); PubMethod (); DebugPrint.PrintObj ect(state, "Testing.state"); DebugPrint.PrintExit("Testing.TestDebugPrint"); } void InMethod1() { DebugPrint.PrintEntry("InMethod1"); // body DebugPrint.PrintExit("InMethod1"); } void InMethod2() { DebugPrint.PrintEntry("InMethod2"); // body DebugPrint.PrintExit("InMethod2"); } public void PubMethod() { DebugPrint.PrintEntry("PubMethod"); InMethod1 (); state++; InMethod2 (); DebugPrint.PrintExit("PubMethod"); } Этот пример демонстрирует трассировку хода вычислений, для чего в начало и конец каждого метода вставлены вызовы отладочных методов, снабжающие нас информацией о ходе вычислений. Такая трассировка иногда бывает крайне полезной на этапе отладки, но, естественно, она не должна присутствовать в финальной версии проекта. Взгляните на результаты, полученные при вызове метода Рис. 23.1. При переходе к конфигурации Release отладочная информация появляться не будет. Классы Атрибут условной компиляции Классы Одна из основных групп методов этих классов — методы печати данных: По умолчанию методы обоих классов направляют вывод в окно • • • Можно и самому создать потомка абстрактного класса, предложив, например, XML-слушателя, направляющего вывод в соответствующий XML-документ. Как видите, система управления выводом очень гибкая, позволяющая получать и сохранять информацию о ходе вычислений в самых разных местах. Помимо свойства • • • • У классов есть и другие свойства и методы, позволяющие, например, заниматься структурированием текста сообщений. Рассмотрим пример работы, в котором отладочная информация направляется в разные каналы — окно вывода, консоль, файл: public void Optima() { double х, у=1; х= у — 2*Math.Sin(у); FileStream f = new FileStreamCDebuginfo.txt", FileMode.Create, FileAccess.Write); TextWriterTraceListener writer1 = new TextWriterTraceListener(f); TextWriterTraceListener writer2 = new TextWriterTraceListener(System.Console.Out); Trace.Listeners.Add(writer1); Debug.Listeners.Add (writer2); Debug.WriteLine("Число слушателей: " + Debug.Listeners.Count); Debug.WriteLine("автоматический вывод из буфера: "+ Trace.AutoFiush); Trace. WriteLinelf (x<0, "Trace: " + "x= " + x.ToString() + " у = " + y); Debug. WriteLine (" Debug: " + "x= " + x.ToString() + " у = " + у); Trace.Flush(); f. Close(); } В коллекцию слушателей вывода к слушателю по умолчанию добавляются еще два слушателя класса Рис. 23.2. Метод Флойда и утверждения Лет двадцать назад большие надежды возлагались на формальные методы доказательства правильности программ, позволяющие доказывать корректность программ аналогично доказательству теорем. Реальные успехи формальных доказательств невелики. Построение такого доказательства не проще написания корректной программы, а ошибки столь же возможны и часты, как и ошибки программирования. Тем не менее, эти методы оказали серьезное влияние на культуру проектирования корректных программ, появление в практике программирования понятий предусловия и постусловия, инвариантов и других важных понятий. Одним из методов доказательства правильности программ был метод Флойда, при котором программа разбивалась на участки, окаймленные утверждениями — булевскими выражениями (предикатами). Истинность начального предиката должна была следовать из входных данных программы. Затем для каждого участка доказывалось, что из истинности предиката, стоящего в начале участка, после завершения выполнения соответствующего участка программы гарантируется истинность следующего утверждения — предиката в конце участка. Конечный предикат описывал постусловие программы. Схема Флойда используется на практике, по крайней мере, программистами, имеющими вкус к строгим методам доказательства. Утверждения становятся частью программного текста. Само доказательство может и не проводиться: чаще всего у программиста есть уверенность в справедливости расставленных утверждений и убежденность, что при желании он мог бы провести и строгое доказательство. В C# эта схема поддерживается тем, что классы Рис. 23.3. В этой ситуации у программиста есть несколько возможностей: • прервать выполнение, нажав кнопку Abort; • перейти в режим отладки (Retry); • продолжить вычисления, проигнорировав уведомление. В последнем случае сообщение о возникшей ошибке будет послано всем слушателям коллекции Рассмотрим простой пример, демонстрирующий нарушение утверждения: public void WriteToFile() { Stream myFile = new FileStream("TestFile.txt",FileMode.Create,FileAccess.Write); TextWriterTraceListener myTextListener = new TextWriterTraceListener(myFile); int у = Debug.Listeners.Add(myTextListener); TextWriterTraceListener myWriter = new TextWriterTraceListener(System.Console.Out); Trace.Listeners.Add(myWriter); Trace.AutoFlush = true; Trace.WriteLine("автоматический вывод из буфера:" + Trace.AutoFlush); int x = 22; Trace.Assert(x<=21, "Перебор"); myWriter.WriteLine("Вывод только на консоль"); //Trace.Flush(); //Вывод только в файл byte[] buf = {(byte)'В;(byte)'у'}; myFile.Write(buf,0, 2); myFile.Close (); } Как и в предыдущем примере, здесь создаются два слушателя, направляющие вывод отладочных сообщений на консоль и в файл. Когда произошло нарушение утверждения Рис. 23.4. Вариацией метода Классы В библиотеке FCL имеются и другие классы, полезные при отладке. Класс Отладка и инструментальная среда Visual Studio.Net Инструментальная среда студии предоставляет программисту самый широкий спектр возможностей слежения за ходом вычислений и отслеживания состояний, в котором находится процесс вычислений. Поскольку все современные инструментальные среды организованы сходным образом и хорошо известны работающим программистам, я позволю себе не останавливаться на описании возможностей среды. Обработка исключительных ситуаций Какой бы надежный код ни был написан, сколь бы тщательной ни была отладка, в версии, переданной в эксплуатацию и на сопровождение, при запусках будут встречаться нарушения спецификаций. Причиной этого являются выше упомянутые законы программотехники. В системе остается последняя ошибка, находятся пользователи, не знающие спецификаций, и если спецификацию можно нарушить, то это событие когда-нибудь да произойдет. В таких исключительных ситуациях продолжение выполнения программы либо становится невозможным (попытка выполнить неразрешенную операцию деления на ноль, попытки записи в защищенную область памяти, попытка открытия несуществующего файла, попытка получить несуществующую запись базы данных), либо в возникшей ситуации применение алгоритма приведет к ошибочным результатам. Что делать при возникновении исключительной ситуации? Конечно, всегда есть стандартный способ — сообщить о возникшей ошибке и прервать выполнение программы. Понятно, что это приемлемо лишь для безобидных приложений; даже для компьютерных игр этот способ не годится, что уж говорить о критически важных приложениях! В языках программирования для обработки исключительных ситуаций предлагались самые разные подходы. Обработка исключений в языках C/C++ Для стиля программирования на языке С характерно описание методов класса как булевых функций, возвращающих bool MyMethod(…){…} if!MyMethod(){// обработка ошибки} {//нормальное выполнение} Недостатки этой схемы понятны. Во-первых, мало информации о причине возникновения ошибки, поэтому либо через поля класса, либо через аргументы метода нужно передавать дополнительную информацию. Во-вторых, блок обработки встраивается в каждый вызов, что приводит к раздуванию кода. Поэтому в C/C++ применяется схема try/catch блоков, суть которой в следующем. Участок программы, в котором может возникнуть исключительная ситуация, оформляется в виде охраняемого try-блока. Если при его выполнении возникает исключительная ситуация, то происходит прерывание выполнения С некоторыми синтаксическими отличиями схема с возобновлением применяется в языках VB/VBA. Многообразие подходов к обработке исключений говорит о том, что не найден единый, удовлетворяющий всех подход. Чуть позже я расскажу о наиболее разумной, с моей точки зрения, схеме. К сожалению, в C# применяется не она. Схема обработки исключений в C# Язык C# наследовал схему исключений языка C++, внеся в нее свои коррективы. Рассмотрим схему подробнее и начнем с синтаксиса конструкции try-catch-finally: try {…} catch (т1 e1) {…} ... catch(Tk ek) {…} finally {…} Всюду в тексте модуля, где синтаксически допускается использование блока, этот блок можно сделать охраняемым, добавив ключевое слово try. Вслед за try-блоком могут следовать catch-блоки, называемые Выбрасывание исключений. Создание объектов В теле try-блока может возникнуть исключительная ситуация, приводящая к выбрасыванию исключений. Формально выбрасывание исключения происходит при выполнении оператора Синтаксически оператор throw[выражение] Выражение В рассматриваемой нами модели исключения являются объектами, класс которых представляет собой наследника класса Exception. Этот класс и многочисленные его наследники является частью библиотеки FCL, хотя и разбросаны по разным пространствам имен. Каждый класс задает определенный тип исключения в соответствии с классификацией, принятой в Framework.Net. Вот лишь некоторые классы исключений ИЗ пространства имен При выполнении оператора Захват исключения Блок catch (Т е) {…} Класс Потенциальных захватчиков может быть много, исключение захватывает лишь один — тот из них, кто стоит первым в списке проверки. Каков порядок проверки? Он довольно естественный. Вначале проверяются обработчики в порядке следования их за Рис. 23.5. О точке большого взрыва и цепочке вызовов мы говорили еще в лекции 2. Исключение возникло в последнем вызванном методе цепочки — на рисунке метод Параллельная работа обработчиков исключений Обработчику исключения — catch-блоку, захватившему исключение, — передается текущее исключение. Анализируя свойства этого объекта, обработчик может понять причину, приведшую к возникновению исключительной ситуации, попытаться ее исправить и в случае успеха продолжить вычисления. Заметьте, в принятой C# схеме без возобновления обработчик исключения не возвращает управление try-блоку, а сам пытается решить проблемы. После завершения catch-блокэ выполняются операторы текущего метода, следующие за конструкцией try-catch-finally. Зачастую обработчик исключения не может исправить ситуацию или может выполнить это лишь частично, предоставив решение оставшейся части проблем вызвавшему методу — предшественнику в цепочке вызовов. Механизм, реализующий такую возможность — это тот же механизм исключений. Как правило, в конце своей работы обработчик исключения выбрасывает исключение, выполняя оператор throw. При этом у него есть две возможности: повторно выбросить текущее исключение или выбросить новое исключение, содержащее дополнительную информацию. Некоторые детали будут пояснены позже при рассмотрении примеров. Таким образом, обработку возникшей исключительной ситуации могут выполнять несколько обработчиков, принадлежащие разным уровням цепочки вызовов. Блок До сих пор ничего не было сказано о важном участнике схемы обработки исключений — блоке Схема Бертрана обработки исключительных ситуаций Схема обработки исключительных ситуаций, предложенная в языке С#, обладает одним существенным изъяном — ее можно применить некорректно. Она позволяет, в случае возникновения исключительной ситуации, уведомить о ее возникновении и спокойно продолжить работу, что в конечном счете приведет к неверным результатам. Из двух зол — прервать вычисление с уведомлением о невозможности продолжения работы или закончить вычисления с ошибочным результатом вычисления — следует выбирать первое. Некорректно примененная схема C# приведет к ошибочным результатам. Приведу несколько примеров. Представьте, оформляется заказ на отдых где-нибудь на Канарах. В ходе оформления возникает исключительная ситуация — нет свободных мест в гостинице — обработчик исключения посылает уведомление с принесением извинений, но оформление заказа продолжается. Вероятно, предпочтительнее отказаться от отдыха на Канарах и выбрать другое место, чем оказаться без крыши над головой, ночуя на берегу океана. Эта ситуация не является критически важной. А что, если в процессе подготовки операции выясняется, что проведение ее в данном случае опасно? Никакие извинения не могут избавить от вреда, нанесенного операцией. Операция должна быть отменена. Бертран Мейер в книге [1], в которой все механизмы, используемые в объектной технологии, тщательно обосновываются, предложил следующую схему обработки исключительных ситуаций. В основе ее лежит подход к проектированию программной системы на принципах Проектирования по Контракту. Модули программной системы, вызывающие друг друга, заключают между собой контракты. Вызывающий модуль обязан обеспечить истинность предусловия, необходимого для корректной работы вызванного модуля. Вызванный модуль обязан гарантировать истинность постусловия по завершении своей работы. Если в вызванном модуле возникает исключительная ситуация, то это означает, что он не может выполнить свою часть контракта. Что должен делать обработчик исключительной ситуации? У него только две возможности — Retry и Rescue. Первая (Retry) — попытаться внести некоторые коррективы и вернуть управление охраняемому модулю, который может предпринять очередную попытку выполнить свой контракт. Модуль может, например в следующей попытке запустить другой алгоритм, использовать другой файл, другие данные. Если все закончится успешно и работа модуля будет соответствовать его постусловию, то появление исключительной ситуации можно рассматривать как временные трудности, успешно преодоленные. Если же ситуация возникает вновь и вновь, тогда обработчик события применяет вторую стратегию (Rescue), выбрасывая исключение и передавая управление вызывающему модулю, который и должен теперь попытаться исправить ситуацию. Важная тонкость в схеме, предложенной Бертраном, состоит в том, что исключение, выбрасываемое обработчиком, следует рассматривать не как панику, не как бегство, а как отход на заранее подготовленные позиции. Обработчик исключения должен позаботиться о восстановлении состояния, предшествующего вызову модуля, который привел к исключительной ситуации, и это гарантирует нахождение всей системы в корректном состоянии. Схема Бертрана является схемой с возобновлением, и она наиболее точно описывает разумную стратегию обработки исключительных ситуаций. Не следует думать, что эта схема не может быть реализована на С#, просто она требует понимания сути и определенной структурной организации модуля. Приведу возможную реализацию такой схемы на С#: public void Pattern () { do { try { bool Danger = false; Success = true; MakeJob(); Danger = CheckDanger(); if (Danger) throw (new MyException()); MakeLastJob(); } catch (MyException me) { if (count > maxcount) throw(new MyException("Три попытки были безуспешны")); Success = false; count++; //корректировка ситуации Console.WriteLine("Попытка исправить ситуацию!"); level +=1; } }while (!Success); } Приведу краткие комментарии к этой процедуре, которую можно рассматривать как некоторый образец организации обработки исключительной ситуации: • Конструкция • В данном образце предполагается, что в теле охраняемого блока анализируется возможность возникновения исключительной ситуации и, в случае обнаружения опасности, выбрасывается собственное исключение, класс которого задан программно. В соответствии с этим тело • Если в методе • Для простоты приведен только один • Когда число попыток еще не исчерпано, обработчик исключения переменной • Как видите, эта схема реализует два корректных исхода обработки исключительной ситуации — Доведем этот образец до реально работающего кода, где угроза исключения зависит от значения генерируемого случайного числа, а обработчик исключения может изменять границы интервала, повышая вероятность успеха. Определим первым делом собственный класс исключений: public class MyException: Exception { public MyException() { } public MyException (string message): base(message) { } public MyException (string message, Exception e): base(message, e) { } } Минимум того, что нужно сделать, определяя свои исключения, — это задать три конструктора класса, вызывающие соответствующие конструкторы базового класса Exception. В классе Random rnd = new Random(); int level = -10; bool Success; //true — нормальное завершение int count =1; // число попыток выполнения const int maxcount =3; Определим теперь методы, вызываемые в теле охраняемого блока: void MakeJob() { Console.WriteLine("Подготовительные работы завершены"); } bool CheckDanger() { //проверка качества и возможности продолжения работ int low = rnd.Next(level,10); if (low > 6) return (false); return(true); } void MakeLastJob() { Console.WriteLine("Все работы завершены успешно"); } В классе public void TestPattern () { Excepts ex1 = new Excepts (); try { ex1.Pattern (); } catch (Exception e) { Console.WriteLine("исключительная ситуация при вызове Pattern"); Console.WriteLine(e.ToString ()); } } Обратите внимание, что вызов метода Рис. 23.6. Класс Рассмотрим устройство базового • • • • • • Из методов класса отметим метод Класс имеет четыре конструктора, из которых три уже упоминались. Один из них — конструктор без аргументов, второй — принимает строку, становящуюся свойством Message, третий — имеет еще один аргумент: исключение, передаваемое свойству В предыдущий пример я внес некоторые изменения. В частности, добавил еще один аргумент при вызове конструктора исключения в catch-блоке метода Pattern; throw(new MyException("Все попытки Pattern безуспешны", me)); В этом случае у создаваемого исключения заполняется свойство static public void PrintProperties (Exception e) { Console.WriteLine("Свойства исключения: "); Console.WriteLine("Targetsite = {0}", e.Targetsite); Console.WriteLine("Source = {0}", e.Source); Console.WriteLine("Message = {0}",e.Message); if (e.InnerException == null) Console.WriteLine("InnerException = null"); else Console.WriteLine("InnerException = {0}", e. InnerException.Message); Console.WriteLine("StackTrace = {0}", e.StackTrace); Console.WriteLine("GetBaseException = {0}", e. GetBaseException ()); } Из-за громоздкости не привожу результаты, но отмечу, что они соответствуют описанию, приведенному в тексте лекции. В заключение темы исключений хочу еще раз подчеркнуть, что корректное применение механизма исключений должно поддерживаться целенаправленными усилиями программиста. Следует помнить о двух важных правилах: • обработка исключений должна быть направлена не столько на уведомление о возникновении ошибки, сколько на корректировку возникшей ситуации; • если исправить ситуацию не удается, то программа должна быть прервана так, чтобы не были получены некорректные результаты, не удовлетворяющие спецификациям программы.
24. Организация интерфейса и рисование в формах Организация интерфейса. Шаблоны форм. Заселение формы элементами управления. Классы элементов управления. Примеры классов. Класс ListBox. Наследование форм. Организация меню, главное меню. Инструментальные панели с кнопками. Рисование в формах. Классы рисования. Кисти и перья. Практически все проекты, построенные в наших лекциях, были консольными приложениями. В реальной жизни консольные проекты — это большая редкость. Причина, по которой из 12 возможных типов проектов мы выбирали наименее используемый, понятна. Нашей целью являлось изучение свойств языка, классов библиотеки FCL, для этих целей консольный проект вполне подходит, позволяя избегать введения не относящихся к сути дела деталей. Теперь цель достигнута — основные средства языка C# рассмотрены, учебный курс завершается. Остались важные темы, требующие более подробного рассмотрения, такие, как, например, работа с атрибутами, создание собственных атрибутов, класс Первое знакомство с Windows-проектами состоялось в лекции 2, я настоятельно рекомендую перечитать ее, прежде чем продолжить чтение данной лекции. Вкратце напомню, как создается и выполняется Windows-проект. По умолчанию он содержит класс Форма и элементы управления Как населить форму элементами управления? Чаще всего, это делается вручную в режиме проектирования. Доступные элементы управления, отображаемые на специальной панели (Toolbox), перетаскиваются на форму. Этот процесс поддерживается особым инструментарием — дизайнером форм (Designer Form). Как только на этапе проектирования вы сажаете на форму элемент управления, немедленно в тексте класса появляются соответствующие строки кода (в лекции 2 об этом подробно рассказано). Конечно, все можно делать и программно — появление соответствующих строк кода приводит к появлению элементов управления на форме. Нужно понимать, что форма — это видимый образ класса Каждый вид элементов управления описывается собственным классом. Библиотека FCL содержит большое число классов, задающих различные элементы управления. Одним из типов проектов, доступных на С#, является проект, создающий элемент управления, так что ничто не мешает создавать собственные элементы управления и размещать их на формах наряду со встроенными элементами. Многие фирмы специализируются на создании элементов управления — это один из видов повторно используемых компонентов. В каких отношениях находятся класс Рис. 24.1. Естественно, все эти классы являются потомками прародителя — класса Взаимодействие форм Обычное Windows-приложение всегда содержит несколько форм. Одни открываются в процессе работы, другие закрываются. В каждый текущий момент на экране может быть открыта одна или несколько форм, пользователь может работать с одной формой или переключаться по ходу работы с одной на другую. Следует четко различать процесс создания формы — соответствующего объекта, принадлежащего классу Заметьте разницу между сокрытием и закрытием формы — между методами Форма, открываемая в процедуре Можно создавать формы как объекты класса Модальные и немодальные формы Первичным является понятие модального и немодального окна. Окно называется модальным, если нельзя закончить работу в открытом окне до тех пор, пока оно не будет закрыто. Модальное окно не позволяет, если оно открыто, временно переключиться на работу с другим окном. Выйти из модального окна можно, только закрыв его. Немодальные окна допускают параллельную работу в окнах. Форма называется модальной или немодальной в зависимости оттого, каково ее окно. Метод Передача информации между формами Часто многие формы должны работать с одним и тем же объектом, производя над ним различные операции. Как это реализуется? Обычная схема такова: объект создается в одной из форм, чаще всего, в главной. При создании следующей формы глобальный объект передается конструктору новой формы в качестве аргумента. Естественно, одно из полей новой формы должно представлять ссылку на объект соответствующего класса, так что конструктору останется только связать ссылку с переданным ему объектом. Заметьте, все это эффективно реализуется, поскольку объект создается лишь один раз, а разные формы содержат ссылки на этот единственный объект. Если такой глобальный объект создается в главной форме, то можно передавать не объект, требуемый другим формам, а содержащий его контейнер — главную форму. Это удобнее, поскольку при этом можно передать несколько объектов, можно не задумываться над тем, какой объект передавать той или иной форме. Иметь ссылку на главную форму часто необходимо, хотя бы для того, чтобы при закрытии любой формы можно было бы открывать главную, если она была предварительно скрыта. Представим себе, что несколько форм должны работать с объектом класса public Books myBooks; В конструкторе главной формы такой объект создается: myBooks = new Books(max_books); где NewBook; public NewBook form2; При создании объекта Класс private Form1 mainform; private Books books; а его конструктор следующий код: mainform = form; books = mainform.myBooks; Теперь объекту private void NewBook_Closed(object sender, System.EventArgs e) { mainform.Show(); } открывающий главную форму. Образцы форм Создание элегантного, интуитивно ясного интерфейса пользователя — это своего рода искусство, требующее определенного художественного вкуса. Здесь все играет важную роль: размеры и расположение элементов управления, шрифты, важную роль играет цвет. Но тема красивого интерфейса лежит вне нашего рассмотрения. Нас сейчас волнует содержание. Полезно знать некоторые образцы организации интерфейса. Одним из образцов, применимых к главной форме, является главная кнопочная форма. Такая форма состоит из текстового окна, в котором описывается приложение и его возможности, и ряда командных кнопок-, обработчик каждой кнопки открывает форму, позволяющую решать одну из задач, которые поддерживаются данным приложением. В качестве примера рассмотрим Windows-приложение, позволяющее работать с различными динамическими структурами данных. Главная кнопочная форма такого приложения показана на рис. 24.2. Рис. 24.2. Обработчик события private void button4_Click(object sender, System.EventArgs e) { //Переход к показу формы для работы со списком FormList f1= new FormList(); f1.Show(); } Как видите, открывается новая форма для работы со списком, но главная форма не закрывается и остается открытой. Можно предложить следующий образец формы, предназначенной для поддержки работы с объектами некоторого класса. Напомню, каждый класс представляет тип данных. Операции над типом данных можно разделить на три категории: конструкторы, команды и запросы. Конструкторы класса позволяют создать соответствующий объект; команды, реализуемые процедурами, изменяют состояние объекта; запросы, реализуемые функциями без побочных эффектов, возвращают информацию о состоянии объекта, не изменяя самого состояния. Исходя из этого, можно сконструировать интерфейс формы, выделив в нем три секции. В первой секции, разделенной на три раздела, будут представлены команды, запросы и конструкторы. Следующая секция выделяется для окон, в которые можно вводить аргументы исполняемых команд. Последняя секция предназначается для окон, в которых будут отображаться результаты запросов. На рис. 24.3 показана форма для списка с курсором, построенная в соответствии с описанным шаблоном. Рис. 24.3. Список с курсором имеет группу команд, позволяющих перемещать курсор влево, вправо, к началу и концу списка, к элементу с заданным номером. Другая группа команд позволяет производить операции по вставке элементов слева или справа от курсора, удалять элемент, отмеченный курсором. Еще одна группа позволяет производить поиск элементов в списке. Запросы позволяют получить данные об активном элементе, отмеченном курсором, определить число элементов в списке и получить другую полезную информацию. Работа со списками (еще один шаблон) Для организации интерфейса разработано большое число элементов управления, часть из них показана на рис. 24.1. Все они обладают обширным набором свойств, методов и событий, их описание может занять отдельную книгу. Такие элементы, как, например, Во многих задачах пользователю предлагается некоторый список товаров, гостиниц, услуг и прочих прелестей, и он должен выбрать некоторое подмножество элементов из этого списка. Элемент управления В списке могут храниться строки, тогда объект совпадает с его отображением. Если же хранятся объекты, то в классе объекта следует переопределить метод ToString, возвращаемый результат которого и будет строкой, отображаемой в списке. Давайте рассмотрим главный вопрос: как список заполняется элементами? Есть несколько разных способов. Новой и интересной технологией, применимой к самым разным элементам управления, является связывание элемента управления с данными, хранящимися в различных хранилищах, прежде всего, в базах данных. Для этого у списка есть ряд свойств — Заполнить список элементами можно еще на этапе проектирования. Для этого достаточно выбрать свойство items; появится специальное окно для заполнения списка строками — элементами списка. Добавлять объекты других классов таким способом невозможно. Но это можно делать при программной работе со свойством items, возвращающим специальную коллекцию объектов, которая задана классом Еще один способ задания элементов списка поддерживается свойством Главное назначение элемента Среди других методов и свойств У элемента управления Рис. 24.4. На форме показаны два списка — private void listBox1_Enter(object sender, System.EventArgs e) { /*** Событие Enter у списка возникает при входе в список ***/ button1.Text = ">"; button2.Text =">>"; } private void listBox2_Enter(object sender, System.EventArgs e) { /*** Событие Enter у списка возникает при входе в список ***/ button1.Text = "<"; button2.Text ="<<"; } Посмотрим, как устроены обработчики события private void button1_Click(object sender, System.EventArgs e) { /* Обработчик события Click кнопки "> <" * Выборочный обмен данными между списками * ListBox1 <-> ListBox2******************/ if(button1.Text == ">") MoveSelectedItems(listBox1, listBox2); else MoveSelectedItems(listBox2, listBoxi); } private void button2_Click(object sender, System.EventArgs e) { /* Обработчик события Click кнопки ">> <<" * Перенос всех данных одного списка в конец другого списка * ListBox1 <-> ListBox2******************/ if (button2.Text == ">>") MoveAllItems(listBox1, listBox2); else MoveAllItems(listBox2, listBox1); } Обработчики событий устроены достаточно просто — они вызывают соответствующий метод, передавая ему нужные аргументы в нужном порядке. Рассмотрим метод, переносящий множество отобранных пользователем элементов из одного списка в другой: private void MoveSelectedItems(ListBox list1, ListBox list2) { /*** Выделенные элементы списка list1 **** *** помещаются в конец списка List2 ***** *** и удаляются из списка list1 ********/ list2.BeginUpdate(); foreach (object item in listl.SelectedItems) { list2.Items.Add(item); } list2.EndUpdate(); ListBox.SelectedlndexCollection indeces = listl.Selectedlndices; list1.BeginUpdate(); for (int i = indeces.Count -1; i>=0; i-) { lList1.Items.RemoveAt(indeces[i]); } list1.EndUpdate(); } Некоторые комментарии к этому тексту. Заметьте, для добавления выделенных пользователем элементов к другому списку используется коллекция list2.Items.AddRange(list1.SelectedItems); поскольку нет автоматического преобразования между коллекциями Для удаления выделенных элементов из списка Намного проще устроен метод, переносящий все элементы списка: private void MoveAllItems(ListBox list1, ListBox list2) { /*** Все элементы списка list1 **** **** переносятся в конец списка list2 **** **** список list1 очищается *************/ list2.Items.AddRange(list1.Items); list1.Items.Clear(); } Добавим еще одну функциональную возможность — разрешим переносить элементы из одного списка в другой двойным щелчком кнопки мыши. Для этого зададим обработчики события private void listBox1_DoubleClick(object sender, System.EventArgs e) { /* Обработчик события Doubleclick левого списка * Выбранный элемент переносится в правый список * ListBox1 <-> ListBox2******************/ MoveSelectedItems(listBox1, listBox2); } private void listBox2_DoubleClick(object sender, System.EventArgs e) { /* Обработчик события Doubleclick правого списка * Выбранный элемент переносится в левый список * ListBox1 <-> ListBox2******************/ MoveSelectedItems(listBox2, listBox1); } Обработчики вызывают уже рассмотренные нами методы. На этом закончим рассмотрение функциональности проектируемого образца формы. Но, скажете вы, остался не заданным целый ряд вопросов: непонятно, как происходит заполнение списков, как сохраняются элементы после завершения переноса, обработчики события Наследование форм Для объектного программиста форма — это обычный класс, а населяющие ее элементы управления — это поля класса. Так что создать новую форму — новый класс, наследующий все поля, методы и события уже существующей формы — не представляет никаких проблем. Достаточно написать, как обычно, одну строку: public class NewForm: InterfacesAndDrawing.TwoLists Нужно учитывать, что имени класса родителя должно предшествовать имя пространства имен. Чаще всего, наследуемые формы создаются в режиме проектирования при выборе пункта меню Add Inherited Form. (Добраться до этого пункта можно двояко. Можно выбрать пункт Project/AddlnheritedForm из главного меню либо выбрать имя проекта в окне проекта и выбрать пункт Add/Add Inherited Form из контекстного меню, открывающегося при щелчке правой кнопкой.) В результате открывается окно Inheritance Picker, в котором можно выбрать родительскую форму. Заметьте, родительская форма может принадлежать как текущему, так и любому другому проекту. Единственное ограничение — проект, содержащий родительскую форму, должен быть скомпилирован как ехе или dll. Вот как Рис. 24.5. При наследовании форм следует обратить внимание на модификаторы доступа элементов управления родительской формы. По умолчанию они имеют статус Наследованную форму можно затем открыть в дизайнере форм, добавить в нее новые элементы и новые обработчики событий или изменить установки наследуемых элементов, если родительская форма предоставила такую возможность. (Хочу предупредить об одном возможном "жучке", связанном с наследованием форм. На одном из моих компьютеров установлена ОС Windows 2000, на другом — Windows ХР. Так вот, в Windows 2000 дизайнер отказывается открывать наследуемую форму, хотя она создается и нормально работает. Это происходит как для Visual Studio 2003, так и для beta2 Visual Studio 2005. В Office ХР все работает нормально. Не могу утверждать совершенно определенно, что это "жучок", поскольку не проводил тщательного исследования. Но полагаю, что предупредить о такой ситуации полезно.) Два наследника формы TwoLists Построим по указанной технологии двух наследников формы Рис. 24.6. Обратите внимание на значки, сопровождающие все наследуемые элементы управления. В классе string[] source_items; string[] selected_items; const int max_items = 20; В конструктор класса добавлен код, инициализирующий массивы: source_items = new string[max_items]; selected_items = new string[max_items]; InitList1 (); Вызываемый в конструкторе закрытый метод класса void InitListl() { //задание элементов источника и инициализация списка формы source_items[0] ="Бертран Мейер: Методы программирования"; //аналогично заполняются другие элементы массива //перенос массива в список ListBox1 int i = 0; while (source_items[i]!= null) { this.listBox1.Items.Add(source_items[i]); i++; } //this.listBox1.DataSource = source_items; } Закомментирована альтернативная возможность заполнения списка формы, использующая свойство private void button3_Click(object sender, System.EventArgs e) { int i =0; foreach (string item in listBox2.Items) { selected_items[i] = item; Debug.WriteLine(selected_items[i]); i++; } this.Hide(); } private void button4_Click(object sender, System.EventArgs e) { foreach(string item in listBox2.Items) { Debug.WriteLine(item); } this.Hide(); } Оба они в Debug-версии проекта выводят данные о книгах, выбранных пользователем, и скрывают затем форму. Но первый из них сохраняет результаты выбора в поле Book[] source_items; Воок[] selected_items; const int max_items = 20; Код, добавляемый в конструктор: source_items = new Book[max_items]; selected_items = new Book[max_items]; InitList1 (); Метод void InitList1() { //задание элементов источника и инициализация списка формы Book newbook; newbook = new Book("Бертран Мейер", "Методы программирования",3,1980); source_items[0] =newbook; //остальные элементы массива заполняются аналогичным образом //перенос массива в список ListBox1 int i = 0; while (source_items[i]!= null) { this.listBox1.Items.Add(source_items[i]); i + +; } } Обработчики событий private void button3_Click(object sender, System.EventArgs e) { int i =0; foreach (object item in listBox2.Items) { selected_items[i] = (Book)item; selected_items[i].PrintBook (); i + +; } this.Hide(); } private void button4_Click(object sender, System.EventArgs e) { Book book; foreach(object item in listBox2.Items) { book = (Book)item; book.PrintBook(); } this.Hide(); } Класс public class Book { //поля string author, title; int price, year; public Book(string a, string t, int p, int y) { author = a; title = t; price = p; year = y; } public override string ToString() { return(title +": " + author); } public void PrintBook() { Debug.WriteLine("автор: " + author + " название: " + title + " цена: " + price.ToString () + " год издания: " + year.ToString ()); } } Обратите внимание, что в классе, как и положено, переопределен метод В завершение проекта нам осталось спроектировать главную форму. Сделаем ее в соответствии с описанным ранее шаблоном кнопочной формой (рис. 24.7). Рис. 24.7. Обработчики событий Рис. 24.8. Важными атрибутами интерфейса являются меню и инструментальные панели с кнопками. Рассмотрим, как организуются эти элементы интерфейса в формах. Меню и панели с кнопками можно создавать как вручную в режиме проектирования, так и программно. Несколько слов о терминологии. Когда мы говорим о меню, то имеем в виду некоторую структуру, организованную в виде дерева. Меню состоит из элементов меню, часто называемых Кроме структуры, заданной главным меню, в форме и в элементах управления разрешается организовывать контекстные меню, появляющиеся (всплывающие) при нажатии правой кнопки мыши. Создание меню в режиме проектирования Для построения в режиме проектирования главного меню и связанной с ним структуры достаточно перетащить на форму элемент управления, называемый После перетаскивания метка с изображением этого элемента управления появляется ниже формы, а на форме появляется элемент меню с информационным полем, в котором можно задать название пункта меню, и двумя указателями на правого брата и старшего сына, позволяющими перейти к следующему пункту меню того же уровня или опуститься на нижний уровень. Технология создания меню вручную интуитивно ясна и не вызывает обычно никаких проблем. На рис. 24.9 показан процесс создания меню. Рис. 24.9. Рассмотрим пример, в котором главное меню содержит 3 пункта — Посадим на форму еще один элемент управления — текстовое окно — и свяжем с командами меню обработчики события Связывание команды меню с обработчиком события в режиме проектирования выполняется стандартным образом — выделяется соответствующая команда меню, затем в окне Вот как выглядят обработчики события private void menuItem4_Click(object sender, System.EventArgs e) { OpenFileDialog openFileDialogl = new OpenFileDialog (); openFileDialogl.ShowDialog(); //код, показывающий, что делать с открытым файлом textBox1.Text = "Открытие Файла!"; } private void menuItem10_Click(object sender, System.EventArgs e) { SaveFileDialog saveFileDialog1 = new SaveFileDialog (); saveFileDialog1l.ShowDialog(); //код, анализирующий результат операции сохранения файла textBox1.Text = "Сохранение Файла!"; } private void menuItem3_Click(object sender, System.EventArgs e) { ColorDialog colorDialog1 = new ColorDialog (); if (colorDialog1.ShowDialog()== DialogResult.OK) this.textBox1.BackColor =colorDialog1.Color; } На рис. 24.10 показано диалоговое окно для выбора цвета, открытое при выборе команды Рис. 24.10. Для полноты картины зададим обработчики событий ДЛЯ команд меню Circle, Rectangle, Line, не выполняющие пока содержательной работы, а лишь информирующие о намерениях: private void menuItem7_Click(object sender, System.EventArgs e) { textBox1.Text = "Рисование круга!"; } private void menuItem8_Click(object sender, System.EventArgs e) { textBox1.Text = "Рисование прямоугольника!"; } private void menuItem9_Click(object sender, System.EventArgs e) { textBox1.Text = "Рисование прямой!"; } Закончу на этом рассмотрение процесса создания меню в режиме проектирования, опуская ряд деталей, например, возможность задания горячих клавишей для элементов меню. Классы меню Все, что можно делать руками, можно делать программно. Рассмотрим классы, используемые при работе с меню. Основным родительским классом является абстрактный класс Menu, задающий базовую функциональность трех своих потомков — классов Вот какие поля формы, задающие объекты меню, были сформированы: private System.Windows.Forms.MainMenu mainMenu1; private System.Windows.Forms.MenuItem menuItem1; //другие элементы меню private System.Windows.Forms.MenuItem menuItem10; Основной код, создаваемый дизайнерами, помещается в метод this.mainMenu1 = new System.Windows.Forms.MainMenu(); this.menuItem1 = new System.Windows.Forms.MenuItem(); … // mainMenu1 this.mainMenu1.Menuitems.AddRange(new System.Windows.Forms.MenuItem[] {this.menuItem1,this.menuItem2,this.menuItem3}); // menuIte1 this.menuItem1.Index = 0; this.menuItem1.Menuitems.AddRange(new System.Windows.Forms.MenuItem[] {this.menuItem4,this.menuItem10}); this.menuItem1.Text = "File"; … // menuItem4 this.menuItem4.Index = 0; this.menuItem4.Text = "Open"; this.menuItem4.Click += new System.EventHandier(this.menuItem4_Click); … // Form1 this.Controls.AddRange(new System.Windows.Forms.Control[] { this.textBox1}); this.Menu = this.mainMenu1; this.Name = "Form1"; this.Text = "Form1"; Надеюсь, что данный программный код прозрачен и не требует дополнительных комментариев. Создание инструментальной панели с командными кнопками Панель с командными кнопками дополняет меню. Панель устроена проще, поскольку здесь нет иерархии. На панели располагаются кнопки, щелчок по каждой из которых запускает на выполнение соответствующую команду, заданную обработчиком события Роль контейнерного класса для командных кнопок играет класс, определяющий панель — Давайте спроектируем панель с тремя кнопками, задающими команды Рис. 24.11. Проанализируем теперь созданный дизайнером программный код. Как всегда начнем с полей класса, хранящих созданные в процессе проектирования элементы: private System.Windows.Forms.ToolBar tooiBar1; private System.Windows.Forms.ImageList imageList1; private System.Windows.Forms.ToolBarButton toolBarButton1; private System.Windows.Forms.ToolBarButton toolBarButton2; private System.Windows.Forms.ToolBarButton toolBarButton3; В методе this.toolBar1 = new System.Windows.Forms.ToolBar(); this.imageList1 = new System.Windows.Forms.ImageList(this.components); this.toolBarButton1 = new System.Windows.Forms.ToolBarButton(); this.toolBarButton2 = new System.Windows.Forms.ToolBarButton(); this.toolBarButton3 = new System.Windows.Forms.ToolBarButton(); … // toolBar1 this.toolBari.Buttons.AddRange(new System.Windows.Forms.ToolBarButton[] {this.toolBarButton1, this.toolBarButton2,this.toolBarButton3}); this.toolBar1.DropDownArrows = true; this.toolBar1.ImageList = this.imageList1; this.toolBar1.Name = "toolBar1"; this.toolBar1.ShowToolTips = true; this.toolBar1.Size = new System.Drawing.Size(432, 42); this.toolBar1.Tablndex = 1; this.toolBar1.ButtonClick += new System.Windows.Forms.ToolBarButtonClickEventHandler this.toolBar1_ButtonClick); // toolBarButton1 this.toolBarButton1.Imagelndex = 0; this.toolBarButton1.Text = "OpenFile"; this.toolBarButton1.ToolTipText = "Диалоговое окно открытия файла"; … Этот текст должен быть понятен без комментариев, а вот об обработчике события private void toolBar1_ButtonClick(object sender, System.Windows.Forms.ToolBarButtonClickEventArgs e) { int buttonNumber = toolBarl.Buttons.IndexOf(e.Button); switch (buttonNumber) { case 0: OpenFileDialog openFileDialog1 = new OpenFileDialog(); openFileDialog1.ShowDialog(); //код, показывающий, что делать с открытым файлом textBox1.Text = "Открытие Файла!"; break; case 1: SaveFileDialog saveFileDialog1 = new SaveFileDialog (); saveFileDialog1.ShowDialog(); //код, анализирующий результат операции сохранения файла textBox1.Text = "Сохранение Файла!"; break; default: ColorDialog colorDialog1 = new ColorDialog(); if (colorDialog1.ShowDialog()== DialogResult.OK) this.textBox1.BackColor =colorDialogl.Color; break; } } В заключение взгляните на спроектированную форму с меню и панелью с командными кнопками. Рис. 24.12. Графика необходима при организации пользовательского интерфейса. Образы информативнее текста. Framework.Net реализует расширенный графический интерфейс GDI +, обладающий широким набором возможностей. Но для рисования в формах достаточно иметь три объекта — перо, кисть и, хочется сказать, бумагу, но третий нужный объект — это объект класса Graphics, методы которого позволяют в формах заниматься графикой — рисовать и раскрашивать. Класс Объекты этого класса зависят от контекста устройства, (графика не обязательно отображается на дисплее компьютера, она может выводиться на принтер, графопостроитель или другие устройства), поэтому создание объектов класса Graphics выполняется не традиционным способом — без вызова конструктора класса. Создаются объекты специальными методами разных классов. Например, метод При рисовании в формах можно объявить в форме поле, описывающее объект класса Graphics: Graphics graph; а в конструкторе формы произвести связывание с реальным объектом: graph = CreateGraphics (); Затем всюду в программе, где нужно работать с графикой, используется глобальный для формы объект protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { Graphics gr = e.Graphics; //перерисовка, использующая методы объекта gr } Для получения этого объекта можно использовать и статические методы самого класса Graphics. Методы класса У класса Graphics большое число методов и свойств. Упомяну лишь о некоторых из них. Группа статических методов класса позволяет создать объект этого класса, задавая например описатель (handle) контекста устройства. Для рисования наиболее важны три группы методов. К первой относится перегруженный метод Класс Методам группы Класс Класс Brush, задающий кисти, устроен более сложно. Начну с того, что класс Brush является абстрактным классом, так что создавать кисти этого класса нельзя, но можно создавать кисти классов-потомков Brush. Таких классов пять — они задают кисть: • • • • • Первые два класса кистей находятся в пространстве имен У каждого из этих классов свои конструкторы. В примере, обсуждаемом далее, рассмотрим создание кистей трех разных классов, там и поговорим о конструкторах классов. Проект "Паутина Безье, кисти и краски" Построим проект для рисования в формах. В одной из форм будем рисовать пером, в другом — кистями различного типа. Главную форму сделаем простой кнопочной формой. Вот как она выглядит. Рис. 24.13. Выбор соответствующей командной кнопки открывает форму для рисования пером или кистью. Паутина Безье В форме BezierWeb будем рисовать несколько кривых Безье, исходящих из одной точки — центра. Положение центра определяется курсором. Перемещая мышь, меняем положение курсора, а, следовательно, и центра, так что рисунок в форме будет все время перерисовываться, следуя за мышью, ( Прежде чем рассмотреть программный код, давайте посмотрим, как выглядят нарисованные программой кривые Безье, исходящие из одной точки. Рис. 24.14. Перейдем к рассмотрению кода. Первым делом добавим в поля формы нужные нам объекты: //fields Point center; Point[] points = new Point[10]; Pen pen; Graphics graph; int count; Точка center будет задавать общую начальную точку для всех рисуемых кривых Безье, массив В конструкторе формы вызывается метод void MyInit() { int сх = ClientSize.Width; int су = ClientSize.Height; points[0] = new Point (0,0); points[1] = new Point(cx/2,0); points [2] = new Point(cx,0); points[3] = new Point(0,cy/2); points[4] = new Point(cx,cy/2); points [5] = new Point(0,cy); points[б] = new Point(cx/2,су); points[7] = new Point(cx,cy); points[8] = new Point (0,0); points [9] = new Point(cx/2,0); graph = this.CreateGraphics (); center = new Point(cx/2,cy/2); count =1; } Рисование кривых Безье выполняется в методе void DrawWeb() { for (int i = 0; i<8; i++) graph.DrawBezier(pen,center,points[i],points[i+2], points[i + 1]); } Метод Главный вопрос, требующий решения: где же вызывать сам метод Вот текст обработчика этого события: private void BezierWeb_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) { pen = SystemPens.Control; DrawWeb(); center.X = e.X; center.Y = e.Y; //pen = new Pen(Color.Aquamarine); pen = SystemPens.ControlText; DrawWeb(); } Метод Перед рисованием кривых цветом переднего плана общая для всех кривых точка center получает координаты курсора мыши, передаваемые аргументом обработчика события. Событие Вызов метода protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { pen = SystemPens.ControlText; DrawWeb(); Debug.WriteLine(count++); } Говоря о рисовании, нельзя не упомянуть о событии Paint. Оно возникает всякий раз, когда область, в которой происходило рисование, повреждена. Причины этого могут быть разные — пользователь свернул форму, изменил ее размеры, произошло перекрытие другой формой, был вызван метод Событие Paint подключают обычно не так, как это делалось, например, для события MouseMove. Вместо этого переопределяют родительский метод В данном контексте перерисовка сводится, как это обычно делается, к вызову метода, выполняющего рисование. Для повышения эффективности можно анализировать поврежденную область и выполнять рисование только в ее пределах. Закончим на этом с рисованием пером и перейдем к рассмотрению рисования кистью. Кисти и краски Создадим в нашем проекте новую форму На рис. 24.15 показана форма после нескольких щелчков кнопки мыши. Конечно, черно-белый рисунок в книге не может передать цвета, особенно смену оттенков для градиентной кисти. На экране дисплея или цветном рисунке все выглядит красивее. А теперь приведем программный код, реализующий рисование. Начнем, как обычно, с полей класса: //fields int сх, су; Graphics graph; Brush brush; Color color; Random rnd; Инициализация полей производится в методе Рис. 24.15. void MyInit() { сх = ClientSize.Width; су = ClientSize.Height; graph = CreateGraphics (); rnd = new Random(); } Рассмотрим теперь основной метод, реализующий рисование фигур различными кистями: void DrawShapes() { for (int i=0; i<3; i + +) { //выбирается цвет — красный, желтый, голубой int numcolor = rnd.Next(3); switch (numcolor) { case 0: color = Color.Blue; break; case 1: color = Color.Yellow; break; case 2: color = Color.Red; break; } //градиентной кистью рисуется эллипс, //местоположение случайно Point top = new Point(rnd.Next(cx), rnd.Next(cy)); Size sz = new Size(rnd.Next(cx-top.X), rnd.Next(cy-top.Y)); Rectangle ret = new Rectangle (top, sz); Point bottom = top + sz; brush = new LinearGradientBrush(top, bottom, Color.White,color); graph.FillEllipse(brush,ret); //сплошной кистью рисуется сектор, //местоположение случайно top = new Point(rnd.Next(cx), rnd.Next(cy)); sz = new Size(rnd.Next(cx-top.X), rnd.Next(cy-top.Y)); ret = new Rectangle(top, sz); brush = new SolidBrush(color); graph.FillPie(brush,ret,30 f,60 f); //узорной кистью рисуется прямоугольник, //местоположение случайно top = new Point(rnd.Next(cx), rnd.Next(cy)); sz = new Size(rnd.Next(cx-top.X), rnd.Next(cy-top.Y)); ret = new Rectangle(top, sz); HatchStyle hs = (HatchStyle)rnd.Next(52); brush = new HatchBrush(hs,Color.White, Color.Black); graph.FillRectangle (brush,ret); } } Приведу некоторые комментарии в дополнение к тем, что встроены в текст метода. Здесь многое построено на работе со случайными числами. Случайным образом выбирается один из возможных цветов для рисования фигуры, ее размеры и положение. Наиболее интересно рассмотреть создание кистей разного типа. Когда создается градиентная кисть. brush = new LinearGradientBrush (top, bottom, Color.White,color); то нужно в конструкторе кисти задать две точки и два цвета. Точки определяют интервал изменения оттенков цвета от первого до второго. В начальной точке имеет место первый цвет, в конечной — второй, в остальных точках — их комбинация. Разумно, как это сделано у нас, в качестве точек выбирать противоположные углы прямоугольника, ограничивающего рисуемую фигуру. Наиболее просто задается сплошная кисть: brush = new SolidBrush(color); Для нее достаточно указать только цвет. Для узорной кисти нужно задать предопределенный тип узора, всего их возможно 52. В нашем примере тип узора выбирается случайным образом: HatchStyle hs = (HatchStyle)rnd.Next(52); brush = new HatchBrush(hs,Color.White, Color.Black); Помимо первого аргумента, задающего тип узора, указываются еще два цвета — первый определяет цвет повторяющегося элемента, второй — цвет границы между элементами узора. Непосредственное рисование кистью осуществляют методы группы graph.FillEllipse(brush,ret); graph.FillPie(brush, ret,30f,60f); graph.FillRectangle(brush,ret); Первый аргумент всегда задает кисть, а остальные зависят от типа рисуемой фигуры. Как правило, всегда задается прямоугольник, ограничивающий данную фигуру. Вызов метода private void RandomShapes_Click(object sender, System.EventArgs e) { DrawShapes(); } На этом поставим точку в рассмотрении данной темы. По сути, этим завершается и наш учебный курс. В последней лекции будет рассмотрен некоторый заключительный проект.
25. Финальный проект В этой заключительной лекции новый материал появляться не будет, не будет и традиционных вопросов в конце лекции. Лекция особенная — она посвящена описанию финального проекта, в котором объединены многие, надеюсь, уже хорошо знакомые элементы. В финальном проекте создается семейство классов, описывающих геометрические фигуры. Проектирование начинается с абстрактного класса поведения, который описывает общие свойства и методы, характерные для всех фигур семейства. Затем, используя наследование, создаются классы конкретных геометрических фигур, начиная с простейших, таких, как круги и прямоугольники, до составных, таких, как класс Person. Мы добавим в наш проект динамические структуры данных, такие, как список с курсором, для хранения в нем фигур семейства. Наконец, мы создадим интерфейс, включающий меню с десятками команд и панель с инструментальными кнопками. Интерфейс позволяет конечному пользователю выполнять различные действия над геометрическими фигурами — создавать, рисовать их на форме, перемещать их с помощью команд и перетаскивать их мышью, менять их размеры и цвет, сохранять в списке и удалять из списка, отображать все фигуры списка или очистить его полностью. Проект может служить образцом полноценного Windows-приложения, примером проектирования в классах с демонстрацией преимуществ, предоставляемых наследованием. Закончим на этом рекламную часть и приступим к делу. Хочу предупредить, вас ждут программные тексты, почти без всяких комментариев. Все нужные комментарии были даны в предыдущих лекциях. С моей точки зрения, наиболее интересная часть программистских книжек — это та, в которой приводится программный код. И значит, эта глава самая интересная. Приведем код класса: using System; using System.Drawing; namespace Shapes { /// /// Figure — это абстрактный класс; прародитель семейства /// классов геометрических фигур. Все фигуры имеют: /// центр — center, масштаб — scale, статус /// перетаскивания — dragged center — объект встроенного /// класса (структуры) Point. Этот объект задает характерную /// точку фигуры — чаще всего ее центр (тяжести) /// scale задает масштаб фигуры, первоначально единичный. /// drugged = true, когда фигура следует за курсором мыши. /// над фигурами определены операции: параллельный /// перенос — Move(а, Ь) масштабирование — Scale(s) /// Показ фигуры — Show. Область захвата — Region_Capture /// возвращает прямоугольник, характерный для фигуры, /// перетаскивание фигуры возможно при установке курсора /// мыши в области захвата. /// abstract public class Figure { /// /// закрытые для клиентов атрибуты класса — center, scale /// protected Point center; protected double scale; protected bool dragged; protected Color color; //Доступ к свойствам public Point center_figure { get {return(center);} set {center = value;} } public double scale_figure { get {return(scale);} set {scale = value;} } public bool dragged figure } get {return(dragged);} set {dragged = value;} } public Color color figure { get {return (color);} set {color = value;} } /// /// базовый конструктор фигур /// /// координата X характерной точки ///фигуры /// Координата Y характерной точки ///фигуры public Figure(int x, int y) { center = new Point(x,y); scale = 1; dragged = false; color = Color.ForestGreen; } /// /// отложенный метод /// Параллельный перенос фигуры на (а, Ь) /// require: true; /// ensure: для любой точки фигуры р(х, у): /// х = old(x) + а; у = old(у) + Ь; /// /// a — перемещение по горизонтали /// вправо /// b — перемещение по вертикали /// вниз /// Замечание: Для того, чтобы фигура при рисовании была /// полностью видимой, координаты всех ее точек должны /// быть в пределах области рисования, public void Move (int a,int b) { center.X +=a; center.Y += b; } /// /// изменяет масштаб фигуры /// /// масштаб изменяется в s раз public void Scale(double s) { scale*=s; } /// /// рисование фигуры в окне, передающем объекты g и реп /// /// графический объект, методы которого /// рисуют фигуру /// перо рисования public abstract void Show(Graphics g, Pen pen, Brush brush); public abstract System.Drawing.Rectangle Region_Capture(); } Абстрактный класс, относящийся к этапу проектирования системы, вместе с тем является важнейшим элементом заключительного семейства классов. В этом проявляется мощь объектно-ориентированного подхода к разработке программных систем. Заметьте, на данном уровне большая часть текста представляет документацию, являющуюся неотъемлемой частью программного проекта. Документация записана в тегах Приведем теперь программные коды классов, являющихся потомками класса Figure. Класс Вот программный код этого класса: using System; using System.Drawing; namespace Shapes { /// /// Класс Ellipse — потомок класса Figure. /// public class Ellipse: Figure { int axisA,axisB; Rectangle rect; public Ellipse (int A, int B, int x, int у): base(x,у) { axisA = A; axisB = B; rect =Init(); } public override void Show(Graphics g, Pen pen, Brush brush) { rect = Init(); g. DrawEllipse(pen,rect); g. FillEllipse (brush, rect); } public override Rectangle Region_Capture() { rect = Init (); return rect; } Rectangle Init() { int a =Convert.ToInt32(axisA*scale); int b =Convert.ToInt32 (axisB*scale); int leftupX = center.X — a; int leftupY = center.Y — b; return (new Rectangle(leftupX,leftupY,2*a,2*b)); } } } Класс Этот класс является потомком класса using System; using System.Drawing; namespace Shapes { /// /// Класс Circle — потомок класса Ellipse. /// public class Circle: Ellipse { public Circle (int radius,int x, int y):base(radius,radius,x,y) { // Круг — это эллипс с равными полуосями (радиусом круга) } } } Здесь опять-таки проявляется мощь наследования. Потомок наследует все свойства и методы родителя. Ему остается только указать собственный конструктор объектов класса, да и в нем главное состоит в вызове конструктора родительского класса с передачей ему нужных аргументов. Класс Этот класс, задающие маленькие кружочки фиксированного радиуса, в свою очередь, является наследником класса using System; namespace Shapes { /// /// Класс LittleCircle — потомок класса Circle. /// public class LittleCircle: Circle { public LittleCircle(int x,int y): base(4,x,y) { // маленький круг радиуса 4 } } } Класс Этот класс является еще одним прямым потомком класса using System; using System.Drawing; namespace Shapes { /// /// Класс Rect — потомок класса Figure. /// public class Rect: Figure { int sideA, sideB; Rectangle rect; public Rect(int sideA,int sideB, int x, int y): base(x,y) { this.sideA = sideA; this.sideB = sideB; rect =Init(); } public override void Show(Graphics g, Pen pen, Brush brush) { rect = Init (); g. DrawRectangle(pen,rect); g. FillRectangle(brush,rect); } public override Rectangle Region_Capture() { rect = Init (); return rect; } Rectangle Init() { int a =Convert.ToInt32(sideA*scale); int b =Convert.ToInt32(sideB*scale); int leftupX = center.X — a/2; int leftupY = center.Y — b/2; return (new Rectangle(leftupX,leftupY,a,b)); } } } Класс Квадрат — это частный случай прямоугольника. Соответствующий класс является потомком класса using System; namespace Shapes { /// /// Класс Square — потомок класса Rect. /// public class Square: Rect { public Square(int side, int x, int y): base(side,side,x,у) { //квадрат — это прямоугольник с равными сторонами } } } Класс Этот класс является прямым потомком класса namespace Shapes { /// /// Класс Person — потомок класса Figure, /// клиент классов Circle, Rect, LittleCircle. /// public class Person: Figure { int head_h; Circle head; Rect body; LittleCircle nose; public Person (int head_h, int x, int y): base(x,y) { // head_h — радиус головы, x,y — ее центр. // остальные размеры исчисляются относительно // размера головы. this.head_h = head_h; head = new Circle(head_h,x,у); int body_x = x; int body_y = у + 3*head_h; int body_w =2*head_h; int body_h = 4*head_h; body = new Rect(body_w, body_h, body_x,body_y); nose = new LittleCircle (x+head_h +2, y); } public override void Show(System.Drawing.Graphics g, System.Drawing.Pen pen, System.Drawing.Brush brush) { int h = Convert.ToInt32(head_h*scale); //head int top_x = center.X — h; int top_y = center.Y — h; g. DrawEllipse(pen, top_x,top_y, 2*h,2*h); g. FillEllipse(brush, top_x,top_y, 2*h,2*h); //body top_y += 2*h; g. DrawRectangle(pen, top_x,top_y, 2*h,4*h); g. FillRectangle(brush, top_x,top_y, 2*h,4*h); //nose top_y — =h; top_x += 2*h; g. DrawEllipse(pen, top_x,top_y, 8,8); g. FillEllipse(brush, top_x,top_y, 8,8); } public override System.Drawing.Rectangle Region_Capture() { int h = Convert.ToInt32(head_h*scale); int top_x = center.X — h; int top_y = center.Y — h; return new System.Drawing.Rectangle(top_x,top_y,2*h,2*h); } } } Список с курсором. Динамические структуры данных Добавим в проект классы, задающие динамические структуры данных. Конечно, можно было бы воспользоваться стандартными… Но для обучения крайне полезно уметь создавать собственные задающие такие структуры данных. Список с курсором — один из важнейших образцов подобных классов: using System; namespace Shapes { /// /// Класс TwoWayList(G) описывает двусвязный список с /// курсором. Элементами списка являются объекты /// TwoLinkable, хранящие, помимо указателей на двух /// преемников, объекты типа G.Курсор будет определять /// текущий (активный) элемент списка. Класс будет /// определять симметричные операции по отношению к курсору. /// Конструкторы: /// Конструктор без параметров будет создавать пустой список /// Запросы: /// empty: require: true; возвращает true для пустого списка /// item: require: not empty(); возвращает активный элемент типа G; /// require: true; возвращает число элементов списка; /// count: count in[0,n] (count == 0) eqviv empty (); /// index: require: not empty(); возвращает индекс активного элемента. /// search_res: require: true; возвращает true, если последний поиск был успешным. /// Команды: /// put_left(elem): require: true; /// ensure: добавить новый элемент (elem) слева от курсора; /// put_right(elem): require: true; /// ensure: добавить новый элемент (elem) справа от курсора; /// remove: require: not empty(); /// ensure: удалить активный элемент; /// особо обрабатывается удаление последнего и единственного элементов /// операции с курсором: /// start: require: true; /// ensure: сделать активным первый элемент; /// finish: require: true; /// ensure: сделать активным последний элемент; /// go_prev: require: not (index = 1); /// ensure: сделать активным предыдущий элемент; /// go_next: require: not (index = count); /// ensure: сделать активным последующий элемент; /// go_i(i): require: (i in [1, count]); /// ensure: сделать активным элемент с индексом i; /// операции поиска: /// search_prev(elem): require: not (index = 1); /// ensure: сделать активным первый элемент elem слева от курсора; /// Успех или неуспех поиска сохранять в булевской переменной search_res /// search_next: require: not (index = count); /// ensure: сделать активным первый элемент elem справа от курсора; /// успех или неуспех поиска сохранять в булевской переменной search_res /// public class TwoWayList { public TwoWayList() { first = cursor = last = null; count = index = 0; search_res = false; }//конструктор /// /// first, cursor, last — ссылки на первый, /// активный и последний элементы списка /// Запросы count, index search_res также /// реализуются атрибутами. /// Запросы empty, item реализуются функциями /// protected TwoLinkable first, cursor, last; protected int count, index; protected bool search_res; //доступ на чтение к закрытым свойствам; public int Count { get { return(count); } } public int Index { get { return(index); } } public bool Search_res { get { return(search_res); } } /// /// require: true; возвращает true для непустого списка /// /// public bool empty() { return(first == null); }//empty /// /// require: not empty(); возвращает активный /// элемент типа G; /// /// public Figure item() { return(cursor.Item); }//item /// /// require: true; /// ensure: добавить новый элемент (elem) слева /// от курсора; /// /// Тип Figure играет роль родового типа G /// хранимого элемента elem public void put_left(Figure elem) { TwoLinkable newitem = new TwoLinkable(); newitem.Item = elem; newitem.Next = cursor; if (empty()) //список пуст { first = cursor = last = newitem; index =1; count = 1; } else { if (index == 1) first =newitem; else cursor.Prev.Next = newitem; newitem.Prev = cursor.Prev; cursor.Prev = newitem; count++; index++; } }//put_right /// /// require: true; /// ensure: добавить новый элемент (elem) справа от курсора; /// /// Тип Figure играет роль родового типа G /// хранимого элемента elem public void put_right(Figure elem) { TwoLinkable newitem = new TwoLinkable(); newitem.Item = elem; newitem.Prev = cursor; if (empty()) //список пуст { first = cursor = last = newitem; index =1; count = 1; } else { if (index == count) last =newitem; else cursor.Next.Prev = newitem; newitem.Next = cursor.Next; cursor.Next = newitem; count++; } }//put_right public void remove() { if (count == 1) { first = last = cursor = null; index=0; } else if(index==l) { first = cursor.Next; cursor.Prev = null; cursor = cursor.Next; } else if(index == count) { last = cursor.Prev; cursor.Next = null; cursor = cursor.Prev; index--; } else { cursor.Prev.Next = cursor.Next; cursor.Next.Prev = cursor.Prev; cursor = cursor.Next; } count--; }//remove /// операции с курсором: /// /// start: require: true; /// ensure: сделать активным первый элемент; /// public void start() { cursor = first; index = 1; }//start /// /// finish: require: true; /// ensure: сделать активным последний элемент; /// public void finish() { cursor = last; index = count; }//finish /// /// go_prev: require: not (index = 1); /// ensure: сделать активным предыдущий элемент; /// public void go_prev() { cursor = cursor.Prev; index-; }// go_prev /// /// go_next: require: not (index = count); /// ensure: сделать активным последующий элемент /// public void go_next() { cursor = cursor.Next; index++; }// go_next /// /// go_i(i): require: (i in [1, count]); /// ensure: сделать активным элемент с индексом /// /// public void go_i(int i) { if(i >index) while (i>index) { cursor = cursor.Next; index++; } else if(i while (i { cursor = cursor.Prev; index-; } }// go_i /// операции поиска: /// /// search_prev(elem): require: not (index = 1); /// ensure: сделать активным первый элемент elem слева от курсора; /// /// искомый элемент public virtual void search_prev(Figure elem) { bool found = false; while (!found && (index!=1)) { cursor = cursor.Prev; index-; found = (elem == item()); } search_res = found; }// search_prev /// /// успех или неуспех поиска сохранять в булевской /// переменной search_res /// search_next: require: not (index = count); /// ensure: сделать активным первый элемент elem справа от курсора; /// успех или неуспех поиска сохранять в булевской /// переменной search_res /// /// public virtual void search_next(Figure elem) { bool found = false; while (!found && (index!=count)) { cursor = cursor.Next; index++; found = (elem == item()); } search_res = found; }//search_next } } Заметьте, класс подробно документирован. Для методов класса указываются предусловия и постусловия. Обратите внимание, в соответствии с принципами контрактного программирования клиент класса, прежде чем вызвать метод, должен проверить выполнимость предусловия, что повышает корректность работы системы в целом. Именно так и будет реализован вызов этих методов в классе формы, где осуществляется работа со списком. Классы элементов списка Рассмотрим классы, описывающие элементы списков — элементы с одним и с двумя указателями: using System; namespace Shapes { /// /// Класс Linkable(T)задает элементы списка, включающие: /// информационное поле типа Т — item /// ссылку на элемент типа Linkable — next /// Функции: /// конструктор new: —> Linkable /// запросы: /// Get_Item: Linkable —> T /// Get_Next: Linkable —> Linkable /// процедуры: /// Set_Item: Linkable*T —> Linkable /// Set_Next: Linkable*Linkable —> Linkable /// Роль типа T играет Figure /// public class Linkable { public Linkable () { item =null; next = null; } /// /// закрытые атрибуты класса /// Figure item; Linkable next; /// /// процедуры свойства для доступа к полям класса /// public Figure Item { get { return(item); } set { item = value; } } public Linkable Next { get { return(next); } set { next = value; } } }//class Linkable /// /// Класс TwoLinkable задает элементы с двумя ссылками /// public class TwoLinkable { public TwoLinkable() { prev = next = null; } /// /// закрытые атрибуты класса /// TwoLinkable prev, next; Figure item; /// /// процедуры свойства для доступа к полям класса /// public Figure Item { get { return(item); } set { item = value; } } public TwoLinkable Next { get { return(next); } set { next = value; } } public TwoLinkable Prev { get { return(prev); } set { prev = value; } } }//class TwoLinkable } Организация интерфейса Создадим теперь интерфейс, позволяющий конечному пользователю работать с объектами наших классов. Как всегда, интерфейс создавался вручную в режиме проектирования. На форме я создал меню с большим числом команд и инструментальную панель с 18 кнопками, команды которых повторяли основную команду меню. Описывать процесс создания интерфейса не буду — он подробно рассмотрен в предыдущей главе. Поскольку вся работа по созданию интерфейса транслируется в программный код формы, то просто приведу этот достаточно длинный текст почти без всяких купюр: using System; using System.Drawing; using System.Collections; using System.ComponentMode1; using System.Windows.Forms; using System.Data; using Shapes; namespace Final { /// /// Эта форма обеспечивает интерфейс для создания, /// рисования, показа, перемещения, сохранения в списке /// и выполнения других операций над объектами семейства /// геометрических фигур. Форма имеет меню и /// инструментальные панели. /// public class Form1 System.Windows.Forms.Form { //fields Graphics graphic; Brush brush, clearBrush; Pen pen, clearPen; Color color; Figure current; TwoWayList listFigure; private System.Windows.Forms.MainMenu mainMenu1 private System.Windows.Forms.ImageList imageList1; private System.Windows.Forms.ToolBar tooiBar1 private System.Windows.Forms.MenuItem menuItem1 // аналогичные определения для других элементов меню private System.Windows.Forms.MenuItem menuItem35; private System.Windows.Forms.ToolBarButton toolBarButton1; // аналогичные определения для других командных кнопок private System.Windows.Forms.ToolBarButton toolBarButton18; private System.ComponentMode1.IContainer components; public Form1() { InitializeComponent (); InitFields(); } void InitFields() { graphic = CreateGraphics (); color = SystemColors.ControlText; brush = new SolidBrush(color); clearBrush = new SolidBrush(SystemColors.Control); pen = new Pen (color); clearPen = new Pen(SystemColors.Control); listFigure = new TwoWayList(); current = new Person (20, 50, 50); } /// /// Clean up any resources being used. /// protected override void Dispose(bool disposing) { if(disposing) { if (components!= null) { components.Dispose (); } } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support — do not modify /// the contents of this method with the code editor. /// private void InitializeComponent () { // Код, инициализирующий компоненты и построенный // дизайнером, опущен } #endregion /// /// Точка входа в приложение — процедура Main, /// запускающая форму /// [STAThread] static void Main() { Application.Run(new Form1()); } private void menuItem7_Click(object sender, System.EventArgs e) { createEllipse (); } void createEllipse () { //clear old figure if (current!= null) current.Show(graphic, clearPen, clearBrush); //create ellipse current = new Ellipse (50, 30, 180,180); } private void menuItem8_Click(object sender, System.EventArgs e) { createCircle (); } void createCircle () { //clear old figure if (current!= null) current.Show(graphic, clearPen, clearBrush); //create circle current = new Circle (30, 180, 180); } private void menuItem9_Click(object sender, System.EventArgs e) { createLittleCircle (); } void createLittleCircle () { //clear old figure if (current!= null) current.Show(graphic, clearPen, clearBrush); //create littlecircle current = new Littlecircle (180, 180); } private void menuItem10_Click(object sender, System.EventArgs e) { createRectangle (); } void createRectangle () { //clear old figure if (current!= null) current.Show(graphic, clearPen, clearBrush); //create rectangle current = new Rect(50, 30, 180,180); } private void menuItem11_Click(object sender, System.EventArgs e) { createSquare(); } void createSquare() { //clear old figure if (current!= null) current.Show(graphic, clearPen, clearBrush); //create square current = new Square(30, 180,180); } private void menuItem12_Click (object sender, System.EventArgs e) { createPerson (); } void createPerson () } //clear old figure if (current!= null) current.Show(graphic, clearPen, clearBrush); //create person current = new Person (20, 180, 180); } private void menuItem13_Click(object sender, System.EventArgs e) { showCurrent (); } void showCurrent() { //Show current current.Show(graphic, pen, brush); } private void menuItem14_Click(object sender, System.EventArgs e) { clearCurrent (); } void clearCurrent() { //Clear current current.Show(graphic, clearPen, clearBrush) } private void menuItem17_Click(object sender, System.EventArgs e) { incScale (); } void incScale () { //Increase scale current.Show(graphic, clearPen, clearBrush) current.Scale (1.5); current.Show(graphic, pen, brush); } private void menuItem18_Click(object sender, System.EventArgs e) { decScale (); } void decScale() { //Decrease scale current.Show(graphic, clearPen, clearBrush) current.Scale(2.0/3); current.Show(graphic, pen, brush); } private void menuItem19_Click(object sender, System.EventArgs e) { moveLeft (); } void moveLeft() { //Move left current.Show(graphic, clearPen, clearBrush) current.Move(-20,0); current.Show(graphic, pen, brush); } private void menuItem20_Click(object sender, System.EventArgs e) { moveRight (); } void moveRight() { //Move right current.Show(graphic, clearPen, clearBrush) current.Move(20,0); current.Show(graphic, pen, brush); } private void menuItem21_Click(object sender, System.EventArgs e) { moveTop (); } void moveTop() { //Move top current.Show(graphic, clearPen, clearBrush) current.Move(0,-20); current.Show(graphic, pen, brush); } private void menuItem22_Click(object sender, System.EventArgs e) { moveDown (); } void moveDown() { //Move down current.Show(graphic, clearPen, clearBrush) current.Move(0, 20); current.Show(graphic, pen, brush); } private void menuItem23_Click(object sender, System.EventArgs e) { //choose color ColorDialog dialog = new ColorDialog(); if (dialog.ShowDialog() ==DialogResult.OK) color =dialog.Color; pen = new Pen (color); brush = new SolidBrush(color); } private void menuItem24_Click(object sender, System.EventArgs e) { //Red color color =Color.Red; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem25_Click(object sender, System.EventArgs e) { //Green color color =Color.Green; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem26_Click(object sender, System.EventArgs e) { //Blue color color =Color.Blue; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem27_Click(object sender, System.EventArgs e) { //Black color color =Color.Black; pen = new Pen (color); brush = new SolidBrush(color); } private void menuItem28_Click(object sender, System.EventArgs e) { //Gold color color =Color.Gold; pen = new Pen(color); brush = new SolidBrush(color); } private void menuItem29_Click(object sender, System.EventArgs e) { //put_left: добавление фигуры в список listFigure.put_left(current); } private void menuItem30_Click(object sender, System.EventArgs e) { //put_right: добавление фигуры в список listFigure.put_right(current); } private void menuItem31_Click(object sender, System.EventArgs e) { //remove: удаление фигуры из списка if(!listFigure.empty()) listFigure.remove (); } private void menuItem32_Click (object sender, System.EventArgs e) { goPrev(); } void goPrev() { //go_prev: передвинуть курсор влево if(!(listFigure.Index == 1)) { listFigure.go_prev(); current = listFigure.item(); } } private void menuItem33_Click(object sender, System.EventArgs e) { goNext(); } void goNext() { //go_next: передвинуть курсор вправо if(!(listFigure.Index == listFigure.Count)) { listFigure.go_next(); current = listFigure.item(); } } private void menuItem34_Click(object sender, System.EventArgs e) { //go_first listFigure.start(); if(!listFigure.empty()) current = listFigure.item(); } private void menuItem35_Click(object sender, System.EventArgs e) { //go_last listFigure.finish(); if(!listFigure.empty()) current = listFigure.item(); } private void menuItem15_Click(object sender, System.EventArgs e) { showList (); } void showList () { //Show List listFigure.start(); while(listFigure.Index <= listFigure.Count) { current = listFigure.item(); current.Show(graphic,pen,brush); listFigure.go_next(); } listFigure.finish (); } private void menuItem16_Click(object sender, System.EventArgs e) { clearList (); } void clearList () { //Clear List listFigure.start(); while(!listFigure.empty()) { current = listFigure.item(); current.Show(graphic,clearPen,clearBrush); listFigure.remove (); } } private void Form1_MouseMove (object sender, System.Windows.Forms.MouseEventArgs e) { if((current!= null) && current.dragged_figure) { current.Show(graphic,clearPen,clearBrush); Point pt = new Point(e.X, e.Y); current.center_figure = pt; current.Show(graphic,pen,brush); } } private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e) { current.dragged_figure = false; } private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { Point mousePoint = new Point (e.X, e.Y); Rectangle figureRect = current.Region_Capture(); if ((current!= null) && (figureRect.Contains(mousePoint))) current.dragged_figure = true; } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { //show current figure current.Show(graphic, pen, brush); } private void toolBar1_ButtonClick(object sender, System.Windows.Forms.ToolBarButtonClickEventArgs e) { int buttonNumber = toolBar1.Buttons.IndexOf(e.Button); switch (buttonNumber) { case 0: createEllipse (); break; case 1: createCircle(); break; case 2: createLittleCircle(); break; case 3: createRectangle(); break; case 4: createSquare(); break; case 5: createPerson (); break; case 6: showCurrent (); break; case 7: clearCurrent (); break; case 8: showList (); break; case 9: clearList (); break; case 10: incScale (); break; case 11: decScale (); break; case 12: moveLeft (); break; case 13: moveRight (); break; case 14: moveTop (); break; case 15: moveDown (); break; case 16: goNext(); break; case 17: goPrev (); break; } } } } Команд меню и кнопок в нашем интерфейсе много, поэтому много и обработчиков событий, что приводит к разбуханию кода. Но каждый из обработчиков событий довольно прост. Ограничусь кратким описанием главного меню: • команды пункта главного меню • команды пункта главного меню • две команды пункта • команды пункта • команды пункта • группа команд пункта • командные кнопки инструментальной панели соответствуют наиболее важным командам меню; • реализована возможность перетаскивания фигур по экрану мышью. В заключение взгляните, как выглядит форма в процессе работы с объектами: Рис. 25.1.
У НАС В ГОСТЯХ
Правда о колбасе У нас в гостях журнал «Химия и жизнь» (со своей колбасой) Волею судьбы я занялся проблемами пищевой промышленности. И первой поставленной передо мной задачей было выяснить, какие химические добавки можно найти в наиболее распространенных продуктах питания. Оказалось, что наряду с загустителями, ароматизаторами, структурообразователями, красителями, подсластителями, подкислителями, белковыми добавками, водоудерживающими агентами и прочим, иногда в продукты питания добавляют и весьма специфические вещества. Например, существуют специальные ароматические смеси, имитирующие вкус определенных продуктов. В колбасы добавляют ароматические композиции, имитирующие вкус докторской, любительской и других колбас (я насчитал не менее 50 имитаторов ароматов). Уже появились добавки, ликвидирующие вкус мяса. Другими словами, сегодня технологи пищевой промышленности могут взять куриное мясо, избавиться от запаха курятины и заменить его, например, запахом говядины. Кроме того, есть добавки, убирающие аромат подпорченного (то есть протухшего) сырого мяса. Экономический эффект налицо. Разработаны белковые составы (на основе молочного и/или соевого белков), которые могут заменять в колбасе мясо. Причем такую замену невозможно обнаружить стандартными методами контроля качества, которые сегодня используют на колбасных заводах. Получается, что колбасу можно выпускать совсем без мяса. Например, на семинаре в ВНИИМП (Всероссийском НИИ мясной промышленности) всем давали попробовать полукопченую колбасу следующего состава (рецептура прилагалась): Говядина жилованная 1 сорта — 5% Эмульсия из говядины — 25% Эмульсия шкурки — 25% Шпиг боковой — 18% Гранулы соевого изолята — 25% Крахмал — 2 % Итого 100% К 100 кг этой смеси добавляют 2,5 кг соли, 7,5 г нитрита натрия и 700 г смеси специй (в данном случае это был имитатор вкуса колбасы «Таллиннская»). Кроме того, можно прибавить 1–2 % молочного белка. Себестоимость такой колбасы около 40 руб/кг. Честно говоря, мне она показалась невкусной. Не очень понятно, зачем в нее все-таки положили 5 % настоящего мяса — его можно было заменить крахмалом или соевым белком, получилось бы еще дешевле. Еще более интересные факты мне сообщили в кулуарах этого же семинара во ВНИИМП. Говорят, что в некоторых местах нашей необъятной страны берут филе недорогой рыбы, с помощью двуокиси титана придают ему белый цвет и продают как филе трески (детали данной технологии мне неизвестны1). А еще есть технология получения филе красной рыбы из более дешевых сортов (плюс соответствующий краситель и ароматизатор). Обо всем известных «крабовых палочках» я уже и не говорю. 1 Пару лет назад был скандал с аналогичной технологией в Закусочных Макдональдс (США и Канада). Они отбеливали куриное мясо, чтобы оно по внешнему виду подходило под их же стандарты. Существует также технология подделки сырого кускового мяса: мелко порубленный фарш мяса второго сорта специальным шприцом впрыскивают в цельные большие куски мяса высшего сорта. При разрезе эту добавку не будет видно. Понятно, что такое нашпигованное мясо продают как высший сорт. Мне тут же пришло в голову, как можно дополнительно оптимизировать подобную технологию. Например, фарш из мяса второго сорта можно заменить эмульсией шкурки (см. выше состав колбасы), а то и сдобренный соответствующими красителем и ароматизатором порошок из рогов и копыт (тоже белок!). Так что если лет через десять вы в магазине купили мясо высшего сорта, нашпигованное всего лишь мясом второго сорта, то можете считать, что вам крупно повезло. Пищевая композиция это то, чем разбавляют мясо, чтобы колбасы получилось побольше. Сотрудники Всероссийского научно-исследовательского института мясной промышленности им. В.М.Горбатова предлагают использовать для производства колбас, сосисок и котлет солодовую пивную дробину. Это отход пивного производства, а точнее, перебродивший солод с дрожжами. Его так много, что рука не поднимается выбросить. Дробину сушат, измельчают и получают муку (такая линия установлена на ООО «Тобо-Очаково» в Москве). Мука эта не токсична, в ней нет вредных микробов и плесени, зато много белков, железа, цинка и витамина Е. Белковый компонент пивной дробины достаточно легко переваривается панкреатином, пищеварительным ферментом крупного рогатого скота, а значит, и людям она пойдет на пользу… Опытные образцы полукопченой колбасы «Егерская», вареной колбасы «Старомосковская», сосисок «Крестьянские» и «Котлет сочных из говядины с чесноком» изготовили на Бирюлевском мясоперерабатывающем комбинате. В колбасы и сосиски обычно добавляют соевую муку, однако на этот раз ее место заняла мука из пивной дробины (от 1 до 3 % сырья). В котлетах дробиной заменили 3 % жирной говядины. Получилась колбаса вполне товарного вида — упругая и равномерно розовая. Разработчики утверждают, что колбасы с дробиной пахнут не хуже обычных, а сосиски к тому же, имеют «приятный вкус и аромат, свойственные готовым сосискам». Котлетный фарш получается розовый, не прилипает к стенкам формовочной машины, а сами котлеты при жарке сохраняют форму и отличаются сочностью. Исследования подмосковных ученых из Института биофизики клетки РАН (Пущино) показали, что сохранить мясной фарш или изделия из него — те же сосиски и вареные колбасы можно и без нитритов или нитратов. Добавляют эти далеко не безвредные вещества, не красоты ради, а потому, что они увеличивают срок хранения продуктов. Альтернативой могут стать вещества, которые есть в любой живой клетке — никотинамид-динуклеотид (НАДН) и аденозинтрифосфорная кислота (АТФ), а также некоторые вполне безвредные дикарбоновые кислоты. Всего килограмм этих веществ на тонну мяса позволит хранить сделанную из этого мяса колбасу не два три дня, как обычно, а два-три месяца. Колбаса без нитритов имеет вполне колбасный розовый цвет благодаря специальным натуральным добавкам. И по вкусу она ничуть не хуже, а то и лучше обычной, поскольку не покрывается неприятным скользким налетом на другой день после покупки. Дело в том, что эти вещества помогают ферментам быстро израсходовать кислород, находящийся в мясе. Когда мясо гомогенизируют, то есть делают фарш, клетки разрушаются. В результате в митохондриях накапливается избыточное количество активных форм кислорода. Образующийся при этом супероксид разрушает белки и липиды, и мясо портится. Следовательно, из фарша надо удалить лишний кислород — потенциальный источник супероксида. Можно просто откачать воздух из емкости с фаршем, но это по лумера, поскольку кислород из цитоплазмы мышечных клеток таким способом удалить невозможно. Другой способ ввести в фарш нитриты и консерванты, которые блокируют превращения клеточного кислорода в супероксид. Увы, есть данные, что нитриты обладают канцерогенным действием, поэтому даже небольших количеств этих соединений хотелось бы избежать. Метод, который разработан в Институте биофизики клетки РАН, принципиально иной: избавляться от кислорода в тканях с помощью так называемых природных субстратов клеточного дыхания, то есть НАДН и АТФ. В митохондриях они окисляются, и на это быстро тратится кислород из клеточной цитоплазмы и кровеносных сосудов. Оказалось, что такой метод действительно предотвращает образование вредных перекисей, а также микробное обсеменение продукции. Колбаса с подобным консервантом хранится в холодильнике почти три месяца. К сожалению, промышленники использовать новую технологию не торопятся. Это и понятно: обычную колбасу, не съеденную сегодня, завтра приходится выбрасывать. А эта пролежит гораздо дольше, и выбрасывать ничего не придется. Явный проигрыш для производителя! А здоровье остается, увы, за кадром — ведь в ГОСТах официально разрешены добавки нитритов до 10 %! Химики из Астрахани разработали простую и быструю методику, позволяющую определить концентрацию фенолов в копченых мясных и рыбных продуктах. Результаты анализа настораживают. Об этих результатах и о самой методике авторы рассказали на II Международном симпозиуме «Разделение и концентрирование в аналитической химии и радиохимии» в конце 2005 года. Оказалось, что в продаже бывает колбаса, которую просто нельзя есть! Дело в том, что любое копчение с точки зрения здоровья — предприятие рискованное. Потому что коптильная жидкость (именно с ее помощью, а не в дыму, сегодня «коптят» большую часть продуктов) — это смесь самых разных соединений, в том числе и весьма опасных, а порой и токсичных. Есть там и различные фенольные соединения, и сам фенол. Конечно, исследователи сначала постарались выяснить, какие именно соединения отвечают за тот самый несравненный аромат копчености, который так нравится любителям копченой рыбки и колбаски. Для начала смешали три наиболее очевидных и как будто самых «выразительных» в смысле аромата вещества, кстати говоря и наименее токсичных. Причем смешали не наобум, а в той же пропорции, что и в конденсате дыма. Увы, оказалось, что аромат такой композиции лишь отдаленно напоминал аромат исходного дымового конденсата. Тогда ученые определили, какие еще фенольные соединения входят в этот конденсат. И среди многочисленных компонентов нашли обычный фенол — вещество весьма токсичное. И немало. Оказалось, что после обработки коптильной жидкостью в некоторых колбасах общая концентрация всех фенолов измеряется миллиграммами на килограмм, а в нескольких образцах достигает 18 мг на каждый килограмм продукта. Конечно, не все эти фенолы в равной степени вредны для человека, но обычного фенола там довольно много. Если же учесть, что его ПДК для воды, например, составляет 1 мг на тонну (0,001 мг/л), то лучше бы технологам придумать, как от него избавляться, прежде чем коптить колбасу и рыбу. А нам до тех пор, пожалуй, лучше сократить потребление копченостей.
СПРАВОЧНИК
Опасные пищевые Е-добавки Классификация пищевых добавок в системе «CODEX ALIMENTARIUS» Е-100 — Е-182: Е-200 — Е-299: Е-300 — Е-399: Е-400 — Е-499: Е-500 — Е-599: Е-600 — Е-699: Е-700 — Е-899: Е-900 — Е-999: Таблица пищевых Е-добавок Примечание. В предлагаемой таблице приведен полный список пищевых Е-добавок, химические названия которых даны в возрастающем порядке цифровых кодов на английском и русском языках. Пищевые добавки сгруппированы по функциональным классам. В таблице после индексов используются следующие обозначения: (!) — вещество входит в список пищевых добавок, не имеющих разрешения к применению в пищевой промышленности в Российской Федерации [1]; (текст на сером фоне); (!!) — вещество входит в список пищевых добавок, запрещенных к применению в пищевой промышленности Российской Федерации (текст на черном фоне); (!?) — вещество не упомянуто в документации Российской Федерации, но включено в Таблицу согласно другому источнику [3]; (полужирный текст на белом фоне со значком).
Красители (Е-100 — Е-199) Код добавки ∙ Название добавки (русское и международное) ∙ Отрицательное действие на организм Е-100 ∙ Куркумины (Curciimin) ∙ _ Е-101 ∙ (i) — Рибофлавин; (ii) — Натриевая соль рибофлавин-5-фосфата l(i)-Riboflavin; (ii)-Riboflavin-5 Phosphate Sodium! ∙ _ E-102 ∙ Тартразин (Tartrcizine) ∙ Е-103 (!) ∙ Алканет, алканин (Alkanet) ∙ Е-104 ∙ Желтый хинолиновый (Quinoline Yellow) ∙ Е-107 (!) ∙ Желтый 2 G (Yellow 2 G) ∙ Е-110 ∙ Желтый “солнечный закат” FCF, оранжево-желтый S (Sunset Yellow FCF, Orange Yellow S) ∙ Е-120 ∙ Кошениль; карминовая кислота; кармины (Cochineal, Carminic Acid, Carmines) ∙ Е-121 (!!) ∙ Цитpycoвый красный 2 (Citrus Bed 2) ∙ Е-122 ∙ Азорубин, кармуазин (Azombine, Carmoisine) ∙ Е-123 (!!) ∙ Амарант (Amaranth) ∙ Е-124 ∙ Понсо 4R (пунцовый®), кошенилевый красный А (Ponceau 4R, Cochineal Red А) ∙ Е-125 (!) ∙ Понсо, пунцовый SX (Ponceau SX) ∙ F-127 (!) ∙ Эритрозин (Erythrosine) ∙ F-128 (!) ∙ Красный 2G (Red 2G) ∙ Е-129 ∙ Красный очаровательный AC (Allura Red АС) ∙ Е-131 ∙ Синий патентованный V (Patent Blue V) ∙ Е-132 ∙ Индиготин, индигокармин (Indigo tine. Indigo Carmine) ∙ Е-133 ∙ Синий блестящий FCF (Brilliant Blue FCF) ∙ Е-140 (!) ∙ (i) Хлорофиллы и (ii) хлорофиллины /Chlorophylls and Chlorophyllins: (I) Chlorophylls (ii) Chlorophyllins/ ∙ _ Е-141 ∙ (i) — Медные комплексы хлорофиллов и (ii) — хлорофиллинов /Copper Complexes of Chlorophylls and Chlorophyllins (I) Copper Complexes of Chlorophylls (ii) Copper Complexes of Chlorophyllins/ ∙ Е-142 ∙ Зеленый S (Greens S) ∙ Е-143 ∙ Зеленый прочный FCF (Fast Green FCF) ∙ _ Е-150а ∙ Сахарный колер I простой (карамель простая) /Plain Caramel/ ∙ Е-150Ь ∙ Сахарный колер II, полученный по “щелочно-сульфитной” технологии (Caustic Sulphite Caramel) ∙ Е-150с ∙ Сахарный колер III, полученный по “аммиачной” технологии (Ammonia Caramel) ∙ E-150d ∙ Сахарный колер IV, полученный по “аммиачно-сульфитной” технологии (Sulphite Ammonia Caramel) ∙ Е-151 ∙ Черный блестящий BN, черный PN (Brilliant Black BN, Black PN) ∙ Е-152 (!) ∙ Уголь /Carbon Black (hydrocarbon)/ ∙ Е-153 (!) ∙ Уголь растительный (Vegetable Carbon) ∙ Е-154 (!) ∙ Коричневый FK (Brown FK) ∙ Е-155 (!) ∙ Коричневый HT (Brown HT) ∙ Е-160а ∙ Каротины: (I)-β-каротин синтетический, (ii) — экстракты натуральных каротинов /Carotenes: (I)-Beta-Carotene (Synthetic), Natural Extracts-(ii)/ ∙ _ E-160b ∙ Аннато, биксин, норбиксин (Annatto, Bixin, Norbixin) ∙ Е-160с ∙ Экстракт паприки, капсантин, капсорубин (Paprika extract. Capsanthin. Capsombin) ∙ _ E-160d (!) ∙ Ликопин (Lycopene) ∙ _ E-160e ∙ β-апо-8-каротиновый альдегид (С 30) /Beta-apo-8’-carotenal (С 30)/ ∙ _ E-160f (!) ∙ Этиловый эфир β-апо-8-каротииовой кислоты (С 30) /Ethyl ester ofbeta-apo-8’-carotenic Acid (С 30)/ ∙ _ E-161a ∙ Флавоксантин (Flavoxanthin) ∙ E-161b ∙ Лугеин (Lutein) ∙ E-161c ∙ Криптоксантин (Ciyptoxanthin) ∙ E-161d ∙ Рубиксантин (Rubixanthin) ∙ E-161e ∙ Виолоксантин (Violoxanthin) ∙ E-161f ∙ Родоксантин (Rhodoxanthin) ∙ E-161g ∙ Кантаксантин (Canthaxanthin) ∙ E-162 ∙ Свекольный красный, бетанин (Beetroot Red, Betanin) ∙ _ E-163 ∙ Антоцианы (Antlwcyanins) ∙ _ E-164 ∙ Шафран (Saffron) ∙ _ E-166 (!) ∙ Сандаловое дерево (Sandalwood) ∙ _ E-170 ∙ Карбонаты кальция (Calcium Carbonates) ∙ _ E-171 ∙ Диоксид титана (Titanium Dioxide) ∙ E-172 ∙ Оксиды и гидроксиды железа (Iron Oxides and Hydroxides) ∙ E-173 (!) ∙ Алюминий (Aluminium) ∙ E-174 (!) ∙ Серебро (Silver) ∙ _ E-175 (!) ∙ Золото (Gold) ∙ _ E-180 (!) ∙ Рубиновый литол BK (Lithol Rubine BK) ∙ E-181 ∙ Танины пищевые (Tannins, Food Grade) ∙ _ E-182 (!) ∙ Орсейл, орсин (Orchil) ∙ _
Консерванты (Е-200 — Е-299) Е-200 ∙ Сорбиновая кислота (Sorbic Acid) ∙ Е-201 ∙ Сорбат натрия (Sodamt Sorbate) ∙ Е-202 ∙ Сорбат калия (Potassium Sorbate) ∙ Е-203 ∙ Сорбат кальция (Calcium sorbate) ∙ _ Е-209 (!) ∙ Пара-гидроксибензойной кислоты гептиловый эфир (Heptyl p-hydroxybenzoate) ∙ _ Е-210 ∙ Бензойная кислота (Benzoic Acid) ∙ Е-211 ∙ Бензоат натрия (Sodamt Benzoate) ∙ Е-212 ∙ Бензоат калия (Potassium Benzoate) ∙ Е-213 (!) ∙ Бензоат кальция (Calcium Benzoate) ∙ Е-214 (!) ∙ Пара-гидроксибензойной кислоты этиловый эфир (Ethyl p-hydroxybenzoate) ∙ Е-215 (!) ∙ Пара-гидроксибензойной кислоты этилового эфира натриевая соль (Sodium Ethyl p-hydroxybenzoate) ∙ Е-216 (!!) ∙ Пара-оксибензойной кислоты пропиловый эфир (Propyl p-hydroxybenzoate) ∙ E-217 (!!) ∙ Паpa-оксибензойной кислоты пропилового эфира натриевая соль (Sodium Propyl p-hydroxybenzoate) ∙ E-218 (!) ∙ Пара-гидроксибензойной кислоты метиловый эфир (Methyl p-hydroxybenzoate) ∙ E-219 (!) ∙ Пара-гидроксибензойной кислоты метилового эфира натриевая соль (Sodium Methyl p-hydroxybenzoate) ∙ E-220 ∙ Диоксид серы (Sulphur Dioxide) ∙ E-221 ∙ Сульфит натрия (Sodium Sulphite) ∙ E-222 ∙ Гидросульфит натрия (Sodium Hydrogen Sulphite) ∙ E-223 ∙ Пиросульфит натрия (Sodium Metabisulphite) ∙ E-224 ∙ Пиросульфит калия (Potassium Metabisulphite) ∙ E-225 (!) ∙ Сульфит калия (Potassium Sulphite) ∙ E-226 (!) ∙ Сульфит кальция (Calcium Sulphite) ∙ E-227 (!) ∙ Гидросульфит кальция (Calcium Hydrogen Sulphite) ∙ _ Е-228 (!) ∙ Гидросульфит калия (бисульфит калия) (Potassium Hydrogen Sulphite) ∙ _ Е-230 (!) ∙ Бифенил, дифенил (Biphenyl, Diphenyl) ∙ Е-231 (!) ∙ Ортофенилфенол (Ortlwphenyl Phenol) ∙ Е-232 (!) ∙ Ортофенилфенол натрия (Sodium Orthophenyl Phenol) ∙ Е-233 (!) ∙ Тиабендазол (Thiabendazole) ∙ Е-234 ∙ Низин (Nisin) ∙ _ Е-235 ∙ Натамицин(пимарицин) /Nataniycin (Pimaricin)/ ∙ _ Е-236 ∙ Муравьиная кислота (Formic Acid) ∙ _ Е-237 (!) ∙ Формиат натрия (Sodium Formate) ∙ _ Е-238 (!) ∙ Формиат кальция (Calcium Formate) ∙ _ Е-239 ∙ Гексаметилентетрамин (Hexcimethylene Tetrcimiiie) ∙ F-240 (!!) ∙ Формальдегид (Formaldehyde) ∙ Е-241 (!) ∙ Гваяковая смола (Gum Guaicum) ∙ Е-242 ∙ Диметилдикарбонат (Dimethyl Dicarbonate) ∙ _ Е-249 ∙ Нитрит калия (Potassium Nitrite) ∙ Е-250 ∙ Нитрит натрия (Sodium Nitrite) ∙ Е-251 ∙ Нитрат натрия (Sodium Nitrate) ∙ Е-252 (!) ∙ Нитрат калия (Potassium Nitrate) ∙ Е-260 ∙ Уксусная кислота (Acetic Acid) ∙ _ Е-261 ∙ Ацетат калия (Potassium Acetate) ∙ _ Е-262 ∙ Ацетаты натрия: ацетат натрия, гцдроацетат натрия (диацетат натрия) /Sodium Acetates (i) Sodium Acetate (ii) Sodium Hydrogen Acetate (Sodium Diacetate)/ ∙ _ Е-263 (!) ∙ Ацетат кальция (Calcium Acetate) ∙ _ Е-264 (!) ∙ Ацетат аммония (Ammonium Acetate) ∙ _ Е-265 ∙ Дегидроацетовая кислота (Deliydroacetic Acid) ∙ _ Е-266 ∙ Дегидроацетат натрия (Sodium Dehydroacetate) ∙ _ Е-270 ∙ Молочная кислота (Lactic Acid) ∙ Е-280 ∙ Пропионовая кислота (Propionic Acid) ∙ Е-281 (!) ∙ Пропионат натрия (Sodium Propionate) ∙ Е-282 (!) ∙ Пропионат кальция (Calcium Propionate) ∙ Е-283 (!) ∙ Пропионат калия (Potassium Propionate) ∙ E-284# (!?) ∙ Борная кислота (Boric Acid) ∙ E-285# (!?) ∙ Тетраборат натрия (бура) /Sodium Tetraborate (Borax)/ ∙ _ Е-290 ∙ Диоксид углерода (Carbon Dioxide) ∙ _ Е-296 ∙ Яблочная (малоновая) кислота (Malic Acid) ∙ _ Е-297 ∙ Фумаровая кислота (Fumaric Acid) ∙ _
Антиоксиданты (Е-300 — Е-399) Е-300 ∙ Аскорбиновая кислота (Ascorbic Acid) ∙ _ Е-301 ∙ Натриевая соль аскорбиновой кислоты (аскорбат натрия) (Sodium Ascorbate) ∙ _ Е-302 (!) ∙ Кальциевая соль аскорбиновой кислоты (аскорбат кальция) /Calcium Ascorbatel ∙ _ Е-303 (!) ∙ Аскорбат калия (Potassium Ascorbate) ∙ _ Е-304 ∙ Аскорбилпальмитат (Ascorbyl Palmitate) ∙ _ Е-305 (!) ∙ Аскорбилстеарат (Ascorbyl Stearate) ∙ _ Е-306 ∙ Концентрат смеси токоферолов (Mixed Tocopherols Concentrate) ∙ _ Е-307 ∙ α-токоферол (Alpha-tocopherol) ∙ _ Е-308 (!) ∙ γ-токоферол синтетический (Synthetic Gamma-tocopherol) ∙ _ Е-309 (!) ∙ δ-токоферол синтетический (Synthetic Delta-tocopherol) ∙ _ Е-310 (!) ∙ Пропилгаллат (Propyl GaUate) ∙ Е-311 (!) ∙ Октилгаллат (Octyl GaUate) ∙ Е-312 (!) ∙ Додецилгаллат (Dodecyl GaUate) ∙ Е-313 (!) ∙ Этилгаллат (Ethyl GaUate) ∙ Е-314 (!) ∙ Гваяковая смола (Guaiac Resin) ∙ _ Е-315 ∙ Эриторбовая (изо-аскорбиновая) кислота lErythorbic (Isoascorbic) Acidl ∙ _ Е-316 ∙ Эриторбат натрия (Sodium Erythorbate) ∙ _ Е-317 (!) ∙ Изо-аскорбинат калия (Potassium Isoascorbate) ∙ _ Е-318 (!) ∙ Изо-аскорбинат кальция (Calcium Isoascorbate) ∙ _ Е-319 ∙ Трет-бугилгидрохинон (Tertiary Butylhyd.oqianone) ∙ _ Е-320 ∙ Бутил гидроксианизол /Butylated Hydroxyatusole (ВНА)/ ∙ Е-321 ∙ Бутлгидрокситолуол /Butylated Hydroxytoluene (BHT)/ ∙ Е-322 ∙ Лецитины (Lecithins) ∙ Е-323 (!) ∙ Аноксомер (Anoxomer) ∙ _ Е-324 (!) ∙ Этоксихин (Ethoxyquin) ∙ _ Е-325 (!) ∙ Лактат натрия (Sodium Lactate) ∙ _ Е-326 ∙ Лактат калия (Potassium Lactate) ∙ _ Е-327 ∙ Лактат кальция (Calcium Lactate) ∙ _ Е-328 (!) ∙ Лактат аммония (Ammonium Lactate) ∙ _ Е-329 (!) ∙ Лактат магния /Magnesium Lactate (D,L-)/ ∙ _ Е-330 ∙ Лимонная кислота (Citric Acid) ∙ Е-331 ∙ Цитраты натрия: (i) — цитрат натрия однозамещенный, (ii) — цитрат натрия двузамещенный, (iii) — цитрат натрия трехзамещенный /Sodium Citrates: (i)-Monosodium Citrate, (ii)-Disodium Citrate, (iii)-Ttisodium Сitratel ∙ _ Е-332 ∙ Цитраты калия: (i) — цитрат калия однозамещенный, (ii) — цитрат калия двузамещенный, (iii) — цитрат калия трехзамещенный /Potassium Citrates: (i)-Monopotassium Citrate, (ii)-Dipotassium Citrate, (iii)-Tripotassium Сitratel ∙ _ Е-333 ∙ Цитраты кальция: (i) — одпозамещенный цитрат кальция, (ii) — двузамещенный цитрат кальция, (iii) — трехзамещенный цитрат кальция /Calcium Citrates: (i)-Monocalcium Citrate, (ii)-Dicalcium Citrate, (iii)-Tricalcium Citrate/ ∙ _ Е-334 ∙ Винная кислота (L(+)-) /Tartaric Acid (L(+)-)/ ∙ _ Е-335 ∙ Тартраты натрия: (i) — тартрат натрия однозамещенный, (ii) — тартрат натрия двузамещенный /Sodium Tartrates: (i) — Monosodium Tartrate (ii) — Disodium Tartrate/ ∙ _ E-336 ∙ Тартраты калия: (i) — тартрат калия однозамещенный, (ii) — тартрат калия двузамещенный /Potassium Tartrates: (i) — Monopotassium Tartrate, (ii) — Dipotassium Tartrate/ ∙ _ Е-337 ∙ Тартрат калия-натрия (Sodium potassium tartrate) ∙ _ Е-338 ∙ Ортофосфорная кислота (Phosphoric Acid) ∙ Е-339 ∙ Ортофосфаты натрия: (i) — ортофосфат натрия однозамещенный, (ii) — ортофосфат натрия двузамещенный, (iii) — ортофосфат натрия трехзамещенный /Sodium Ortopliosphates: (i)-Monosodium Ortophosphate, (ii)-Disodium Ortophosphate, (iii)-Trisodium Ortophosphate/ ∙ Е-340 ∙ Ортофосфаты калия: ортофосфат калия, однозамещенный, ортофосфат калия двузамещенный, ортофосфат калия трехзамещенный /Potassium Ortophosphates (i) Monopotassium Ortophosphate (ii) Dipotassium Ortophosphate (iii) Tripotassium Ortophosphate/ ∙ Е-341 ∙ Ортофосфаты кальция: ортофосфат кальция однозамещенный, ортофосфат кальция двузамещенный, ортофосфат кальция /Calcium Phosphates (i) Monocalcium Ortophosphate (ii) Dicalcium Ortophosphate (iii) Tricalcium Ortophosphate/ ∙ Е-342 ∙ Ортофосфаты аммония: ортофосфат аммония однозамещенный, ортофосфат аммония двузамещенный! Ammonium Phosphates (i) Monoammonium Ortophosphate (ii) Diammonium Ortophosphate/ ∙ _ Е-343 (!) ∙ Ортофосфаты магния: (i) ортофосфат магния однозамещенный, (ii) ортофосфат магния двузамещенный, (iii) ортофосфат магния трехзамещенный /Magnesium Ortophosphates: (i) Monomagnesium Ortophosphate (ii) Dimagnesium Ortophosphate (iii) Trimagnesium Ortophosphatel/ ∙ Е-344 (!) ∙ Цитрат лецитина (Lecitin Citrate) ∙ _ Е-345 (!) ∙ Цитрат магния (Magnesium Citrate) ∙ _ Е-349 (!) ∙ Малат аммония (Ammonium Malate) ∙ _ Е-350 (!) ∙ Малаты натрия: малат натрия, малат натрия однозамещенный /Sodium Malates (i) Sodium Malate (ii) Sodium Hydrogen Malatel ∙ _ Е-351 (!) ∙ Малат калия (Potassium Malate) ∙ _ Е-352 (!) ∙ Малаты кальция: малат кальция, малат кальция однозамещенный /Calcium Malates (i) Calcium Malate (ii) Calcium Hydrogen Malatel ∙ _ Е-353 ∙ Мета-винная кислота (Metatartaric Acid) ∙ _ Е-354 ∙ Тартрат кальция (Calcium Tartrate) ∙ _ Е-355 (!) ∙ Адипиновая кислота (Adipic Acid) ∙ _ Е-356 (!) ∙ Адипат натрия (Sodium Adipate) ∙ _ Е-357 (!) ∙ Адипат калия (Potassium Adipate) ∙ _ Е-359 (!) ∙ Адипат аммония (Ammonium Adipate) ∙ _ Е-363 ∙ Янтарная кислота (Succinic Acid) ∙ _ Е-365 (!) ∙ Фумараты натрия (Sodium Fumarates) ∙ _ Е-366 (!) ∙ Фумараты калия (Potassium Fumarates) ∙ _ Е-367 (!) ∙ Фумараты кальция (Calcium Fumarates) ∙ _ Е-368 (!) ∙ Фумараты аммония (Ammonium Fumarates) ∙ _ Е-370 (!) ∙ 1,4-гептоноллактон (1,4-Heptonolactone) ∙ _ Е-375 (!) ∙ Никотиновая кислота (Nicotinic Acid) ∙ _ Е-380 ∙ Цитраты аммония (аммонийные соли лимоннной кислоты) /Ammonium Citrates/ ∙ _ Е-381 (!) ∙ Аммоний железо цитрат (Ferric Ammonium Citrate) ∙ _ Е-383 ∙ Глицерофосфат кальция (Calcium Glyce ophosphate) ∙ _ Е-384 (!) ∙ Изопропилцитратная смесь (Isopropil Citrates) ∙ _ Е-385 ∙ Кальций динатриевая соль этилецдиаминтриуксусной кислоты (CaNa2 ЭДТА) /Calcium Disodium Ethylene Diamine Tetra-acetate (Calcium Disodium EDTA)/ ∙ _ Е-386 ∙ Этилендиаминтетраацетат динатрий (Disodium Ethylene Diamine Tetra-acetate ∙ _ Е-387 (!) ∙ Оксистеарин (Oxystearin) ∙ _ Е-388 (!) ∙ Тиопропионовая кислота (Thiodipropionic Acid) ∙ _ Е-389 (!) ∙ Дилаурилтиодипропионат (Dilauryl Thiodipropionate) ∙ _ Е-390 (!) ∙ Дистеарилтиодипропионат (Dustearyl Thiodipropionate) ∙ _ Е-391 ∙ Фитиновая кислота (Phytic Acid) ∙ _ Е-399 (!) ∙ Лактобионат кальция (Calcium Lactobionale) ∙ _
Стабилизаторы, эмульгаторы (Е-400 — Е-599) Е-400 ∙ Альгиновая кислота (Alginic Acid) ∙ _ Е-401 ∙ Альгинат натрия (Sodium Alginate) ∙ _ Е-402 ∙ Альгинат калия (Potassium Alginate) ∙ _ Е-403 (!) ∙ Альгинат аммония (Ammonium Alginate) ∙ _ Е-404 ∙ Альгинат кальция (Calcium Alginate) ∙ _ Е-405 ∙ Пропан-1,2-диол альгинат (Propan-1,2-diol alginate) ∙ _ Е-406 ∙ Агар (Agar) ∙ _ Е-407 ∙ Каррагинан и его соли (Carrageenan and its Salts) ∙ Е-407а# (!?) ∙ Переработанные морские водоросли (Eucheuma Processed Eucheuma Seaweed) ∙ Е-408* (!) ∙ Гликан пекарских дрожжей (Bakers Yeast Glycan) ∙ _ Е-409* (!) ∙ Арабиногалактан (Arabinogalactan) ∙ _ Е-410 ∙ Камедь рожкового дерева (Carob Bean Gum) ∙ _ Е-411 ∙ Овсяная камедь (Oat Gum) ∙ _ Е-412 ∙ Гуаровая камедь (Giiar Gum) ∙ _ Е-413 ∙ Трагакант (Tragacanth) ∙ _ Е-414 ∙ Гуммиарабик /Acacia Gum (Gum Arabic)/ ∙ _ Е-415 ∙ Ксантановая камедь (Xanthan Gum) ∙ _ Е-416 ∙ Карайи камедь (Каraуа Gum) ∙ _ Е-417 ∙ Тары камедь (Tara Gum) ∙ _ E-418* (!) ∙ Геллановая камедь (Gellan Gum) ∙ _ Е-419* (!) ∙ Гхатти камедь (Gum Chatty) ∙ _ Е-420 ∙ Сорбит, сорбитовый сироп /Sorbitol (i) Sorbitol (ii) Sorbitol Syrup/ ∙ _ Е-421 ∙ Маннит (Mannitol) ∙ _ Е-422 ∙ Глицерин (Glycerol) ∙ _ Е-425# (!) ∙ Коньяк смола, коньяк глюкоманнан /Konjac 0) Konjac Gum (ii) Konjac Glucomannane/ ∙ Е-429 (!) ∙ Пептоны (Peptones) ∙ _ Е-430 (!) ∙ Полиоксиэтилен(8) стеарат (Polyoxy ethylene (8) Stearate) ∙ _ Е-431 (!) ∙ Полиоксиэтилен(40) стеарат (Polyоху ethylene (40) Stearate) ∙ _ Е-432 (1) ∙ Полиоксиэтиленсорбитан монолаурат (полисорбат 20, твин 20) /Polyохуethylene Sorbitan Monolaurate (Polysorbate 20)/ ∙ _ Е-433 (!) ∙ Полиоксиэтиленсорбитан моноолеат (полисорбат 80, твин 80) /Polyохуethylene Sorbitan Monooleate (Polysorbate 80)/ ∙ _ Е-434 (!) ∙ Полиоксиэтиленсорбитан монопальмитат (полисорбат 40, твин 40) /Polyoxyethylene Sorbitan Monopalmitate (Polysorbate 40)/ ∙ _ Е-435 (!) ∙ Полиоксиэтиленсорбитан моностеарат (полисорбат 60, твин 60) /Polyохуethylene Sorbitan Monostearate (Polysorbate 60)/ ∙ _ Е-436 (!) ∙ Полиоксиэтиленсорбитан тристеарат (полисорбат 65) /Polyoxy ethylene Sorbitan Tristearate (Polysorbate 65)/ ∙ _ Е-440 ∙ Пектины: пектин, амидопектин /Pectins (i) Pectin (ii)Amidcited Pectin/ ∙ _ Е-441 (!) ∙ Рапсовое масло гидрогенизированное с высоким содержанием глицерина (Superglycerinated Hydrogenated Rapeseed Oil) ∙ _ Е-442 (!) ∙ Фосфатида аммонийные соли (Ammonium Phosphatides) ∙ _ Е-443 (!) ∙ Бромированное растительное масло (Brominated Vegetable Oil) ∙ _ Е-444 (!) ∙ Изо-бутиратацетат сахарозы (Sucrose Acetate Isobutyrate) ∙ _ Е-445 ∙ Эфиры глицерина и смоляных кислот (Glycerol Esters of Wood rosins) ∙ _ Е-446 (!) ∙ Сукпистеарин (Succistearin) ∙ _ Е-450 ∙ Пирофосфаты: двузамещенный пирофосфат натрия, трехзамещенный пирофосфат натрия, тетранатрийпирофосфат, двузамещенный пирофосфат калия, тетракалийдифосфат, дикалыдийпирофосфат, кальцийдигидропирофосфат /Diphosphates (I) Disodium Diphosphate (ii) Trisodium Diphosphatc(iii) Tetrasodium Diphosphate (iv) Dipotassium Diphosphate (v) Tetrapotassium Diphosphate (vi) Dicalcium Diphosphate (vii) Calcium Dihydrogen diphosphate/ ∙ Е-451 ∙ Трифосфаты: трифосфат натрия 5-замещенный, трифосфат калия 5-замещенный /Triphosphates (I) Pentasodium Triphosphate (ii) Pentapotassium Triphosphate/ ∙ Е-452 ∙ Полифосфаты: полифосфат натрия, полифосфат калия, полифосфат натрия-кальция полифосфат кальция /Polyphosphates (I) Sodium Polyphosphates (ii) Potassium Polyphosphates (iii) Sodium Calcium Polyphosphate (iv) Calcium Polyphophates/ ∙ Е-459# (!?) ∙ β-циклодекстрин (Beta-cyclodextrine) ∙ Е-460 ∙ Целлюлоза: микрокристаллическая целлюлоза, целлюлоза в порошке /Cellulose (I) Microcrystalline Cellulose (ii) Powdered Cellulose/ ∙ _ Е-461 ∙ Метилцеллюлоза (Methyl Cellulose) ∙ Е-462 (!) ∙ Этилцеллюлоза (Ethyl Cellulose) ∙ Е-463 (!) ∙ Гидроксипропилцеллюлоза (Hydroxypropyl Cellulose) ∙ Е-464 ∙ Гвдроксипропил метилцеллюлоза (Hydroxypropyl Methyl Cellulose) ∙ Е-465 (!) ∙ Этилметилцеллюлоза (Ethyl Methyl Cellulose) ∙ Е-466 ∙ Карбоксиметилцеллюлоза, натрийкарбоксиметилцеллюлоза (Carboxy Methyl Cellulose, Sodium Carboxy Methyl Cellulose) ∙ Е-467 (!) ∙ Этилгидроксиэтилцеллюлоза (Ethyl Hydroxyethyl Cellulose) ∙ _ Е-468# (!?) ∙ Карбоксиметилцеллюлозы натриевая соль трехмерная (Crosslinked Sodium Carboxymethyl Cellulose) ∙ Е-469# (!?) ∙ Гидролизуемая под действием ферментное карбоксиметилцеллюлоза (Enzymically Hydrolysed Carboxymethylcellulose) ∙ Е-470а# (!?) ∙ Натриевые, калиевые и кальциевые соли жирных кислот (Sodium, Potassium and Calcium Salts of Fatty Acids) ∙ _ Е-470b# (!?) ∙ Магниевые соли жирных кислот (Magnesium Salts of Fatty Acids) ∙ _ Е-471 ∙ Моно- и диглицериды жирных кислот (Mono- and Diglycerides of Fatty Acids) ∙ _ Е-472а ∙ Эфиры моно- и диглицеридов уксусной и жирных кислот (Acetic Acid Esters of Mono- and Diglycerides of Fatty Acids) ∙ _ Е-472Ь ∙ Эфиры моно- и диглицеридов молочной и жирных кислот (Lac tic Acid Esters of Mono- and Diglycerides of Fatty Acids) ∙ _ Е-472с ∙ Эфиры моно- и диглицеридов лимонной и жирных кислот (Citric acid Esters of Mono- and Diglycerides of Fatty Acids) ∙ _ E-472d ∙ Эфиры Mono- и диглицеридов винной и жирных кислот (Tartaric Acid Esters of Monoam Diglycerides of Fatty Acids) ∙ _ E-472e ∙ Эфиры глицерина, диацетилвинной и жирных кислот (Diacetyltartaric and Fatty Acid Esters of Glycerol) ∙ _ E-472f ∙ Смешанные эфиры глицерина, винной, уксусной и жирных кислот (Mixed Tartaric, Acetic and Fatty Acids Esters of Glycerol) ∙ _ E-472g ∙ Сукцинилированные моноглицериды (Succinylated Monoglycerides) ∙ _ E-473 ∙ Эфиры сахарозы и жирных кислот (Sucrose Esters of Fatty Acids) ∙ _ E-474* (!) ∙ Сахароглицериды (Sucroglycerides) ∙ _ E-475 ∙ Эфиры полиглицеридов и жирных кислот (Polyglycerol Esters of Fatty Acids) ∙ _ E-476* (!) ∙ Полиглицерин полирицинолеаты (Polyglycerol Polyricinoleate) ∙ _ E-477* (!) ∙ Пропан-1,2-диоловые эфиры жирных кислот /Ргораnе-1,2-diol Esters of Fatty Acids/ ∙ _ Е-478* (!) ∙ Эфиры лактилированных жирных кислот глицерина и пропиленгликоля (Lactylated Fatty Acid Esters of Glycerol and Propilene Glycol) ∙ _ Е-479b* (!) ∙ Термически окисленное соевое и бобовое масло с моно- и диглипердами жирных кислот /Thermally Oxidized Soya Bean Oil Interacted with Mono- and Diglycerides of Fatty Acids/ ∙ _ Е-480* (!) ∙ Диоктилсульфосукцинат натрия (Dioctyl Sodium Sulphosuccinate) ∙ _ Е-481 ∙ Стеароил-2-лактилат натрия (S Stearoyl-2-lactylcite) ∙ _ Е-482* (!) ∙ Стеароил-2-лактилат кальция (Calcium Stearoyl-2-lactylate) ∙ _ Е-483* (!) ∙ Стеарилтартрат (Stearyl Tartrate) ∙ _ Е-484* (!) ∙ Стеарилцитрат (Stearyl Citrate) ∙ _ Е-485* (!) ∙ Стеароилфумарат натрия (Sodium Stearoyl Fumarate) ∙ _ Е-486* (!) ∙ Стеароилфумарат кальция (Calcium Stearoyl Fumarate) ∙ _ Е-487* (!) ∙ Лаурилсульфат натрия (Sodium Lauiylsulfate) ∙ _ Е-488* (!) ∙ Этоксилированные моно- и диглицериды (Ethoxylated Mono- and Di-glycerides) ∙ _ Е-489* (!) ∙ Эфир кокосового масла и метилгликозида /Methyl Glucoside — Coconut Oil Ester/ ∙ _ Е-491* (!) ∙ Сорбитан моностеарат СПЭН60 (Sorbitan Monostearate) ∙ _ Е-492* (!) ∙ Сорбитан тристеарат (Sorbitan Tristearate) ∙ _ Е-493* (!) ∙ Сорбитан монолаурат, СПЭН 20 (Sorbitan Monolaurate) ∙ _ Е-494* (!) ∙ Сорбитанмоноолеат, СПЭН 80 (Sorbitan Monooleate) ∙ _ Е-495* (!) ∙ Сорбитанмонопальмитат, СПЭН 40 (Sorbitan Monopalmitate) ∙ _ Е-496 (!) ∙ Сорбитан триолеат, СПЭН 85 (Sorbitan Trioleat) ∙ _ Е-500 ∙ Карбонаты натрия: карбонат натрия, гидрокарбонат натрия, секвикарбонат натрия /Sodium Carbonates (I) Sodium Carbonate (ii) Sodium Hydrogen Carbonate (iii) Sodium Sesquicarbonate/ ∙ _ Е-501 ∙ Карбонаты калия: карбонат калия, гидрокарбонат калия /Potassium Carbonates (I) Potassium Carbonate (ii) Potassium Hydrogen Carbonate/ ∙ _ Е-503 ∙ Карбонаты аммония: карбонат аммония, гвдрокарбонат аммония /Ammonium Carbonates (I) Ammonium Carbonate (ii) Ammonium Hydrogen Carbonate/ ∙ _ Е-504 ∙ Карбонаты магния: карбонат магния, гидроксикарбонат магния, гидроксикарбонат магния /Magnesium Carbonates (I) Magnesium Carbonate (ii) Magnesium Hydroxide Carbonate (syn. Magnesium Hydrogen carbonate)/ ∙ _ Е-505* (!) ∙ Карбонат железа (Ferrous Carbonate) ∙ _ Е-507 ∙ Соляная кислота (Hydrochloric Acid) ∙ _ Е-508 ∙ Хлорид калия (Potassium Chloride) ∙ _ Е-509 ∙ Хлорид кальция (Calcium Chloride) ∙ _ Е-511 ∙ Хлорид магния (Magnesium Chloride) ∙ _ Е-512* (!) ∙ Хлорид олова (Stannous Chloride) ∙ _ Е-513 ∙ Серная кислота (Sulphuric Acid) ∙ _ Е-514 ∙ Сульфаты натрия: сульфат натрия, гидросульфат натрия /Sodium Sulphates (i) Sodium Sulphate (ii) Sodium Hydrogen Sulphate/ ∙ _ Е-515 ∙ Сульфаты калия: сульфат калия, гидросульфат калия /Potassium Sulphates (i) Potassium Sulphate (ii) Potassium Hydrogen Sulphate/ ∙ _ Е-516 ∙ Сульфат кальция (Calcium Sulphate) ∙ _ Е-517 ∙ Сульфат аммония (Ammonium Sulphate) ∙ _ Е-519* (!) ∙ Сульфат меди (Cuprie Sulphate) ∙ _ Е-520* (!) ∙ Сульфат алюминия (Aluminium Sulphate) ∙ _ Е-521* (!) ∙ Сульфат алюминия-натрия (кваспы алюмонатриевые) /Aluminium Sodium Sulphate/ ∙ _ Е-522 (!) ∙ Сульфат алюминия-калия (квасцы алюмокалдиевые) /Aluminium Potassium Sulphate/ ∙ _ Е-523 (!) ∙ Сульфат алюминия-аммония (квасцы алюмоаммиачные) /Aluminium Ammonium Sulphate/ ∙ _ Е-524 ∙ Гидроксид натрия (Sodium Hydroxide) ∙ _ Е-525 ∙ Гидроксид калия (Potassium Hydroxide) ∙ _ Е-526 ∙ Гидроксид кальция (Calcium Hydroxide) ∙ _ Е-527 ∙ Гидроксид аммония (Ammonium Hydroxide) ∙ _ Е-528 ∙ Гидроксид магния (Magnesium Hydroxide) ∙ _ Е-529 ∙ Оксид кальция (Calcium Oxide) ∙ _ Е-530 ∙ Оксид магния (Magnesium Oxide) ∙ _ Е-535 (!) ∙ Ферроцианид натрия (Sodium Ferrocyanide) ∙ _ Е-536 ∙ Ферроцианид калия (Potassium Ferrocyanide) ∙ _ Е-537 (!) ∙ Гексацианоманганат железа (Ferrous Hexacyanomanganate) ∙ _ Е-538 (!) ∙ Фферроцианид кальция (Calcium Ferrocyanide) ∙ _ Е-541 (!) ∙ Алюмофосфат натрия: (i) кислотный, (ii) основный /Sodium Aluminium Phosphate: (i) Acidic (ii) Basic/ ∙ _ Е-542 (!) ∙ Костный фосфат, основа его фосфат кальция 3-х основный /Bone Phosphate (Essentiale Calcium Phosphate, Tribasic)/ ∙ _ E-550 (!) ∙ Силикаты натрия: (i) силикат натрия, (ii) мета-силикат натрия /Sodium Silicates: (i) Sodium Silicate (ii) Sodium Metasilicate/ ∙ _ Е-551 ∙ Диоксид кремния (Silicon Dioxide) ∙ _ Е-552 (!) ∙ Силикат кальция (Calcium Silicate) ∙ _ Е-553а ∙ (i) Силикат магния, (ii) трисиликат магния /(i) Magnesium Silicate (ii) Magnesium Trisilicate/ ∙ _ Е-553Ь ∙ Тальк (Talc) ∙ _ Е-554 (!) ∙ Алюмосиликат натрия (Sodium Aluminium Silicate) ∙ _ Е-555 (!) ∙ Алюмосиликат калия (Potassium Aluminium Silicate) ∙ _ Е-556 (!) ∙ Алюмосиликат кальция (Calcium Aluminium Silicate) ∙ _ Е-557 (!) ∙ Силикат цинка (Zink Silicate) ∙ _ Е-558 ∙ Бентонит (Bentonite) ∙ _ Е-559 (!) ∙ Алюмосиликат (каолин) /Aluminium Silicate (Kaolin)/ ∙ _ Е-560 (!) ∙ Силикат калия (Potassium Silicate) ∙ _ Е-570 ∙ Жирные кислоты (Fatty Acids) ∙ _ Е-574 (!) ∙ Глюконовая кислота (D-) /Gluconic Acid (D-)/ ∙ _ Е-575 ∙ Глюконо-6-лактон (Glucono-delta-lactoiie) ∙ _ Е-576 (!) ∙ Глюконат натрия (Sodium Gluconate) ∙ _ Е-577 (!) ∙ Глюконат калия (Potassium Gluconate) ∙ _ Е-578 ∙ Глюконат кальция (Calcium Gluconate) ∙ _ Е-579* (!) ∙ Глюконат железа (Ferrous Gluconate) ∙ _ Е-580* (!) ∙ Глюконат магния (Magnesium Gluconate) ∙ _ Е-585 ∙ Лактат железа (Ferrous Lactate) ∙ _
Усилители вкуса и аромата (Е-600 — Е-699) Е-620 ∙ Глутаминовая кислота (Glutamic Acid) ∙ Е-621 ∙ Глутамат натрия однозамещенный (Monosodium Glutamate) ∙ Е-622 (!) ∙ Глутамат калия однозамещенный (Monopotassium Glutamate) ∙ _ Е-623 (!) ∙ Диглутамат кальция (Calcium Glutamate) ∙ _ Е-624 (!) ∙ Глутамат аммония однозамещенный (Monoammonium Glutamate) ∙ _ Е-625 (!) ∙ Глутамат магния (Magnesium Glutamate) ∙ _ Е-626 ∙ Гуаниловая кислота (Guanylic Acid) ∙ Е-627 ∙ Гуанилат натрия двузамещенный (Disodium Guanylate) ∙ Е-628 (!) ∙ 5’-гуанилат калия двузамещенный (Dipotassium 5’-guanylate) ∙ Е-629 (!) ∙ 5’-гуанилат кальция (Calcium 5’-guanylate) ∙ Е-630 ∙ Инозиновая кислота (Inosinic Acid) ∙ Е-631 ∙ Инозинат натрия двузамещенный (Disodium Inosinate) ∙ Е-632 (!) ∙ Инозинат калия двузамещенный (Dipotassium Inosinate) ∙ Е-633 (!) ∙ 5’-инозинат кальция (Calcium 5’-inosiiuite) ∙ Е-634 (!) ∙ 5’-рибунуклеотиды кальция (Calcium 5'-ribonucleotides) ∙ E-635 (!) ∙ 5-рибунуклеотиды натрия двузамещенные (Disodium 5'-ribonucleotides) ∙ Е-640 (!)∙ Глицин и его натриевые соли (Glycine and its Sodium Salt) ∙ _ Е-641 (!) ∙ L-лейцин (L-leucine) ∙ _
Пеногасители (Е-900 — Е-999) и другие вещества E900 ∙ Диметилполисилоксан (Dimethyl Polysiloxaiie) ∙ _ Е-901 ∙ Пчелиный воск, белый и желтый (Beeswax, White and Yellow) ∙ Е-902 ∙ Воск свечной (Candelilla Wax) ∙ _ Е-903 ∙ Воск карнаубский (Camauba Wax) ∙ _ Е-904 ∙ Шеллак (Shellac) ∙ _ Е-905а ∙ Вазелиновое масло “пищевое” (Mineral Oil, Food Grade) ∙ _ Е-905Ь ∙ Вазелин /Petrolatum (Petroleum Jelly)/ ∙ _ Е-905с ∙ Парафин (Petroleum Wax) ∙ _ Е-906 (!) ∙ Бензойная смола (Benzoin Gum) ∙ _ Е-908 (!) ∙ Воск рисовых отрубей (Rice bran Wax) ∙ _ Е-909 (!) ∙ Спермацетовый воск (Spermaceti Wax) ∙ _ Е-910 (!) ∙ Восковые эфиры (Wax Esters) ∙ _ Е-911 (!) ∙ Жирных кислот метиловые эфиры (Methyl Esters of Fatty Acids) ∙ _ E-912# (!?) ∙ Эфиры монтаниновой кислоты (Montanic Acid Esters) ∙ _ Е-913 (!) ∙ Ланолин (Lanolin) ∙ _ E-914# (!?) ∙ Окисленный полиэтиленовый воск (Oxidized Polyethy ene Wax) ∙ _ Е-916 (!) ∙ Кальция йодат (Calcium lodate) ∙ _ Е-917 (!) ∙ Калия йодат (Potassium lodate) ∙ _ Е-918 (!) ∙ Оксиды азота (Nitrogen Oxides) ∙ _ Е-919 (!) ∙ Нитрозил хлорид (Nitrosyl Chloride) ∙ _ Е-920 ∙ L-цистеин (L–Cysteuie) ∙ _ Е-922 (!) ∙ Персульфат калия (Potassium Persulphate) ∙ _ Е-923 (!) ∙ Персульфат аммония (Ammonium Persulphate) ∙ _ Е-924а (!) ∙? ∙ Е-924Ь (!) ∙ Бромат кальция (Calcium Bromate) ∙ Е-925 (!) ∙ Хлор (Chlorine) ∙ _ Е-926 (!) ∙ Лиоксид хлора (Chlorine Dioxide) ∙ _ Е-927Ь ∙ Карбамид (Carbamide) ∙ _ Е-928 ∙ Пероксид бензоила (Benzoyl Peroxide) ∙ _ Е-929 (!) ∙ Перекись апетона (Acetone Peroxide) ∙ _ Е-930 ∙ Пероксид кальция (Calcium Peroxide) ∙ _ E-938# (!?) ∙ Аргон (Argon) ∙ _ E-939# (!?) ∙ Г елий (Helium) ∙ _ Е-940 ∙ Дихлордифторметан, хладон-12 (Dichlorodifluoromethane) ∙ _ Е-941 ∙ Азот (Nitrogen) ∙ _ Е-942 (!) ∙ Диазомонооксид (Nitrous Oxide) ∙ _ Е-943а (!) ∙ Бутан (Butane) ∙ _ Е-943Ь (!) ∙ Изобутан (Isobutane) ∙ _ Е-944 (!) ∙ Пропан (Propane) ∙ _ Е-945 (!) ∙ Хлопентафторэтан (Chloropentafluoroethane) ∙ _ Е-946 (!) ∙ Октафтор циклобутан (Octafluorocyclobutane) ∙ _ E-948# (!?) ∙ Кислород (Oxygen) ∙ _ Е-950 ∙ Ацесульфам калия (Acesulfame Potassium) ∙ _ Е-951 ∙ Аспартам (Aspartame) ∙ _ Е-952 ∙ Цикламовая кислота и ее натриевые, калиевые и кальциевые соли (Cyclamic Acid and its Na and Ca Salts) ∙ _ Е-953 ∙ Изомальтит (Isvmaltitol) ∙ _ Е-954 ∙ Сахарин и его натриевые, калиевые и кальциевые соли (Saccharin and its Na, К and Ca Salts) ∙ Е-957 (!) ∙ Тауматин (Thaumatin) ∙ _ E-959 (!) ∙ Неогесперидин Дигидрохалкон (Neohesperidine Dihydrochalcone) ∙ _ Е-958 ∙ Глицирризин (Glycyrrhizin) ∙ _ Е-965 ∙ Мапътит: (i) — Мальтит, (ii) — мальтитный сироп /Maltitol: (i) - Maltitol (ii) - Maltitol Syrup/ ∙ _ Е-966 ∙ Лактит (Lactitol) ∙ _ Е-967 ∙ Ксилит (Xylitol) ∙ _ Е-999 ∙ Экстракт Квиллайи (Quillaia extract) ∙ _ Е-1000 (!) ∙ Холевая кислота (Cholic Acid) ∙ _ Е-1001 (!) ∙ Соли и эфиры холина (Clwline Salts and Esters) ∙ _ Е-1101 ∙ Протеазы: (i) протеаза (ii) папаин (iii) бромелайн (iv) фицин /Proteoses (i) Protease (ii) Papain (iii) Bromelain (iv) Ficin/ ∙ _ Е-1102 ∙ Глкжозооксвдаза (Glucose Oxidase) ∙ _ Е-1103 ∙ Инвертазы (Invertases) ∙ _ Е-1104 ∙ Липазы (Lipases) ∙ _ Е-1105 (!) ∙ Лизоцим (Lysozyme) ∙ Е-1200 ∙ Полидекстроза (Polydextrose) ∙ _ Е-1201 ∙ Поливинилпирролидон (Polyvinylpyrrolidone) ∙ _ Е-1202 ∙ Поливинилполипирролидон (Polyvinylpolypytrolidone) ∙ _ E-1404# (!?) ∙ Окисленный крахмал (Oxidized Starch) ∙ _ E-1410# (!?) ∙ Монокрахмалфосфат (Monostarch Phosphate) ∙ _ E-1412# (!?) ∙ Дикрахмалфосфат (Distarch Phosphate) ∙ _ E-1413# (!?) ∙ Фосфатированный дикрах малфосфат (Phosphated Distarch Phosphate) ∙ _ E-1414# (!?) ∙ Ацетилированный дикрахмалфосфат (Acetylated Distarch Phosphate) ∙ _ E-1420# (!?) ∙ Ацетилированный крахмал (Acetylated Starch) ∙ _ E-1422# (!?) ∙ Ацетилдикрахмаладипат (Acetylated Distarch Adipate) ∙ _ E-1440# (!?) ∙ Гидроксипропилкрахмал (Hydroxy propyl Starch) ∙ _ E-1442# (!?) ∙ Гидроксипропилдикрахмалфосфат (Hydroxy propyl Distarch Phosphate) ∙ _ E-1450# (!?) ∙ Крахмалнатрийоктенилсукцинат Starch (Sodium Octenyl Succinate) ∙ _ E-1451# (!?) ∙ Ацетилированный окисленный крахмал (Acetylated Oxidised Starch) ∙ E-1503 (!) ∙ Касторовое масло (Castor Oil) ∙ _ E-1505 ∙ Триэтилцитрат (Trietlryl Citrate) ∙ _ E-1518 ∙ Глицерил триацетат (триацетин) /Glyceryl Triacetate (triacetin)/ ∙ _ E-1520 ∙ Пропиленгликоль (Propylene Glycol) ∙ _ E-1521 (!) ∙ Полиэтиленгликоль (Polyetylene Glycol) ∙ _ Перечень Е-добавок составлен на основании следующих материалов: [1] Пищевые добавки. Дополнения к “Медико-биологическим требованиям и санитарным нормам качества продовольственного сырья и пищевых продуктов” (№ 5061-89), — М., Государственный комитет санитарно-эпидемиологического надзора Российской Федерации, 1994 г. [2] Постановление Главного государственного санитарного врача РФ от 18.01.2005 г. [3] Food Additives in the European Union, — The Department of Food Science and Technology The University of Reading, UK Food Law (compiled by Dr David Jukes). Дополнительные сведения о токсичности некоторых упоминаемых в данной брошюре веществ можно найти в книгах, имеющихся в Российской Государственной библиотеке (бывшей Государственной библиотеке им. В.И. Ленина): 1. 2. Оценка некоторых пищевых добавок и контаминантов. 41 доклад объединенных экспертов ФАО/ВОЗ по пищевым добавкам, Женева, — М: “Медицина”, 1994 г. — 72 с. 3. Оценка некоторых пищевых добавок и контаминантов. 37 докладов объединенных экспертов ФАО/ВОЗ по пищевым добавкам, Женева, — М: “Медицина”, 1974 г. — 48 с. 4. Химия пищевых добавок: Тезисы докладов Всесоюзной конференции г. Черновцы, — Киев: НПО “Пищевые добавки”, 1989 г. — 256 с. 5. Принципы оценки безопасности пищевых добавок и контаминантов в продуктах питания, — М.: “Медицина”, 1991 г. — 158 с. 6. * * * В РФ официально запрещены 7 кодифицированных пищевых добавок: ∙ Е121 — цитрусовый красный; ∙ Е123 — амарант; ∙ Е240 — формальдегид; ∙ Е216 — пропиловый эфир парогидроксибензойной кислоты; ∙ Е217 — натриевая соль пропилового эфира парогидроксибензойной кислоты; — улучшители муки и хлеба: ∙ Е924а — бромат калия; ∙ Е9246 — бромат кальция. Тяжелые металлы, относящиеся к токсичным элементам: РЬ — плюмбум (свинец). Cd — кадмий. As — арсеникум (мышьяк). Sb — сурьма, Hg — ртуть, Zn — цинк, Fe — железо, Аl — алюминий, Ва — барий, Сu — медь, Сr — хром, Те — теллур, Be — бериллий. Примечание. Тяжелые металлы не выводятся из организма. РАМН недавно «сообщила, что серебро и золото (не говоря о
ЮМОР
Язык ученых Исследователи используют, как правило, стандартные фразы, чей формальный характер является результатом взаимного списывания. Данные фразы одновременно показывают, насколько богат словарный запас ученого, чтобы скрыть собственную несостоятельность, неумение и незнание. Интернет-журналу "Домашняя лаборатория" требуются редакторы разделов. Если вы увлекаетесь какой-нибудь идеей или темой, то можете попробовать себя в качестве редактора. В вашу задачу будет входить формирование материала по этой теме, то есть поиск его в Интернет или литературе, и компоновка в соответствии с какой-либо идеей. Можно добавлять свои комментарии и статьи. Каких-либо рамок и ограничений нет, требование одно — чтобы было интересно. Обычно по таким разделам сообщается, кто его ведет. Окончательное оформление остается за редакцией. Все предложения направлять на адрес редакции: [email protected] В связи с началом очередного учебного года, библиотека Homelab http://homelab.atspace.com/library.html временно осуществляет дополнительную Запись для читателей журнала «Домашняя лаборатория». Для записи необходимо сообщить ваш ник на форуме библиотеки по e-mail адресу редакции журнала, до выхода следующего номера журнала. Требуется фамильярность с FTP. Подробности — на странице библиотеки. Удачи! НА ОБЛОЖКЕ Структурная модель пенициллина, открытого первым в ряду антибиотиков, изменивших жизнь человечества. Один этот антибиотик спас жизнь миллионов людей. Об Александре Флеминге, открываем пенициллин, читайте в разделе «Биографии». notes
Примечания
1 Книга вышла достаточно давно, в 2002 году, поэтому редакция искренне считает, что взгляды автора устарели и уже принадлежат истории. На всякий случай — редакция их не разделяет.
2 Есть некоторые сдвиги в этом направлении.
3 Есть некоторые сдвиги в этом направлении.
4 Сильно преувеличенное Заявление. Качественных копий книг, сделанных фотоаппаратом (очевидно, имеется в виду цифровая камера) практически нет.
5 А еще и секретные чертежи.
6 Но для OCR эта программа прекрасно подходит.
7 Смотрите, например, первый номер журнала «Домашняя лаборатория» за 2007 год.
8 На рисунке, для простоты восприятия, нарисовано лишь одно зеркало, тогда как у типового сканера их не менее трех-четырех.
9 Победой при Баннокберне (1314) закончились Войны за независимость, которые вели шотландцы против английского господства. —
10 Торфяники, поросшие вереском (англ.)
11 Директор школы (англ.).
12 Из рассказов
13 Двухколесный экипаж с местом для кучера сзади (англ.).
14 Трансвааль и Оранжевая республика.
15 Президент Трансвааля.
16 Столица Трансвааля.
17 Член Королевского хирургического колледжа (англ.)
18 Солдатами территориальных войск назывались волонтеры, проходившие военную подготовку в таких полках, как, например, Лондонский шотландский полк. Поэтому среди них, скорее всего можно было найти хороших стрелков. —
19
20 Акромегалия — чрезмерный рост отдельных частей тела, особенно конечностей и лицевого скелета.
21
22 Видный ученый из Рокфеллеровского института. —
23 Отношение опсонической способности крови данного индивидуума к опсонической способности крови здорового индивидуума, принятой за единицу. —
24 Я должен взять у вас кровь для анализа (англ.).
25 Сэр Почти Прав (англ.).
26 Целительная сила природы (лат.)
27
28 Игра, при которой сбивают пробки с бутылок.
29 Новое название Бактериологического отделения. —
30 Бог позаботился, чтобы эта страна оставалась неизвестной, пока народ ее не будет готов. Тогда он избрал меня своим посланником, и я нашел эту страну, и она стала общим достоянием (
31 Полукустарниковое растение, содержащее эфирное масло.
32 Вы опрыскаете меня иссопом, и я очищусь (англ.).
33 Когда через несколько лет лорд Уэбб-Джонсон, президент Королевского хирургического колледжа, Золотая медаль которого была только что присуждена Флемингу, передал ему записи Листера, Флеминг сказал: «Очень жаль, что опыты, проведенные в ноябре 1871 года, не были доведены до конца. Листер уже тогда набрел на мысль о пенициллине, но он выращивал либо неудачные плесени, либо неудачные бактерии, а возможно, и то и другое. Если бы ему улыбнулась судьба, вся история медицины изменилась бы и Листер при жизни увидел бы то, что он всегда искал: нетоксичный антисептик. Со времен Листера и Пастера ученые пытались убить один микроб другим. Идея была правильна, но для ее осуществления пришлось ждать дня, когда фортуна решила, что споры плесени заразят одну из моих культур, а потом, несколько лет спустя, настал и другой день, когда химики занялись веществом, выделяемым этой плесенью, и дали нам чистый пенициллин. Листер, несомненно, был бы счастлив, если бы такая удача выпала на его долю».
34 Сыр того же типа, что и рокфор. Это сравнение, наверное, не понравится ни сыроварам рокфора, ни стильтона. —
35 Группа Института тропических заболеваний и гигиены напечатала описание своей работы сперва в журнале «Общество связи химии и промышленности», а затем в журнале «Биохимия» в 1932 г. —
36 В 1938 году гитлеровцы вторглись в Австрию.
37 Элементы, чья природа была в те времена неизвестна, вызывающие распад некоторых химических составных частей основы конъюнктивы. Самый важный из этих факторов был открыт в 1939 году Чэйном. —
38 Оксфордской единицей пенициллина называют минимальное количество этого вещества, которое, будучи растворено в кубическом сантиметре воды, может задержать развитие золотистого стафилококка с образованием стерильных пятен диаметром 2–5 сантиметров. —
39 Кукурузный экстракт. —
40 Кто заслужил пальму, тот ею и удостоен (лат.).
41 Paul de Kruif, Life among the doctors, Harcouit Brace, 1949. (
42 Paul de Kruif.
43 Из стихотворения
44 Самолет-снаряд ФАУ-1.
45
Ты добился крупнейшего успеха,
Возможного на каком-либо поприще.
Стал членом Королевского общества.
С полным и явным признанием.
Вознесся ты на пьедестал высокий,
Без страха быть низвергнутым.
Ты восхвален в палате лордов,
В палате общин слава о тебе идет.
Ты с легкостью завоевал корону медицины.
Ты видишь, как бегут на счет твой в банк доллары.
Писатели воздают тебе хвалу,
И поэты воспевают твой подвиг.
Король наш сильный, добрый
Пожаловал тебя титулом баронета.
Ты еще не стар, но знаешь,
Что прожил не напрасно.
И знаешь — до тебя еще никто не сделал большего,
Чтоб облегчить страданья.
Строить воздушные замки —
Свойственно мечтательной юности.
И не тебе объяснять,
Что истина — дороже всего (англ.).
46 Дом милосердия.
47 День присуждения университетских степеней (англ.).
48 Да (англ.)
49 Год завоевания Англии норманнами.
50 Английский национальный гимн.
51
52
53 Мы говорили друг с другом и друг о друге, хотя оба молчали.
54 Ректорская лаконичность (лат.).
55 Большая стерилизующая терапия (лат.).
56
57 Закрытая средняя школа для мальчиков.
58 Мэр в Шотландии (англ.)
59 Под контаминантами (от англ.
60 Ферменты — это белковые макромолекулы, которые выполняют роль катализаторов в живых организмах.
61 Дополнительно, смотрите номера 2, 5 и 7 журнала за этот год.
62 Можно обернуть банку при разливе среды чем-нибудь теплоизолирующим, так как сильно охлажденная среда любит схватываться в самый неподходящий момент.
63 Я Можно сухие чашки Петри заворачивать в фольгу и прожаривать в духовке часа полтора. Вынуть их духовки после полного остывания. Преимущество — сушить не надо. Но следует заметить, что сухая стерилизация менее надежна.
64 Такие пробирки необходимо без промедления стерилизовать в скороварке. Можно залить раствором «Белизны» перед выбросом содержимого и отмывкой пробирки для того, чтобы зараза не накапливалась в помещении.
65 Полиэтиленовый пакет с герметичной застежкой.
66 Пожалуй это самое простое средство достижения такой температуры для проведения некоторых экзотических высокотемпературных реакций.
67 В настоящее время как для сварки, так и для резки металлов, выпускаются и продаются специальные термитные «карандаши» (сварочные и режущие карандаши Лебедева).
68 Наверное это не очень хорошая идея — зажигать термит в помещении.
69 Запомните! Вам нужен именно железный, а не медный купорос. Последний продаётся там же, обладает красивым синим цветом и для нас не пригоден. Железный купорос перед использованием просушивать не надо!
70 Внимание! Сода каустическая и сода пищевая вам не подойдут.
71 Сера. В магазине "Садовод" или на рынке среди удобрений. Не берите "Серу коллоидную" — в ней слишком много примесей. Мелкий желтый порошок, не растворяется в воде, плохо горит с резким удушливым запахом. Иногда встречается сера в комках — она подойдет.
72 Селитры натриевая и калиевая. Ищите среди удобрений, лучше всего весной — в другое время можете не найти. Белый порошок, хорошо растворяющийся в воде.
73 Горит и неплохо. При аккуратном поджигании горят даже некоторые 40 градусные водки.
74 Правда, это умение даровано не всем программам, вот Roxio Easy CD Creator оно даровано, а, например, Stomp Record Now! — нет.
75 Уменьшение свободного пространства объясняется тем, что каждая открываемая сессия требует для своего размещения определенного места, однако если удалению одних файлов сопутствует запись других, то открывать новую сессию все равно приходится и в этом случае, накладные расходы на удаление отсутствуют.