Как сделать агрегаты Linq, когда может быть пустой набор?
У меня есть набор Linq Things
, где Thing
имеет свойство Amount
(десятичное).
Я пытаюсь сделать агрегат для этого для определенного подмножества вещей:
var total = myThings.Sum(t => t.Amount);
и это работает хорошо. Но затем я добавил условие, которое оставило меня без Вещей в результате:
var total = myThings.Where(t => t.OtherProperty == 123).Sum(t => t.Amount);
И вместо получения total = 0 или null, я получаю сообщение об ошибке:
System.InvalidOperationException: нулевое значение не может быть присвоено член с типом System.Decimal, который является типом с недействительным значением.
Это действительно противно, потому что я не ожидал такого поведения. Я бы ожидал, что итоговое значение будет равно нулю, может быть, ноль - но, конечно же, не будет генерировать исключение!
Что я делаю неправильно? Какое обходное решение/исправление?
EDIT - пример
Спасибо всем за ваши комментарии. Здесь некоторый код, скопированный и вставленный (не упрощенный). Это LinqToSql (возможно, поэтому вы не смогли воспроизвести мою проблему):
var claims = Claim.Where(cl => cl.ID < 0);
var count = claims.Count(); // count=0
var sum = claims.Sum(cl => cl.ClaimedAmount); // throws exception
Ответы
Ответ 1
Я могу воспроизвести вашу проблему со следующим запросом LINQPad против Northwind:
Employees.Where(e => e.EmployeeID == -999).Sum(e => e.EmployeeID)
Здесь есть две проблемы:
-
Sum()
перегружен
- LINQ to SQL следует семантике SQL, а не семантике С#.
В SQL, SUM(no rows)
возвращает null
, а не ноль. Однако вывод типа для вашего запроса дает decimal
как параметр типа, а не decimal?
. Исправление состоит в том, чтобы помочь ввести тип вывода, выбрать правильный тип, т.е.:
Employees.Where(e => e.EmployeeID == -999).Sum(e => (int?)e.EmployeeID)
Теперь будет использована правильная перегрузка Sum()
.
Ответ 2
Чтобы получить результат с нулевым значением, вам нужно указать сумму в тип с нулевым значением, а затем обработать случай Sum
, возвращающий значение null.
decimal total = myThings.Sum(t => (decimal?)t.Amount) ?? 0;
Еще один вопрос, посвященный обоснованию (ir).
Ответ 3
он выдает исключение, потому что результат объединенного запроса sql равен null, и этот атрибут не может быть назначен десятичному var. Если вы сделали следующее, то ваша переменная была бы нулевой (я предполагаю, что ClaimedAmount десятичный):
var claims = Claim.Where(cl => cl.ID < 0);
var count = claims.Count(); // count=0
var sum = claims.Sum(cl => cl.ClaimedAmount as decimal?);
тогда вы должны получить желаемую функциональность.
Вы также можете сделать ToList() в точке оператора where, а затем сумма вернет 0, но это может испортить то, что было сказано в другом месте об агрегатах LINQ.
Ответ 4
Кажется, что лучше всего придерживаться чего-то простого, как
decimal total = decimal.Zero;
foreach (Thing myThing in myThings) {
if (myThing.OtherProperty == 123) {
total = total + myThing.Amount;
}
}
Кроме того, этот пример работает для меня (как предложил Крейг)
Использование этого класса...
public class Location
{
public string Map { get; set; }
public int Top { get; set; }
public int Left { get; set; }
}
И эта настройка...
List<Location> myThings = new List<Location>();
myThings.Add(new Location()
{
Map = "A",
Top = 10,
Left = 10
});
var total = myThings.Where(t => t.Map == "B").Sum(t => t.Top);
Получите в общей сложности 0.
Ответ 5
Если t имеет свойство, подобное "HasValue", тогда я бы изменил выражение на:
var total =
myThings.Where(t => (t.HasValue) && (t.OtherProperty == 123)).Sum(t => t.Amount);