Summing a sequence dynamically
In an early draft of the book, most of the section demonstrating the use of dynamic typing in purely managed code was dedicated to the task of summing a sequence dynamically. A change to the behaviour of the language made me revisit that section and amend it to include a wider variety of examples, but the task of dynamic summation is still interesting enough to be worth documenting. This article will go into some of the possible options. To make it more approachable for visitors who don't have the book, I'll start from scratch rather than just continuing from where the book left off. (This also means if there are any areas in the book which didn't make sense to you, there's a chance that this page will provide an alternative explanation which may "click" better.)
What are we trying to achieve?
Enumerable.Sum
in LINQ to Objects has many, many overloads - and even so, it won't work for everything. For example,
we might want to sum TimeSpan
values - or short integers, or unsigned integers - but none
of those scenarios is catered for in the framework. Let's see if we can do better. It would be nice
if we could express a statically typed generic solution with a constraint that the type must
have a public static T operator +(T, T)
operator, but that option isn't available
to us. Instead, we'll use dynamic typing.
Choosing a signature: trying IEnumerable<dynamic>
The first choice we need to make is what signature we want for our DynamicSum
method.
One option would be to use IEnumerable<dynamic>
as a parameter, and return
dynamic
as well. You would think this would be easy, but it has two significant issues:
- The most common element types we want to sum are value types - and as
generic variance doesn't apply to value types, we can't convert from (say)
IEnumerable<int>
toIEnumerable<dynamic>
. We'd have to use something likevalues.Select(x => (dynamic) x).Sum()
which isn't very pleasant, or build a collection usingdynamic
from the start. - There's no natural "zero" value. It's easy enough if we've got at least one value in the sequence, but otherwise what should we return? The integer 0? The double 0.0? The decimal 0.0m? A null reference? We could specify a zero value as another argument, of course... but again, it's slightly annoying.
For the sake of showing what the code might look like, let's take the zero
value as another parameter. Another alternative might be to throw an exception if
the sequence is empty, as methods like Max
do. Here's an implementation:
public static class DynamicExtensions
{
public static dynamic DynamicSum(this IEnumerable<dynamic> source, dynamic zero)
{
dynamic total = zero;
foreach (var item in source)
{
total = total + item;
}
return total;
}
}
That looks about as simple as it gets - although many of the corner cases we'll run into later also apply here. Let's try it out:
public class Test
{
public static void Main()
{
TimeSpan oneHour = TimeSpan.FromHours(1);
TimeSpan tenMinutes = TimeSpan.FromMinutes(10);
TimeSpan threeSeconds = TimeSpan.FromSeconds(3);
dynamic[] values = new dynamic[] { oneHour, tenMinutes, threeSeconds};
dynamic timeSpanSum = values.DynamicSum(TimeSpan.Zero);
Console.WriteLine("Sum with 'zero' as time span: {0}", timeSpanSum);
DateTime now = DateTime.Now;
Console.WriteLine("Current date/time: {0}", now);
dynamic dateTimeSum = values.DynamicSum(now);
Console.WriteLine("Sum with 'zero' as date/time: {0}", dateTimeSum);
}
}
Here we're using a fairly natural "zero" value the first time - TimeSpan.Zero
.
In the second call, we're providing the current date and time. It may not sound like
a zero to you or me, but it's good enough for the method - it's happy to add TimeSpan
values to DateTime
values, with the return value being another DateTime
at each step. Here are the results of one run:
Sum with 'zero' as time span: 01:10:03 Current date/time: 04/11/2010 07:39:24 Sum with 'zero' as date/time: 04/11/2010 08:49:27
That's as far as I want to go with IEnumerable<dynamic>
.
Let's take a different approach now.
Summing a statically typed sequence dynamically
Okay, so we'll restrict ourselve to the more common requirement of summing values all of one
type (or at least of a compatible type). Now we'll be okay to sum a byte array, or a
List<TimeSpan>
or whatever.
We still have the knotty question of what to use as a zero point, but at least now we know the source and result type, we can take the default value for that type. This does have some downsides, as we'll see later, but let's keep it simple to start with.
public static T DynamicSum<T>(this IEnumerable<T> source)
{
T total = default(T);
foreach (dynamic item in source)
{
total = total + item;
}
return total;
}
This is almost the same code as we had before, but using T
in most of the
places we had dynamic
before. In fact, dynamic
only comes up in a
single place: the declaration of item
.
Unfortunately, this code isn't quite right. It breaks if you pass it a byte array, with this error (at execution time):
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Cannot implicitly convert type 'int' to 'byte'. An explicit conversion exists (are you missing a cast?) at CallSite.Target(Closure , CallSite , Object ) at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0) at DynamicExtensions.DynamicSum[T](IEnumerable`1 source) at Test.Main()
The problem is this line:
total = total + item;
The type of the expression on the right-hand side of the assignment operator is dynamic
,
because it's the result of a dynamic addition. We're then trying to assign it back to total
.
Now when we try to add two bytes together, the result is an int
- and there's no implicit
conversion from byte
to int
. There's an explicit conversion though, so we
can get round the problem by just including that:
public static T DynamicSum<T>(this IEnumerable<T> source)
{
T total = default(T);
foreach (dynamic item in source)
{
total = (T) (total + item);
}
return total;
}
Note that you'd have to do exactly the same thing in a statically typed method summing an array of bytes... unless you used something like this:
total += item;
The C# spec explicitly translates a compound assignment expression like this to have an implicit
cast. Unfortunately, although this worked in early betas of .NET 4 in a dynamic context, it doesn't
work now. Arguably the more explicit version shows what we're doing more clearly anyway. In particular,
it naturally suggests that the cast could fail for some reason. In our dynamic case, this could happen
if we had an addition operator which returned a different type, or (as in our byte
and int
case) if the result ended up out of the range of the original type. Whether you
would want overflow to cause an exception or not is a design decision - you might even want to have
two methods, one with a checked
block and one with an unchecked
block,
to let the caller decide.
Handling nullable types
The overloads for Enumerable.Sum
which take sequences of nullable types (such as
IEnumerable<int?>
) don't behave quite the same way as the others. If we were to
add every item in the sequence, the result would be null - because adding anything to
a null value results in a null value, according to the rules of C# lifted operators on nullable
types.
Instead, the normal LINQ to Objects methods just skip any null values. Even though the return types of the methods are nullable, the returned value is never null, even if the input sequence is empty or only contains null values.
We could try to approach this by making the addition conditional on the item being null in a simple way:
// Warning: unhelpful code. See note below.
public static T DynamicSum<T>(this IEnumerable<T> source)
{
T total = default(T);
foreach (dynamic item in source)
{
if (item != null)
{
total = (T) (total + item);
}
}
return total;
}
Unfortunately, this doesn't work properly when using nullable types because the
total
variable ends up with the value of null
to start with - so
even if the sequence doesn't contain any null values, we'll end up with a null result.
We could fix this to some extent by finding the first non-null value and then summing any non-null values thereafter - but we'd still have the problem of what to do with an empty sequence, or one only containing null values. To follow the LINQ to Objects model, this should still return the relevant zero value, instead of null.
The simplest approach is just to add an overload for the nullable value case, leaving the original code in place. If we make the type parameter the non-nullable type, but accept a sequence of the nullable type, we can get the appropriate zero value as normal:
public static T? DynamicSum<T>(this IEnumerable<T?> source)
where T : struct
{
T total = default(T);
foreach (T? item in source)
{
if (item != null)
{
dynamic value = item.Value;
total = (T) (total + value);
}
}
return total;
}
That does leave one problem: if we call DynamicSum
from a generic method which doesn't know
whether or not it's dealing with a nullable type. That sounds like a job for dynamic typing, and indeed
we can call the right method appropriately at execution time, like this:
public static dynamic VeryDynamicSum(this object source)
{
dynamic dynamicSource = source;
return DynamicSum(dynamicSource);
}
Note that we can't make the source
parameter dynamic, as you can't write extension methods
targeting dynamic
... but targeting object
is almost as good.
I'd argue that the name could be better chosen, admittedly...
Conclusion
There are, of course, other approaches that we could take when dynamically summing a sequence. Hopefully this article has at least provided you with a few thoughts about the kind of issues you should think about when dealing with dynamic typing.