Как выполнить агрегаты Linq, когда может быть пустой набор?

17

У меня есть коллекция 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
    
задан Shaul Behr 16.03.2010 в 16:08
источник
  • Вау - это действительно противно! Если это так, как Linq определяется с пустым набором результатов, это был печальный выбор дизайнеров языка, так как это потребует КАЖДОГО использования агрегата, который будет завернут в тест для пустого набора. –  MtnViewMark 16.03.2010 в 16:11
  • Это поможет, если вы явно указали типы. Я просто попробовал новый десятичный знак [] {1}. Где (i => i! = 1) .Sum () в LINQPad и получил 0, как и ожидалось. –  Craig Stuntz 16.03.2010 в 16:20
  • @Craig Stuntz - я тестировал, используя что-то более сложное (объект «Location» (строка Map, int Top, int Left), и он все еще работал для меня - так же, как вы сказали: List <Location> myThings = new Список <Location> (); myThings.Add (новое местоположение () {Map="A", Top = 10, Left = 10}); var total = myThings.Where (t => t.Map == "B" ) .Sum (t => t.Top); –  Fenton 16.03.2010 в 16:30
  • Нет ничего плохого в коде, который опубликовал Шауль. Ему нужно опубликовать некоторый код, который действительно сломан, чтобы мы могли диагностировать реальную проблему. –  LukeH 16.03.2010 в 16:32
  • Обновление опубликовано - похоже, проблема в LinqToSql –  Shaul Behr 16.03.2010 в 18:07
Показать остальные комментарии

4 ответа

23

Я могу воспроизвести вашу проблему со следующим запросом LINQPad против Northwind:

Employees.Where(e => e.EmployeeID == -999).Sum(e => e.EmployeeID)

Здесь есть две проблемы:

  1. Sum() перегружено
  2. LINQ to SQL следует семантике SQL, а не семантике C #.

В SQL SUM(no rows) возвращает null , а не ноль. Однако вывод типа для вашего запроса дает вам decimal в качестве параметра типа вместо decimal? . Исправление состоит в том, чтобы помочь ввести тип вывода, выбрать правильный тип, т. Е.:

Employees.Where(e => e.EmployeeID == -999).Sum(e => (int?)e.EmployeeID)

Теперь будет использована правильная перегрузка Sum() .

    
ответ дан Craig Stuntz 16.03.2010 в 18:31
источник
  • +1 - и см. мой комментарий к @Leom Burke, что я думаю, что это была ошибка дизайна со стороны Microsoft. Очевидно, что желаемый тип «десятичный?», Поэтому заставить меня явно объявить его действительно глупо. –  Shaul Behr 16.03.2010 в 18:40
  • Неясно, что L2S следует делать с десятичной перегрузкой для Sum, учитывая, что она следует за семантикой SQL. Компилятор C #, OTOH, конечно же, не должен использовать десятичную дробь? перегрузка, поскольку некоторый случайный поставщик LINQ может не следовать семантике C #. Компилятор делает все правильно, так как вы не указали никаких намеков на тип. Ответ L2S, по крайней мере, спорный. –  Craig Stuntz 16.03.2010 в 18:52
  • + answer credit - колебался между вами и @Leom Burke, потому что вы оба правильно ответили. Но вы дали намного больше фона и объяснений, так что вы получаете кредит. Благодаря! :) –  Shaul Behr 16.03.2010 в 19:05
5

Чтобы получить результат, не содержащий NULL, вам нужно указать сумму в тип с нулевым значением, а затем обработать случай Sum , возвращающий значение null.

decimal total = myThings.Sum(t => (decimal?)t.Amount) ?? 0;

Есть еще один вопрос, посвященный обоснованию (ir) .

    
ответ дан Edward Brey 12.01.2014 в 01:49
источник
2

он выдает исключение, потому что результат объединенного запроса 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.

    
ответ дан Leom Burke 16.03.2010 в 18:26
источник
  • +1 Вы правы - но это очень противно MS, чтобы исключить исключение. ClaimedAmount определяется как десятичное число, поэтому почему вы должны объявить его «десятичным?»? просто чтобы вы могли получить его в совокупности? Это глупо. –  Shaul Behr 16.03.2010 в 18:37
  • Вам не нужно объявлять его как десятичную? Хотя вы можете бросить его! –  Craig Stuntz 16.03.2010 в 18:58
  • Да, вот что я имел в виду ... :) –  Shaul Behr 16.03.2010 в 19:03
0

Кажется, что лучше всего придерживаться чего-то простого, как

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.

    
ответ дан Fenton 16.03.2010 в 16:23
источник
-1

Если t имеет свойство, подобное «HasValue», тогда я бы изменил выражение на:

var total = 
     myThings.Where(t => (t.HasValue) && (t.OtherProperty == 123)).Sum(t => t.Amount); 
    
ответ дан DotNetWala 16.03.2010 в 16:30
источник
  • Определенно нет. Все дело в том, что набор пуст - нет «t» для вызова HasValue! –  Shaul Behr 16.03.2010 в 18:07