Тема 6. Основы программирования на языке C# 6.9 Строки и регулярные выражения • Построение строк • Форматирующие выражения • Регулярные выражения С самого начала в данном курсе постоянно использовались строки, и вам уже известно, что существует жесткое отображение ключевого слова C# string на базовый класс .NET System.String. Класс System.String очень мощный универсальный класс, хотя вовсе не единственный в арсенале .NET, который предназначен для обработки строк. Мы начнем эту главу с ознакомления с основными средствами System.String, а затем рассмотрим некоторые неординарные вещи, которые можно делать со строками с помощью других классов .NET в частности тех, что определены в пространствах имен System.Text и System.Text.RegularExpressions. В этом разделе рассматриваются перечисленные ниже темы. 1 Построение строк. Если нужно выполнять повторяющиеся модификации строки, например, чтобы составить длинную строку перед тем, как отобразить ее, или передать другому методу или приложению, то в этом случае класс String будет весьма неэффективным. Для таких ситуаций больше подходит другой класс System.Text.StringBuilder, поскольку он спроектирован как раз для этих случаев. Форматирующие выражения. В этой главе также внимательно рассматриваются форматирующие выражения, которые использовались в методе Console.WriteLine() на протяжении последних нескольких глав. Эти форматирующие выражения обрабатываются с применением двух полезных интерфейсов IFormatProvider и IFormattable и, реализуя эти интерфейсы в собственных классах, разработчик может фактически определять собственные форматирующие последовательности, с тем, чтобы Console.WriteLine() и другие подобные методы отображали значения объектов классов определенным образом, заданным разработчиком. Регулярные выражения. Платформа .NET также предоставляет некоторые очень сложные классы, имеющие дело с ситуациями, когда необходимо идентифицировать или извлекать подстроки, удовлетворяющие определенным сложным критериям. Например, можно находить все вхождения в строку повторяющегося символа или группы символов; находить все слова, начинающиеся с буквы “s” и содержащих минимум одну букву “n”; либо искать строки, похожие на идентификационный код работника или номер карточки социального страхования. Хотя можно создать методы, выполняющие подобную обработку с применением класса String, писать такие методы довольно-таки обременительно. Вместо этого лучше использовать ряд классов из пространства имен System.Text.RegularExpressions, которые специально предусмотрены для подобных операций. Изучение System.String Прежде чем рассматривать все прочие строковые классы, в этом разделе мы проведем краткий обзор некоторых доступных методов класса String. System.String класс, специально предназначенный для хранения строк и выполнения огромного числа действий с ними. К тому же, в связи с исключительной важностью этого типа данных, в C# предусмотрено специальное ключевое поле и синтаксис, который, в частности, упрощает манипулирование строками. Выполнять конкатенацию строк можно с использованием перегруженных операций: string message1 = "Hello"; message1 += ", There"; string message2 = message1 + "!"; // возвращает "Hello" // возвращает "Hello, There" // возвращает "Hello, There!" Язык C# также позволяет извлекать определенные символы из строк, применяя синтаксис индексатора массива: string message = "Hello"; char char4 = message[4]; // возвращает 'o'; Это позволяет выполнять такие часто встречающиеся задачи, как замена символов, удаление пробелов и капитализация (т.е. перевод первых букв всех слов в верхний регистр). Ключевые методы класса String представлены в табл. 9.1. Таблица 9.1. Основные методы класса String Метод Compare 2 CompareOrdinal Concat CopyTo Format IndexOf IndexOfAny Назначение Сравнивает содержимое строк, принимая во внимание культуру (локаль) при определении эквивалентности определенных символов. То же, что и Compare, но без учета локальных установок. Комбинирует отдельные экземпляры строк в одну строку (конкатенация) Копирует определенное число символов, начиная с определенной позиции, в новый экземпляр массива. Формирует строку, содержащую различные значения, и указывает, каким образом каждое из них должно быть отформатировано. Находит первое вхождение заданной подстроки или символа в строке. Join Находит первое вхождение в строку любого символа из набора. Вставляет экземпляр строки в другой экземпляр строки в определенную позицию. Строит новую строку, комбинируя содержимое массива строк. LastIndexOf То же, что и IndexOf, но находит последнее вхождение. LastIndexOfAny То же, что и IndexOfAny, но находит последнее вхождение. PadLeft Дополняет строку до заданной длины повторяющимся символом слева. PadRight Substring Дополняет строку до заданной длины повторяющимся символом справа. Заменяет вхождения определенного символа или подстроки другим символом или подстрокой. Разбивает строку на массив подстрок, используя в качестве разделителя заданный символ. Извлекает подстроку, начиная с определенной позиции строки. ToLower Преобразует символы строки в нижний регистр. ToUpper Преобразует символы строки в верхний регистр. Trim Удаляет ведущие и хвостовые пробелы. Insert Replace Split Следует иметь в виду, что табл. 9.1 не является исчерпывающей. Она предназначена лишь для того, чтобы дать представление о средствах, обеспечиваемых строками. Построение строк Как уже упоминалось, String чрезвычайно мощный класс, реализующий огромное количество очень полезных методов. Однако с классом String связан недостаток, который делает его очень неэффективным при выполнении повторяющихся модификаций заданной строки на самом деле String является неизменяемым (immutable) типом данных, в том смысле, что однажды инициализированный строковый объект уже не может быть изменен. Методы и операции, модифицирующие содержимое строк, на самом деле создают новые строки, копируя при необходимости содержимое старых. Для примера рассмотрим следующий код: string greetingText = "Hello from all the guys at Wrox Press. "; greetingText += "We do hope you enjoy this book as much as we enjoyed writing it."; Что происходит, когда выполняется этот код? Во-первых, создается объект System.String, и этот объект инициализируется текстом "Hello from all the guys at Wrox Press.". (Обратите внимание на пробел после точки.) Когда это происходит, .NET вы- 3 деляет достаточно памяти, чтобы поместился весь текст (39 символов), и переменной greetingText присваивается ссылка на этот экземпляр строки. Следующая строка кода синтаксически выглядит так, будто некоторый дополнительный текст добавляется к строке, хотя на самом деле это не так. Вместо этого создается новый экземпляр строки достаточно большой, чтобы уместился составной текст длиной в 103 символа. Исходный текст, "Hello from all the guys at Wrox Press. ", копируется в этот новый экземпляр строки вместе с добавочным текстом "We do hope you en j oy this book as much as we enjoyed writing it.". Затем адрес, хранящийся в переменной greetingText, обновляется, чтобы указывать на новый объект String. Старый объект String остается без ссылок (т.е. ни одна переменная более не указывает на него), и поэтому при следующем запуске сборщика мусора будет удален из памяти. Само по себе все это выглядит достаточно неплохо, но представьте, что нужно перекодировать строку, заменив каждый символ (кроме знаков препинания) на другой, с ASCIIкодом, следующим в алфавитном порядке, в качестве части очень простой схемы шифрования. Это преобразует нашу строку в “Ifmmp gspn bmm uif hvstbu Xspy Qsftt. Xf ep ipqf zpv fokpz uijt cppl bt nvdi bt xf fokpzfe xsjujoh ju”. Существует несколько способов решения такой задачи, но простейший (если ограничиться использованием класса String) и, предположительно, наиболее эффективный, заключается в применении метода String.Replace(), который заменяет все вхождения одной подстроки на другую. С применением Replace() код шифрования текста будет выглядеть так: string greetingText = “Hello from all the guys at Wrox Press. “ greetingText += "We do hope you enjoy this book as much as we enjoyed writing it."; for(int i = 'z'; i>='a' ; i--) { char old1 = (char)i; char new1 = (char) (i+1) ; greetingText = greetingText.Replace(old1, new1); } for(int i = ‘Z’; i>=’A’; i--) { char old1 = (char)i; char new1 = (char) (i+1); greetingText = greetingText.Replace(old1, new1); } Console.WriteLine("Зашифрованная строка:\n" + greetingText); Для простоты этот код не преобразует Z в А или z в а. Эти буквы кодируются, соответственно, как [ и {. 4 Здесь метод Replace() работает весьма интеллектуальным образом, не создавая новой строки, если он может выполнить изменение в старой. Исходная строка содержит 23 разных символа в нижнем регистре и 3 в верхнем. Таким образом, метод Replace() должен распределить память для новой строки 26 раз, причем каждая новая строка будет длиной 103 символа. Это значит, что в результате такого процесса шифрования появятся строковые объекты, способные в сумме сохранить 2678 символов, и они будут находиться в куче и дожидаться, когда сборщик мусора уничтожит их. Понятно, что у приложений, интенсивно использующих текстовую обработку, возникнут серьезные проблемы с производительностью. Чтобы справиться с этим, разработчики из Microsoft предусмотрели класс System.Text.StringBuilder. Класс StringBuilder не настолько развит, как String в смысле количества методов, которые им поддерживаются. Обработка, которую может выполнять StringBuilder, ограничена подстановкой, добавлением и удалением текста из строк. Однако все это он делает гораздо более эффективно. Когда строка конструируется классом String, выделяется ровно столько памяти, сколько необходимо для хранения данной строки. Однако StringBuilder поступает лучше и обычно выделяет больше памяти, чем нужно в данный момент. У вас, как разработчика, есть возможность указать, сколько именно памяти должен выделить StringBuilder, но если вы этого не сделаете, то будет выбран объем по умолчанию, который зависит от размера начального текста, инициализирующего экземпляр StringBuilder. Класс StгingBui1deг имеет два главных свойства: Length, показывающее длину строки, содержащуюся в объекте в данный момент; Capacity, указывающее максимальную длину строки, которая может поместиться в выделенную для объекта память. Любые модификации строки происходят внутри блока памяти, выделенного экземпляру StringBuilder. Это делает добавление подстрок и замену индивидуальных символов строки очень эффективными. Удаление или вставка подстрок неизбежно остаются менее эффективными, потому что при этих операциях приходится перемещать в памяти части строки. Выделять новую память и, возможно, полностью перемещать ее содержимое приходится только при выполнении ряда действий, которые приводят к превышению выделенной емкости строки. В дополнение к избыточной памяти, выделяемой изначально на основе экспериментов, StringBuilder имеет свойство удваивать свою емкость, когда происходит переполнение, а новое значение емкости не установлено явно. Например, воспользовавшись объектом StringBuilder для конструирования первоначальной строки приветствия, можно написать такой код: StringBuilder greetingBuilder = new StringBuilder("Hello from all the guys at Wrox Press. ", 150); greetingBuilder.AppendFormat ( "We do hope you enjoy this book as much as we enjoyed writing it"); Для использования класса StringBuilder необходимо указать ссылку на пространство имен System.Text. Этот код устанавливает начальную емкость StringBuilder равной 150. Всегда лучше сразу указывать емкость, превышающую предполагаемую длину строки, чтобы объекту StringBuilder не приходилось заново выделять память при переполнении. По умолчанию устанавливается емкость в 16 символов. Теоретически емкость можно задать максимально большим числом, которое помещается в int, хотя система, вероятно, сообщит о нехватке памя- ти, если вы попытаетесь выделить максимально возможные 2 миллиарда символов (это теоретический максимум емкости StringBuilder). Когда приведенный ранее код выполняется, он сначала создает объект StringBuilder, выглядящий так, как показано на рис. 9.1. Рисунок 9.1 Объект StringBuilder Затем при вызове метода AppendFormat() остаток текста помещается в пустое пространство без необходимости перераспределения памяти. Однако реальная эффективность, которую несет с собой применение StringBuilder, проявляется при выполнении повторных подстановок текста. Например, шифрование текста так же, как это делалось раньше, можно сделать без какого-либо дополнительного выделения памяти: 5 StringBuilder greetingBuilder = new StringBuilder("Hello from all the guys at Wrox Press. ",150); greetingBuilder.AppendFormat( "We do hope you enjoy this book as much as we enjoyed writing it"); Console.WriteLine("Незашифрованная строка:\n" + greetingBuilder); for(int i = 'z'; i>='a' ; i--) { char old1 = (char)i; char new1 = (char) (i+1); greetingBuilder = greetingBuilder.Replace(old1, new1); } for (int i = 'Z'; i>='A' ; i--) { char old1 = (char)i; char new1 = (char) (i+1); greetingBuilder = greetingBuilder.Replace(old1, new1); } Console.WriteLine("Зашифрованная строка:\n" + greetingBuilder.ToString()) ; В этом коде используется метод StringBuilder.Replace(), который выполняет ту же работу, что и String.Replace(), но без копирования строки в процессе. Общая память, выделенная для строк в этом примере, не превышает 150 символов для экземпляра StringBuilder, а также памяти, которая распределялась во время выполнения строковых операций внутри вызова System.WriteLine(). Обычно StringBuilder будет использоваться для выполнения различных манипуляций со строками, a String для хранения или отображения финального результата. Члены класса StringBuilder Мы уже видели пример конструктора StringBuilder, принимающего в параметрах начальную строку и емкость. Однако существуют и другие. Например, можно передавать только одну строку: StringBuilder sb = new StringBuilder("Hello"); Или же создавать пустой StringBuilder с заданной емкостью: StringBuilder sb = new StringBuilder(20); Помимо свойств Length и Capacity, есть еще доступное только для чтения свойство MaxCapacity, указывающее предел, до которого может расти данный экземпляр String- Builder. По умолчанию этот предел составляет int.MaxValue (примерно 2 миллиарда, как уже упоминалось), но его значение можно установить в меньшую величину при конструировании объекта StringBuilder: //Здесь устанавливается начальная емкость 100, но максимальная - 500. //Таким образом, этот StringBuilder не может вырасти более чем до 500 IIсимволов, в противном случае будет сгенерировано исключение. StringBuilder sb = new StringBuilder(100, 500); Значение емкости можно устанавливать в любой момент, однако если указать его меньшим, чем длина текущей содержащейся в нем строки, или большим, чем максимально допустимое, то будет сгенерировано исключение: StringBuilder sb = new StringBuilder("Hello"); sb.Capacity = 100; В табл. 9.2 перечислены основные методы класса StringBuilder. Таблица 9.2. Основные методы класса StringBuilder Метод Назначение Append() Добавляет строку к текущей строке. Добавляет строку, сформированную в соответствии со спецификатором формата. AppendFormat() 6 Insert() Вставляет подстроку в строку. Remove() Удаляет символ из текущей строки. Заменяет все вхождения символа другим символом или вхождения подстроки другой подстрокой. Возвращает текущую строку в виде объекта System.String (переопределение метода класса System.Object). Replace() ToString() Для каждого из этих методов предусмотрено множество перегрузок. AppendFormat() на самом деле вызывается внутри Console.WriteLine() и отвечает за обработку таких выражений форматирования, как {0:0}, заменяя их конкретными значениями. Этот метод рассматривается в следующем разделе. Приведений (явных или неявных) StringBuilder к String не существует. Если нужно получить содержимое StringBuilder в виде String, следует пользоваться методом ToString(). Теперь, когда мы представили класс StringBuilder и описали некоторые способы его использования для повышения производительности, нужно отметить, что этот класс не всегда обеспечивает необходимую увеличенную производительность. В основном, StringBuilder стоит применять при необходимости манипулирования многими строками. Однако если требуется сделать что-то простое, например, соединить две строки, то, скорее всего, вы обнаружите, что System.String сделает это лучше. Форматирующие строки До сих пор для кода примеров этой книги было написано множество классов, и обычно они реализовывали метод ToString() для того, чтобы отображать содержимое своих экземпляров. Однако довольно часто у пользователей возникает потребность отобразить содержимое переменных другим способом в зависимости от национальных и культурных стандартов. Наиболее очевидным примером может служить класс System.DateTime в .NET. Например, может понадобиться отобразить одну и ту же дату как 10 June 2010, l0 Jun 2010, 6/10/10 (США), 10/6/10 (Великобритания) или 10.06.2010 (Германия). Аналогично, структура Vector из раздела 6.7 реализует метод Vector.ToString() для отображения вектора в формате (4, 56, 8). Однако существует другой общепринятый способ записи векторов в форме 4i + 56 j + 8k. Если хотите, чтобы разрабатываемые классы были дружественными к пользователю, они должны предлагать средства для отображения своих строковых представлений в любом из форматов, которые могут понадобиться пользователю. В исполняющей среде .NET определен стандартный способ достижения этого интерфейс IFormattable. Описание способа добавления этого важного свойства к пользовательским классам и структурам и представляет собой тему настоящего раздела. Как известно, при вызове Console.WriteLine() должен указываться формат, в котором нужно отобразить значение переменной. Таким образом, мы воспользуемся этим методом в качестве примера, хотя большая часть обсуждения относится к любой ситуации, когда возникает потребность в форматировании строки. Например, если требуется отобразить значение переменной в окне списка или текстовом поле, то при этом для получения соответствующего строкового представления переменной обычно применяется метод String.Format(). Однако действительные спецификаторы формата, используемые для указания конкретного формата, идентичны тем, что передаются методу Console.WriteLine(). Поэтому мы сосредоточим внимание на примере Console.WriteLine(). Начнем с рассмотрения того, что происходит, когда форматная строка применяется к примитивному типу, а отсюда станет ясно, как следует включать спецификаторы формата для пользовательских классов и структур. В разделе 6.2 строки формата применялись в Console.Write() и Console.WriteLine() следующим образом: 7 double d = 13.45; int i = 45; Console.WriteLine( "Значение double равно {0,10:E}, a int содержит {1}", d, i); Сама строка формата содержит большую часть отображаемого текста, но всякий раз, когда в нее должно быть вставлено значение переменной, в фигурных скобках указывается индекс. В фигурные скобки может быть включена и другая информация, относящаяся к формату данного элемента, например, та, что описана ниже. Количество символов, которое займет представление элемента, снабженное префиксом-запятой. Отрицательное число указывает, что элемент должен быть выровнен по левой границе, а положительное по правой. Если элемент на самом деле занимает больше символов, чем ему отведено форматом, он отображается полностью. Спецификатор формата предваряется двоеточием. Это указывает, каким образом необходимо отформатировать элемент. Например, можно указать, должно ли число быть форматировано как денежное значение, либо его следует отобразить в научной нотации. В табл. 9.3 перечислены часто используемые спецификаторы формата для числовых типов, которые уже кратко упоминались в разделе 6.2. Таблица 9.3. Часто используемые спецификаторы формата для числовых типов СпециПрменяется к фикатор Значение Пример C Числовым типам Символ местной валюты D $4834.50 (США) 4834.50 руб (Россия) 4834 Только к целочис- Обычное целое ленным типам Числовым типам Экспоненциальная (науч- 4.834E+003 ная) нотация Числовым типам С фиксированной десятич- 4834.50 ной точкой 4834.5 Числовым типам Обычные числа E F G N P X Формат числа, принятый в 4,384.50 (США) данной местности 4 384,50 (Россия) 432,000.00% Числовым типам Процентная нотация 0x1120 Только к целочис- Шестнадцатеричный ленным типам формат Числовым типам Если целое число требуется дополнить нулями, можно воспользоваться спецификатором формата 0 (ноль), повторив его столько раз, сколько составляет требуемая длина. Например, спецификатор формата 0000 отобразит 3 в виде 0003, 99 в виде 0099 и т.д. Полный список спецификаторов формата значительно длиннее, поскольку другие типы данных добавляют собственные спецификаторы. Цель настоящего раздела состоит в том, чтобы показать, как определять собственные спецификаторы для пользовательских классов. Как форматируется строка Для примера форматирования строки рассмотрим следующий оператор: Console.WriteLine( "Значение double равно {0,10:Е}, a int содержит {1}", d, i); Console.WriteLine() просто передает полный набор параметров статическому методу String.Format(). Этот же метод необходимо использовать при форматировании строк, например, для отображения в текстовом поле. Такая реализация перегрузки WriteLine() с тремя параметрами обычно выглядит следующим образом: 8 // Возможная реализация Console.WriteLine() public void WriteLine(string format, object arg0, object arg1) { this.WriteLine(string.Format(this.FormatProvider, format, new object[](arg0, arg1})); } Перегрузка этого метода с одним параметром, которая использовалась в предыдущем примере кода, просто выводит содержимое переданной строки без проведения какого-либо форматирования. String.Format() конструирует результирующую строку, заменяя каждый спецификатор формата подходящим строковым представлением соответствующего объекта. Однако, как мы уже видели, процесс построения строки требует применения экземпляра StringBuilder вместо экземпляра String. В данном примере экземпляр StringBuilder создается и инициализируется первой известной частью строки текстом "Значение double равно". Затем вызывается метод StringBuilder.AppendFormat(), которому передается первый спецификатор формата { 0,10 : Е}, а также ассоциированный с ним объект типа double, чтобы добавить строковое представление объекта к конструируемой строке. Процесс продолжается повторными вызовами StringBuilder.Append() и StringBuilder.AppendFormat() до тех пор, пока не будет построена полная форматированная строка. Рассмотрим наиболее интересную часть этого процесса: вызов StringBuilder. AppendFormat() определяет собственно форматирование объекта. Сначала здесь проверяется, реализует ли объект интерфейс IFormattable из пространства имен System. Вы можете счесть это довольно-таки несложным: можно попытаться привести объект к типу этого интерфейса и посмотреть, получится ли это, или же воспользоваться ключевым словом is. Если такая проверка завершится неудачей, то AppendFormat() вызовет метод ToString(), который все объекты либо наследуют от System.Object, либо переопределяют. Именно это и происходило до сих пор, поскольку ни один из упомянутых до сих пор классов не реализовывал интерфейс IFormattable. Вот почему переопределений Object.ToString() было достаточно, чтобы получить возможность отображать структуры и классы, такие как Vector, в операторах Console.WriteLine(). Однако все предопределенные примитивные числовые типы реализуют этот интерфейс, а значит, для этих типов, и в частности, для double и int из последнего примера, базовый метод ToString(), унаследованный от System.Object, вызываться не будет. Чтобы понять, что же происходит на самом деле, нужно познакомиться с интерфейсом IFormattable. IFormattable определяет только один метод, который также вызывает ToString(). Однако в отличие от версии System.Object, которая не имеет параметров, этот метод принимает два параметра. В следующем коде показано определение IFormattable: interface IFormattable { string ToString(string format, IFormatProvider formatProvider); } Первый параметр этой перегрузки ToString() ожидает строку, которая специфицирует запрошенный формат. Другими словами, это описательная часть, появляющаяся в фигурных скобках в строке, изначально переданной Console.WriteLine() или String.Format(). Например, в примере исходный оператор выглядел так: Console.WriteLine( "Значение double равно {0,10:Е}, a int содержит (1)", d, i); 9 Таким образом, при обработке первого спецификатора, {0,10:Е}, эта перегрузка будет вызвана с переменной d типа double, а первый переданный ей параметр будет иметь значение Е. Метод StringBuilder.AppendFormat() передает здесь текст, следующий за двоеточием в соответствующем спецификаторе формата исходной строки. В настоящей книге мы не беспокоимся о втором параметре ToString(). Он представляет собой ссылку на объект, реализующий интерфейс IFormatProvider. Этот интерфейс предоставляет дополнительную информацию, которая может потребоваться ToString() для форматирования таких объектов, как те, что зависят от национальных культурных стандартов (“культура” в контексте .NET аналогична локальным установкам в Windows; если вы форматируете денежные величины или даты, то эта информация может понадобиться). Если вы будете вызывать перегрузку ToString() непосредственно в своем коде, то нужно будет применить этот объект. Однако StringBuilder.AppendFormat() передает в этом параметре null. Если formatProvider равен null, то ожидается, что ToString() использует текущие локальные установки системы. Возвращаясь к примеру, мы видим, что первый элемент, который нужно форматировать, имеет тип double, и для него запрошена экспоненциальная (научная) нотация со спецификатором формата Е. Метод StringBuilder.AppendFormat() устанавливает, что double реализует интерфейс IFormattable, а потому вызывает перегрузку ToString() с двумя параметрами, передавая ей строку "Е" в первом параметре и null во втором. Теперь дело реализации этого метода в типе double вернуть строковое представление double в соответствующем формате, принимая во внимание переданный спецификатор и текущие региональные установки (текущую культуру). Затем StringBuilder.AppendFormat() при необходимости дополнит возвращенную строку пробелами, чтобы она заполнила поле с указанной в форматной строке шириной 10 символов. Следующий объект, подлежащий форматированию, имеет тип int, для которого никакой специальный формат не запрашивается (спецификатор формата просто {1}). Поскольку формат не запрошен, StringBuilder.AppendFormat() передает null-ссылку вместо форматной строки. Ожидалось, что будет вызван int.ToString() с двумя параметрами. Но поскольку никакой особый формат не требуется, то в данном случае будет вызван метод ToString() без параметров. Весь описанный процесс форматирования представлен на рис. 9.2. 10 Рисунок 9.2 Пример выполнения форматирования Пример FormattableVector Теперь, когда вы изучили построение форматированной строки, в этом разделе мы расширим пример Vector, который был приведен в разделе 6.7 с тем, чтобы можно было форматировать векторы различными способами. Код примера доступен для загрузки на сайте издательства. Имея представление об основополагающих принципах, код не покажется сложным. Все, что необходимо сделать это реализовать IFormattable и воспользоваться реализацией перегрузки ToString(), определенной интерфейсом. Ниже перечислены спецификаторы формата, которые будут применяться. N должен интерпретироваться как запрос числа, известного под названием Norm (норма) в классе Vector. Это просто сумма квадратов его компонентов, которая в соответствии с законами математики равна квадрату длины вектора и обычно изображается между двумя двойными вертикальными линиями, например, ||34.5||. VE должен интерпретироваться как запрос отображения каждого компонента в специфическом формате с применением к компонентам double спецификатора Е (2.3Е+01, 4.5Е+02, 1.0Е+00). IJK должен интерпретироваться как запрос для отображения вектора в форме 23i + 450 j + lk. Все остальные должны обеспечивать просто возврат представления Vector по умолчанию (23, 450, 1.0). Для простоты мы не будем реализовывать опции комбинированного отображения вектора IJK и научного формата. Однако мы реализуем спецификаторы формата в форме, не зависящей от регистра, т. е. чтобы вместо IJK можно было применять ijk. Следует отметить, что конкретный вид строк, используемых в качестве спецификаторов формата, полностью во власти разработчика. Чтобы достичь всего этого, первым делом модифицируем объявление Vector, реализовав интерфейс IFormattable: struct Vector:IFormattable { public double x, y, z; // Начальная часть Vector Теперь добавим реализацию перегрузки ToString() с двумя параметрами: 11 public string ToString(string format, IFormatProvider formatProvider) { if(format == null) { return ToString(); } string formatUpper = format.ToUpper(); switch (formatUpper) { case "N": return "|| " + Norm().ToString() + " ||"; case "VE": return String.Format("( {0:E}, {1:E}, {2:E} )", x, y, z); case "IJK": StringBuilder sb = new StringBuilder(x.ToString(),30); sb.Append(" i + "); sb.Append(y.ToString()); sb.Append(" j + "); sb.Append(z.ToString()); sb.Append(" k"); return sb.ToString(); default: return ToString(); } } Вот и все, что нужно сделать. Обратите внимание на предосторожность проверку format на равенство null, предпринимаемую перед вызовом любого метода с этим параметром. Дело в том, что этот метод необходимо сделать насколько возможно устойчивым. Спецификаторы формата для всех примитивных типов не зависят от регистра, и такого же поведения другие разработчики будут ожидать от вашего класса. Для спецификатора формата VE необходимо каждый компонент отформатировать в научной нотации, поэтому мы просто используем String.Format() для достижения этого. Поля х, у и z имеют тип double. В случае спецификатора формата IJK нужно просто собрать в строку несколько подстрок, и для увеличения производительности при этом применяется объект StringBuilder. Для полноты картины воспроизведем также разработанную ранее перегрузку ToString() без параметров: public override string ToString() { return " ( " + x + " , " + у + " , " + z + " )"; } И, наконец, необходимо добавить метод Norm(), вычисляющий квадрат (норму) вектора, поскольку он не был реализован ранее во время разработки структуры Vector: public double Norm() { return x*x + y*y + z*z; } Теперь можно протестировать форматируемый вектор с помощью следующего кода: static void Main() { Vector v1 = new Vector(1,32,5); Vector v2 = new Vector(845.4, 54.3, -7.8); Console.WriteLine( "\nB формате IJK:\nvl будет {0, 30:IJK}\nv2 будет {1,30:IJK}", v1, v2); Console.WriteLine( "\nВ формате по умолчанию:\nvl будет {0,30}\nv2 будет {1,30}", v1, v2); Console.WriteLine( "\nB формате VE:\nvl будет (0,30:VE)\nv2 будет {1,30:VE}", v1, v2); Console.WriteLine( "ХпНормы:\nдля vl норма {0, 20:М}\пдля v2 норма {1,20:N}", v1, v2) ; } Результат запуска этого примера представлен ниже: FormattableVector В формате IJK: v1 будет 1 i + 32 j + 5 k v2 будет 845.4 i + 54.3 j + -7.8 k 12 В формате по умолчанию: v1 будет ( 1 , 32 , 5 ) v2 будет (845.4 , 54.3, -7.8) В формате VE: v1 будет ( 1.000000Е+000, 3.200000Е+001, 5.000000Е+000 ) v2 будет ( 8.454000Е+002, 5.430000Е+001, -7.800000Е+000 ) Нормы: для vl норма || 1050 || для v2 норма || 717710.49 || Это доказывает корректную работу пользовательских спецификаторов. Регулярные выражения Регулярные выражения это часть небольшой технологической области, невероятно широко используемой в огромном диапазоне программ, но редко применяемой разработчиками. Регулярные выражения можно представить себе как мини-язык программирования, имеющий одно специфическое назначение: находить подстроки в больших строковых выражениях. Это ре новая технология; изначально она появилась в среде UNIX и обычно используется в языке программирования Perl. Разработчики из Microsoft перенесли ее в Windows, где до недавнего времени эта технология применялась в основном со сценарными языками. Однако теперь регулярные выражения поддерживаются множеством классов .NET из пространства имен System.Text.RegularExpressions. Случаи применения регулярных выражений можно встретить во многих частях среды .NET Framework. В частности, вы найдете их в серверных элементах управления проверкой ASP.NET. Для тех, кто ранее не сталкивался с языком регулярных выражений, в этом разделе даны основные сведения о регулярных выражениях, а также связанных с ними классах .NET. Если вы уже знакомы с регулярными выражениями, то, скорее всего, после беглого просмотра раздела сразу перейдете к базовым классам .NET. Следует отметить, что механизм регулярных выражений .NET спроектирован так, чтобы в основном обеспечивать совместимость с регулярными выражениями Perl 5, хотя имеет также и некоторые дополнительные средства. Введение в регулярные выражения Язык регулярных выражений предназначен специально для обработки строк. Он включает два средства. Набор управляющих» кодов для идентификации специфических типов символов. Наверняка вам знакомо применение символа * для представления любой подстроки в выражениях DOS (например, команда Dir Re* выводит на экран имена всех файлов, начинающихся на Re). В регулярных выражениях используется множество последовательностей, подобных этой, чтобы представлять такие элементы, как любой одиночный символ, разделитель слов, один необязательный символ и т.д Система для группирования частей подстрок и промежуточных результатов таких действий. С помощью регулярных выражений можно выполнять достаточно сложные и высокоуровневые действия над строками, примеры которых описаны ниже. . 13 Идентифицировать (и возможно, помечать к удалению) все повторяющиеся слова в строке (например, привести строку “Компьютерные книги книги” к виду “Компьютерные книги”). Сделать заглавными первые буквы всех слов (например, имея строку “это есть Заголовок”, получить “Это Есть Заголовок”). Преобразовать первые буквы всех слов длиннее трех символов в заглавные (например, “this is a Title” в “This is a Title”). Обеспечить правильную капитализацию предложений. Выделить различные элементы в URL (например, имея http://www.wrox.com, выделить протокол, имя компьютера, имя файла и т.д.). Разумеется, все эти задачи можно решить на C# с использованием различных методов System.String и System.Text.StringBuilder. Однако в некоторых случаях это потребует написания большого объема кода С#. Если вы используете регулярные выражения, то весь этот код сокращается буквально до нескольких строк. По сути, вы создаете экземпляр объекта System.Text.RegularExpressions.RegEx (или, что еще проще, вызываете статический метод RegEx()), передаете ему строку для обработки, а также само регулярное выражение (строку, включающую инструкции на языке регулярных выражений) и все готово. Строка регулярных выражений на первый взгляд выглядит подобно обычной строке, но с разбросанными по ней управляющими последовательностями и прочими символами со специальным значением. Например, последовательность \b означает начало или конец слова (границу слова), поэтому если требуется указать, что нужно найти символы th в начале слов, это значит, что необходимо искать регулярное выражение \bth (т.е. последовательность -t-h). Если же нужно найти все появления th в конце слова, то следует записать так: th\b. Однако регулярные выражения могут оказаться и намного более сложными и включать, например, средства для сохранения частей текста, найденных в результате поиска. В настоящем разделе мы лишь кратко очертим возможности регулярных выражений. Дополнительные сведения о регулярных выражениях можно почерпнуть из книги “Освой самостоятельно регулярные выражения за 10 минут” (ИД ".Вильямс”, 2004 г.). Предположим, что приложение должно преобразовывать телефонные номера США в интернациональный формат. В США телефонные номера имеют формат 314-123-1234, что часто записывается в виде (314)123-1234. При преобразовании из национального формата в интернациональный необходимо включить +1 (код страны США), а также добавить скобки вокруг кода региона: +1(314)123-1234. Для поиска и замены это не слишком сложно, но все же потребует некоторых усилий по кодированию, если применять для этого класс String (что будет означать применение методов, доступных в System.String). Язык регулярных выражений позволяет построить короткую строку, с помощью которой можно достичь того же результата. В этом разделе рассматривается только очень простой пример, который сосредоточен на поиске в строке определенной подстроки без ее модификации. Пример RegularExpressionsPlayaround В оставшейся части этого раздела мы разработаем короткий пример, иллюстрирующий некоторые из средств регулярных выражений, а также способы применения механизма регулярных выражений C# для проведения поиска и отображения результатов. В качестве примера документа используется текст из введения к книге Professional ASP.NET 4 in C# and VB издательства Wrox Press: string Text = @"This comprehensive compendium provides a broad and thorough investigation of all aspects of programming with ASP.NET. Entirely revised and updated for the fourth release of .NET, this book will give you the information you need to master ASP.NET and build a dynamic, successful, enterprise Web application."; Несмотря на все переносы строк, это допустимый код на С#. Он хорошо иллюстрирует применение буквальных строк, снабженных префиксом 14 Исходный текст будем называть входной строкой. Для начала ознакомления с классами .NET, обслуживающими регулярные выражения, мы начнем с простого текстового поиска, который не включает никаких управляющих последовательностей или команд языка регулярных выражений. Предположим, что требуется найти все вхождения подстроки ion. Строка для поиска называется шаблоном (pattern). Используя регулярные выражения и переменную Text, объявленную ранее, можно записать так: const string pattern = "ion"; MatchCollection myMatches = Regex.Matches (myText, pattern, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); foreach (Match nextMatch in myMatches) { Console.WriteLine(nextMatch.Index); } В коде используется статический метод Matches() класса Regex из пространства имен System.Text.RegularExpressions. Этот метод принимает в параметре некоторый входной текст, шаблон и набор необязательных флагов, взятых из перечисления RegexOptions. В данном случае указано, что весь поиск должен быть независимым от регистра. Другой флаг, ExplicitCapture, модифицирует способ нахождения соответствия таким образом, что поиск может стать немного более эффективным. Почему увидим позже (хотя он имеет и другие применения, которые здесь не раскрыты). Метод Matches() возвращает ссылку на объект MatchCollection. Соответствие (match) это термин, применяемый к результатам поиска экземпляра шаблона в выражении. Оно представляется классом System.Text.RegularExpressions.Match. Таким образом, у нас возвращается коллекция соответствий MatchCollection, содержащая все соответствия, каждое из которых представлено объектом Match. В предыдущем коде просто выполняется итерация по коллекции и используется свойство Index класса Match, которое возвращает индекс символа во входном тексте, где обнаружено соответствие. В результате запуска этого кода получается три экземпляра соответствий. В табл. 9.4 показана часть информации о перечислениях RegexOptions. До сих пор вы не увидели ничего принципиально нового по сравнению с другими базовыми классами .NET. Однако мощь регулярных коллекций кроется в строке шаблона. Дело в том, что строка шаблона может содержать не только простой текст. Таблица 9.4. Перечисления RegexOptions Имя члена Описание CultureInvariant Предписывает игнорировать национальные установки строки. Модифицирует способ поиска соответствия, обеспечивая тольExplicitCapture ко буквальное соответствие. IgnoreCase Игнорирует регистр символов во входной строке. Удаляет из строки незащищенные управляющими символами IgnorePatternWhitespace пробелы и разрешает комментарии, начинающиеся со знака фунта или хеша. Изменяет значения символов ^ и $ так, что они применяются к Multiline началу и концу каждой строки, а не только к началу и концу всего входного текста. Предписывает читать входную строку справа налево вместо направления по умолчанию – слево направо (что удобно для RightToLeft некоторых азиатских и других языков, которые читаются в таком направлении). Специфицирует однострочный режим, в котором точка (.) симSingleline волизирует соответствие любому символу. 15 Как уже вскользь упоминалось, она также может включать то, что называется метасимволами специальными символами, задающими команды, а также управляющие последовательности, которые работают подобно управляющим последовательностям С#. Это символы, предваренные знаком обратного слеша (\) и имеющие специальное назначение. Например, предположим, что требуется найти слова, начинающиеся с буквы n. Для этого можно воспользоваться управляющей последовательностью \b, символизирующей границу слова (граница слова это точка, в которой буквенно-цифровому символу предшествует символ пробела или знака препинания, либо наоборот, следует за ним). Напишем следующий код: const string pattern = "ion"; MatchCollection myMatches = Regex.Matches(myText, pattern, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); Обратите внимание на символ @ перед строкой. Механизму регулярных выражений .NET во время выполнения необходимо передать строку \b, при этом не нужно, чтобы обратный слеш интерпретировался компилятором C# как начало управляющей последовательности. Если цель состоит в нахождении слов, оканчивающихся последовательностью ion, потребуется написать так: const string pattern = @"ion\b"; Если нужно найти все слова, начинающиеся с буквы а и оканчивающиеся на ion (чему в данном примере соответствует только слова application), то придется включить в код еще кое-что. Понятно, что нужен шаблон, который начинается с \bа и оканчивается ion\b, но что будет в середине? Необходимо как-то сообщить приложению, что между а и ion может быть произвольное количество любых символов, кроме пробельных. Правильный шаблон выглядит следующим образом: const string pattern = @"\ba\S*ion\b"; Если вы ранее имели дело с регулярными выражениями, то, возможно, уже сталкивались с подобными цепочками символов. На самом деле они работают достаточно логично. Управля- ющая последовательность \S означает любой символ, не являющийся пробельным. Символ * называется квантификатором (quantifier). Он указывает, что предшествующий ему символ может быть повторен любое количество раз, включая ноль. Последовательность \S* означает любое количество любых символов, кроме пробельных. Показанный выше шаблон, таким образом, будет соответствовать любому отдельному слову, которое начинается с а и оканчивается на ion. В табл. 9.5 приведена часть основных специальных символов или управляющих последовательностей, которые можно применять в регулярных выражениях. Этот список далеко не полон более полный можно найти в документации MSDN. Таблица 9.5. Специальные символы, применяемые в регулярных выражениях Символ Значение Пример Соответствует ^ $ ^B X$ B, но только как первый символ текста X, но только как последний символ текста . * + ? 16 \s \S \b \B Начало входного текста Конец входного текста Любой одиночный символ кроме символа перевода строки (\n) Предыдущий символ может повторяться 0 или более раз Предыдущий символ может повторяться 1 или более раз Предыдущий символ может повторяться 0 или 1 раз Любой пробельный символ Любой символ, не являющийся пробелом Граница слова Любая позиция, кроме границы слова i.ation Isation, ization ra*t rt, rat, raat, raaat и т. д. ra+t rat, raat, raaat и т. д. ra?t только rt \s [пробел]a, \ta, \na \SF aF, rF, cF ion\b Любое слово, заканчивающееся на ion \BX\B Любой символ X в середине слова и rat Если нужно найти один из метасимволов, это можно сделать, защитив соответствующий символ в шаблоне обратным слешем. Например, . (одиночная точка) означает любой одиночный символ кроме перевода строки, в то время как \. означает точку. Можно запросить соответствие, которое содержит альтернативные символы, включив их в квадратные скобки. Например, [1| с] означает один символ, который может быть 1 или с. Если нужно найти любое вхождение слов man или mар, следует использовать последовательность ma[n|р]. Внутри квадратных скобок можно также задавать диапазон, например, [a-z] означает любую одиночную латинскую букву, [А-Е] любую букву верхнего регистра от А до Е, а [0-9] одиночную десятичную цифру. Если требуется найти целое число (т.е. последовательность, содержащую только символы от 0 до 9), можно написать [0-9]+. Обратите внимание на использование символа + для указания того, что должна быть, как минимум, одна цифра, поможет быть и больше т. е. соответствовать будут 9, 83, 854 и т.д. Отображение результатов В этом разделе мы закодируем пример RegularExpressionsPlayaround, чтобы вы прочувствовали, как работают регулярные выражения. Ядром примера будет метод под названием WriteMatches(), который выводит на консоль все соответствия из MatchCollection в более детализированном формате. Для каждого соответствия отображается индекс позиции, где оно было найдено во входной строке, строка соответствия, а также слегка удлиненная строка, которая состоит из найденного соответствия плюс до десяти окружающих символов из входного текста до пяти с каждой стороны (если вхождение найдено в начале или конце строки, окружающих символов будет отображено меньше пяти). Другими словами,-соответствие для слова messaging, которое встречается ближе к концу входного текста, показанного выше, должно отобразиться как and messaging of d (пять символов до соответствия и пять после), но соответствие завершающего слова data будет отображено как g of data. (только один символ после соответствия), поскольку после этого входная строка заканчивается. Такая удлиненная строка яснее позволит увидеть, где находится соответствие во входной строке. 17 static void WriteMatches(string text, MatchCollection matches) { Console.WriteLine("Исходный текст: \n\n" + text + "\n"); Console.WriteLine("Количество соответствий: " + matches.Count); foreach (Match nextMatch in matches) { int index = nextMatch.Index; string result = nextMatch.ToString(); int charsBefore = (index < 5) ? index : 5; int fromEnd = text.Length – index - result.Length; int charsAfter = (fromEnd < 5) ? fromEnd : 5; int charsToDisplay = charsBefore + charsAfter + result.Length; Console.WriteLine("Индекс: {0}, \tCTpoKa: {1}, \t{2}", index, result, text.Substring(index-charsBefore, charsToDisplay)); } } Большая часть обработки в этом методе реализует логику определения того, сколько дополнительных символов с какой стороны нужно показать в удлиненной подстроке, чтобы не пересечь начало и конец входного текста. Обратите внимание на использование еще одного свойства объекта Match Value, которое содержит строку, идентифицирующую соответствие. Помимо этого RegularExpressionsPlayaround просто содержит ряд методов с именами вроде Find1, Find2 и т.д., выполняющих некоторые поиски на базе примера этого раздела. Например, Find2 ищет любую строку, которая содержит символ а в начале слова: static void Find2() { ' string text = @"This comprehensive compendium provides a broad and thorough investigation of all aspects of programming with ASP.NET. Entirely revised and updated for the 3.5 Release of .NET, this book will give you the information you need to master ASP.NET and build a dynamic, successful, enterprise Web application.”; string pattern = @"\ba"; MatchCollection matches = Regex.Matches(text, pattern, RegexOptions.IgnoreCase); WriteMatches(text, matches); } Вместе с этим присутствует простой метод Main(), который можно редактировать, выбирая нужный метод Find<n>(): static void Main() { Find1(); Console.ReadLine(); } Этот код также нуждается в использовании пространства имен RegularExpressions: using System; using System.Text.RegularExpressions; Запуск примера с вызовом Find2() даст такой результат: RegularExpressionsPlayaround Исходный текст: This comprehensive compendium provides a broad and thorough investigation of all aspects of programming with ASP.NET. Entirely revised and updated for the 3.5 Release of .NET, this book will give you the information you need to master ASP.NET and build a dynamic, successful, enterprise Web application. Количество соответствий: 1 Индекс: 291, Строка: application, Web application. Соответствия, группы и захваты 18 Одним из замечательных свойств регулярных выражений является возможность группирования символов. Это работает точно так же, как составные операторы в С#. В C# вы можете сгруппировать любое количество операторов, поместив их в фигурные скобки, и результат будет трактоваться как один составной оператор. В шаблонах регулярных выражений можно группировать любые символы (включая метасимволы и управляющие последовательности), при этом результат воспринимается как одиночный символ. Единственное отличие в использовании круглых скобок вместо фигурных. Результирующая последовательность называется группой. Например, шаблон (аn)+ находит любые повторения последовательности аn. Квантификатор + относится только к предшествующему ему символу, но поскольку два символа сгруппированы вместе, теперь он относится к любым повторам аn как единого целого. Это значит, что если применить (аn)+ к входному тексту bananas came to Europe late in the annals of history, то будет идентифицировано соответствие фрагмента аnаn в слове bananas. С другой стороны, если указать шаблон аn+, то программа выберет аnn из annals, а также две отдельных последовательности аn из bananas. Выражение (аn)+ находит вхождения аn, аnаn, аnаnаn и т.д., в то время как выражение аn+ обнаруживает вхождения аn, аnn, аnnn и т.п. Вас может удивить, почему в предыдущем примере (аn)+ указывает, на anan из слова bananas, но не находит ни одного из двух вхождений ап в том же слове. Дело в том, что существует правило, запрещающее перекрытие соответствий. Если есть несколько подходящих фрагментов, которые перекрываются, то из них по умолчанию выбирается самое длинное соответствие. Однако группы на самом деле обеспечивают еще большую мощь. По умолчанию, когда вы формируете часть шаблона в группу, то также просите механизм регулярных выражений запомнить все соответствия, подходящие к группе, как и любое соответствия всего шаблона. Другими словами, вы трактуете эту группу как шаблон, для которого должно быть найдено соответствие и возвращено по его собственному порядку. Это может оказаться чрезвычайно полезным, когда требуется разбить строку на части-компоненты Например, универсальные идентификаторы ресурсов URI имеют формат: <протокол>://<адрес>:<порт> где <порт> не обязателен. Пример: http://www.wrox.com:4355. Предположим, требуется извлечь протокол, адрес и порт из строки URI, о которой известно, что она может или не может содержать пробел (но не знак препинания) немедленно после URI. Вы можете сделать это с помощью следующего выражения: \b(\S+)://<[^:]+)<?::(\S+))?\b Вот как это выражение работает: сначала ведущие и завершающие последовательности \b обеспечивают, что будет рассматриваться только определенные части текста, представляющие отдельные слова. Внутри шаблона первая группа, (\S+)://, идентифицирует один или более непробельных символов, за которыми следует ://; в данном случае в начале URI идет http://. Скобки заставляют сохранить http в виде группы. Последующий фрагмент, ([^:]+), идентифицирует подстроку www.wrox.com в URI. Эта группа завершается либо с концом слова (завершающий \b), либо двоеточием (:) как маркером следующей группы. Следующая группа идентифицирует порт (:4355). Символ ? говорит о том, что последняя группа не обязательна если не будет :хххх, это не помешает соответствию. И это очень 19 важно, поскольку номер порта не всегда присутствует в URI фактически, чаще он отсутствует. Однако на самом деле все еще несколько сложнее. Требуется указать, что двоеточие также может как присутствовать, так и нет, при этом оно не должно включаться в группу. Это достигается применением двух вложенных друг в друга групп. Внутренняя группа, (\S+), идентифицирует все, что следует за двоеточием (например, 4355). Внешняя группа содержит внутреннюю группу с предшествующим двоеточием, и эта группа, в свою очередь, предваряется последовательностью ?:. Эта же последовательность говорит о том, что группа за знаком вопроса не должна сохраняться (интересует 4355, а не :4355). Не пугайтесь двух двоеточий, следующих подряд; первое из них является частью последовательности ?:, которая говорит “не сохранять эту группу”, а второе это искомый текст. Если запустить этот шаблон для приведенной ниже строки, будет получено одно соответствие: http://www.wrox.com. Я только что нашел этот интереснейший URI на http:// как же его... ах, да! http://www.wrox.com Внутри этого соответствия мы обнаруживаем только что упомянутые три группы, а также четвертую, представляющую само соответствие. Теоретически можно сделать так, чтобы сама группа возвращала одно или более одного соответствия. Каждое из этих индивидуальных соответствий называется захватом (capture). То есть первая группа (\S+) имеет один захват http. Вторая группа также имеет один захват (www.wrox.com). Третья группа, однако, не имеет захватов, потому что в данном URI не указан номера порта. Обратите внимание на строку, содержащую второй фрагмент http://. Хотя фрагмент соответствует первой группе, он все же не будет захвачен при поиске, потому что полное поисковое выражение не соответствует этой части текста. У нас нет возможности показать примеры кода на С#, использующего группы и захваты, но вам следует знать, что класс .NET RegularExpressions поддерживает группы и захваты через классы Group и Capture. К тому же существуют классы GroupCollection и CaptureCollection, представляющие коллекции групп и захватов. Класс Match предлагает свойство Groups, которое возвращает соответствующий объект GroupCollection. Соответственно, класс Group реализует свойство Captures, которое возвращает CaptureCollection. Отношения между всеми этими объектами показаны на рис. 9.3. Вы можете решить не возвращать объект Group каждый раз, когда нужно сгруппировать какие-то символы. С созданием экземпляра объекта связаны достаточно высокие накладные расходы, которые не нужны, если все, что необходимо это сгруппировать несколько символов как часть поискового шаблона. Это можно отключить, если начать группу с последовательности символов ?: для индивидуальной группы, как мы поступили в примере с URI, или же для всех групп, указав флаг RegExOptions.ExplicitCaptures в методе RegEx.Matches(), как было сделано в предыдущих примерах. 20 Рисунок 9.3 Отношения между соответствиями, группами и захватами Итоги При работе с .NET Framework в вашем распоряжении имеется множество доступных типов данных. Одним из наиболее широко используемых типов (особенно в приложениях для хранения и извлечения данных) является тип данных String. В связи с его важностью мы посвятили целую главу описанию использования данных этого типа и манипулирования ими в приложениях. В прошлом работа со строками достаточно часто ограничивалась их разделением и склеиванием с помощью конкатенации. В среде .NET Framework все эти операции можно делать, используя класс StringBuilder для достижения более высокой, нежели ранее, производительности. И последним аспектом (по порядку, но не по значению) из числа рассмотренных было применение великолепного инструмента регулярных выражений, для поиска и анализа строк.