Почему сопоставитель строк по умолчанию не поддерживает переходную последовательность?

Я знаю, что эта проблема была отмечена до, более или менее сжатой, но я все же создаю этот новый поток, потому что снова столкнулся с проблемой при написании unit test.

Сравнение строк по умолчанию (то есть сопоставимое с культурой сравнение с регистром, которое мы получаем с string.CompareTo(string), Comparer<string>.Default, StringComparer.CurrentCulture, string.Compare(string, string) и другими), нарушает транзитивность, когда строки содержат дефисы (или минус знаки, я говорю о простых символах U + 002D).

Вот простой пример:

static void Main()
{
  const string a = "fk-";
  const string b = "-fk";
  const string c = "Fk";

  Console.WriteLine(a.CompareTo(b));  // "-1"
  Console.WriteLine(b.CompareTo(c));  // "-1"
  Console.WriteLine(a.CompareTo(c));  // "1"

  var listX = new List<string> { a, b, c, };
  var listY = new List<string> { c, a, b, };
  var listZ = new List<string> { b, c, a, };
  listX.Sort();
  listY.Sort();
  listZ.Sort();
  Console.WriteLine(listX.SequenceEqual(listY));  // "False"
  Console.WriteLine(listY.SequenceEqual(listZ));  // "False"
  Console.WriteLine(listX.SequenceEqual(listZ));  // "False"
}

В верхней части мы видим, как сбой транзитивности. a меньше b, а b меньше c, но a не может быть меньше c.

Это противоречит документированному поведению в кодировке Unicode, которая гласит, что:

... для любых строк A, B и C, если A < B и B < C, то A < С.

Теперь сортировка списка с помощью a, b и c в точности аналогична попытке присвоить руки "Rock" , "Paper" и "Ножницы" в известной непереходной игре. Невозможная задача.

Последняя часть моего примера кода показывает, что результат сортировки зависит от начального порядка элементов (и в списке нет двух элементов, которые сравнивают "равно" (0)).

Linq listX.OrderBy(x => x) также зависит, конечно. Это должно быть стабильным, но вы получаете странные результаты при заказе коллекции, содержащей a, b и c вместе с другими строками.

Я попробовал это со всеми CultureInfo на моей машине (так как это зависит от культуры), включая "инвариантную культуру", и каждая из них имеет ту же проблему. Я пробовал это с .NET 4.5.1, но я считаю, что более старые версии имеют одинаковую ошибку.

Заключение: при сортировке строк в .NET с помощью сравнения по умолчанию результаты непредсказуемы, если в некоторых строках содержатся дефисы.

Какие изменения были внесены в .NET 4.0, вызвавшие это поведение?

Уже было замечено, что это поведение непоследовательно в разных версиях платформы: в .NET 3.5 строки с дефисами могут быть надежно отсортированы. Во всех версиях структуры вызов System.Globalization.CultureInfo.CurrentCulture.CompareInfo.GetSortKey предоставляет уникальные DeyData для этих строк, поэтому почему они не отсортированы правильно?

Ответ 1

Обсуждение Microsoft Connect Вот несколько способов для обхода:

static int CompareStringUsingSortKey(string s1, string s2)
{
    SortKey sk1 = CultureInfo.InvariantCulture.CompareInfo.GetSortKey(s1);
    SortKey sk2 = CultureInfo.InvariantCulture.CompareInfo.GetSortKey(s2);
    return SortKey.Compare(sk1, sk2);
}

Ответ 2

Технически вы сравниваете fk, fk и, uhh, Fk друг с другом. Какой из них вы бы выбрали?

Наборы символов включают в себя не знающие символы. Метод Compare (String, String) не учитывает такие символы, когда он выполняет культурно-чувствительное сравнение. Например, если следующий код выполняется на .NET Framework 4 или более поздней версии, чувствительное к культуре сравнение "животного" с "ani-mal" (с использованием мягкого дефиса или U + 00AD) указывает на то, что две строки являются эквивалент.

using System;

public class Example
{
    static void Main()
    {
          string s1 = "ani\u00ADmal";
          string s2 = "animal";

              Console.WriteLine("Comparison of '{0}' and '{1}': {2}", 
                    s1, s2, String.Compare(s1, s2));
     }
}
// The example displays the following output: 
//       Comparison of 'ani-mal' and 'animal': 0

Чтобы распознать невежественные символы в сравнении строк, вызовите метод Compare (String, String, StringComparison) и поставьте значение CompareOptions.Ordinal или CompareOptions.OrdinalIgnoreCase для параметра compareType.

Подробнее: http://msdn.microsoft.com/en-us/library/84787k22 (v = vs .110).aspx