List.Sort в С#: вызывается сопоставитель с нулевым объектом
Я получаю странное поведение, используя встроенную функцию С# List.Sort с пользовательским сопоставлением.
По какой-то причине он иногда вызывает метод сравнения класса сравнения с нулевым объектом в качестве одного из параметров. Но если я проверю список с отладчиком, в коллекции нет нулевых объектов.
Мой класс сравнения выглядит следующим образом:
public class DelegateToComparer<T> : IComparer<T>
{
private readonly Func<T,T,int> _comparer;
public int Compare(T x, T y)
{
return _comparer(x, y);
}
public DelegateToComparer(Func<T, T, int> comparer)
{
_comparer = comparer;
}
}
Это позволяет передать делегат методу List.Sort, например:
mylist.Sort(new DelegateToComparer<MyClass>(
(x, y) => {
return x.SomeProp.CompareTo(y.SomeProp);
});
Таким образом, вышеупомянутый делегат будет генерировать исключение нулевой ссылки для параметра x, даже если никакие элементы mylist не равны.
UPDATE: Да, я абсолютно уверен, что это параметр x, который бросает исключение нулевой ссылки!
UPDATE: Вместо использования метода Framework List.Sort я попробовал собственный метод сортировки (т.е. новый BubbleSort(). Sort (mylist)), и проблема исчезла. Как я и подозревал, метод List.Sort по какой-то причине передает значение null.
Ответы
Ответ 1
Эта проблема возникает, когда функция сравнения не согласована, так что x < y не всегда означает y < Икс. В вашем примере вы должны проверить, как сравниваются два экземпляра типа SomeProp.
Вот пример, который воспроизводит проблему. Здесь он вызван функцией патологического сравнения "compareStrings". Это зависит от начального состояния списка: если вы изменили начальный порядок на "C", "B", "A", то исключений не будет.
Я бы не назвал это ошибкой в функции Сортировка - это просто требование совместимости функции сравнения.
using System.Collections.Generic;
class Program
{
static void Main()
{
var letters = new List<string>{"B","C","A"};
letters.Sort(CompareStrings);
}
private static int CompareStrings(string l, string r)
{
if (l == "B")
return -1;
return l.CompareTo(r);
}
}
Ответ 2
Вы уверены, что проблема не в том, что SomeProp
есть null
?
В частности, со строками или значениями Nullable<T>
.
С помощью строк лучше использовать:
list.Sort((x, y) => string.Compare(x.SomeProp, y.SomeProp));
(редактировать)
Для нулевой безопасной оболочки вы можете использовать Comparer<T>.Default
- например, для сортировки списка по свойству:
using System;
using System.Collections.Generic;
public static class ListExt {
public static void Sort<TSource, TValue>(
this List<TSource> list,
Func<TSource, TValue> selector) {
if (list == null) throw new ArgumentNullException("list");
if (selector == null) throw new ArgumentNullException("selector");
var comparer = Comparer<TValue>.Default;
list.Sort((x,y) => comparer.Compare(selector(x), selector(y)));
}
}
class SomeType {
public override string ToString() { return SomeProp; }
public string SomeProp { get; set; }
static void Main() {
var list = new List<SomeType> {
new SomeType { SomeProp = "def"},
new SomeType { SomeProp = null},
new SomeType { SomeProp = "abc"},
new SomeType { SomeProp = "ghi"},
};
list.Sort(x => x.SomeProp);
list.ForEach(Console.WriteLine);
}
}
Ответ 3
Я тоже столкнулся с этой проблемой (пустая ссылка передана моей пользовательской реализации IComparer) и, наконец, выяснила, что проблема связана с использованием непоследовательной функции сравнения.
Это была моя первоначальная реализация IComparer:
public class NumericStringComparer : IComparer<String>
{
public int Compare(string x, string y)
{
float xNumber, yNumber;
if (!float.TryParse(x, out xNumber))
{
return -1;
}
if (!float.TryParse(y, out yNumber))
{
return -1;
}
if (xNumber == yNumber)
{
return 0;
}
else
{
return (xNumber > yNumber) ? 1 : -1;
}
}
}
Ошибка в этом коде заключалась в том, что Compare будет возвращать -1 всякий раз, когда одно из значений не может быть правильно проанализировано (в моем случае это было вызвано неправильным форматированием строковых представлений числовых значений, чтобы TryParse всегда терпело неудачу).
Обратите внимание, что в случае, если оба x и y были отформатированы неправильно (и, следовательно, TryParse не удалось выполнить оба из них), вызов Compare (x, y) и Compare (y, x) даст тот же результат: -1. Это я считаю главной проблемой. При отладке Compare() будет передан пустой указатель на строку как один из его аргументов в какой-то момент, даже если сортировка коллекции не будет содержать пустую строку.
Как только я исправил проблему TryParse и обеспечил согласованность моей реализации, проблема исчезла, а Compare больше не передавали нулевые указатели.
Ответ 4
Ответ на вопрос полезен. Я согласен с ним в том, что NullReference связано с вызовом CompareTo на свойство null. Без использования класса расширения вы можете:
mylist.Sort((x, y) =>
(Comparer<SomePropType>.Default.Compare(x.SomeProp, y.SomeProp)));
где SomePropType - тип SomeProp
Ответ 5
Для целей отладки вы хотите, чтобы ваш метод был нулевым. (или, по крайней мере, поймать исключение null-ref) и обработать его каким-то жестко-кодированным способом). Затем используйте отладчик, чтобы посмотреть, какие другие значения будут сравниваться, в каком порядке и какие вызовы будут успешными или неудачными.
Затем вы найдете свой ответ, и затем вы можете удалить нуль-безопасность.
Ответ 6
Вы можете запустить этот код...
mylst.Sort((i, j) =>
{
Debug.Assert(i.SomeProp != null && j.SomeProp != null);
return i.SomeProp.CompareTo(j.SomeProp);
}
);
Ответ 7
Я сам наткнулся на эту проблему и обнаружил, что это связано с свойством NaN
в моих вводах. Здесь минимальный тестовый пример, который должен приводить к исключению:
public class C {
double v;
public static void Main() {
var test =
new List<C> { new C { v = 0d },
new C { v = Double.NaN },
new C { v = 1d } };
test.Sort((d1, d2) => (int)(d1.v - d2.v));
}
}