Странное поведение OrderBy Linq
У меня есть список, который заказан с использованием OrderBy()
Функция Linq, которая возвращает IOrderedEnumerable
,
var testList = myList.OrderBy(obj => obj.ParamName);
ParamName - это объект, который может содержать как целое число, так и строку. Вышеуказанный orderBy упорядочивает список на основе целочисленного значения. Теперь я работаю с foreach над testList и изменяю свойство ParamName на некоторую строку, основанную на его целочисленном значении, следующим образом:
using (var sequenceEnum = testList.GetEnumerator())
{
while (sequenceEnum.MoveNext())
{
sequenceEnum.Current.ParamName = GetStringForInteger(int.Parse(Convert.ToString(sequenceEnum.Current.ParamName)));
}
}
Затем произошло следующее: порядок элементов в списке после предыдущего цикла был нарушен, и список был упорядочен на основе присвоенной строки, а не на начальном упорядочении.
Однако порядок сохраняется, когда я использую .ToList()
в сочетании с .OrderBy()
пункт.
Может кто-нибудь помочь мне, что здесь происходит?
Пример выходных данных:
4 ответа
Редактировать: Мы все неправильно поняли вашу проблему. Причина, по которой он сортирует неправильный путь, состоит в том, что вы сравниваете "B" и "AA" и ожидаете, что AA будет после B, как в Excel, что, конечно, не произойдет в алфавитном порядке.
Укажите явный компаратор при упорядочении или преобразуйте ParamName в Int, прежде чем выполнять упорядочение с помощью.
Причина, по которой Linq обычно возвращает элементы IEnumerable, заключается в том, что у него ленивое поведение при оценке. Это означает, что он будет оценивать результат, когда он вам нужен, а не когда вы его создаете.
Вызов ToList заставляет linq оценить результат, чтобы сгенерировать ожидаемый список.
TL; DR будьте очень осторожны при выполнении запросов linq и изменении набора исходных данных перед извлечением результата.
Объект IEnumerable не представляет собой последовательность объектов, он представляет собой алгоритм, необходимый для предоставления вам по запросу первого элемента последовательности в качестве "текущего элемента" и для предоставления вам следующего элемента после текущего элемента.
Когда был изобретен linq, было решено, что в linq используется концепция отложенного выполнения, часто называемая отложенной оценкой. В описании MSDN перечисляемых функций, которые используют отложенное выполнение, вы найдете следующую фразу:
Этот метод реализован с использованием отложенного выполнения. Немедленное возвращаемое значение - это объект, в котором хранится вся информация, необходимая для выполнения действия. Запрос, представленный этим методом, не выполняется до тех пор, пока объект не будет перечислен ни путем непосредственного вызова его метода GetEnumerator, ни с помощью foreach.
Если вы создаете IEnumerable и изменяете объекты, с которыми работает объект IEnumerable, это изменение может повлиять на результат. Это сравнимо с функцией, которая возвращает другое значение, если параметры, на которые действует функция, изменены:
int x = 4;
int y = 5;
int MyFunction()
{
return x + y;
}
int a = MyFunction();
y = 7;
int b = MyFunction();
Теперь б не равно а. Похоже на ваш IEnumerable:
List<...> myList = CreateMySequence()
var IEnumerable<...> myOrder = myList.OrderBy(...);
myOrder не содержит результат, но похож на функцию, которая может вычислить результат для него. Если вы измените один из параметров, которые использует myOrder, результат может измениться:
myList.Add(someElement);
var myResult = myOrder.ToList();
myResult изменился, потому что вы изменили функцию.
Причина, по которой было изобретено отложенное выполнение, состоит в том, что довольно часто вам не нужно перечислять все элементы последовательности. В следующих случаях было бы напрасной тратой времени на обработку, если бы вы создали полную последовательность:
- Я хочу только первый элемент,
- Я хочу пропустить 3 элемента, а затем взять два элемента,
- Я хочу первый элемент со значением х
- Я хочу знать, содержит ли последовательность какой-либо элемент вообще
Конечно, есть функции, которые должны создать полную последовательность, как только вы запросите первый элемент:
- Если вы хотите, чтобы первое в отсортированной последовательности, все элементы должны быть отсортированы, чтобы найти первый.
- Если вы хотите первый элемент группы элементов, где все элементы в группе имеют одинаковое значение определенного свойства X (Enumerable.GroupBy)
Как правило, целесообразно сохранять все последовательности как IEnumerable как можно дольше, пока вам не понадобятся результаты или пока не будут изменены источники, используемые для создания последовательности.
Последнее важно при получении данных из базы данных, из файла, из Интернета: вам нужно будет создать последовательность, прежде чем ваше соединение будет закрыто.
Следующее не будет работать
using (var myDbContext = new MyDbContext)
{
return MyDbContext.Customers.Where(customer => customer.Age > 18);
}
Запрос к базе данных не выполняется до того, как вы удалили myDbContext при выходе из оператора using. Поэтому вы получите исключение, как только вы попросите любой элемент в последовательности.
Причина заключается в отдельном выполнении запросов в EF, это означает, что фактический запрос к БД не выполняется до тех пор, пока вы явно не загрузите его в память, например, с помощью.ToList().
Как вы хорошо сказали,.OrderBy() возвращает IOrderedEnumerable, который работает с идиомой foreach. Так почему бы не упростить это сделать что-то вроде следующего?
foreach(var item in testList)
{
item.ParamName = GetStringForInteger(int.Parse(Convert.ToString(item.ParamName)));
}
Как все здесь упоминали, это потому, что Linq лениво оценивают. Вы можете прочитать больше здесь: https://blogs.msdn.microsoft.com/ericwhite/2006/10/04/lazy-evaluation-and-in-contrast-eager-evaluation/
Что вы хотите сделать, это, вероятно, это:
var testList = myList.OrderBy(obj => obj.ParamName).Select(obj =>
{
obj.ParamName = GetStringForInteger(int.Parse(Convert.ToString(obj.ParamName)));
return obj;
});