James Michael Hare

...hare-brained ideas from the realm of software development...
posts - 136 , comments - 1095 , trackbacks - 0

My Links

News

Welcome to my blog! I'm a Sr. Software Development Engineer in Seattle, WA. I've been doing C++/C#/Java development for over 18 years, but have definitely learned that there is always more to learn!

All thoughts and opinions expressed in my blog and my comments are my own and do not represent the thoughts of my employer.

Blogs I Read

MCC Logo MVP Logo

Follow BlkRabbitCoder on Twitter

Tag Cloud

Archives

Post Categories

C#/.NET Little Wonders: Five Easy Sequence Aggregators

Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders post can be found here.

Today we will look at five easy ways to aggregate sequences.  Often times when we’re looking at a sequence of objects, we want to do perform some sort of aggregation across those sequences to find a calculated result from the sequence.

The methods we will be looking at are LINQ extension methods from the Enumerable static class which do just that.  Like most of the LINQ extension methods discussed before, these operate on IEnumerable<TSource> sequences.

Sum() – calculate the total value across sequence

So, as you’d expect, the Sum() method in the Enumerable static class is useful for computing the total of values in a sequence.  This can be done in one of two ways depending on the form of the extension method invoked (of course, ignoring the implicit source parameter):

  • Sum()
    • Sums the values of the sequence.
    • Source type must be one of the following types: int, long, single, double, decimal or a Nullable wrapped variant.
  • Sum(Func<TSource, X> projection)
    • Sums the results of a projection on items in the sequence.
    • Technically, X must be one of the following types: int, long, single, double, decimal or Nullable wrapped variant.
    • However, if the projection is a lambda or anonymous delegate it can infer int from narrower numeric types.

Notice a few things here.  First of all, even though many types in C# support addition, the Sum() method – no projection - only supports int, long, single, double, and decimal.  

   1: // works for any sequences of allowed numeric types
   2: double[] data = { 3.14, 2.72, 1.99, 2.32 };
   3: var result = data.Sum();
   4:  
   5: // does NOT work for sequences of disallowed numeric types
   6: short[] shortData = { 1, 2, 5, 7 };
   7:  
   8: // Compiler ERROR: no form of Sum() exists for short, uint, etc
   9: var shortResult = shortData.Sum();

Also notice that you can operate on the Nullable versions of the allowed types.  Now we know that nullable math can be a tricky thing in .NET, but with Sum() we need not worry, because all null values are excluded from the summation:

   1: // notice a nullable sequence of int
   2: var data = new List<int?> { 1, 3, 9, 13, null, 7, 12, null };
   3:  
   4: // answer will be 45 since nulls disregarded
   5: var result = data.Sum();

The second, projection form is a bit more interesting.  Sometimes, it’s possible we do have sequences of numbers to add up directly, but often times we have sequences of more complex objects, and we want to sum up a particular field.  This is where the second form that takes a projection comes in handy.  By specifying a projection, we are telling Sum() how to extract the field we want to sum from each item in the sequence.

To illustrate, let’s assume a simple sequence of a simple POCO called Employee:

   1: public sealed class Employee
   2: {
   3:     public string Name { get; set; }
   4:     public double Salary { get; set; }
   5:     public short Dependents { get; set; }
   6:  
   7:     // etc...
   8: }

    Now, let’s say that we have a sequence of these employees:

       1: var employees = new List<Employee>
       2:     {
       3:         new Employee { Name = "Bob", Salary = 35000.00, Dependents = 0 },
       4:         new Employee { Name = "Sherry", Salary = 75250.00, Dependents = 1 },
       5:         new Employee { Name = "Kathy", Salary = 32000.50, Dependents = 0 },
       6:         new Employee { Name = "Joe", Salary = 17500.00, Dependents = 2 },
       7:         // etc
       8:     };

    We can then extract the Salary property using a projection and take a sum as follows:

       1: // once again, the projection result type MUST be a supported type
       2: var totalSalary = employees.Sum(e => e.Salary);

    While the projection form of Sum() on the surface seems to be restricted to projections of the same restricted types (int, long, single, double, decimal), the projection form WILL allow projections of smaller types if we use lambda expressions or anonymous delegates.

       1: // these work because the lambda and anonymous delegate return type are inferred
       2: employees.Sum(e => e.Dependents);
       3: employees.Sum(delegate(Employee e) { return e.Dependents; });

    This is because the result of the lambda expression and anonymous delegates can be inferred from usage, and the inference will automatically widen small numeric types (like short) to int.  If we tried to do this with a method group or delegate variable defined with a non-supported return type, however, we would get an error because there is no inference involved, and hence there is no suitable match.

    Average() – returns average value in sequence

    The Average() method works just like its Sum() counterpart, except that it takes the sum and divides it by the number of items involved in the sum.  What do I mean by involved?  Remember that Sum() does not include null values – and Average() does exactly the same.  That is, Average() takes the average of all non-null values, so those null values do not count as part of the total nor the number of items the total is divided by.  For example:

       1: var intList = new int?[] { 10, 20, 30, null };
       2:  
       3: // returns 20, not 15 since only 3 non-null values
       4: Console.WriteLine(intList.Average());

    The behavior for Average() in terms of exceptions and null returns exactly matches the Sum() method.  So we won’t dive into it in any more detail.

    Min() – returns smallest value in sequence

    The Min() extension method is useful for examining a sequence and returning the smallest value from it.  The basic format of Min() looks like (ignoring, again, the implicit source parameter):

    • Min()
      • Finds the smallest value in the sequence.
      • Throws if no object in sequence implements IComparable or IComparable<T>.
      • Throws exception if sequence is empty and source type is a value type.
      • Returns null if sequence is empty and X is reference type or Nullable wrapped value type.
    • Min(Func<TSource, X> projection)
      • Finds the smallest value in the results of a projection on the sequence.
      • Throws if no result of the projection to X implements IComparable or IComparable<T>.
      • Throws exception if sequence is empty and X is value type.
      • Returns null if sequence is empty and X is a reference type or Nullable wrapped value type.

    The Min() method largely works largely as you’d expect it to, except with a few quirks.  First of all Min() tries to support virtually any type as long as that type implements IComparable or IComparable<T>.  Thus it is not restricted to numeric types, like Sum() is, and can be used on any comparable objects (including value types like DateTime, TimeSpan):

       1: var shortList = new short[] { 1, 3, 7, 9, -9, 33 };
       2:  
       3: // returns -9
       4: var smallest = shortList.Min();
       5:  
       6: // find smallest number of dependents 
       7: var minDependents = employees.Min(e => e.Dependents);

    Also, Min() does not use a generic constraint to limit the type parameters to those that support the IComparable  interfaces.  Instead it throws a run-time exception – but only if the sequence is non-empty and no object in it implements one of the IComparable interfaces.  Thus, given our definition of Employee above, the first call below would return null (sequence is empty), and the second call will throw (sequence non-empty but contains no IComparable objects).

       1: // Succeeds: empty sequences yield null for reference and Nullable wrapped value types
       2: var result1 = Enumerable.Empty<Employee>().Min();
       3:  
       4: // Throws: employees is non empty, and Employee does not implement IComparable
       5: var result2 = employees.Min();

    The same thing is true for the result of projections.  If a projection results in no values that are IComparable, and the sequence is not empty it will throw.  Similarly if a projection results in a value type but the sequence is empty, it will throw.

    Finally, note that for value types, if the sequence is empty an exception will be thrown, since it can’t determine a valid minimum from no items and can’t return null:

       1: // throws, int is comparable, but since sequence empty can't determine min value.
       2: var result3 = Enumerable.Empty<int>().Min();
       3:  
       4: // also would throw, even though Employee is reference type, result of projection is 
       5: // value type and the sequence is empty.
       6: var result4 = Enumerable.Empty<Employee>().Min(e => e.Dependents);

     

    Max() – returns largest value in the sequence

    The Max() behaves exactly like Min() except that it returns the largest value as opposed to the smallest value.  So, we can use these to get the largest value in a sequence or the largest value from a projection over a sequence:

       1: // returns 33
       2: var biggestShort = shortList.Max();
       3:  
       4: // returns 75250.0
       5: var highestSalary = employees.Max(e => e.Salary);

    The behavior for Max() in terms of exceptions and null returns exactly matches the Min() method, so let’s not dally on it but instead move on to a really interesting one…

     

    Aggregate() – returns a custom aggregation over a sequence

    So what happens if you have a sequence of values, and you want to perform your own custom aggregation, and none of the other four aggregation methods will do?

    The answer is the Aggregate() method.  This handy extension method lets you perform a custom aggregation upon a sequence.

    There are three forms of Aggregate():

    • Aggregate(Func<TSource, TSource, TSource> function)
      • Applies a function that takes an accumulator value and a next value and returns the result.
      • Both the value and sequence type are same.
      • The seed value is the first value in the sequence.
    • Aggregate(TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> function)
      • Applies a function that takes an accumulator value and the next item in a sequence and returns a result.
      • The value and sequence type can be different or same.
      • A seed value must be supplied to initialize the result.
    • Aggregate(TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> function, Func<TAccumulate, TResult> resultProjection)
      • Same as above but takes in addition an extractor for the result from the accumulator.

    This all probably looks quite complex.  Just remember the basic premise here: the delegate being performed on the sequence always takes a current total and the next value, and returns the new total.  This is then applied iteratively down the sequence.

    For example, if we wanted to do a product (multiplication) of all numbers in a sequence:

       1: var numbers = new int[] { 1, 3, 9, 2 };
       2:  
       3: // the function takes the current total and multiplies it by next to get new total.
       4: var product = numbers.Aggregate((total, next) => total * next);

    Notice that in the code above, our delegate’s first parameter gives us the current total, which we then multiply against the next value and return.  This return value becomes the new total that will be passed into the same delegate called on the next value and so forth.  Also note that the seed value in this form of the aggregate is the first value in the sequence (in our case 1).

    But what if the sequence we are using contains more complex objects and we want to perform an aggregation off of one of its fields?  We can use the 2nd or 3rd form where the accumulator type and item type can differ.  These versions do not use a projection like the other aggregation methods, but instead just give you the whole item and it’s up to you to decide how that item is combined with the total.

       1: // perhaps we want to get a total value equal to the employees
       2: // salary divided by the number of dependents (and self) they have
       3: var weirdCalculation = employees.Aggregate(0.0, 
       4:     (result, next) => result + next.Salary / (next.Dependents + 1));

     

    So you see, we can do quite complex aggregate calculations across a sequence if we choose!  The key thing to remember is that the function you provide leaves you in charge of taking the next “item” and applying it to the running “total”.

    In fact, I usually like to name my lambda parameters result and next or total and item to help distinguish what each parameter is instead of relying on single letter lambda names, since this is definitely one of the more confusing aggregate methods in terms or readability.

    Summary

    So there you have it, five very easy aggregation methods.  Well, four easy and one that may be a little more complex, yet quite powerful!  These methods make it easy to perform aggregations on your sequences so that you don’t have to code the loops and calculations yourself.  They’re quick, they’re easy to use, they’re easy to read, and they’re fully tested.  Enjoy!

     

      Technorati Tags: ,, , , , , , , ,

    Print | posted on Thursday, August 25, 2011 9:05 PM | Filed Under [ My Blog C# Software .NET Little Wonders ]

    Feedback

    Gravatar

    # re: C#/.NET Little Wonders: Five Easy Sequence Aggregators

    In the min example, shouldn't it be Enumerable.Empty<Employee>().min();
    You are missing .min()...:)
    8/26/2011 12:26 AM | Arish
    Gravatar

    # re: C#/.NET Little Wonders: Five Easy Sequence Aggregators

    What Arish said, but for

    var minDependents = employees(e => e.Dependents);

    too.
    8/26/2011 2:54 AM | Eric Bolton
    Gravatar

    # re: C#/.NET Little Wonders: Five Easy Sequence Aggregators

    @Arish, @Eric: Thanks for the catch! The Live Writer code plugin is kinda a bear to edit after the fact at times :-)
    8/26/2011 6:28 AM | James Michael Hare
    Gravatar

    # re: C#/.NET Little Wonders: Five Easy Sequence Aggregators

    One thing to note -- The values used in Aggregate need not be numeric. I frequently use it with a list of objects and a StringBuilder as the TAccumulate
    8/26/2011 10:00 AM | James Curran
    Gravatar

    # re: C#/.NET Little Wonders: Five Easy Sequence Aggregators

    @James: Very true! Since you're in charge of the aggregation in the delegate, you have a lot of flexibility in the values you can use and results you can create.
    8/26/2011 10:03 AM | James Michael Hare
    Post A Comment
    Title:
    Name:
    Email:
    Comment:
    Verification:
     
     

    Powered by: