Классы Group и GroupCollection

Коллекция GroupCollection возвращается при вызове свойства Group объекта Match. Имея эту коллекцию, можно добраться до каждого объекта Group, в нее входящего. Класс Group является наследником класса Capture и, одновременно, родителем класса Match. От своего родителя он наследует свойства Index, Length и Value, которые и передает своему потомку.

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

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

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

Классы Capture и CaptureCollection

Коллекция CaptureCollection возвращается при вызове свойства Captures объектов класса Group и Match. Класс Match наследует это свойство у своего родителя - класса Group. Каждый объект Capture, входящий в коллекцию, характеризует соответствие, захваченное в процессе поиска, - соответствующую подстроку. Но поскольку свойства объекта Capture передаются по наследству его потомкам, то можно избежать непосредственной работы с объектами Capture. По крайней мере, в моих примерах не встретится работа с этим объектом, хотя "за кулисами" он непременно присутствует.

Перечисление RegexOptions

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

Класс RegexCompilationInfo

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

15. Лекция: Регулярные выражения

15.3 Классы Group и GroupCollection - student2.ru

Примеры работы с регулярными выражениями

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

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Образец={1}\ tНайдено={2}", str,strpat,found); } return(found);}//FindMatch

В качестве входных аргументов функции передается строка str, в которой ищется вхождение, и строка strpat, задающая образец - регулярное выражение. Функция возвращает найденную в результате поиска подстроку. Если соответствия нет, то возвращается пустая строка. Функция начинает свою работу с создания объекта pat класса Regex, конструктору которого передается образец поиска. Затем вызывается метод Match этого объекта, создающий объект match класса Match. Далее анализируются свойства этого объекта. Если соответствие обнаружено, то найденная подстрока возвращается в качестве результата, а соответствующая информация выводится на печать. (Чтобы спокойно работать с классами регулярных выражений, я не забыл добавить в начало проекта предложение: using System.Text.RegularExpressions.)

Поскольку запись регулярных выражений - вещь, привычная не для всех программистов, я приведу достаточно много примеров:



