Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Работа со строками
Автор: OlegAxenow

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

Неизменяемые строки

Важнейшим из типов для работы со строками для нас является System.String. Это элементарный ссылочный тип, который представляет собой неизменяемую последовательность Unicode-символов.

Элементарность типа System.String означает, конечно же, не простоту его реализации, а упрощенный синтаксис для работы с ним (в том числе, использование литеральных строк в коде). То, что String является ссылочным типом (reference type), говорит о том, что память для строк выделяется в управляемой куче, а не в стеке.

Безусловно, основным отличием типа String от его аналогов вне .NET Framework (например, CString в MFC) является его неизменяемость. Это значит, что нельзя изменить строку (ни добавить, ни удалить, ни изменить символы). Любые методы String, манипулирующие его содержимым (к примеру, ToUpper) не изменяют содержимое текущей строки, а возвращают новую строку. Есть только один способ изменить строку – использовать небезопасный (unsafe) код, но практической выгоды от этого никакой, поскольку такие строки могут некорректно сравниваться.

Неизменяемость строк, при первом знакомстве, кажется неудобной, но, во-первых, есть тип StringBuilder (о нем речь пойдет ниже), а во-вторых, у неизменяемых строк есть свои преимущества:

  • если строки идентичны, две строковые ссылки могут указывать на один объект, что позволяет более экономно использовать память и быстрее сравнивать строки – этот механизм называется интернированием строк.
  • не нужно синхронизировать потоки при работе со строками
  • можно выполнить последовательность операций со строкой, не изменяя ее:
    if (str.TrimStart().ToUpper().StartsWith(“SELECT”))…
    При этом нужно отметить, что хотя в примере создается два промежуточных объекта, на них нет ссылок в коде, и поэтому их память освободится при очередной сборке мусора.

Нужно отметить еще одну отличительную особенность типа String – он весьма тесно интегрирован с CLR (историческая справка – даже в ассемблере есть специальные команды для операций над строками). Это, разумеется, повышает эффективность работы со строками. Именно неизменяемостью и тесной интеграцией с CLR объясняется то, что класс String – изолированный (sealed). В противном случае, написав производный класс, мы могли бы нарушить связь CLR и String.

Интернирование строк

Говоря о строках, нельзя не упомянуть об интернировании строк (тем более что эта тема обойдена вниманием в документации).

Если отбросить детали, механизм интернирования строк состоит в следующем:

  1. В рамках процесса (а не домена приложения) существует одна внутренняя хэш-таблица, ключами которой являются строки, а значениями – ссылки на них.
  2. Во время JIT-компиляции литеральные строки последовательно заносятся в таблицу (естественно, что одна литеральная строка встречается в таблице один раз).
  3. На этапе выполнения ссылки на литеральные строки присваиваются из этой таблицы.
  4. Можно поместить строку во внутреннюю таблицу во время выполнения с помощью метода String.Intern. Также можно проверить, содержится ли строка во внутренней таблице с помощью метода String.IsInterned.

 

А теперь посмотрим, к чему все это приводит. Проведем эксперимент – сравним  две ссылки на строки с одинаковым значением. Метод с таким кодом:

 

string str = “literal string”;

return object.ReferenceEquals(str, “literal string”);

 

вернет true, потому что реально оба параметра ссылаются на один объект (из внутренней хэш-таблицы). Разумеется, если строку формировать динамически, то она не будет помещена во внутреннюю хэш-таблицу. Чтобы сделать это, нужно вызвать метод Intern:

 

string str = “literal”;

/*

 * Следующая строчка кода создаст новую строку

 * и присвоит ссылку на нее переменной str,

 * а не изменит «старую» строку, на которую ссылается str.

 */

str += “ string”;

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

str = string.Intern(str);

return object.ReferenceEquals(str, “literal string”);

 

Нетрудно догадаться, что сравнение интернированных строк происходит значительно быстрее (только не стоит впадать в эйфорию, и интернировать все, что можно – не забывайте о накладных расходах на вызов метода Intern).

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

Более подробно механизм интернирования строк описан в книге Джеффри Рихтера «Программирование на платформе Microsoft .NET Framework» ([1]).

Операции со строками

Иногда наступает момент, когда нам приходится работать с символами, составляющими строку. Изменить мы их, конечно, не можем, но зато можем узнать их количество (свойство Length), получить символ по индексу (свойство-индексатор Chars) получить массив символов, содержащий всю строку, или ее часть (метод ToCharArray). Помимо этого, можно найти индекс символа в строке (методы IndexOf/LastIndexOf). Также есть возможность найти один символ из некоторого набора (методы IndexOfAny/LastIndexOfAny).

