Boris Eetgerink

October 2, 2022

.NET 7 generic math and rounding integer divisions

.NET has the well known feature/bug that dividing two integers returns a rounded integer instead of a double. I'm interested in how that rounding is handled in .NET 7's new generic math.

Traditional implementation

Dividing two integers returns a rounded integer. Rounding is done towards zero, effectively chopping off the decimal places:

int average = (1 + 5 + 10) / 3; // 5

Just setting the return type to a double won't work, as the division result is first rounded to an int and then cast to a double:

double average = (1 + 5 + 10) / 3; // 5

Returning a double from a division only works if one of the operands of the division is also a double:

double average = (1 + 5 + 10) / 3d; // 5.333...

To use as an example, I'll convert the average calculation into a function and force it to return a double:

double average = Average(1, 5, 10); // 5.333...

double Average(params int[] numbers) => numbers.Sum() / (double) numbers.Length;

I know I could've used the Linq extension method numbers.Average(), which returns a double, but let's just say that this could be a much more complicated calculation and I'm just using average as an example.

The reason for introducing generic math is that if I want to average double numbers instead of integers, I'd have to make another function accepting doubles instead of integers:

double average = Average(1, 5, 10); // 5.333...

double Average(params double[] numbers) => numbers.Sum() / numbers.Length;

The implementation is slightly different. numbers.Sum() now returns a double, so casting one of the operands of the division is no longer required.

Generic math implementation

At the time of writing, I am working with Visual Studio 17.4 preview 2.1 and .NET 7 RC1.

When researching generic math I found out that Linq is only partially supported. That makes the generic implementation somewhat more complicated than the traditional implementation:

using System.Numerics;

int averageInt = Average<int, int>(1, 5, 10); // 5
double averageDouble = Average<int, double>(1, 5, 10); // 5.333...

TResult Average<TInput, TResult>(params TInput[] numbers)
    where TInput : INumber<TInput>
    where TResult : INumber<TResult> =>
    TResult.CreateChecked(numbers.Aggregate(TInput.Zero, (agg, t) => agg + t)) / TResult.CreateChecked(numbers.Length);

As you can see I can now set the return type to an int, which rounds the result again. I don't want that, I want to force the caller of the function to have a floating point number as the return type. Luckily I can. There's not just the INumber<T> interface, but also the IFloatingPoint<T> interface, which is implemented by the double number type, amongst others. Changing the generic constraint results in the following code:

using System.Numerics;

// int averageInt = Average<int, int>(1, 5, 10); // won't compile
double averageDouble = Average<int, double>(1, 5, 10); // 5.333...

TResult Average<TInput, TResult>(params TInput[] numbers)
    where TInput : INumber<TInput>
    where TResult : IFloatingPoint<TResult> =>
    TResult.CreateChecked(numbers.Aggregate(TInput.Zero, (agg, t) => agg + t)) / TResult.CreateChecked(numbers.Length);

Conclusion

I respect the amount of work that went into implementing generic math, but it feels only partially done. Linq is so ubiquitous in .NET code, that partial support for generic math feels like a huge oversight from the .NET team. I hope full support for Linq is added in .NET 8!

As for integer division in generic math: It has to be backwards compatible with the traditional implementation, so you still have to decide how to deal with it in your own code. It's great however that you can force a specific implementation by changing a generic constraint.

About Boris Eetgerink

Hey, thanks for reading my blog. Subscribe below for future posts like this one and check my personal site in order to contact me.