Generic Math Capabilities With .NET 7

February 19, 2024#Software Development
Article
Author image.

Scott DePouw, Senior Consultant

Every now and then I need to do some math in my code, for some required business logic rule or other purpose. As an easy example, let’s apply a discount to a given number. To further reduce complexity, we’ll just toss it in an extension method:

namespace Sandbox;

public static class MathExtensions
{
  public static decimal ApplyDiscount(this decimal price, decimal discount)
  {
    price * discount; // Our super complex logic
  }
}

// In our code
decimal amountCharged = 5.00m.ApplyDiscount(.5); // $2.50

What if we had to apply our same discounting business rules to more than just decimal types, though? You can, of course, just implement your logic a second time:

public static int ApplyDiscount(this int price, int discount)
{
  return price * discount;
}

You can repeat this for any other types that come down your development pipeline that require support. Naturally, we don’t like repeating ourselves since our business logic could change, and it’d be a maintenance headache if we only remembered to change it in five out of the six locations we put it.

From .NET 7 onward, we have a way out of this repetitive nightmare!

.NET 7 and Generic Math

The concept of generic math was introduced in .NET 7. In particular we’re going to use ISignedNumber<T> and adjust our code to use that instead.

using System.Numerics;

namespace Sandbox;

public static class MathExtensions
{
  public static T ApplyDiscount<T>(this T price, T discount)
    where T : ISignedNumber<T>
  {
    return price * discount;
  }
}

// In our code
decimal amountCharged = 5.00m.ApplyDiscount(.5); // $2.50
int amountChargedFlat = 20.ApplyDiscount(2); // $40.00; poor customer!

Generally-speaking, you can use ISignedNumber<T> to implement operations between two values of the same numeric type (int, float, decimal, et al). Most online examples use Add to demonstrate this, but I decided to have a little fun and do multiplication instead (which admittedly is just a shortcut for addition, but I digress).

ISignedNumber<T> vs. INumber<T>

There is an INumber<T> interface which works similarly, providing mathematical operations to a generic set of numeric types. This was the first interface I used when discovering this capability, but I quickly discovered that char implements INumber<T> too! It wouldn’t make a whole lot of sense to see 'A'.ApplyDiscount('B') as a possibility in our code.

You may have a use for it depending on your needs, but if you want to restrict your generic math operations to actual numbers, stick with ISignedNumber<T>, as char does not implement it.

Non-Math Operations

Taking advantage of the interface can let you implement other operations that only care about having a number, regardless of type.

public class PriceNotificaitonService
{
  public void NotifyOfPriceChange<T>(T newPrice)
    where T : ISignedNumber<T>
  {
    Console.WriteLine($"The new price is {newPrice:C}!");
  }
}

// Program.cs (console example)
PriceNotificationService service = new();
service.NotifyOfPriceChange(42); // int
service.NotifyOfPriceChange(2.44f); // float
service.NotifyOfPriceChange(28.99m); // decimal

Output:

The new price is $42.00!
The new price is $2.44!
The new price is $28.99!

Limitations

Generic math variables can only operate among themselves. Notice in our example we weren’t passing in a decimal for the second parameter. Normally C# is perfectly happy performing math between, say, an int and a decimal:

decimal amountCharged = 50 * .5m; // $25.00

But if you try that with a generic T where T : ISignedNumber<T> argument, it won’t compile:

public static TSignedNumber ApplyDiscount<TSignedNumber>(this TSignedNumber price, decimal discount)
  where TSignedNumber : ISignedNumber<TSignedNumber>
{
  // Error CS0019 : Operator '*' cannot be applied to operands of type 'TSignedNumber' and 'decimal'
  return price * discount;
}

If you’re working with same-typed numbers and need to apply similar business logic across types, generic math with ISignedNumber<T> has you covered.


Copyright © 2024 NimblePros - All Rights Reserved