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

C# 泛型类中的算术运算

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (10投票s)

2009年2月23日

CC (ASA 2.5)

8分钟阅读

viewsIcon

78010

downloadIcon

679

讨论在泛型类中进行算术运算,以及一个简化操作的小工具。

引言

我经常处理矩阵。我处理矩阵的次数足够多,以至于在某个时候,我决定为矩阵和半矩阵创建自己的泛型类。除其他外,我希望我的类提供一个方法来计算行或列中各项的总和。我立刻遇到了一个问题。如何计算两个泛型对象的总和?

为了简化问题,我们假设我们正在处理 `List<T>` 的一个派生类,并且我们想添加一个名为 `Sum()` 的方法。

我们从派生一个类自 `List<T>` 开始

class SummableList : List{}

让我们继续添加 `Sum()` 方法

class SummableList<t> : List<t> {
    public T Sum() {
        T result = default(T);
        foreach (T item in this)
            result += item;
        return result;
    }
}

不幸的是,这将无法编译。它无法编译,因为编译器不知道如何将两个 `object` 加在一起。在 .NET 中,一个未受约束的类型参数(`List<T>` 中的 `T`)被假定为 `System.Object` 类型,而对于一个普通的 `object`,我们能做的非常有限。

有一些方法可以绕过这个问题,但它们都有其缺点。我的目标是能够以尽可能接近上述示例的语法编写我的泛型类,并且遇到的麻烦尽可能少。

约束会有帮助吗?

到目前为止,我们知道我们的代码将无法编译,因为类型参数被假定为 `object`。如果我们约束类型参数为一个编译器知道如何相加的类型,那会怎么样?那么,关键在于选择一个合适的类。好吧,与其试图找到一个合适的类,不如我们自己来创建一个。

/// <summary>
/// An abstract base class that defines
/// all of the mathematical operations we might want to do.
/// </summary>
public abstract class MathClass {
    public abstract MathClass Add(MathClass other);
    public abstract MathClass Subtract(MathClass other);
    public abstract MathClass Divide(MathClass other);
    public abstract MathClass Divide(int other);
    
    public static MathClass operator +(MathClass a, MathClass b) {
        return a.Add(b);
    }
    
    public static MathClass operator -(MathClass a, MathClass b) {
        return a.Subtract(b);
    }
    
    public static MathClass operator /(MathClass a, MathClass b) {
        return a.Divide(b);
    }
    
    public static MathClass operator /(MathClass a, int b) {
        return a.Divide(b);
    }
}

现在,我们只需要将任何我们可能想要进行算术运算的类派生自这个新的 `MathClass`。然后,我们可以如下编写我们的 `SummableList<T>`,并为其添加约束。

/// <summary>
/// Then we can write our SummableList by placing
/// a constraint on the type parameter.
/// Now our summable list can only contain
/// classes that derive from MathClass
/// </summary>

public class SummableListC<T> : List<T> where T : MathClass, new() {

    public T Sum() {
        T result = new T();

        foreach (T thing in this) {
            result = (T)(result + thing);
        }

        return result;
    }

    public T Average() {
        return (T)(Sum() / Count);
    }
}

但是,问题在于我们只能处理派生自 `MathClass` 的类。我们不能使用 `int` 或 `double`。不仅如此,我们必须编写的派生类并不那么直接。

/// <summary>
/// Then we have to create a new class derived
/// from MathClass for any type that we might 
/// want to use in our list.
/// Here is the class that lets us use integers in our list.
/// </summary>

public class IntMathClass : MathClass {

    private int fValue = 0;

    public IntMathClass(int value) {
        fValue = value;
    }

    public IntMathClass() {
        fValue = 0;
    }

    public static implicit operator IntMathClass(int a) {
        return new IntMathClass(a);
    }

    public static implicit operator int(IntMathClass a) {
        return a.fValue;
    }

    public override MathClass Add(MathClass other) {
        if (other is IntMathClass) {
            return new IntMathClass(this.fValue + (IntMathClass)other);
        }

        return this;
    }

    public override MathClass Subtract(MathClass other) {
        if (other is IntMathClass) {
            return new IntMathClass(this.fValue - (IntMathClass)other);
        }

        return this;
    }

    public override MathClass Divide(MathClass other) {
        if (other is IntMathClass) {
            return new IntMathClass(this.fValue / (IntMathClass)other);
        }

        return this;
    }

    public override MathClass Divide(int other) {
        return new IntMathClass(this.fValue / other);
    }
}

仅仅是为了能够在列表中使用整数,就需要做这么多工作。然而,它确实允许我们这样做。

SummableList<IntMathClass> list = new SummableList<IntMathClass>();

list.Add(4);
list.Add(5);
list.Add(6);
list.Add(7);

//list.Sum() would return 22
//list.Average() would return 5

如果您是一名应用程序开发人员,您只需要使用这个 `SummableList<T>`,那么这样工作得相当好。但是,如果您是必须编写所有 `MathClass` 子类的人呢?有更好的方法。

接口约束呢?

如果我们使用接口约束,那么我们可以简单地将接口应用于任何我们需要的类。此外,接口定义比抽象类简单得多。

/// <summary>
/// Instead of an abstract base class 
/// we define an interface that defines all
/// of the mathematical operations we might want to do.
/// </summary>

public interface IArithmetic<T> {
    T Add(T a);
    T Subtract(T a);
    T Divide(T a);
    T Divide(int a);
}

这简单多了,但我们仍然需要在任何我们想使用的类中实现这个接口。我们还必须为像 `int` 和 `double` 这样的内置类型编写替代品,因为我们不能简单地将接口添加到它们上面。这是允许我们在 `SummableList<T>` 中使用整数的接口。

/// <summary>
/// Them we have to implement that interface in any type that we might 
/// want to use in our list.
/// If we want to use any base types, we have to make an entirely 
/// new class that wraps the base type.
/// This is OK, but wrapping and unwrapping of base types can get messy.
/// Here is the class that lets us use integers in our list.
/// </summary>

public class IntMathI : IArithmetic<IntMathI> {

    private int fValue = 0;

    public IntMathI() {
    }

    public IntMathI(int value) {
        fValue = value;
    }

    public static implicit operator IntMathI(int a) {
        return new IntMathI(a);
    }

    public static implicit operator int(IntMathI a) {
        return a.fValue;
    }

    #region IArithmetic<intmathi> Members

    public IntMathI Add(IntMathI a) {
        return new IntMathI(this.fValue + a.fValue);
    }

    public IntMathI Subtract(IntMathI a) {
        return new IntMathI(this.fValue - a.fValue);
    }

    public IntMathI Divide(IntMathI a) {
        return new IntMathI(this.fValue / a.fValue);
    }

    public IntMathI Divide(int a) {
        return new IntMathI(this.fValue / a);
    }

    #endregion
}

现在,我们可以通过在类型参数上放置接口约束来编写我们的 `SummableList<T>`。

/// <summary>
/// Then we can write our SummableList by placing a constraint on the type parameter.
/// Now our summable list can only contain classes that implement the IArithmetic interface
/// </summary>

public class SummableList<T> : List<T> where T : IArithmetic<T>, new() {

    public T Sum() {
        T result = new T();

        foreach (T thing in this) {
            result = result.Add(thing);
        }

        return result;
    }

    public T Average() {
        return Sum().Divide(Count) ;
    }
}

但是,请注意,在我们的泛型类中,我们现在必须使用像 `.Add()` 和 `.Divide()` 这样的方法进行算术运算,而不是使用像 `+` 和 `/` 这样的运算符。这是因为接口中不能定义静态方法,而运算符重载是以静态方法的形式定义的。没有这些静态方法,我们就不能在泛型类中使用运算符,而必须改用这些方法。

