SomeString.IndexOf(someString) возвращает 1 вместо 0 в .NET 4

Недавно мы обновили все наши проекты с .NET 3.5 до .NET 4. Я столкнулся с довольно странной проблемой в отношении string.IndexOf().

Мой код явно делает что-то немного другое, но в процессе изучения проблемы я обнаружил, что вызов IndexOf() в строке с самим собой возвратил 1 вместо 0. Другими словами:

string text = "\xAD\x2D";          // problem happens with "­-dely N.China", too;
int index = text.IndexOf(text);    // see update note below.

Дал мне индекс 1, а не 0. Несколько вещей, чтобы отметить эту проблему:

  • Проблемы, связанные с этими дефисами (первый символ - мягкий дефис Юникода, второй - обычный дефис).

  • Я проверил дважды, это не происходит в .NET 3.5, но в .NET 4.

  • Изменение IndexOf() для сравнения по порядку устраняет проблему, поэтому по какой-то причине первый символ игнорируется по умолчанию IndexOf.

Кто-нибудь знает, почему это происходит?

ИЗМЕНИТЬ

Извините, ребята, немного набросились на оригинальный пост и дважды нашли скрытую черту. Я обновил строку, это должно возвращать индекс 1 вместо 2, если вы вставляете его в правильный редактор.

Update:

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

Ответ 1

Ваша строка существует из двух символов: мягкая дефис (код кода Unicode 173) и дефис (код кода Unicode 45).

Wiki: в соответствии со стандартом Unicode мягкая дефис не отображается, если линия не сломана в этой точке.

При использовании "\xAD\x2D".IndexOf("\xAD\x2D") в .NET 4, кажется, игнорируется, что вы ищете мягкий дефис, возвращая начальный индекс 1 (индекс \x2D). В .NET 3.5 это возвращает 0.

Больше удовольствия, если вы запустите этот код (поэтому, когда вы ищете мягкий дефис):

string text = "\xAD\x2D";
string shy = "\xAD";
int i1 = text.IndexOf(shy);

тогда i1 становится 0, независимо от используемой версии .NET. Результат text.IndexOf(text); действительно меняется, что с первого взгляда выглядит как ошибка для меня.

Насколько я могу отследить структуру, более старые версии .NET используют InternalCall для IndexOfString() (я не могу понять, к какому API), а из .NET 4 сделан QCall до InternalFindNLSStringEx(), который, в свою очередь, вызывает FindNLSStringEx().

Проблема (я действительно не могу понять, является ли это предполагаемым поведением) действительно возникает при вызове FindNLSStringEx:

LPCWSTR lpStringSource = L"\xAD\x2D";
LPCWSTR lpStringValue = L"\xAD";

int length;

int i = FindNLSStringEx(
    LOCALE_NAME_SYSTEM_DEFAULT,
    FIND_FROMSTART,
    lpStringSource,
    -1,
    lpStringValue,
    -1,
    &length,
    NULL,
    NULL,
    1);

Console::WriteLine(i);

i = FindNLSStringEx(
    LOCALE_NAME_SYSTEM_DEFAULT,
    FIND_FROMSTART,
    lpStringSource,
    -1,
    lpStringSource,
    -1,
    &length,
    NULL,
    NULL,
    1);

Console::WriteLine(i);

Console::ReadLine();

Печатает 0 и затем 1. Обратите внимание, что length, параметр out, указывающий длину найденной строки, равен 0 после первого вызова и 1 после второго; мягкий дефис считается равным 0.

Обходной путь заключается в использовании text.IndexOf(text, StringComparison.OrdinalIgnoreCase);, как вы уже отметили. Это делает QCall InternalCompareStringOrdinalIgnoreCase(), который в свою очередь вызывает FindStringOrdinal(), который возвращает 0 для обоих случаев.

Ответ 2

Кажется, это ошибка в .NET4, а новые изменения вернулись в .NET4 Beta1 к предыдущей версии, такой же, как .NET 2.0/3.0/3.5.

Что нового в BCL в .NET 4.0 CTP (блоги MSDN):

Изменения безопасности строк в .NET 4

По умолчанию частичные совпадающие перегрузки в System.String(StartsWith, EndsWith, IndexOf и LastIndexOf) по умолчанию были изменены как агностические (порядковые).

Это изменение повлияло на поведение метода String.IndexOf, изменив их для выполнения сравнения по порядку (байта для байта) по умолчанию, будет изменено на использование CultureInfo.InvariantCulture вместо CultureInfo.CurrentCulture.

UPDATE для .NET 4 Beta 1

Чтобы поддерживать высокую совместимость между .NET 4 и предыдущими версиями, мы решили вернуть это изменение. Поведение String по умолчанию, частичное совпадение перегрузок и методы String и Char ToUpper и ToLower теперь ведут себя так же, как в .NET 2.0/3.0/3.5. Изменения в исходном поведении присутствуют в .NET 4 Beta 1.


Чтобы исправить это, измените метод сравнения строк на перегрузку, которая принимает перечисление System.StringComparison в качестве параметра, и укажите либо Ordinal, либо OrdinalIgnoreCase.

// string contains 'unicode dash' \x2D
string text = "\xAD\x2D"; 

// woks in .NET 2.0/3.0/3.5 and .NET 4 Beta 1 and later
// but seems be buggy in .NET 4 because of 'culture-sensitive' comparison        
int index = text.IndexOf(text); 

// fixed version
index = text.IndexOf(text, StringComparison.Ordinal); 

Ответ 3

Из документации (мой акцент):

Этот метод выполняет поиск слов (чувствительный к регистру и чувствительный к культуре) с использованием текущей культуры.

Т.е. некоторые отдельные кодовые точки будут считаться равными.

Что произойдет, если вы используете перегрузку, которая принимает значение StringComparison и передает StringComparison.Ordinal, чтобы избежать культурных зависимостей?