Тонкости работы со строками в Delphi - Сравнение строк

ОГЛАВЛЕНИЕ


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

Для типов PChar и AnsiString, которые являются указателями, понятие равенства двух строк может толковаться двояко: либо как равенство указателей, либо как равенство содержимого памяти, на которую эти указатели указывают. Второй вариант предпочтительнее, т.к. он ближе к интуитивному понятию равенства строк. Для типа AnsiString реализован именно этот вариант, т.е. сравнивать такие строки можно, ни о чём не задумываясь. Более сложные ситуации мы проиллюстрируем примером Comparisons. В нём девять кнопок, и обработчик каждой из них иллюстрирует одну из возможных ситуаций.
procedure TForm1.Button1Click(Sender: TObject);
var
  P1, P2: PChar;
begin
  P1 := StrNew('Test');
  P2 := StrNew('Test');
  if P1 = P2 then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
  StrDispose(P1);
  StrDispose(P2);
end;
В данном примере мы увидим надпись "Не равно". Это происходит потому, что в этом случае сравниваются указатели, а не содержимое строк, а указатели здесь будут разные. Попытка сравнить строки PChar с помощью оператора сравнения - весьма распространённая ошибка у начинающих. Для сравнения таких строк следует использовать специальную функцию - StrComp.

Следующий пример, на первый взгляд, в плане сравнения ничем не отличается от только что рассмотренного:
procedure TForm1.Button2Click(Sender: TObject);
var
  P1, P2: PChar;
begin
  P1 := 'Test';
  P2 := 'Test';
  if P1 = P2 then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
end;
Разница только в том, что строки хранятся не в динамической памяти, а в сегменте кода. Тем не менее, на экране появится надпись "Равно". Это происходит, разумеется, не потому, что сравнивается содержимое строк, а потому, что в данном случае два указателя оказываются равны. Компилятор поступает достаточно интеллектуально: видя, что в разных местах используется одна и та же константа, он выделяет для неё место только один раз, а потом помещает в разные указатели один адрес. Поэтому сравнение даёт правильный (с интуитивной точки зрения) результат.

Такое положение дел только запутывает ситуацию со сравнением PChar: написав подобный тест, человек может сделать вывод, что строки PChar сравниваются не по указателю, а по значению, и действовать под руководством этого заблуждения.

Раз уж мы столкнулись с такой особенностью компилятора, немного отвлечёмся от сравнения строк и копнём этот вопрос немного глубже. В частности, распространяется ли интеллект компилятора на константы типа AnsiString.
procedure TForm1.Button3Click(Sender: TObject);
var
  S1, S2: string;
begin
  S1 := 'Test';
  S2 := 'Test';
  if Pointer(S1) = Pointer(S2) then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
end;
В этом примере на экран будет выведено "Равно". Как мы видим, указатели равны, т.е. и здесь компилятор проявил интеллект.

Рассмотрим чуть более сложный случай:
procedure TForm1.Button4Click(Sender: TObject);
var
  P: PChar;
  S: string;
begin
  S := 'Test';
  P := 'Test';
  if Pointer(S) = P then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
end;
В этом случае указатели окажутся не равны. Действительно, с формальной точки зрения константа типа AnsiString отличается от константы типа PChar: в ней есть счётчик ссылок (равный -1) и длина. Однако если забыть о существовании этой добавки, эти две константы одинаковы: четыре значащих символа и один #0, т.е. компилятор, в принципе, мог бы обойтись одной константой. Тем не менее, на это ему интеллекта уже не хватило.

Но вернёмся к сравнению строк. Как мы знаем, строки AnsiString сравниваются по значению, а PChar - по указателю. А что будет, если сравнить AnsiString с PChar?
procedure TForm1.Button5Click(Sender: TObject);
var
  P: PChar;
  S: string;
begin
  S := 'Test';
  P := 'Test';
  if S = P then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
end;
Этот код выдаст "Равно". Как мы знаем из предыдущего примера, значения указателей не будут равны, следовательно, производится сравнение по содержанию, т.е. именно то, что и требуется. Если исследовать код, который генерирует компилятор, то можно увидеть, что сначала неявно создаётся строка AnsiString, в которую копируется содержимое строки PChar, а потом сравниваются две строки AnsiString. Сравниваются, естественно, по значению.

