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.
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.
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.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.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#?
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.
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>