Сравнение строк – тема полезная, но скучная. У типа String есть два статических метода – CompareOrdinal и Compare и экземплярный метод CompareTo, которые сравнивают строки. Метод CompareOrdinal делает это максимально быстро, поскольку просто сравнивает кодовые значения. Если же нужно учитывать регистр букв или региональные стандарты – используйте метод CompareTo, который всегда использует объект CultureInfo вызывающего потока или Compare, в который можно передать объект CultureInfo отличный от Thread.CurrentCulture.

Подробнее об использовании типа String см. в книге Джеффри Рихтера «Программирование на платформе Microsoft .NET Framework» ([1]) и MSDN ([2]).

Многоязыковые приложения

Раз уж речь зашла о региональных стандартах, то нужно сказать и о реализации многоязыковых приложений (world-ready applications). Конечно, это тема отдельной статьи (или даже книги), поэтому я лишь немного расскажу об этом, оправдываясь тем, что есть раздел MSDN, посвященный данной теме ([3]).

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

Динамическое формирование строк

Прочитав о неизменяемых строках, Вы, возможно, задаетесь вопросом: как формировать строки динамически? Тип System.Text.StringBuilder решает поставленную задачу. У него есть поле с внутренним массивом символов и набор методов, позволяющих эффективно манипулировать содержимым этого массива. При этом не нужно заботиться о размере массива символов – при необходимости StringBuilder сам создаст новый массив и скопирует в него символы.

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

Из-за того, что у типа StringBuilder почему-то нет некоторых методов, присущих типу String (ToUpper, Trim и т.д.), при построении строк иногда приходится использовать временные объекты String, что не слишком удобно. Будем надеяться, что в будущем подобные методы появятся и в StringBuilder

Нужно также отметить, что тип StringBuilder не обеспечивает безопасное обращение к своим методам из разных потоков, что наверняка обусловлено соображениями производительности. Еще один важный аспект производительности – вычисление приблизительного размера буфера заранее. Задание параметра capacity при создании экземпляра StringBuilder может значительно ускорить работу с ним.

И еще, - не стоит впадать в крайности. Например, я не стал бы использовать тип StringBuilder для того, чтобы сложить две строки в конструкторе формы.

Преобразование объекта в строку и строки в объект

У любого объекта (поскольку все типы унаследованы от System.Object) есть виртуальный метод ToString, который возвращает строковое представление объекта. Базовая реализация этого метода просто возвращает название типа объекта. Но у многих типов (особенно элементарных) этот метод перегружен и возвращает более полезную информацию (например, для объекта типа double метод ToString возвращает его значение в виде строки в соответствии с региональными стандартами).

Разумеется, метода ToString без параметров иногда бывает недостаточно (допустим, когда нужно представить число в шестнадцатеричном виде или в соответствии с конкретными региональными стандартами). Именно для решения таких задач многие типы реализуют интерфейс IFormattable, предоставляющий метод ToString с двумя параметрами – форматирующей строкой и объектом, реализующим интерфейс IFormatProvider. Например, для вывода числа 12.34 в денежных единицах Пакистана можно использовать такой код:

 

12.34f.ToString("C", new System.Globalization.CultureInfo("ur-PK"));

 

Как другой вариант решения задачи форматирования, можно использовать соответствующие методы типа Convert (которые просто используют метод ToString интерфейса IFormattable, если же переданный объект не реализует этот интерфейс, у него вызывается метод ToString без параметров). Конечно, сфера применения типа Convert не ограничивается форматированием строк – достаточно посмотреть список его методов. Только с помощью этого типа можно использовать кодирование base64, позволяющее относительно экономно преобразовывать байтовый массив в строку. Вызов метода ToBase64String для массива (0, 100):

 

Convert.ToBase64String(new byte[] {0, 100});

 

вернет строку "AGQ=", которую можно преобразовать обратно в байтовый массив с помощью метода FromBase64String. Приятная особенность кодировки base64 состоит в том, что получившаяся строка не будет содержать «плохих» символов (кавычки, апострофы, перевод каретки и т.п.) что иногда бывает весьма полезно (например, для записи в XML-файл).

 

Многие типы позволяют получать объект из строки с помощью метода Parse. Для элементарных числовых типов можно дополнительно задать IFormatProvider и флаги NumberStyles (для типа DateTime задаются флаги DateTimeStyles).

Если Вы хотите получить число из его шестнадцатеричной записи, это можно сделать следующим образом:

 

