Search code examples
c#types

How to create type-safe numerical types?


Context

Suppose I'm writing an app about cakes.
I need to store the weight of cakes in kg and their FCR (frosting/chocolate ratio. I made that up). I can store these values as float. The problem I see with that is that I can assign a weight-in-kg value to an FCR field.

C#'s type system can prevent errors like this. If I create a class WeightInKg and a class FrostingChocolateRatio, I won't be able to assign one to the other.

Issue

I will then need to implement all numerical operators (+ - * / > < == etc) again. These are already annoying to implement, because they are only mere wrappers over the functionality of float. However, as these structs are both based on float, all those wrapper methods are virtually identical. This will again be the case for every other such float-based type.

What I have tried / thought about

  • Good old OO inheritance. An abstract class FloatValue can provide all the numerical operations, but these then return the FloatValue type instead of the (type safe) subclass type. I also feel like a struct should be used for something that is inherently a bare-bones value, and structs don't support sub-classes.
  • Generics. I am currently using this; a struct Quantity<T> with all the numerical operations implemented. For T, I then use empty "marker classes", which only exist to identify a certain type of quantity (e.g. FrostingChocolateRatio). This works, but constantly using Quantity<WhatIReallyWant> is awkward and produces more visual clutter in the code.

Question

Is this as close as I can get to what I want, or are there cleaner ways to have such type-safe values in C#?

Addition

As PaulF mentioned, FrostingChocolateRatio is not an ideal example because the math works differently for ratios. However, I'm out of creativity for today; just assume it does exactly the same as WeightInKg.
The point is that there are several different types of values which behave exactly like float, but it doesn't make sense to add a centimeter to a liter.


Solution

  • Found this on a whim again today after, wow, 6 years - and two things have changed in the meantime.

    The first is my experience as a developer. Back then, copy-pasting the same code to several different parts felt "wrong". Having a numeric value which you e.g. can not divide felt incomplete.
    From my current point of view, however... We were not trying to write a feature complete number library. We needed a handful of operations for 2 or 3 different types of values. Copy-pasting the code where needed was absolutely sufficient, and the most time-efficient way to solve this problem.

    The second part is on the technical side. In .NET Core, the default .NET numeric types use a long list of interfaces now, each of which describes a small subset of the possible operations. This is perfect to pick and choose the parts actually needed in this situation:

      public readonly struct UInt64 : 
        IComparable,
        IConvertible,
        ISpanFormattable,
        IFormattable,
        IComparable<ulong>,
        IEquatable<ulong>,
        IBinaryInteger<ulong>,
        IBinaryNumber<ulong>,
        IBitwiseOperators<ulong, ulong, ulong>,
        INumber<ulong>,
        IComparisonOperators<ulong, ulong, bool>,
        IEqualityOperators<ulong, ulong, bool>,
        IModulusOperators<ulong, ulong, ulong>,
        INumberBase<ulong>,
        IAdditionOperators<ulong, ulong, ulong>,
        IAdditiveIdentity<ulong, ulong>,
        IDecrementOperators<ulong>,
        IDivisionOperators<ulong, ulong, ulong>,
        IIncrementOperators<ulong>,
        IMultiplicativeIdentity<ulong, ulong>,
        IMultiplyOperators<ulong, ulong, ulong>,
        ISpanParsable<ulong>,
        IParsable<ulong>,
        ISubtractionOperators<ulong, ulong, ulong>,
        IUnaryPlusOperators<ulong, ulong>,
        IUnaryNegationOperators<ulong, ulong>,
        IUtf8SpanFormattable,
        IUtf8SpanParsable<ulong>,
        IShiftOperators<ulong, int, ulong>,
        IMinMaxValue<ulong>,
        IUnsignedNumber<ulong>,
        IBinaryIntegerParseAndFormatInfo<ulong>