It’s a very rare requirement, but sometimes in .NET you have to create your own primitive and make it behave as close as possible to a native CTS (common type system) type. “That shouldn’t be hard” would be your first thought, until you start considering all the scenarios in which it could be used.
I would very much advise against creating your own primitive type and instead reuse one of the built in types. If none of the built-in types provide the functionality you are after try searching for a nuget. Only when you have exhausted the previous two options then start thinking whether it’s worth it and may be there’s a legitimate reason that type doesn’t exist.
Creating a custom type is fairly simple, but then you need to consider the following:
Whether you want to make it COM visible
How the type is going to perform serialization
Whether its string representation should be customizable
Providing all the operator overloads so it behaves like a built in type.
Implicit and explicit conversions
Comparison to other types
Persistence to various databases
Soon, something which you thought should take no more than 20 lines of code, could well exceed several thousands lines of code!
I once had to implement a Fraction type. I did so by having two fields of type long to accommodate the necessary precision: numerator and denominator (since most of the CPU nowadays are 64 bit using an int32 would unlikely bring any performance benefits).
Be prepared for a lot of “boring” code. What really helped me to organise the code was the use of partial classes. I would have a partial class for each interface or specific functionality I was trying to implement. For example all the operator methods would go into a separate file Function.Operators.cs.
One thing to consider when writing operations methods is that each operations method should also be exposed as a simple static method to ensure maximum compatibility with languages other than C#, that essentially doubles your source code for operations.
Then I would do the same for each of the interfaces it had to implement. And there are a lot of them if you want your type to be at home in .NET:
public readonly partial struct Fraction :
Bear in mind that I didn’t have to care about it being used from COM, otherwise I would have had to add another attribute [ComVisible(true)] and then deal with consequences of having to make it usable from COM.
I also had to provide some default values for the type, similar to what you would find in double, float, int etc:
public static readonly Fraction Zero = new Fraction(0, 1);
public static readonly Fraction One = new Fraction(1, 1);
public static readonly Fraction MinusOne = new Fraction(-1, 1);
public static readonly Fraction SmallestFraction = new Fraction(1, long.MaxValue);
public static readonly Fraction Pi = new Fraction(3126535, 995207);
public const long LargestDenominatorThanCanBeReduced = 3037000499;
Obviously the code was fully covered in unit and performance tests to ensure you don’t break anything along the way and don’t introduce a change that negatively impacts the performance.
Because it was for one of the clients I simply cannot take the code I have written. I have to rewrite the code from memory all over again. I think I’m 50% there.
Leave a Reply