ПРОВЕРИТЬ ОБНОВЛЕНИЕ 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.
