C# 中的泛型运算符/数值
如何创建使用 +,-, / 和 * 等运算符的“泛型”类和方法。
介绍
在 C# 中,您不能基于运算符(如 +)为对象创建泛型方法或类。例如,以下代码将无法编译
public static T Add<T>(T a, T b)
{
return a+b;
}
当处理类型各异但行为相同(或仅略有不同)的数据时,这可能是一个真正的问题。例如,图像通常以字节(0-255)、16 位无符号整数(0-65k)甚至浮点变量的形式存储。同样,矩阵可能包含整数、双精度或小数数据。然而,在 C# 中,没有简单的方法来编写一个泛型方法或类来处理这些数据。
虽然已提出了一些解决方案(例如 https://codeproject.org.cn/Articles/8531/Using-generics-for-calculations),但这些解决方案通常需要包装器、辅助类、不直观的代码以及/或较差的性能。
背景
当泛型类在 .NET 应用程序中使用时,它会在需要时创建,并且与应用程序中的任何其他类一样“真实”且独特。例如,考虑泛型类 Collar<T>
public class Collar<T>
{
T Animal{get;set;}
public Collar(T a)
{
Animal = a;
}
}
如果我们在应用程序中创建 Collar<Dog>
和 Collar<Cat>
,编译器将创建 Collar<Dog>
和 Collar<Cat>
作为不同的类型。也就是说,尽管它们可能具有相似的行为并共享代码(如我们所见),但从运行时的角度来看,它们是完全不同的类型(如我们所见)。
我们无法为数值运算创建泛型类型或泛型方法的原因是,我们目前无法使用接口来指定运算符(如 +、-、/ 和 *),也无法像这样使用 where
子句来强制要求它们
public class Image<T> where T:+,-,*,/ //illegal
{}
解决方案
解决此问题的方法是自定义预处理步骤,该步骤利用一个非常类似于泛型定义的“模板”类来构建您可以在代码中使用的预指定类型。它在编译发生之前,将这些类型的代码直接注入到您的项目中。这些模板类型与泛型类型的唯一区别在于它们
- 命名略有不同(例如,
Calc_Double
,而不是Calc<Double>
) - 在编译前指定
- 具有使用运算符的能力
在下面的第一个示例中,我将自动生成 Image<T>
对象,其中一个将存储字节(Bytes
)作为像素值,一个将存储 int
,另一个将存储 float
。这些图像将具有 GetPixel
、SetPixel
和 MaxPixel
方法。第二个示例创建一个类,它具有用于 double
、int
和 float
的数学方法。
Using the Code
添加预处理器
第一步是告诉 VS 使用本文附加的预处理器。用记事本打开您的 .csproj 文件,滚动到文件底部,您会看到如下注释掉的文本
<!-- To modify your build process,
add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"></Target>
<Target Name="AfterBuild"></Target>
-->
删除此注释并写入以下内容
<Target Name="BeforeBuild">
<Exec WorkingDirectory="FOLDER_OF_PREPROCESSOR"
Command="GenericNumericsPreprocessor.exe "$(MSBuildProjectDirectory)"
"$(MSBuildProjectFullPath)"" />
</Target>
其中 FOLDER_OF_PREPROCESSOR
是保存已构建的预处理器可执行文件(GenericNumericsPreprocessor.exe)的目录。您也可以将此可执行文件粘贴到您项目的项目文件夹中,并将 WorkingDirectory
保留为空字符串。
在项目中添加 <T> 占位符
正如我们所见,我们无法使用泛型来创建使用 +、- 或 * 等运算符处理 int
或 float
等数据类型
的方法。取而代之的是,我们将使用一个 <T> **P**lace**H**older 类型(TPH
),它指定您想要的每个运算符和转换。当文件经过预处理后,所有包含此运算符的地方都将被您想要的类型替换。
由于此占位符指定了我们要使用的所有运算符,因此不会导致 Intellisense 级别的编译错误。本文附带的项目中包含一个占位符示例。它看起来像这样
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)
{
//Removes compiler warning about ==
return base.Equals(obj);
}
public override int GetHashCode()
{
//Removes compiler warning about .Equals
return base.GetHashCode();
}
}
创建泛型模板
要创建模板,请像往常一样为您的泛型类创建一个类,但是
- 不要在任何地方写入
<T>
- 不要为字段、属性、参数等使用
T
,而应使用TPH
- 在文件顶部,写入
#pragma expandgeneric
,后跟一个空格分隔的您希望创建的类型列表(请参阅示例)
您可以使用几个选项。有关这些选项,请参阅第二个示例。
对于我们的图像类,我们在构建前得到以下结果:
#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;
}
}
构建您的代码
以正常方式构建代码现在将创建 Image_Byte
、Image_Int
和 Image_Float
。它们将被追加到与 Image
相同的文件中,折叠在一个名为 GENERIC EXPANSION
的 #region
中。
#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
}
您现在可以随心所欲地使用这些类型。您对 Image
所做的任何更改都将在您下次构建后反映在其他类型中。
Image_Byte imb = new Image_Byte(50, 50);
imb.SetPixel(1, 1, Byte.MaxValue);//Set to a byte
Byte b = imb.GetPixel(1, 1);//Get a byte
Image_Float imf = new Image_Float(50, 50);
imf.SetPixel(1, 1, 0.7f);//set to a float
float f = imf.GetPixel(1, 1);//get a float
Image_Int imi = new Image_Int(50, 50);
imi.SetPixel(1, 1, int.MaxValue);//set to an int
int i = imi.GetPixel(1, 1);//get an int
示例 2:泛型数学
我们可以创建一个泛型 Math
类来为每种基本类型执行计算。在这里,我们将创建一个 MathStuff
类,该类具有以下方法:
IsEven
- 一个扩展方法,如果指定的数字是偶数,则返回true
Distance
- 计算两点之间的距离SmallerThanMax
和SmallerThanMin
- 计算一个数字比其类型能容纳的最大值和最小值分别大或小多少。
创建 <T> 占位符
与前一步一样,我们创建一个 <T
> 占位符。这里我们只处理数字,所以我们将其命名为 Number
结构体。我不会粘贴完整的代码,因为它与上面的 TPH
几乎相同,但请确保 Number
可以与其他原始数字相互转换。也为它提供 MinValue
和 MaxValue
,就像所有其他原始数字一样。
struct Number: IEquatable<Number>, IComparable<Number>
{
public static Number MaxValue = 1;
public static Number MinValue = 0;...
//Conversions To Number
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; }
...
}
创建泛型模板
为此,我们将创建 MathStuff
类。在类的顶部,我们指定我们要替换 Number
(而不是默认名称 TPH),并且我们想要 double
、float
和 int
类型
#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
using System;
namespace ExampleUsage
{
static class MathStuff
{
}
}
现在,因为 MathStuff
不是我们希望重命名为 MathStuff_Double
/MathStuff_Float
/etc. 的类,所以我们告诉预处理器不要重命名扩展。考虑到将要进行复制,我们需要将类设置为部分类,以便编译器将扩展解释为一个类。
#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
#pragma expandGeneric noRename
using System;
namespace ExampleUsage
{
static partial class MathStuff
{
}
}
现在我们将方法添加到 MathStuff
/// <summary>
/// Returns the length of the longest side of the right angle triangle.
/// Casts result to Number, so loss of precision is possible
/// </summary>
/// <param name="side1">Length of side 1</param>
/// <param name="side2">Length of side 2</param>
public static Number Pythagoras(Number side1, Number side2)
{
double result = Math.Sqrt(side1 * side1 +
side2 * side2);//Math.Sqrt only accepts a double and returns a double;
return (Number)result;
}
/// <summary>
/// Returns the distance between two points.
/// Casts result to Number, so loss of precision is possible
/// </summary>
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;
}
/// <summary>
/// Returns this number subtracted from largest number its type can be
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
public static Number SmallerThanMax(Number n)
{
return Number.MaxValue- n;
}
/// <summary>
/// Returns this number added to the largest number its type can be
/// </summary>
public static Number LargerThanMin(Number n)
{
return n + Number.MinValue;
}
public static bool IsEven(this Number num)
{
return num % 2 == 0;
}
构建您的代码
我们现在构建代码,并且类已扩展以覆盖我们的三种基本类型(为简洁起见,我在这里仅粘贴了每个类的第一个方法)
#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
#pragma expandGeneric noRename
using System;
namespace ExampleUsage
{
static partial class MathStuff
{
/// <summary>
/// Returns the length of the longest side of the
/// right angle triangle. Casts result to Number, so loss of precision is possible
/// </summary>
/// <param name="side1">Length of side 1</param>
/// <param name="side2">Length of side 2</param>
public static Number Pythagoras(Number side1, Number side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);//Math.Sqrt only accepts a double and returns a double;
return (Number)result;
}
/// <summary>
/// Returns the distance between two points.
/// Casts result to Number, so loss of precision is possible
/// </summary>
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;
}
/// <summary>
/// Returns this number divided by the largest number its type can be
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
public static double ProportionOfMax(Number n)
{
return n / Number.MaxValue;
}
/// <summary>
/// Returns this number divided by the largest number its type can be
/// </summary>
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
{
/// <summary>
/// Returns the length of the longest side of the right angle triangle.
/// Casts result to double, so loss of precision is possible
/// </summary>
/// <param name="side1">Length of side 1</param>
/// <param name="side2">Length of side 2</param>
public static double Pythagoras(double side1, double side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);//Math.Sqrt only accepts a double and returns a double;
return (double)result;
}
...
}
static partial class MathStuff
{
/// <summary>
/// Returns the length of the longest side of the right angle triangle.
/// Casts result to float, so loss of precision is possible
/// </summary>
/// <param name="side1">Length of side 1</param>
/// <param name="side2">Length of side 2</param>
public static float Pythagoras(float side1, float side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);//Math.Sqrt only accepts a double and returns a double;
return (float)result;
}
...
}
static partial class MathStuff
{
/// <summary>
/// Returns the length of the longest side of the right angle triangle.
/// Casts result to int, so loss of precision is possible
/// </summary>
/// <param name="side1">Length of side 1</param>
/// <param name="side2">Length of side 2</param>
public static int Pythagoras(int side1, int side2)
{
double result = Math.Sqrt(side1 * side1 + side2 *
side2);//Math.Sqrt only accepts a double and returns a double;
return (int)result;
}
...
}
#endregion GENERIC EXPANSION
}
我们现在可以使用这些方法
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));
}
输出
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
从最后一行可以看出,在为浮点和整数类型的数值创建类/方法时,不一定总是使用泛型运算符的好主意。
留下评论
限制
我仅在几个小时内构建了这个预处理器,以加快另一个项目的开发速度。它不具备“商业级”的健壮性。特别是,当您
- 在
using
、命名空间定义和类名周围的代码格式不正确时 - 在代码文件中包含嵌套命名空间时
预处理程序小巧、简单且注释丰富。如果您愿意,添加或修改功能会非常简单。
最后的限制是,您对“模板”所做的任何更改都将在您下次构建后反映在创建的类型中。然而,由于此过程发生在编译之前,因此即使项目中存在其他已知错误但仍会阻止构建,您也可以强制执行此过程。对于任何想要修改预处理器或将其转换为 VS 扩展的人来说,代码都附属于该项目。
一个“ hack ”?
我承认我个人认为这是一个“ hack ”。但是,这是一个开发过程的“ hack ”,而不是 CLR 的“ hack ”,并且它产生了干净、可维护的代码。它实现速度快、可修改且简单。创建的类型被正常编译,并且表现得好像您自己花费了数小时来编写它们。