Generic Operators

Namespace: MiscUtil
Types involved: Operator, Operator<T>

Note: these classes were written by Marc Gravell. I claim no credit for their brilliance! Jon.

Background

The Operator class provides high-performance support for the common operators (+, -, *, etc), but using generics - meaning it can for for any type with suitable operators defined. For a full discussion of why this isn't provided by default, see the generic operators discussion page.

With regular C# you might use the operators, for instance:

int c = a + b;

There is no native equivalent for generics; however, the Operator class provides an alternative:

T c = Operator.Add(a,b);

The Add method is actually a generic method:

public static T Add<T>(T a, T b)
{
    ...
}

The compiler is able to use type inference to work out the type of Tautomatically. The system uses the Expression class from .NET 3.5, which uses standard operators declared on types, as well as IL-level operators (such as Int32 or Single) and Nullable<T> - making it very versatile.

Nullable<T>

A brief aside for Nullable<T>; recall that the rules for lifted operators with Nullable<T> mean that the result of a lifted operator with a null operand is generally null. You should give consideration to how you want to treat nulls, and code accordingly. Fortunately, this is simple, and the JIT-compiler does a good job of removing unnecessary defensive code.

Also; commonly code needs to consider zero - particularly as the seed for sum operations; however, default(T) is not zero if T is nullable - it is null. To help with this, an Operator<T>.Zero static property is provided, which will be the true zero for T.

Example

The two following examples illustrate most of the key points for working with the Operator class; they are heavily simplified (but generic) implementations of the Sum() and Average() methods from LINQ.

First we'll consider Add(); this discards any null values found and returns the sum of the non-null values; if no (non-null) values are found, zero is returned in all cases. This can be implemented very simply:

public static T Sum<T>(this IEnumerable<T> source)
{
    T sum = Operator<T>.Zero;
    foreach (T value in source)
    {
        if (value != null)
        {
            sum = Operator.Add(sum, value);
        }
    }
    return sum;
}

We start at zero, and simply accumulate the values. If called with a non-nullable struct type (Int32 for example), then the "if" test will be removed by the JIT, simply calling the "true" portion (since such a value can never be null).

As a slightly more involved example, we'll consider Average(); like Sum() this also discards any null values, and returns the mean average (sum divided by the number of non-null values). Since an integer division (T divided by Int32) is a very common requirement, an additional Operator method is provided for convenience.

Also, note that the behaviour for the empty-case (where no non-null values are found) varies: if T is nullable, it returns null; otherwise it throws an exception.

public static T Average<T>(this IEnumerable<T> source) 
{
    int count = 0;
    T sum = Operator<T>.Zero;
    foreach (T value in source)
    {
        if (value != null)
        {
            sum = Operator.Add(sum, value);
            count++;
        }
    }
    if (count == 0)
    {
        sum = default(T);
        if (sum != null)
        {
            throw new InvalidOperationException();
        }
        return sum;
    }
    else
    {
        return Operator.DivideInt32(sum, count);
    }
}

As per the expected behaviour, this starts similar to Sum(), but also tracking the count of the values. If we have a non-zero count, we use DivideInt32() to obtain the average. Otherwise we use default(T); if this is null, then T is nullable and we can return this value (null); if not, throw an exception.

While these examples illustrate the ease of use, you are not limited to these operations.

Supported Operators

All of the most common operators are available:

Mathematical (single type)

Equality/inequality (also consider Comparer<T> and EqualityComparer<T>):

Bitwise:

Conversion:

If a critical operator has been missed (and can be sensibly defined in generic terms) then please let me know.

For advanced scenarios, there is also limited support for operators with very different operand types (think DateTime + Timespan => DateTime). Sometimes you can design around the need to use them, but a limited subset are provided for convenience:

Mathematical (dual type):

Limitations

The main slight weakness here is that is cannot provide compile-time safety; it won't attempt to resolve the operators until runtime. In reality this is rarely an issue, and is comparable to saying that Comparer<T>.Default is unreliable because T might not implement IComparable<T> (etc). It is true, but then: you aren't likely to try and call Sum() (etc) on such a type.

It doesn't support scenarios where the return is unrelated to either operand - i.e. TReturn Operator(TArg1, TArg3) {...}. Again, this is unlikely to be an issue in reality; if somebody has a genuine use-case for this, please let me know!


Back to the main MiscUtil page.