Для строк ShortString сравнение указателей невозможно, две таких строки всегда сравниваются по значению. Правила хранения констант и сравнения с другими типами следующие:

   1. Константы типа ShortString также размещаются в сегменте кода только один раз, сколько бы раз они ни повторялись в тексте.
   2. При сравнении строк ShortString и AnsiString первая сначала конвертируется в тип AnsiString, а потом выполняется сравнение.
   3. При сравнении строк ShortString и PChar строка PChar конвертируется в ShortString, затем эти строки сравниваются.

Последнее правило таит в себе подводный камень, который иллюстрируется следующим примером:
procedure TForm1.Button6Click(Sender: TObject);
var
  P: PChar;
  S: ShortString;
begin
  P := StrAlloc(300);
  FillChar(P^, 299, 'A');
  P[299] := #0;
  S[0] := #255;
  FillChar(S[1], 255, 'A');
  if S = P then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
  StrDispose(P);
end;
Здесь формируется строка типа PChar, состоящая из 299 символов "A". Затем формируется строка ShortString, состоящая из 255 символов "А". Очевидно, что эти строки не равны, потому что имеют разную длину. Тем не менее, на экране появится "Равно".

Происходит это вот почему: строка PChar оказывается больше, чем максимально допустимый размер строки ShortString. Поэтому при конвертировании лишние символы просто отбрасываются. Получается строка длиной 255 символов, которая совпадает со строкой ShortString, с которой мы её сравниваем. Отсюда вывод: если строка ShortString содержит 255 символов, а строка PChar - более 255 символов, и её первые 255 символов совпадают с символами строки ShortString, операция сравнения ошибочно даст положительный результат, хотя эти строки не равны.

Избежать этой ошибки поможет либо явное сравнение длины перед сравнением строк, либо приведение одной из сравниваемых строк к типу AnsiString (второй аргумент при этом также будет приведён к этому типу). Следующий пример даёт правильный результат "Не равно":
procedure TForm1.Button7Click(Sender: TObject);
var
  P: PChar;
  S: ShortString;
begin
  P := StrAlloc(300);
  FillChar(P^, 299, 'A');
  P[299] := #0;
  S[0] := #255;
  FillChar(S[1], 255, 'A');
  if string(S) = P then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
  StrDispose(P);
end;
Учтите, что конвертирование в AnsiString - операция дорогостоящая в смысле процессорного времени (в этом примере будут выделены, а потом освобождены два блока памяти), поэтому там, где нужна производительность, лучше вручную сравнить длину, а ещё лучше - вообще по возможности избегать сравнения строк разных типов, так как без конвертирования это в любом случае не обходится.

Теперь зададимся глупым, на первый взгляд, вопросом: если мы приведём строку AnsiString к PChar, будут ли равны указатели? Проверим:
procedure TForm1.Button8Click(Sender: TObject);
var
  S: string;
  P: PChar;
begin
  S := 'Test';
  P := PChar(S);
  if Pointer(S) = P then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
end;
Вполне ожидаемый результат - "Равно". Можно, например, перенести строку из сегмента кода в динамическую память с помощью UniqueString - результат не изменится. Однако выводы делать рано. Рассмотрим следующий пример:
procedure TForm1.Button9Click(Sender: TObject);
var
  S: string;
  P: PChar;
begin
  S := '';
  P := PChar(S);
  if Pointer(S) = P then
    Label1.Caption := 'Равно'
  else
    Label1.Caption := 'Не равно';
end;
От предыдущего он отличается только тем, что строка S имеет пустое значение. Тем не менее, на экране мы увидим "Не равно". Связано это с тем, что приведение строки AnsiString к типу PChar на самом деле не является приведением типов. Это - скрытый вызов функции _LStrToPChar, и сделано это для того, чтобы правильно обрабатывать пустые строки.

Значение '' (пустая строка) для строки AnsiString означает, что память для неё вообще не выделена, а указатель имеет значение nil. Для типа PChar пустая строка - это ненулевой указатель на символ #0. Нулевой указатель также может рассматриваться как пустая строка, но не всегда - иногда это рассматривается как отсутствие какого бы то ни было значения, даже пустого (аналог NULL в базах данных). Чтобы решить это противоречие, функция _LStrToPChar проверяет, пустая ли строка хранится в переменной, и, если не пустая, возвращает этот указатель, а если пустая, то возвращает не nil, а указатель на символ #0, который специально для этого размещён в сегменте кода. Таким образом, в случае пустой строки PChar(S) <> Pointer(S), потому что приведение строки AnsiString к указателю другого типа - это нормальное приведение типов без дополнительной обработки значения.