不过,这确实有效,现在我们可以这样写。

SummableList<IntMathI> list = new SummableList<IntMathI>();

list.Add(4);
list.Add(5);
list.Add(6);
list.Add(7);

//list.Sum() would return 22
//list.Average() would return 5

所以,这有效,而且比处理抽象基类简单得多,但仍然有限制。同样,如果您是应用程序开发人员,并且只需要使用 `SummableList<T>`,那么这样工作得很好。但是,如果您正在编写自己的泛型类,无法使用运算符是一个不可接受的缺点。同样,有更好的方法。

介绍 Calculator 工具

经过深思熟虑,我提出了一个 Calculator 工具,它不限于基类或接口,并且仍然允许类程序员使用熟悉的语法和运算符。

该工具分为三个部分

  1. 接口 `ICalculator<T>` 定义了可以执行的所有操作。
  2. `ICalculator<T>` 的结构实现定义了任何特定类型如何执行操作。
  3. 类 `Number<T>` 代表泛型类中的任何数字。它使用反射自动根据 `T` 的类型创建正确的 `ICalculator<T>`。它具有接口 `ICalculator<T>` 中定义的要执行的操作方法实现。它定义了运算符,使类程序员的算术运算变得容易。

ICalculator<T>

`ICalculator<T>` 接口定义了使用此工具的泛型类中可能执行的所有操作。

/// <summary>
/// This interface defines all of the operations that can be done in generic classes
/// These operations can be assigned to operators in class Number<T>
/// </summary>

/// <typeparam name="T">Type that
/// we will be doing arithmetic with</typeparam>
public interface ICalculator<T> {
    T Sum(T a, T b);
    T Difference(T a, T b);
    int Compare(T a, T b);
    T Multiply(T a, T b);
    T Divide(T a, T b);
    T Divide(T a, int b);
    //for doing integer division which is needed to do averages
}

虽然这确实是一个接口,但该工具不要求任何类实现它。该接口在工具内部使用。

ICalculator<T> 的结构实现

对于我们可能进行算术运算的每种类型,必须有一个 `ICalculator<T>` 的实现。附加代码包含 `int32`、`int64`、`single`、`double` 和 `string` 的实现。

这是 `int32` 实现的外观。

/// <summary>
/// ICalculator<T> implementation for Int32 type
/// </summary>

struct Int32Calculator : ICalculator<Int32> {
    public Int32 Sum(Int32 a, Int32 b) {
        return a + b;
    }

    public Int32 Difference(Int32 a, Int32 b) {
        return a - b;
    }

    public int Compare(Int32 a, Int32 b) {
        return Difference(a, b);
    }

    public int Multiply(Int32 a, Int32 b) {
        return a * b;
    }

    public int Divide(Int32 a, Int32 b) {
        return a / b;
    }
}

这是一段相当直接的代码,它只是定义了如何为任何给定类型执行操作。任何想使用实用程序中未包含的类型的程序员都可以轻松地为任何其他类型添加一个结构。事实上,实用程序包含 `string` 的结构实现。我包含它只是为了表明任何类型都可以添加,而无需修改类型本身。

类 Number<T>

类 `Number<T>` 是所有工作发生的地方。使用此工具的程序员使用 `Number<T>` 来代表其泛型类中的数字。当使用 `Number<T>` 时,它使用反射来创建正确的 `ICalculator<T>` 的实例。正是这个 `ICalculator<T>` 实例执行实际的计算。

有一个 `if` 链来决定创建哪个 `ICalculator<T>`,但 `Number<T>` 使用一个静态变量和单例模式来确保 `if` 链每个类型只进入一次。

这是 `Number<T>` 的代码。

/// <summary>
/// This class uses reflection to automatically create the correct 
/// ICalculator<T> that is needed for any particular type T.
/// </summary>
/// <typeparam name="T">Type that we
/// will be doing arithmetic with</typeparam>

