ПРОВЕРИТЬ ОБНОВЛЕНИЕ 3 ниже Я узнал, что проблема, с которой я столкнулся, связана с известной серьезной проблемой с С# -строчными сопоставлениями для .Net 4.0, 4.0 client и 4.5, что приведет к несогласованному порядку сортировки списков строк (что приведет к тому, что вывод будет зависеть от порядка в используемый вход и алгоритм сортировки). Проблема была сообщена Microsoft в декабре 2012 года и закрыта как "не будет исправлена". Доступна работа, но она намного медленнее, что вряд ли практично для больших коллекций.
При внедрении неизменяемого PatriciaTrie я хотел сравнить его производительность с System.Collections.Generic.SortedList. Я использовал следующий файл https://github.com/rkapsi/patricia-trie/blob/master/src/test/resources/org/ardverk/collection/hamlet.txt, чтобы создать список слов для тестирования.
При вставке каждого слова из С# SortedList, используя Comparer<string>.Default
или StringComparer.InvariantCulture
в качестве сопоставителя клавиш, количество вставленных записей не может быть восстановлено с использованием обычных методов поиска (например, ContainsKey
возвращает false), но ключ присутствует в списке, как это наблюдается при повторении списка.
Еще более любопытно, что компаратор возвращает значение "0" при сравнении ключа, полученного из отсортированного списка, с помощью ключа поиска, который невозможно найти с помощью ContainsKey
.
Полный пример ниже демонстрирует эту проблему в моей системе.
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
// the problem is possibly related to comparison.
var fail = true;
var comparer = fail ? StringComparer.InvariantCulture : StringComparer.Ordinal;
// read hamlet (contains duplicate words)
var words = File
.ReadAllLines("hamlet.txt")
.SelectMany(l => l.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
.Select(w => w.Trim())
.Where(w => !string.IsNullOrEmpty(w))
.Distinct(comparer)
.ToArray();
// insert hamlet words in the sorted list.
var list = new SortedList<string, int>(comparer);
var ndx = 0;
foreach (var word in words)
list[word] = ndx++;
// search for each of the added words.
foreach (var keyToSearch in words)
{
if (!list.ContainsKey(keyToSearch))
{
// was inserted, but cannot be retrieved.
Console.WriteLine("Error - Key not found: \"{0}\"", keyToSearch);
// however when we iterate over the list, we see that the entry is present
var prefix = keyToSearch.Substring(0, Math.Min(keyToSearch.Length, 3));
foreach (var wordCloseToSearchKey in list.Keys.Where(s => s.StartsWith(prefix)))
{
// and using the SortedList supplied comparison returns 0, signaling equality
var comparisonResult = list.Comparer.Compare(wordCloseToSearchKey, keyToSearch);
Console.WriteLine("{0} - comparison result = {1}", wordCloseToSearchKey, comparisonResult);
}
}
}
// Check that sort order of List.Keys is correct
var keys = list.Keys.ToArray();
BinarySearchAll("list.Keys", keys, list.Comparer);
CheckCorrectSortOrder("list.Keys", keys, list.Comparer);
// Check that sort order of Array.Sort(List.Keys) is correct
var arraySortedKeys = CopySortSearchAndCheck("Array.Sort(List.Keys)", keys, list.Comparer);
// Check that sort order of the Array.Sort(input words) is correct
var sortedInput = CopySortSearchAndCheck("Array.Sort(input words)", words, list.Comparer);
Console.ReadLine();
}
static string[] CopySortSearchAndCheck(string arrayDesc, string[] input, IComparer<string> comparer)
{
// copy input
var sortedInput = new string[input.Length];
Array.Copy(input, sortedInput, sortedInput.Length);
// sort it
Array.Sort(sortedInput, comparer);
// check that we can actually find the keys in the array using bin. search
BinarySearchAll(arrayDesc, sortedInput, comparer);
// check that sort order is correct
CheckCorrectSortOrder(arrayDesc, sortedInput, comparer);
return sortedInput;
}
static void BinarySearchAll(string arrayDesc, string[] sortedInput, IComparer<string> comparer)
{
// check that each key in the input can be found using bin. search
foreach (var word in sortedInput)
{
var ix = Array.BinarySearch(sortedInput, word, comparer);
if (ix < 0)
// and it appears it cannot!
Console.WriteLine("Error - {0} - Key not found: \"{1}\"", arrayDesc, word);
}
}
static void CheckCorrectSortOrder(string arrayDesc, string[] sortedKeys, IComparer<string> comparer)
{
for (int n = 0; n < sortedKeys.Length; n++)
{
for (int up = n + 1; up < sortedKeys.Length; up++)
{
var cmp = comparer.Compare(sortedKeys[n], sortedKeys[up]);
if (cmp >= 0)
{
Console.WriteLine(
"{0}[{1}] = \"{2}\" not < than {0}[{3}] = \"{4}\" - cmp = {5}",
arrayDesc, n, sortedKeys[n], up, sortedKeys[up], cmp);
}
}
for (int down = n - 1; down > 0; down--)
{
var cmp = comparer.Compare(sortedKeys[n], sortedKeys[down]);
if (cmp <= 0)
{
Console.WriteLine(
"{0}[{1}] = \"{2}\" not > than {0}[{3}] = \"{4}\" - cmp = {5}",
arrayDesc, n, sortedKeys[n], down, sortedKeys[down], cmp);
}
}
}
}
}
Есть ли у кого-нибудь объяснение этого неожиданного и нечетного поведения?
При изменении сравнения, используемого SortedList, на StringComparer.Ordinal
(например, изменяя fail
на false
в приведенном выше примере) проблема исчезает, что, похоже, указывает на проблему сравнения, но я не совсем понять почему.
ОБНОВЛЕНО Как отметил Себастьян, описанная здесь проблема не обнаруживается в профилях клиентов .Net 3.5 и 3.5. Он работает на .Net 4.0, 4.0 и 4.5.
После некоторого дополнительного копания я заметил, что если я возьму отсортированные ключи из списка, и я запустил Array.BinarySearch
на этих клавишах, он также вернет отрицательные (не найденные) значения для тех же ключей, которые не найдены с помощью SortedList.ContainsKey
. Таким образом, это предполагает, что порядок сортировки ключей неверен.
Если я возьму уже отсортированные ключи из списка и отсортирую их с помощью Array.Sort
, порядок сортировки вывода отличается для ключей, которые были проблематичными.
Итак, я добавил функцию проверки (с помощью сопоставления списка), если порядок сортировки заданного массива правильный (т.е. предыдущий ключ всегда меньше, последующий ключ всегда больше) и ограничил ввод словами, которые отличный от сравнения. Я применил эту функцию на 3 разных входах (все используют один и тот же компаратор):
- Коллекция отсортированных ключей.
- вывод массива Array.Sort на эти клавиши.
- вывод массива Array.Sort на вход из файла.
Результат (2) и (3) идентичен и отличается от (1). Однако выполнение Array.BinarySearch на выходах Array.Sort(2) и (3) снова не может найти одни и те же ключи (возврат < 0). Также функция, которая проверяет правильность порядка сортировки, указывает, что для всех трех случаев порядок сортировки вокруг задействованных проблемных клавиш неверен.
В этот момент я просто надеюсь, что я сделал что-то невероятно глупое, и есть простое объяснение. Надеюсь, кто-то может указать на это.
Код примера обновляется с помощью моих дополнительных экспериментов по устранению неполадок, снимок экрана выводится здесь http://imgur.com/DU8SCsA.
ОБНОВЛЕНИЕ 2 Хорошо, я сузил проблему до того, что мне кажется очень серьезной проблемой с С# строковыми сопоставителями, представленными с .Net 4.0.
Итак, предположим, что у нас есть 3 значения: a1, a2 и a3. Для любой корректной сортировки мы ожидаем, что если a1 < a2
и a2 < a3
, чтобы сравнение было последовательным, как следствие, также имеет место следующее: a1 < a3
.
Однако это не относится к строковым сопоставлениям С# (по крайней мере для Comparer<string>.Default
и StringComparer.InvariantCulture
).
Маленькая программа, приведенная ниже, иллюстрирует эту конкретную проблему:
class Program
{
static void Main(string[] args)
{
var comparer = StringComparer.InvariantCulture;
var a1 = "A";
var a2 = "a\'";
var a3 = "\'a";
PrintComparison("a1", a1, "a2", a2, comparer);
PrintComparison("a2", a2, "a3", a3, comparer);
PrintComparison("a1", a1, "a3", a3, comparer);
Console.ReadLine();
}
public static void PrintComparison(string firstSymbol, string first, string secondSymbol, string second, IComparer<string> comparer)
{
var cmp = comparer.Compare(first, second);
var result = cmp == 0 ? "=" : cmp < 0 ? "<" : ">";
Console.WriteLine("{0} {1} {2} ({3} {1} {4})", firstSymbol, result, secondSymbol, first, second);
}
}
Это его вывод:
a1 < a2 (A < a')
a2 < a3 (a' < 'a)
a1 > a3 (A > 'a)
Вывод, по-видимому, заключается в том, что небезопасно полагаться на порядок сортировки, определенный с помощью компиляторов строки С#, или я что-то не хватает?
ОБНОВЛЕНИЕ 3Эта проблема, по-видимому, была сообщена MS в декабре 2012 года и закрыта статусом "не будет исправлена", что довольно неутешительно; см. ссылку, размещенную в комментариях ниже (кажется, я не могу публиковать здесь из-за моих ограниченных точек репутации). В этом также приведено обходное решение, которое я выполнил и использовал для проверки того, что это действительно устраняет проблемы, наблюдаемые со стандартными компараторами.
public class WorkAroundStringComparer : StringComparer
{
private static readonly Func<CompareInfo, string, CompareOptions, int> _getHashCodeOfString;
private readonly CompareInfo _compareInfo;
private readonly CompareOptions _compareOptions;
static WorkAroundStringComparer()
{
// Need this internal method to compute hashcode
// as an IEqualityComparer implementation.
_getHashCodeOfString = BuildGetHashCodeOfStringDelegate();
}
static Func<CompareInfo, string, CompareOptions, int> BuildGetHashCodeOfStringDelegate()
{
var compareInfoType = typeof(CompareInfo);
var argTypes = new[] { typeof(string), typeof(CompareOptions) };
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
var methods = compareInfoType.GetMethods(flags).ToArray(); ;
var method = compareInfoType.GetMethod("GetHashCodeOfString", flags, null, argTypes, null);
var instance = Expression.Parameter(compareInfoType, "instance");
var stringArg = Expression.Parameter(typeof(string), "string");
var optionsArg = Expression.Parameter(typeof(CompareOptions), "options");
var methodCall = Expression.Call(instance, method, stringArg, optionsArg);
var expr = Expression.Lambda<Func<CompareInfo, string, CompareOptions, int>>(methodCall, instance, stringArg, optionsArg);
return expr.Compile();
}
public WorkAroundStringComparer()
: this(CultureInfo.InvariantCulture)
{
}
public WorkAroundStringComparer(CultureInfo cultureInfo, CompareOptions compareOptions = CompareOptions.None)
{
if (cultureInfo == null)
throw new ArgumentNullException("cultureInfo");
this._compareInfo = cultureInfo.CompareInfo;
this._compareOptions = compareOptions;
}
public override int Compare(string x, string y)
{
if (ReferenceEquals(x, y))
return 0;
if (ReferenceEquals(x, null))
return -1;
if (ReferenceEquals(y, null))
return 1;
var sortKeyFor_x = _compareInfo.GetSortKey(x, _compareOptions);
var sortKeyFor_y = _compareInfo.GetSortKey(y, _compareOptions);
return SortKey.Compare(sortKeyFor_x, sortKeyFor_y);
}
public override bool Equals(string x, string y)
{
return Compare(x, y) == 0;
}
public override int GetHashCode(string obj)
{
return _getHashCodeOfString(_compareInfo, obj, _compareOptions);
}
}
Проблема с этим обходным решением заключается в том, что он вряд ли применим для значительных коллекций, потому что он на порядок медленнее, чем, например, StringComparer.InvariantCulture
.
Время, затрачиваемое при сортировке списка слов 1000 раз с использованием обоих сопоставлений:
StringComparer.InvariantCulture : 00:00:15.3120013
WorkAroundStringComparer : 00:01:35.8322409
Таким образом, я все еще надеюсь, что Microsoft пересмотрит или кто-то знает жизнеспособную альтернативу. В противном случае единственная опция, которая остается, возвращается с использованием StringComparer.Ordinal
.