Introduction
In C#, you cannot create generic methods, or classes, for objects based on their operators (such as +). For example, the following will not compile:
public static T Add<T>(T a, T b)
{
return a+b;
}
This can be a real problem when dealing with data that varies in type, but not (or only slightly) in behaviour. For example, images are commonly stored as bytes (0-255), 16-bit unsigned integers (0-65k), and even as floating point variables. Matrices, similarly, may hold integer-, double-, or decimal-data. However, in C#, there is no simple way to write a generic method or class to process such data.
While some solutions to this problem have been proposed (e.g. http://www.codeproject.com/Articles/8531/Using-generics-for-calculations), these solutions typically require wrappers, helper classes, unintuitive code, and/or poorer performance.
Background
When a generic class is used in a .NET application, it is created as needed, and is as 'real' and unique as any other class in the application. For example, take the generic class Collar<T>
:
public class Collar<T>
{
T Animal{get;set;}
public Collar(T a)
{
Animal = a;
}
}
If we create a Collar<Dog>
and a Collar<Cat>
in our application, the compiler will create the types Collar<Dog>
and a Collar<Cat>
as distinct types. That is to say, that while they may have a similar behaviour and share code (as we see it), they are completely different types from one another (as the runtime sees it).
The reason that we cannot create generic types or generic methods for numerical operations, is that we cannot currently use interfaces to specify operators, such as +, -, /, and *, and we cannot require them using a where
clause like so:
public class Image<T> where T:+,-,*,/
{}
The Solution
The solution to this problem is a custom pre-processing step which utilises a class 'template', very similar to a generic definition, to build pre-specified types that you can use in your code. It injects the code for these types into your project directly, before compilation takes place. These templated types only differ from generics in that they:
- Are named slightly differently (e.g.
Calc_Double
, rather than Calc<Double>
) - Are specified prior to compilation
- Have the ability to use operators
In the first example below, I will automatically generate Image<T>
objects, one of which will hold pixel values as Bytes
, one which will hold int
s, and another which will hold float
s. The images will have a GetPixel
, SetPixel
, and MaxPixel
methods. The second example creates a single class that has mathematical methods for double
s, int
s, and float
s.
Using the Code
Add the Preprocessor
The first step is to tell VS to use the preprocessor attached to this article. Open your .csproj file in Notepad, and scroll to the bottom, where you will see commented text like so:
Delete this comment and write the following:
<Target Name="BeforeBuild">
<Exec WorkingDirectory="FOLDER_OF_PREPROCESSOR"
Command="GenericNumericsPreprocessor.exe "$(MSBuildProjectDirectory)"
"$(MSBuildProjectFullPath)"" />
</Target>
Where FOLDER_OF_PREPROCESSOR
is the directory that holds the built the preprocessor executable (GenericNumericsPreprocessor.exe). You can also paste this executable into the project folder of your project, and leave WorkingDirectory
as an empty string.
Add <T> Placeholder to your Project
As we have seen, we cannot use generics to make methods that use operators such as +, - or * for datatypes
such as int
or float
. Instead, we will use a <T> PlaceHolder type (TPH
), which specifies every operator and conversion that you wish. When the file is preprocessed, everywhere that holds this operator will be replaced by the types that you wish.
As this placeholder specifies all operators we wish to use, it will not result in intellisense-level compilation errors. An example placeholder is in the project attached to this article. It looks like so:
public class TPH : IEquatable<TPH>, IComparable<TPH>
{
#region OPERATORS
public static bool operator !=(TPH c1, TPH c2)
{
return false;
}
public static bool operator <(TPH c1, TPH c2)
{
return false;
}
public static bool operator <=(TPH c1, TPH c2)
{
return false;
}
public static bool operator ==(TPH c1, TPH c2)
{
return false;
}
public static bool operator >=(TPH c1, TPH c2)
{
return false;
}
public static bool operator >(TPH c1, TPH c2)
{
return false;
}
public static TPH operator +(TPH c1, TPH c2)
{
return null;
}
public static TPH operator -(TPH c1, TPH c2)
{
return null;
}
public static TPH operator *(TPH c1, TPH c2)
{
return null;
}
public static TPH operator /(TPH c1, TPH c2)
{
return null;
}
public static TPH operator ++(TPH c1)
{
return null;
}
public static TPH operator --(TPH c1)
{
return null;
}
#endregion
#region Conversions
public static implicit operator TPH(byte d)
{
return null;
}
public static explicit operator TPH(short d)
{
return null;
}
public static implicit operator TPH(int d)
{
return null;
}
public static explicit operator TPH(long d)
{
return null;
}
public static explicit operator TPH(float d)
{
return null;
}
public static explicit operator TPH(double d)
{
return null;
}
#endregion
#region IComparable<ExpandDud> Members
int IComparable<TPH>.CompareTo(TPH other)
{
return 0;
}
#endregion
#region IEquatable<ExpandDud> Members
bool IEquatable<TPH>.Equals(TPH other)
{
return false;
}
#endregion
public override bool Equals(object obj)
{
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
Create a Generic Template
To create a template, create a class as you normally would for your generic class, but
- Do not write
<T>
anywhere - Instead of using
T
for fields, properties, arguments, etc., use TPH
- At the top of the file, write
#pragma expandgeneric
followed by a space-delimited list of types that you wish to create (see example)
There are a few options you can use. For these, see the second example.
For our image classes, we get the following (before build):
#pragma expandgeneric byte int float
namespace PreprocessExample
{
public class Image
{
private TPH[,] pixels;
public Image(int width, int height)
{
pixels = new TPH[width, height];
}
public void SetPixel(int x, int y, TPH val)
{
pixels[x, y] = val;
}
public TPH GetPixel(int x, int y)
{
return pixels[x, y];
}
public TPH GetBrightestPixel()
{
TPH brightest = 0;
foreach (TPH cur in pixels)
{
if (cur > brightest)
{
brightest = cur;
}
}
return brightest;
}
}
Build Your Code
Building your code in the normal way will now create Image_Byte
, Image_Int
, and Image_Float
. They will be appended to the same file as Image
, collapsed in a #region
called GENERIC EXPANSION
.
#pragma expandgeneric byte int float
namespace PreprocessExample
{
public class Image
{
private TPH[,] pixels;
public Image(int width, int height)
{
pixels = new TPH[width, height];
}
public void SetPixel(int x, int y, TPH val)
{
pixels[x, y] = val;
}
public TPH GetPixel(int x, int y)
{
return pixels[x, y];
}
public TPH GetBrightestPixel()
{
TPH brightest = 0;
foreach (TPH cur in pixels)
{
if (cur > brightest)
{
brightest = cur;
}
}
return brightest;
}
}
#region GENERIC EXPANSION
public class Image_Byte
{
private byte[,] pixels;
public Image_Byte(int width, int height)
{
pixels = new byte[width, height];
}
public void SetPixel(int x, int y, byte val)
{
pixels[x, y] = val;
}
public byte GetPixel(int x, int y)
{
return pixels[x, y];
}
public byte GetBrightestPixel()
{
byte brightest = 0;
foreach (byte cur in pixels)
{
if (cur > brightest)
{
brightest = cur;
}
}
return brightest;
}
}
public class Image_Int
{
private int[,] pixels;
public Image_Int(int width, int height)
{
pixels = new int[width, height];
}
public void SetPixel(int x, int y, int val)
{
pixels[x, y] = val;
}
public int GetPixel(int x, int y)
{
return pixels[x, y];
}
public int GetBrightestPixel()
{
int brightest = 0;
foreach (int cur in pixels)
{
if (cur > brightest)
{
brightest = cur;
}
}
return brightest;
}
}
public class Image_Float
{
private float[,] pixels;
public Image_Float(int width, int height)
{
pixels = new float[width, height];
}
public void SetPixel(int x, int y, float val)
{
pixels[x, y] = val;
}
public float GetPixel(int x, int y)
{
return pixels[x, y];
}
public float GetBrightestPixel()
{
float brightest = 0;
foreach (float cur in pixels)
{
if (cur > brightest)
{
brightest = cur;
}
}
return brightest;
}
}
#endregion GENERIC EXPANSION
}
You can use these types now as you would like. Any changes you make to Image
will be reflected in the other types after you next build.
Image_Byte imb = new Image_Byte(50, 50);
imb.SetPixel(1, 1, Byte.MaxValue);
Byte b = imb.GetPixel(1, 1);
Image_Float imf = new Image_Float(50, 50);
imf.SetPixel(1, 1, 0.7f);
float f = imf.GetPixel(1, 1);
Image_Int imi = new Image_Int(50, 50);
imi.SetPixel(1, 1, int.MaxValue);
int i = imi.GetPixel(1, 1);
Example 2: Generic Math
We can create a generic Math
class to do calculations for each primitive type. Here, we will create a MathStuff
class that has the following methods:
IsEven
- An extension method that returns true
if the number specified is even Distance
- Calculates the distance between two points SmallerThanMax
and SmallerThanMin
- Calculates how much larger or smaller a number is than the maximum and minimum values that its type can hold.
Create <T> Placeholder
Like in the previous step, we create a <T
> placeholder. We will only be dealing with numbers here, so let's make this a struct
called Number
. I won't paste the entire code for this, as it is almost identical as TPH
, above, but ensure that Number
can be converted to and from other primitive numbers. Also give it a MinValue
and MaxValue
, just like all other primitive numbers:
struct Number: IEquatable<Number>, IComparable<Number>
{
public static Number MaxValue = 1;
public static Number MinValue = 0;...
public static explicit operator Number(short d)
{ return 0; }
public static explicit operator Number(long d)
{ return 0; }
public static explicit operator Number(float d)
{ return 0; }
public static explicit operator Number(double d)
{ return 0; }
public static implicit operator Number(int d)
{ return 0; }
public static implicit operator Number(byte d)
{ return 0; }
public static explicit operator sbyte(Number d)
{ return 0; }
public static explicit operator short(Number d)
{ return 0; }
public static explicit operator int(Number d)
{ return 0; }
public static explicit operator long(Number d)
{ return 0; }
public static explicit operator byte(Number d)
{ return 0; }
public static explicit operator ushort(Number d)
{ return 0; }
public static explicit operator uint(Number d)
{ return 0; }
public static explicit operator ulong(Number d)
{ return 0; }
public static explicit operator float(Number d)
{ return 0; }
public static implicit operator double(Number d)
{ return 0; }
...
}
Create a Generic Template
For this, we will create our MathStuff
class. At the top of the class, we specify that we want to replace Number
(rather than the default name, TPH), and we want the types double
, float
and int
:
#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
using System;
namespace ExampleUsage
{
static class MathStuff
{
}
}
Now, because MathStuff
is not a class we want renamed to MathStuff_Double
/MathStuff_Float
/etc., we tell the preprocessor to not rename the expansions. Given that copies are going to be made, we need to make the class a partial class, so that the compiler interprets the expansions as a single class.
#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
#pragma expandGeneric noRename
using System;
namespace ExampleUsage
{
static partial class MathStuff
{
}
}
Now we add our methods to MathStuff
:
public static Number Pythagoras(Number side1, Number side2)
{
double result = Math.Sqrt(side1 * side1 +
side2 * side2);
return (Number)result;
}
public static Number Distance(Number x0, Number y0, Number x1, Number y1)
{
Number dist_x = x1 - x0;
Number dist_y = y1 - y0;
Number distance = Pythagoras(dist_x, dist_y);
return distance;
}
public static Number SmallerThanMax(Number n)
{
return Number.MaxValue- n;
}
public static Number LargerThanMin(Number n)
{
return n + Number.MinValue;
}
public static bool IsEven(this Number num)
{
return num % 2 == 0;
}
Build Your Code
We now build our code, and the class is expanded to cover our three primitive types (I have only pasted the first methods of each class here for the sake of brevity):
#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
#pragma expandGeneric noRename
using System;
namespace ExampleUsage
{
static partial class MathStuff
{
public static Number Pythagoras(Number side1, Number side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);
return (Number)result;
}
public static Number Distance(Number x0, Number y0, Number x1, Number y1)
{
Number dist_x = x1 - x0;
Number dist_y = y1 - y0;
Number distance = Pythagoras(dist_x, dist_y);
return distance;
}
public static double ProportionOfMax(Number n)
{
return n / Number.MaxValue;
}
public static double ProportionOfMin(Number n)
{
return n / Number.MinValue;
}
public static bool IsEven(this Number num)
{
return num % 2 == 0;
}
}
#region GENERIC EXPANSION
static partial class MathStuff
{
public static double Pythagoras(double side1, double side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);
return (double)result;
}
...
}
static partial class MathStuff
{
public static float Pythagoras(float side1, float side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);
return (float)result;
}
...
}
static partial class MathStuff
{
public static int Pythagoras(int side1, int side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);
return (int)result;
}
...
}
#endregion GENERIC EXPANSION
}
We can now use these methods:
double evenNo = 8;
if (evenNo.IsEven())
{
Console.WriteLine(evenNo + " is even");
}
int largeInt = 2000000000;
int intProportion = MathStuff.SmallerThanMax(largeInt);
Console.WriteLine(largeInt + " is " + intProportion +
" smaller than the maximum size it can be");
{
float x0 = 0;
float y0 = 0;
float x1 = 5;
float y1 = 5;
float distance_float = MathStuff.Distance(x0, y0, x1, y1);
Console.WriteLine(String.Format("Float: Distance between
{0},{1} and {2},{3}: {4}", x0, y0, x1, y1, distance_float));
}
{
int x0 = 0;
int y0 = 0;
int x1 = 5;
int y1 = 5;
int distance_int = MathStuff.Distance(x0, y0, x1, y1);
Console.WriteLine(String.Format("Int: Distance between {0},{1}
and {2},{3}: {4}", x0, y0, x1, y1, distance_int));
}
Output
8 is even
2000000000 is 147484647 smaller than the maximum size it can be
Float: Distance between 0,0 and 5,5: 7.071068
Int: Distance between 0,0 and 5,5: 7
As you can see from the last line, it may not always be a good idea to use generic operators when creating classes/methods for both floating-point and integer-based numerics.
Leaving Notes
Limitations
I built this preprocessor in only a few hours to speed up development for another project. It is not robust to a 'commercial scale'. In particular, it may fail to work if you:
- Have badly formatted code around the
using
s, namespace definition, and class name - Have nested namespaces within your code file
The preprocessing program is small, simple, and heavily commented. It would be very simple to add or modify functionality if you wished.
The final limitation is that any change you make to the 'template' will only be reflected in the created types once you build. However as this process takes place before compilation, you can force this process to take place even when there are known errors elsewhere in your project that would otherwise prevent a build. For anyone who would like to modify the preprocessor, or perhaps convert it to a VS extension, the code is attached to this project.
A Hack?
I will admit that I personally consider this solution to be a hack. However, it is a hack of the developing process, not the CLR, and results in clean, maintainable, code. It is fast to implement, modifiable, and simple. The types created are compiled normally and behave as if you, yourself, have invested hours in writing them.