public class Number<T> {

    private T value;

    public Number(T value) {
        this.value = value;
    }

    /// <summary>
    /// Big IF chain to decide exactly which ICalculator needs to be created
    /// Since the ICalculator is cached, this if chain is executed only once per type
    /// </summary>
    /// <returns>The type of the calculator that needs to be created</returns>

    public static Type GetCalculatorType() {
        Type tType = typeof(T);
        Type calculatorType = null;
        if (tType == typeof(Int32)) {
            calculatorType = typeof(Int32Calculator);
        }
        else if (tType == typeof(Int64)) {
            calculatorType = typeof(Int64Calculator);
        }
        else if (tType == typeof(Double)) {
            calculatorType = typeof(DoubleCalculator);
        }
        else if (tType == typeof(string)) {
            calculatorType = typeof(StringCalculator);
        }
        else {
            throw new InvalidCastException(String.Format("Unsupported Type- Type {0}" + 
                  " does not have a partner implementation of interface " + 
                  "ICalculator<T> and cannot be used in generic " + 
                  "arithmetic using type Number<T>", tType.Name));
        }
        return calculatorType;
    }

    /// <summary>

    /// a static field to store the calculator after it is created
    /// this is the caching that is refered to above
    /// </summary>
    private static ICalculator<T> fCalculator = null;

    /// <summary>

    /// Singleton pattern- only one calculator created per type
    /// 
    /// </summary>
    public static ICalculator<T> Calculator {
        get {
            if (fCalculator == null) {
                MakeCalculator();
            }
            return fCalculator;
        }
    }

    /// <summary>

    /// Here the actual calculator is created using the system activator
    /// </summary>

    public static void MakeCalculator() {
        Type calculatorType = GetCalculatorType();
        fCalculator = Activator.CreateInstance(calculatorType) as ICalculator<T>;
    }

    /// These methods can be called by the applications
    /// programmer if no operator overload is defined
    /// If an operator overload is defined these methods are not needed
    #region operation methods

    public static T Sum(T a, T b) {
        return Calculator.Sum(a, b);
    }

    public static T Difference(T a, T b) {
        return Calculator.Difference(a, b);
    }

    public static int Compare(T a, T b) {
        return Calculator.Compare(a, b);
    }

    public static T Multiply(T a, T b) {
        return Calculator.Multiply(a, b);
    }

    public static T Divide(T a, T b) {
        return Calculator.Divide(a, b);
    }

    public static T Divide(T a, int b) {
        return Calculator.Divide(a, b);
    }

    #endregion

    /// These operator overloads make doing the arithmetic easy.
    /// For custom operations, an operation method
    /// may be the only way to perform the operation
    #region Operators
        
        //IMPORTANT:  The implicit operators
        //allows an object of type Number<T> to be
        //easily and seamlessly wrap an object of type T. 
        public static implicit operator Number<T>(T a) {
            return new Number<T>(a);
        }

        //IMPORTANT:  The implicit operators allows 
        //an object of type Number<T> to be
        //easily and seamlessly wrap an object of type T. 
        public static implicit operator T(Number<T> a) {
            return a.value;
        }

        public static Number<T> 
               operator +(Number<T> a, Number<T> b) {
            return Calculator.Sum(a.value, b.value);
        }

        public static Number<T> 
               operator -(Number<T> a, Number<T> b) {
            return Calculator.Difference(a, b);
        }

        public static bool operator >(Number<T> a, Number<T> b) {
            return Calculator.Compare(a, b) > 0;
        }

        public static bool operator <(Number<T> a, Number<T> b) {
            return Calculator.Compare(a, b) < 0;
        }

        public static Number<T> 
               operator *(Number<T> a, Number<T> b) {
            return Calculator.Multiply(a, b);
        }

