2010-03-16 11 views
17

Tengo una colección de Linq de Things, donde Thing tiene una propiedad Amount (decimal).¿Cómo hacer agregados Linq cuando puede haber un conjunto vacío?

estoy tratando de hacer un agregado en esto por un cierto subconjunto de las Cosas:

var total = myThings.Sum(t => t.Amount); 

y que funciona muy bien. Pero luego he añadido una condición que me dejó sin cosas en el resultado:

var total = myThings.Where(t => t.OtherProperty == 123).Sum(t => t.Amount); 

Y en vez de conseguir Total = 0 o nula, me sale un error:

System.InvalidOperationException: The null value cannot be assigned to a member with type System.Decimal which is a non-nullable value type.

Eso es realmente desagradable, porque no esperaba ese comportamiento. Hubiera esperado que el total fuera cero, tal vez nulo, ¡pero sin duda no arrojaría una excepción!

¿Qué estoy haciendo mal? ¿Cuál es la solución/solución?

EDITAR - ejemplo

Gracias a todos por sus comentarios. Aquí hay un código, copiado y pegado (no simplificado). Es LinqToSql (tal vez por eso no se podía reproducir mi problema):

var claims = Claim.Where(cl => cl.ID < 0); 
var count = claims.Count(); // count=0 
var sum = claims.Sum(cl => cl.ClaimedAmount); // throws exception 
+1

Guau, ¡eso * es * realmente desagradable! Si esa es la forma en que Linq se define con conjuntos de resultados vacíos, fue una elección triste por parte de los diseñadores del lenguaje, ya que requerirá CADA uso de un agregado para ser envuelto en una prueba para un conjunto vacío. – MtnViewMark

+2

Ayudaría si mostrara los tipos explícitamente. Acabo de probar 'new decimal [] {1} .Where (i => i! = 1) .Sum()' en LINQPad y obtuve 0, como se esperaba. –

+0

@ Craig Stuntz - esto puede deberse a que no está accediendo a una propiedad en su Suma() - es decir, no puede manejar ningún resultado con Suma(), pero no con Suma (t => t.Tallar) como t.La suma es ser llamado en "t", que es nulo. – Fenton

Respuesta

22

puedo reproducir el problema con la siguiente consulta LINQPad contra Neptuno:

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

Hay dos cuestiones aquí:

  1. Sum() está sobrecargado
  2. LINQ a SQL sigue la semántica de SQL, no Semántica C#.

En SQL, SUM(no rows) devuelve null, no cero. Sin embargo, la inferencia de tipo para su consulta le da decimal como parámetro de tipo, en lugar de decimal?. La solución es ayudar a la inferencia de tipos seleccionar el tipo correcto, es decir .:

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

Ahora se utilizará el correcto Sum() sobrecarga.

+0

+1 - y vea mi comentario a @Leom Burke que creo que esto fue un error de diseño por parte de Microsoft. Es obvio que el tipo deseado es "decimal", por lo que forzarme a declararlo explícitamente es realmente tonto. –

+0

No está claro qué debería hacer L2S con la sobrecarga 'decimal' para' Suma', dado que sigue la semántica de SQL. El compilador de C#, OTOH, ciertamente * no debería * usar la sobrecarga 'decimal?' Porque algún proveedor de LINQ aleatorio podría no seguir la semántica de C#. El compilador está haciendo lo correcto, ya que no le ha dado ningún tipo de pistas. La respuesta de L2S es al menos discutible. –

+0

+ crédito de respuesta - estaba vacilando entre usted y @Leom Burke, porque ambos respondieron bien. Pero usted ha dado muchos más antecedentes y explicaciones, para que pueda obtener el crédito. ¡Gracias! :) –

0

Casi parece mejor seguir con algo tan simple como

decimal total = decimal.Zero; 

foreach (Thing myThing in myThings) { 
    if (myThing.OtherProperty == 123) { 
     total = total + myThing.Amount; 
    } 
} 

excepción, este ejemplo funciona para mí (como se sugiere Craig)

el uso de esta clase ...

public class Location 
{ 
    public string Map { get; set; } 
    public int Top { get; set; } 
    public int Left { get; set; } 
} 

Y esto creó ...

 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); 

CONSEGUIRLE de un total de 0.

+2

No, usted * no debería * hacer esto, ya que no usará la implementación 'Suma' del proveedor de consultas. Por ejemplo, con LINQ to SQL, buscará todas las filas en el cliente en lugar de hacer una 'SUMA' en el servidor. –

+0

@ Craig Stuntz: no estoy seguro del nivel de detalle que existe en la pregunta para confirmar que su afirmación es verdadera. – Fenton

+1

Lo que escribí es cierto en general, no solo para esta pregunta: ** ¡No reinvente los agregados de LINQ! ** Se implementan de la forma en que lo están por muy buenas razones. –

-1

Si T tiene una propiedad como un 'HasValue', entonces me gustaría cambiar la expresión de:

var total = 
    myThings.Where(t => (t.HasValue) && (t.OtherProperty == 123)).Sum(t => t.Amount); 
+2

Definitivamente no. El punto es que el conjunto está vacío: ¡no hay una "t" para llamar a HasValue! –

2

lanza una excepción porque el El resultado de la consulta sql combinada es nulo y no se puede asignar a la var. decimal. Si lo hizo el siguiente entonces su variable sería nula (supongo ClaimedAmount es decimal):

var claims = Claim.Where(cl => cl.ID < 0); 
var count = claims.Count(); // count=0 
var sum = claims.Sum(cl => cl.ClaimedAmount as decimal?); 

entonces usted debe conseguir la funcionalidad que usted desea.

También podría hacer ToList() en el punto de la instrucción where y luego la suma devolvería 0, pero eso no coincidiría con lo que se ha dicho en otros lugares sobre los agregados LINQ.

+0

+1 Tienes razón, pero eso es muy desagradable para la MS para lanzar una excepción allí. ClaimedAmount se define como un decimal, así que ¿por qué deberías declararlo como "decimal"? solo para que pueda tenerlo en un agregado? Eso es tonto. –

+0

No tiene que * declarar * como 'decimal?'. ¡Sin embargo, * tú * puedes * ** moldearlo! –

+0

Sí, eso es lo que quise decir ... :) –

5

Para obtener un resultado que no admite nulos, debe convertir el importe a un tipo que admite nulos y, luego, manejar el caso de Sum devolviendo nulo.

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

Hay otra pregunta dedicada al (ir)rationale.

Cuestiones relacionadas