public void TestSinglePat(){ //поиск по образцу первого вхождения string str,strpat,found; Console.WriteLine("Поиск по образцу"); //образец задает подстроку, начинающуюся с символа a, //далее идут буквы или цифры. str ="start"; strpat =@"a\w+"; found = FindMatch(str,strpat); str ="fab77cd efg"; found = FindMatch(str,strpat); //образец задает подстроку,начинающуюся с символа a, //заканчивающуюся f с возможными символами b и d в середине strpat = "a(b|d)*f"; str = "fabadddbdf"; found = FindMatch(str,strpat); //диапазоны и escape-символы strpat = "[X-Z]+"; str = "aXYb"; found = FindMatch(str,strpat); strpat = @"\u0058Y\x5A"; str = "aXYZb"; found = FindMatch(str,strpat);}//TestSinglePat

Некоторые комментарии к этой процедуре.

Регулярные выражения задаются @-константами, описанными в лекции 14. Здесь они как нельзя кстати.

В первом образце используется последовательность символов \w+, обозначающая, как следует из таблицы 15.1, непустую последовательность латиницы и цифр. В совокупности образец задает подстроку, начинающуюся символом a, за которым следуют буквы или цифры (хотя бы одна). Этот образец применяется к двум различным строкам.

В следующем образце используется символ * для обозначения итерации. В целом регулярное выражение задает строки, начинающиеся с символа a и заканчивающиеся символом f, между которыми находится возможно пустая последовательность символов из b и d.

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

Взгляните на результаты, полученные при работе этой процедуры.

Классы Group и GroupCollection - student2.ru
Рис. 15.1. Регулярные выражения. Поиск по образцу

15. Лекция: Регулярные выражения

15.4 Классы Group и GroupCollection - student2.ru

Пример "чет и нечет"

Не всякий класс языков можно описать с помощью регулярных выражений. И даже тогда, когда такая возможность есть, могут потребоваться определенные усилия для корректной записи соответствующего регулярного выражения. Рассмотрим, например, язык L1 в алфавите T={0,1}, которому принадлежат пустое слово и слова, содержащие четное число нулей и четное число единиц. В качестве другого примера рассмотрим язык L2, отличающийся от первого тем, что в нем число единиц нечетно. Оба языка можно задать регулярными выражениями, но корректная запись непроста и требует определенного навыка. Давайте запишем регулярные выражения, определяющие эти языки, и покажем, что C# справляется с проблемой их распознавания. Вот регулярное выражение, описывающее первый язык:

(00|11)*((01|10)(00|11)*(01|10)(00|11)*)*

Дадим содержательное описание этого языка. Слова языка представляют возможно пустую последовательность из пар одинаковых символов. Далее может идти последовательность, начинающаяся и заканчивающаяся парами различающихся символов, между которыми может стоять произвольное число пар одинаковых символов. Такая группа может повторяться многократно. Регулярное выражение короче и точнее передает описываемую структуру слов языка L1.

Язык L2 описать теперь совсем просто. Его слова представляют собой единицу, окаймленную словами языка L1.

Прежде чем перейти к примеру распознавания слов языков L1 и L2, приведу процедуру FindMatches, позволяющую найти все вхождения образца в заданный текст:

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

Входные аргументы у процедуры те же, что и у функции FindMatch, ищущей первое вхождение. Я не стал задавать выходных аргументов процедуры, ограничившись тем, что все результаты непосредственно выводятся на печать в самой процедуре. Выполнение процедуры, так же, как и в FindMatch, начинается с создания объекта pat класса Regex, конструктору которого передается регулярное выражение. Замечу, что класс Regex, так же, как и класс String, относится к неизменяемым (immutable) классам, поэтому для каждого нового образца нужно создавать новый объект pat.

В отличие от FindMatch, объект pat вызывает метод Matches, который определяет все вхождения подстрок, удовлетворяющих образцу, в заданный текст. Результатом выполнения метода Matches является автоматически создаваемый объект класса MatchCollection, хранящий коллекцию объектов уже известного нам класса Match, каждый из которых задает очередное вхождение. В процедуре используются свойства коллекции и ее элементов для получения в цикле по элементам коллекции нужных свойств - индекса очередного вхождения подстроки в строку, ее длины и значения.

Вот процедура, в которой многократно вызывается 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); //четное число нулей и нечетное единиц string strodd = strpat + "1" + strpat; FindMatches(str, strodd);}//TestMultiPat

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

Классы Group и GroupCollection - student2.ru
Рис. 15.2. Регулярные выражения. Пример "чет и нечет"

Пример "око и рококо"

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

Console.WriteLine("око и рококо");strpat="око"; str = "рококо";FindMatches(str, strpat);strpat="123";str= "0123451236123781239";FindMatches(str, strpat);

На рис. 15.3 показаны результаты поисков.

Классы Group и GroupCollection - student2.ru
Рис. 15.3. Регулярные выражения. Пример "око и рококо"

Пример "кок и кук"

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

Console.WriteLine("кок и кук");strpat="(т|к).(т|к)";str="кок тот кук тут как кот";FindMatches(str, strpat);

Вот результаты работы этого фрагмента кода.

Классы Group и GroupCollection - student2.ru
Рис. 15.4. Регулярные выражения. Пример "кок и кук"

15. Лекция: Регулярные выражения

15.5 Классы Group и GroupCollection - student2.ru

Пример "обратные ссылки"

В этом примере рассматривается ранее упоминавшаяся, но не описанная возможность задания в регулярном выражении обратных ссылок. Можно ли описать с помощью регулярных выражений язык, в котором встречаются две подряд идущие одинаковые подстроки? Ответ на это вопрос отрицательный, поскольку грамматика такого языка должна быть контекстно-зависимой, и нужна память, чтобы хранить уже распознанные части строки. Аппарат регулярных выражений, предоставляемый классами пространства RegularExpression, тем не менее, позволяет решить эту задачу. Причина в том, что расширение стандартных регулярных выражений в Net Framework является не только синтаксическим. Содержательные расширения связаны с введением понятия группы, которой отводится память и дается имя. Это и дает возможность ссылаться на уже созданные группы, что и делает грамматику языка контекстно-зависимой. Ссылка на ранее полученную группу называется обратной ссылкой. Признаком обратной ссылки является пара символов "\k", после которой идет имя группы. Приведу пример:

Console.WriteLine("Ссылка назад - второе вхождение слова");strpat = @"\s(?<word>\w+)\s\k'word'";str = "I know know that, You know that!";FindMatches(str, strpat);

Рассмотрим более подробно регулярное выражение, заданное строкой strpat. В группе, заданной скобочным выражением, после знака вопроса идет имя группы "word", взятое в угловые скобки. После имени группы идет шаблон, описывающий данную группу, в нашем примере шаблон задается произвольным идентификатором "\w+"(? так? кто кого задает?). В дальнейшем описании шаблона задается ссылка на группу с именем "word". Здесь имя группы заключено в одинарные кавычки. Поиск успешно справился с поставленной задачей, подтверждением чему являются результаты работы этого фрагмента кода.

Классы Group и GroupCollection - student2.ru
Рис. 15.5. Регулярные выражения. Пример "обратные ссылки"

Пример "Дом Джека"

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

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

Регулярное выражение, заданное строкой strpat, определяет множество разделителей. Заметьте, в качестве разделителя задан пробел, повторенный сколь угодно много раз, либо пара символов - запятая и пробел. Разделители задаются регулярными выражениями. Метод Split применяется к объекту pat класса Regex. В качестве аргумента методу передается текст, подлежащий расщеплению. Вот как выглядит массив слов после применения метода Split.

Классы Group и GroupCollection - student2.ru
Рис. 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(?<tel>\((\d|-)*\)\s(\d|-)+)\s"; string pat2= @"Адрес:\s(?<addr>[0-9А-Яа-я \-\,\.]+)\s"; string pat3 =@"e-mail:\s(?<em>[a-zA-Z.@]+)\s"; string compat = pat1+pat2+pat3; string tel="", addr = "", em = "";

Строки s4 и s5 представляют строку разбираемого документа. Их две, для того чтобы можно было проводить эксперименты, когда атрибуты в документе представлены в произвольном порядке. Каждая из строк pat1, pat2, pat3 задает одну именованную группу в регулярном выражении, имена групп - tel, Адрес, e-mail - даются в соответствии со смыслом атрибутов. Сами шаблоны подробно описывать не буду - сделаю лишь одно замечание. Например, шаблон телефона исходит из того, что номеру предшествует код, заключенный в круглые скобки. Поскольку сами скобки играют особую роль, то для задания скобки как символа используется пара - "\(". Это же касается и многих других символов, используемых в шаблонах, - точки, дефиса и т.п. Строка compat представляет составное регулярное выражение, содержащее все три группы. Строки tel, addr и em нам понадобятся для размещения в них результатов разбора. Применим вначале к строкам s4 и s5 каждый из шаблонов pat1, pat2, pat3 в отдельности и выделим соответствующий атрибут из строки. Вот код, выполняющий эти операции:

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);

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

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

И эта задача успешно решается. Взгляните на результаты разбора текста.

Классы Group и GroupCollection - student2.ru
Рис. 15.7. Регулярные выражения. Пример "Атрибуты"

На этом и завершим рассмотрение регулярных выражений а также лекции, посвященные работе с текстами в C#.

16. Лекция: Классы

16.1 Классы Group и GroupCollection - student2.ru

Две роли класса в ООП. Синтаксис описания класса. Поля и методы класса. Конструкторы и деструкторы. Статические поля и методы. Статические конструкторы. Поля только для чтения. Закрытые поля. Стратегии доступа к полям класса. Процедуры свойства. Индексаторы. Примеры.

Классы Group и GroupCollection - student2.ru

Классы Group и GroupCollection - student2.ru

Классы Group и GroupCollection - student2.ru

Классы и ООП

Объектно-ориентированное программирование и проектирование построено на классах. Любую программную систему, выстроенную в объектном стиле, можно рассматривать как совокупность классов, возможно, объединенных в проекты, пространства имен, решения, как это делается при программировании в Visual Studio .Net.

Две роли классов

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

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

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

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

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

Синтаксис класса

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

[атрибуты][модификаторы]class имя_класса[:список_родителей]{тело_класса}

Атрибутам будет посвящена отдельная лекция. Возможными модификаторами в объявлении класса могут быть модификаторы new, abstract, sealed, о которых подробно будет говориться при рассмотрении наследования, и четыре модификатора доступа, два из которых - private и protected - могут быть заданы только для вложенных классов. Обычно класс имеет атрибут доступа public, являющийся значением по умолчанию. Так что в простых случаях объявление класса выглядит так:

public class Rational {тело_класса}

В теле класса могут быть объявлены:

  • константы;
  • поля;
  • конструкторы и деструкторы;
  • методы;
  • события;
  • делегаты;
  • классы (структуры, интерфейсы, перечисления).

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

Поля класса

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

Доступ к полям

Каждое поле имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal. Атрибутом доступа по умолчанию является атрибут private. Независимо от значения атрибута доступа, все поля доступны для всех методов класса. Они являются для методов класса глобальной информацией, с которой работают все методы, извлекая из полей нужные им данные и изменяя их значения в ходе работы. Если поля доступны только для методов класса, то они имеют атрибут доступа private, который можно опускать. Такие поля считаются закрытыми, но часто желательно, чтобы некоторые из них были доступны в более широком контексте. Если некоторые поля класса A должны быть доступны для методов класса B, являющегося потомком класса A, то эти поля следует снабдить атрибутом protected. Такие поля называются защищенными. Если некоторые поля должны быть доступны для методов классов B1, B2 и так далее, дружественных по отношению к классу A, то эти поля следует снабдить атрибутом internal, а все дружественные классы B поместить в один проект (assembly). Такие поля называются дружественными. Наконец, если некоторые поля должны быть доступны для методов любого класса B, которому доступен сам класс A, то эти поля следует снабдить атрибутом public. Такие поля называются общедоступными или открытыми.

16. Лекция: Классы

16.2 Классы Group и GroupCollection - student2.ru Классы Group и GroupCollection - student2.ru

Методы класса

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

Доступ к методам

Каждый метод имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal. Атрибутом доступа по умолчанию является атрибут private. Независимо от значения атрибута доступа, все методы доступны для вызова при выполнении метода класса. Если методы имеют атрибут доступа private, возможно, опущенный, то тогда они доступны только для вызова и только внутри методов самого класса. Такие методы считаются закрытыми. Понятно, что класс, у которого все методы закрыты, абсурден, поскольку никто не смог бы вызвать ни один из его методов. Как правило, у класса есть открытые методы, задающие интерфейс класса, и закрытые методы. Интерфейс - это лицо класса и именно он определяет, чем класс интересен своим клиентам, что он может делать, какие сервисы предоставляет клиентам. Закрытые методы составляют важную часть класса, позволяя клиентам не вникать во многие детали реализации. Эти методы клиентам класса недоступны, они о них могут ничего не знать, и, самое главное, изменения в закрытых методах никак не отражаются на клиентах класса при условии корректной работы открытых методов.

Если некоторые методы класса A должны быть доступны для вызовов в методах класса B, являющегося потомком класса A, то такие методы следует снабдить атрибутом protected. Если некоторые методы должны быть доступны только для методов классов B1, B2 и так далее, дружественных по отношению к классу A, то такие методы следует снабдить атрибутом internal, а все дружественные классы B поместить в один проект. Наконец, если некоторые методы должны быть доступны для методов любого класса B, которому доступен сам класс A, то такие методы снабжаются атрибутом public.

Методы-свойства

Методы, называемые свойствами (Properties), представляют специальную синтаксическую конструкцию, предназначенную для обеспечения эффективной работы со свойствами. При работе со свойствами объекта (полями) часто нужно решить, какой модификатор доступа использовать, чтобы реализовать нужную стратегию доступа к полю класса. Перечислю пять наиболее употребительных стратегий:

  • чтение, запись (Read, Write);
  • чтение, запись при первом обращении (Read, Write-once);
  • только чтение (Read-only);
  • только запись (Write-only);
  • ни чтения, ни записи (Not Read, Not Write).

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

Приведу вначале пример, а потом уточню синтаксис этих методов. Рассмотрим класс Person, у которого пять полей: fam, status, salary, age, health, характеризующих соответственно фамилию, статус, зарплату, возраст и здоровье персоны. Для каждого из этих полей может быть разумной своя стратегия доступа. Возраст доступен для чтения и записи, фамилию можно задать только один раз, статус можно только читать, зарплата недоступна для чтения, а здоровье закрыто для доступа и только специальные методы класса могут сообщать некоторую информацию о здоровье персоны. Вот как на C# можно обеспечить эти стратегии доступа к закрытым полям класса:

public class Person{ //поля (все закрыты) string fam="", status="", health=""; int age=0, salary=0; //методы - свойства /// <summary> ///стратегия: Read,Write-once (Чтение, запись при ///первом обращении) /// </summary> public string Fam { set {if (fam == "") fam = value;} get {return(fam);} } /// <summary> ///стратегия: Read-only(Только чтение) /// </summary> public string Status { get {return(status);} } /// <summary> ///стратегия: Read,Write (Чтение, запись) /// </summary> 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);} } /// <summary> ///стратегия: Write-only (Только запись) /// </summary> public int Salary { set {salary = value;} }}

Рассмотрим теперь общий синтаксис методов-свойств. Пусть name - это закрытое свойство. Тогда для него можно определить открытый метод-свойство (функцию), возвращающую тот же тип, что и поле name. Имя метода обычно близко к имени поля (например, Name). Тело свойства содержит два метода - get и set, один из которых может быть опущен. Метод get возвращает значение закрытого поля, метод set - устанавливает значение, используя передаваемое ему значение в момент вызова, хранящееся в служебной переменной со стандартным именем value. Поскольку get и set - это обычные процедуры языка, то программно можно реализовать сколь угодно сложные стратегии доступа. В нашем примере фамилия меняется, только если ее значение равно пустой строке и это означает, что фамилия персоны ни разу еще не задавалась. Статус персоны пересчитывается автоматически при всяком изменении возраста, явно изменять его нельзя. Вот пример, показывающий, как некоторый клиент создает и работает с полями персоны:

public void TestPersonProps(){ Person pers1 = new Person(); pers1.Fam = "Петров"; pers1.Age = 21; pers1.Salary = 1000; Console.WriteLine ("Фам={0}, возраст={1}, статус={2}", pers1.Fam, pers1.Age, pers1.Status); pers1.Fam = "Иванов"; pers1.Age += 1; Console.WriteLine ("Фам={0}, возраст={1}, статус={2}", pers1.Fam, pers1.Age, pers1.Status);}//TestPersonProps

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

Классы Group и GroupCollection - student2.ru
Рис. 16.1. Методы-свойства и стратегии доступа к полям

16. Лекция: Классы

16.3 Классы Group и GroupCollection - student2.ru Классы Group и GroupCollection - student2.ru

Индексаторы

Свойства являются частным случаем метода класса с особым синтаксисом. Еще одним частным случаем является индексатор. Метод-индексатор является обобщением метода-свойства. Он обеспечивает доступ к закрытому полю, представляющему массив. Объекты класса индексируются по этому полю.

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

Добавим в класс Person свойство children, задающее детей персоны, сделаем это свойство закрытым, а доступ к нему обеспечит индексатор:

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++;} }}

Имя у индексатора - this, в квадратных скобках в заголовке перечисляются индексы. В методах get и set, обеспечивающих доступ к массиву children, по которому ведется индексирование, анализируется корректность задания индекса. Закрытое поле count_children, хранящее текущее число детей, доступно только для чтения благодаря добавлению соответствующего метода-свойства. Запись в это поле происходит в методе set индексатора, когда к массиву children добавляется новый элемент.

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

public void TestPersonChildren(){ Person pers1 = new Person(), pers2 = new Person(); pers1.Fam = "Петров"; pers1.Age = 42; pers1.Salary = 10000; pers1[pers1.Count_children] = pers2; pers2.Fam ="Петров"; pers2.Age = 21; pers2.Salary = 1000; Person pers3= new Person("Петрова"); pers1[pers1.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, pers1[0].Age, pers1[0].Status); Console.WriteLine ("Дочь={0}, возраст={1}, статус={2}", pers1[1].Fam, pers1[1].Age, pers1[1].Status);}

Заметьте, индексатор создает из объекта как бы массив объектов, индексированный по соответствующему полю, в данном случае по полю children. На рис. 16.2 показаны результаты вывода.

Классы Group и GroupCollection - student2.ru
Рис. 16.2. Работа с индексатором класса

Операции

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

Наши рекомендации