        public static Number<T> 
               operator /(Number<T> a, Number<T> b) {
            return Calculator.Divide(a, b);
        }

        public static Number<T> 
               operator /(Number<T> a, int b) {
            return Calculator.Divide(a, b);
        }
        #endregion
}

最终的 SummableList<T>

有了这三部分,`SummableList<T>` 现在就可以在不使用任何约束、抽象类或接口的情况下编写了。

class SummableList<T> : List<T> {

    public T Sum() {
        Number<T> result = default(T);

        foreach (T item in this)
            result += item;

        return result;
    }

    public T Average() {
        Number<T> sum = Sum();
        return sum / Count;
    }
}

这看起来非常像我们最初说的无法编译的第一个示例。不同之处在于 `Sum()` 方法中的结果变量类型是 `Number<T>` 而不是 `T`。此外,我还额外添加了一个 `Average()` 方法。

您可能会注意到 `Sum()` 方法应该返回一个 `T`,而最后一行代码返回变量 `result`,它是一个 `Number<T>`。这是可能的,因为 `Number<T>` 类定义了两个隐式运算符,它们告诉编译器如何将 `T` 隐式转换为 `Number<T>`。

//IMPORTANT:  The implicit operators allows an object of type Number<T> to be 
//easily and seamlessly wrap an object of type T. 

public static implicit operator Number<T>(T a) {
    return new Number<T>(a);
}

public static implicit operator T(Number<T> a) {
    return a.value;
}

这使得类程序员能够无缝地使用 `Number<T>` 来替代 `T`。而且,由于 `Number<T>` 为其定义了算术方法和运算符,因此它允许程序员在通常无法进行算术运算的地方进行算术运算。

添加类型或操作

由于没有需要使用的基类或接口即可使用此工具,因此它相当独立。但是,可能会出现您希望将此工具与未包含的类型一起使用,或者您希望执行尚未定义的操作的情况。以下是如何操作:

添加类型

  1. 将类型添加到 `Number<T>.GetCalculatorType(Type tType)`。
  2. 创建 `ICalculator<T>` 的实现。

添加操作

  1. 向 `ICalculator<T>` 接口添加操作。
  2. 向 `ICalculator<T>` 的每个实现添加操作。
  3. 向 `Number<T>` 类添加操作和运算符重载。

性能

附加解决方案包含一个 SpeedTest,它将使用本文介绍的三种方法中的每一种将任意数量的随机数相加,并显示经过的时间。在千万次迭代中,该工具大约快 0.2 到 0.3 秒。

MiscUtil

还有另一种完全不同的方法。这种方法需要 .NET 3.5,并使用 LINQ 类动态生成和编译表达式。它使用任何类的运算符重载来生成所需的表达式。这意味着它可以处理任何实现了合适运算符的类。

动态编译代码可能会非常慢,但 Jon Skeet 和 Marc Gravell 开发了一个名为MiscUtil的项目,该项目包含一个框架,可以缓存编译的表达式,使其速度非常快。在我粗略的性能测试中,在一百万次迭代中,它不像我的工具那么快,但它能处理任何实现了运算符的类这一点非常有吸引力。另外,以这种方式使用 LINQ 是非常、非常酷

参考文献

以下文章在我试图弄清楚如何处理我的矩阵时对我帮助很大。

  1. Gunnerson, Eric. “Generics Algorithms” Eric Gunnerson's C# Compendium. 2003年11月13日. http://blogs.msdn.com/ericgu/archive/2003/11/14/52852.aspx
  2. Klaehn, Rüdiger. “Using Generics for calculations” The Code Project. 2004年10月11日. https://codeproject.org.cn/KB/cs/genericnumerics.aspx
  3. Skeet, Jon and Gravell, Mark. “Generic operators” Jon Skeet's C# and .NET articles and links. Last accessed July 13, 2008. http://www.yoda.arachsys.com/csharp/genericoperators.html
© . All rights reserved.