65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的泛型运算符/数值

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (23投票s)

2013年10月7日

CPOL

6分钟阅读

viewsIcon

43124

downloadIcon

380

如何创建使用 +,-, / 和 * 等运算符的“泛型”类和方法。

介绍 

在 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。这些图像将具有 GetPixelSetPixelMaxPixel 方法。第二个示例创建一个类,它具有用于 doubleintfloat 的数学方法。

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 &quot;$(MSBuildProjectDirectory)&quot; 
&quot;$(MSBuildProjectFullPath)&quot;" />
</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();
    }
}

创建泛型模板

要创建模板,请像往常一样为您的泛型类创建一个类,但是

  1. 不要在任何地方写入 <T>
  2. 不要为字段、属性、参数等使用 T,而应使用 TPH
  3. 在文件顶部,写入 #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_ByteImage_IntImage_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 - 计算两点之间的距离
  • SmallerThanMaxSmallerThanMin - 计算一个数字比其类型能容纳的最大值和最小值分别大或小多少。

创建 <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),并且我们想要 doublefloatint 类型

#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 ”,并且它产生了干净、可维护的代码。它实现速度快、可修改且简单。创建的类型被正常编译,并且表现得好像您自己花费了数小时来编写它们。 

© . All rights reserved.