Home » Создание сайтов и программирование » Работа со строками в .NET

Работа со строками в .NET

Хотя работа со строками довольно хорошо описана в книге Дж. Рихтера, я решил затронуть эту тему, т.к. не у всех есть возможность прочитать эту книгу, а неправильное использование строк является частой ошибкой и ведет к снижению производительности приложения. К тому же, в книге имеется одна небольшая неточность по работе класса StringBuilder.

В .NET все строки являются экземплярами класса System.String (алиас string в C#). Он неявно наследуется от System.Object и реализует интерфейсы IComparable, ICloneable, IConvertible, IEnumerable:

[Serializable]
public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable

C# рассматривает String как примитивный тип, поэтому для инициализации строковой переменной литералом следует использовать такой синтаксис:

String s = “This is a string.“;

Компилятор размещает строковые литералы в метаданных модуля, а доступ к ним в run-time происходит с использованием так называемого механизма интернирования строк (string interning). Об интернировании я расскажу немного позже.
Так работать не будет:

String s = new String(“This is a string.“);

Т.к. String не имеет конструктора, принимающего String. Конструкторы String позволяют создать экземпляр из символа (char) или массива символов (char[]). При вызове из C++ with Managed Extensions или небезопасного кода C# (использующего указатели) применяются конструкторы, принимающие char* или sbyte*.
C# использует синтаксис С++ для задания специальных символов типа перевода строки или табуляции:

String s = “Line\r\nNew Line“;

При выводе, например, с помощью Console.Write(s), “New Line” будет выведено на новой строке.
“\r\n” соответствует константе vbCrLf в Visual Basic 6.0. Но существует более правильный способ перевода строки. Класс System.Environment содержит read-only свойство NewLine, возвращающее соответствующую строку, в зависимости от платформы.

String s = “Line” + Environment.NewLine + “New Line“;

Например, на платформе UNIX Environment.NewLine вернет \n.

String s = “Column1\tColumn2“;

Column1 и Column2 будут разделены табуляцией.
Символ обратной косой черты (“\”) является служебным и при использовании внутри строки его необходимо удваивать:

String path = “c:\\Program Files\\Microsoft Visual Studio 2003\\“;

Однако, при помощи символа @ (коммерческое эт) можно определить так называемые “дословные строки” (verbatim strings). В этом случае компилятор воспринимает обратную косую черту как обычный символ. Этот код эквивалентен предыдущему:

String path = @”c:\Program Files\Microsoft Visual Studio 2003\“;

Важнейшим свойством строк в .NET является их неизменяемость (immutable). Это означает что созданную строку невозможно изменить. Следствием этого является то, что любой метод класса String не изменяет его. Например:

string s = “Some String“; Console.WriteLine(s.ToUpper());

В этом примере создается строковый объект s, у которого вызывается метод ToUpper(). Это приводит к созданию нового строкового объекта (“SOME STRING”), который выводится на консоль. При этом s не изменилось.  Часто, для достижения результата, приходится вызывать последовательно несколько методов класса String:

string newString = s.Trim().Substring(10, 20).ToLower();

Этот код создает 3 новых строки, две из которых сразу становятся мусором. Не стоит особо беспокоиться об этом, т.к. такие кратковременно живущие объекты легко уничтожаются сборщиком мусора.
Однако, если вам необходимо часто формировать строки в своем приложении, то множество промежуточных строковых объектов все же может сказаться на производительности.
Для этого в .NET Framework имеется специальный класс StringBuilder. Он позволяет модифицировать содержащуюся в нем строку без потери производительности (не создавая промежуточных объектов).
Для этого используются следующме методы класса: Append, AppendFormat, Insert, Remove, Replace.
Как же работает класс StringBuilder? Джеффри Рихтер в своей книге “Программирование на платформе .NET FRAMEWORK” пишет:
У объекта StringBuilder предусмотрено поле с ссылкой на массив структур Char. Используя члены StringBuilder, вы можете эффективно манипулировать этим массивом, сокращая строку и изменяя символы строки. При увеличении строки, представляющей ранее выделенный массив символов, StringBuilder автоматически выделит память для нового, большего по размеру массива, скопирует символы и приступит к работе с новым массивом. Прежний массив станет мусором.
Возможно, что в бета версии .NET, по которой Рихтер писал свою книгу, так и было. Но давайте заглянем сами в класс StringBuilder (с помощью утилиты .NET Reflector). Класс содержит следующее объявление:

internal string m_StringValue;

Как вы помните, модификатор доступа internal (внутренний) эквивалентен public для классов той же сборки и private для классов других сборок.
Далее, методы, модифицирующие строку, вызывают внутренние небезопасные (internal unsafe) методы класса String, которые манипулируют строкой напрямую:

internal unsafe void ReplaceCharInPlace(char oldChar, char newChar, int startIndex, int count, int currentLength)
{
int num1 = startIndex + count;
fixed (char* local1 = &this.m_firstChar)
{
    for (int num2 = startIndex; num2 < num1; num2++)
    {
     if (local1[num2] == oldChar)
     {
        local1[num2] = newChar;
     }
    }
}
}

Благодаря модификатору доступа internal эти методы могут вызывать только классы сборки mscorlib, что исключает проблемы, присущие неуправляемым приложениям.
В заключение, хотелось бы рассказать немного об очень полезном методе Format класса String. Он, также, как и метод AppendFormat класса StringBuilder позволяет эффективно сформировать строку из строки со спецификаторами формата и строковых представлениях объектов. На самом деле, в методе Format создается объект StringBuilder и вызывается его метод AppendFormat. Это позволяет вам сэкономить несколько строк кода.
Вот одна из наиболее часто используемых перегрузок метода Format:

public static string Format( string format, params object[] args );

Вот как правильнее написать пример с переводом строки:

String s = String.Format(“Line{0}New Line“, Environment.NewLine);

Вот еще один пример:

int x = 10; int y = 20; String s = String.Format(“{0} + {1} = {2}“, x, y, x + y);

Если вы не знакомы со спецификаторами формата, обязательно прочтите о них в MSDN. Это очень удобное и эффективное средство для форматирования строк.
На этом краткий обзор строк в .NET пока завершен. Я многого не рассказал, но для первого знакомства этого вполне достаточно.

  • Anonymous

    Спасибо, Сергей, очень интересная статья.

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

    Также на производительность сильно влияет сравнение строк; например распространённой ошибкой является сравнение строк типа: s1.ToUpper() == s2.ToUpper(), вместо String.Compare(s1, s1, true), true – ignore case. Или, например, проверка, не пустая ли строка: вместо “if (s1 == null || s1 == string.Empty)” используем “if (String.IsNullOrEmpty(s1))” и т.д.

    В FxCop содержится довольно много правил такого типа.

  • Anonymous

    Спасибо за ваш комментарий.

    Хочу добавить, что метод IsNullOrEmpty() появился во второй версии фреймоворка и эквивалентен проверке (использовавшейся в первой версии):

    (s1 == null || s1.Lenght == 0)

    Вот код этого метода, полученный через reflector:

    public static bool IsNullOrEmpty(string value)

    {

         if (value != null)

         {

               return (value.Length == 0);

         }

         return true;

    }