int.Parse("FF", System.Globalization.NumberStyles.HexNumber);

 

Еще у нескольких десятков типов есть метод Parse (например, у System.Net.IPAddress), но не для всех типов он выполняет функцию преобразования строки в объект этого типа (к примеру, HttpChannel.Parse).

По какой-то причине, из всех элементарных числовых типов в .NET 1.1 только у типа System.Double есть метод TryParse, который позволяет получить число из строки без генерации исключения в случае несоответствия формата строки. С другой стороны, с помощью сочетания флагов NumberStyles, можно проверять строки и для целых чисел (но результат преобразования, конечно, все равно будет иметь тип double).

Преобразование между различными кодировками

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

В .NET Framework все строки состоят только из двухбайтовых символов Unicode и это, поверьте мне, здорово. Некоторые проблемы возникают лишь тогда, когда нужно записать текст в файл или поток. По умолчанию для этого используется кодировка UTF-8. К сожалению, не всегда можно использовать только кодировку UTF-8. Случаи бывают разные – например, нужно прочитать данные из файла, записанного в кодировке Cyrillic(Windows).

Для решения подобных задач служит тип System.Text.Encoding, позволяющий осуществлять преобразования из одной кодировки в другую. Для стандартных кодировок (ASCII, BigEndianUnicode, Unicode, UTF7, UTF8) у типа Encoding есть соответствующие статические свойства. Чтобы использовать другие кодировки, требуется вызвать метод GetEncoding, передав ему либо название кодировки, либо номер кодовой страницы.

Построчное чтение текста из файла, записанного в знакомой многим русской кодировке MS-DOS можно реализовать так:

 

using(System.IO.StreamReader streamReader866 =

new System.IO.StreamReader(@"C:\Temp\Test866.txt",

System.Text.Encoding.GetEncoding(866)))

{

String singleLine;

while ((singleLine = streamReader866.ReadLine()) != null)

{

Console.WriteLine(singleLine);

}

}

 

Если нужно произвести перекодирование массива байт в строку и обратно, то это можно сделать с помощью соответствующих методов типа Encoding GetBytes и GetString.

При записи Unicode-строк стоит обратить внимание на указатель порядка байтов (byte order mark, BOM). Он используется для того, чтобы можно было узнать порядок байтов, не зная этого заранее. А управлять его присутствием можно, указав соответствующий параметр в конструкторе Encoding (или используя Encoding.Default вместо Encoding.UTF8 - в этом случае BOM использоваться не будет). Для записи строк в файлы (или потоки, если выразиться точнее) обычно используется тип StreamWriter. Он также принимает дополнительный параметр типа Encoding, чтобы обеспечить возможность записи строк в различных кодировках:

 

using(System.IO.StreamWriter streamWriter1252 =

new System.IO.StreamWriter(@"C:\Temp\Test1252.txt",

      false,

System.Text.Encoding.GetEncoding("windows-1252")))

{

streamWriter1252.Write("Hello world");

}

.NET Framework 2.0

Никаких серьезных новшеств для класса String в .NET Framework 2.0 не наблюдается. Разумеется, радует обилие классов, поддерживающих метод TryParse. Еще одно хорошее нововведение – статический метод IsNullOrEmpty, использование которого улучшает читаемость кода.

Появились методы для нормализации Unicode-строк – Normalize и IsNormalized. Для большинства обычных задач эти методы вряд ли пригодятся. Теоретически, они полезны при реализации сортировки, например, поскольку один и тот же символ Unicode может быть представлен несколькими наборами байт. Остальные новые методы - ToLowerInvariant, ToUpperInvariant – в комментариях не нуждаются. Еще одно новшество – возможность в методе Split убрать пустые результирующие строки с помощью опции StringSplitOptions.RemoveEmptyEntries.

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

Регулярные выражения

Было бы несправедливо закончить рассказ о строках, не упомянув регулярные выражения (regular expressions). Для чего нужны регулярные выражения? Если сформулировать кратко – для обработки текста, выходящей за рамки стандартных методов типа String.

Регулярные выражения широко применяются при разработке web-приложений (например, для проверки правильности ввода адреса электронной почты обычно используется компонент RegularExpressionValidator).

Освоив язык регулярных выражений ([4], [5]) можно решать достаточно сложные задачи по обработке текста буквально несколькими строчками кода. Для того чтобы убедиться в этом, достаточно посмотреть на простые примеры, приведенные в [4].


Может пригодится:


Автор: OlegAxenow
Прочитано: 10381
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру