16 November

C# 11 – Generic Math Support

min. read

Reading Time: 4 minutes
C# 11
C# 11

Let’s list all the features that C# 11 brings to the table. Let’s discuss them all one by one, with their use cases, and see how they can come in handy. At the bottom of each article, you can find a link to all 14 new C# features!

Generic Math Support

This feature can be called multiple names, either Generic Math Support or Abstraction over static members. I for this article use the first one because it’s mostly that. This is a complex topic but its usages are simple and neat!

Let’s start with what’s a problem with the current C# 10 implementation.

The problem

Let’s consider the following example. We have an array of “numbers” and we assumed it should be all of the type int. And we want to take the biggest number unless it’s bigger than 10, then we want the sum of all of them.

var numbers = new int[] { 1, 2, 3, 4 };

Console.WriteLine(IsBiggerOrSum(numbers));

int IsBiggerOrSum(int[] values)
{
    int sum = 0;
    int max = 0;
    foreach (int value in values)
    {
        sum += value;
        if(value > max)
            max = value;
    }
    return sum > 10 ? sum : max;
}

The code is pretty straightforward and self-explanatory. But now we expanded our scope. The data we got from some files with our numbers is no longer just int. It can contain double as well. What should we do? The only sensible way to handle that is to copy this method with a different signature.

double IsBiggerOrSumDouble(double[] values)
{
    double sum = 0;
    double max = 0;
    foreach (double value in values)
    {
        sum += value;
        if(value > max)
            max = value;
    }
    return sum > 10 ? sum : max;
}

Now the code get’s somewhat out of hand. Not only that we need to have 2 methods to maintain, it also generated a problem in choosing a proper overload should we handle it as doubles or as ints?

The generic problems

We are still considering this as C# 10 or before. We might consider writing this method as generic. So let’s do that. You replace your variable sum with T and you get this beauty.

Cannot convert initializer type 'int' to target type 'T'

So, how to deal with that? We can declare our T as struct! We can also add IComparable<T> which would also help! But here we are stuck at sum += value;

T IsBiggerOrSum<T>(T[] values) where T : struct, IComparable<T>
{
    T sum = default(T);
    T max = default(T);
    foreach (T value in values)
    {
        sum += value; // Cannot apply operator '+=' to operands of type 'T' and 'T'
        if(value.CompareTo(max) > 0)
            max = value;
    }
    return sum > 10 ? sum : max; // Cannot apply operator '>' to operands of type 'T' and 'int'
}

And there is not really anything else than just some fancy switching with double casting to object and to double and then back to object to cast it back to T. What fun!

T IsBiggerOrSum<T>(T[] values) where T : struct, IComparable<T>
{
    T sum = default(T);
    T max = default(T);
    foreach (T value in values)
    {
        switch (value)
        {
            case int i:
                sum = (T)(object)((int)(object)sum + i);
                break;
            case double d:
                sum = (T)(object)((double)(object)sum + d);
                break;
            default:
                throw new NotSupportedException();
        }
        if(value.CompareTo(max) > 0)
            max = value;
    }
    switch (sum)
    {
        case int i:
            return i > 10 ? sum : max;
        case double d:
            return d > 10 ? sum : max;
        default:
            throw new NotSupportedException();
    }
}

C# 11 to the rescue

T IsBiggerOrSum<T>(T[] values, T cmpMax) where T : INumber<T>
{
    T sum = T.Zero;
    T max = T.Zero;
    foreach (T value in values)
    {
        sum += value;
        if(value.CompareTo(max) > 0)
            max = value;
    }
    return sum > cmpMax ? sum : max;
}

Unfortunately in order to compare our last check to the number we need to force move it to cmpMax. There is also an option to use T.Parse(“10”) which as you might have guessed might not be the most optimal use case.

Real-world use case

You really might think that this has no use to you, but I will prove at least some of you wrong! In Game Development we almost always want to set some value with defined parameters like Min, Max, and Current. And this value sometimes should be of type float and sometimes of type int.

Imagine a game like The Witcher. We have our character that has statistics like Intelligence, Strenght, or Dexterity, which is usually described as int. As we would Add or Remove one point at a time, there should be no way to have this statistic partial. But an integer would not be enough for mana, or HP as when we get damage to our character we want to keep all the fractions to make all possible combinations of weapons, and statistically valid and precise.

Before that, we had to have 2 huge classes with some generic magic between them. Now we can create a clean implementation of that exact class.

class Statistic<T> where T : INumber<T>
{
    
    public T Min { get; set; }
    public T Max { get; set; }
    public T Current { get; set; }
    
    public Statistic(T min, T max, T current)
    {
        Min = min;
        Max = max;
        Current = current;
    }
    
    public T Add(T value)
    {
        var temp = Current + value;
        if (temp.CompareTo(Min) < 0)
            temp = Min;
        if (temp.CompareTo(Max) > 0)
            temp = Max;
        Current = temp;
        return Current;
    }
    
    public T Subtract(T value)
    {
        var temp = Current - value;
        if (temp.CompareTo(Min) < 0)
            temp = Min;
        if (temp.CompareTo(Max) > 0)
            temp = Max;
        Current = temp;
        return Current;
    }
    
    public T Set(T value)
    {
        if (value.CompareTo(Min) < 0)
            value = Min;
        if (value.CompareTo(Max) > 0)
            value = Max;
        Current = value;
        return Current;
    }
}

// Our character class
Statistic<int> Inteligence = new(0, 20, 5);
Statistic<int> Strength = new(0, 20, 5);
Statistic<int> Dexterity = new(0, 20, 5);
Statistic<float> Health = new(0, 100, 100);
Statistic<float> Mana = new(0, 50, 50);

// Deal damage to the player
Health.Subtract(10);

// Heal the player
Health.Add(10);

// Level up the player
Inteligence.Add(1);

Clean and useful!

Further readings on dotnet Github pages


Author

Tomasz Juszczak

CTO /

Technical Lead

Tomasz Juszczak

About prog

Founded in 2016 in Warsaw, Poland. Prographers mission is to help the world put the sofware to work in new ways, through the delivery of custom tailored 3D and web applications to match the needs of the customers.


Let's talk

SEND THE EMAIL