C# 如何添加两个泛型值

47

有人能解释一下为什么这个不行吗?我想实现无论数字类型如何都可以将两个值相加。

public static T Add<T> (T number1, T number2)
{
    return number1 + number2;
}

当我编译这个时,会出现以下错误:
Operator '+' cannot be applied to operands of type 'T' and 'T'

1
C#编译器会自动处理这些,以避免您编写有错误的代码。如果编译器不知道T是什么,它就不知道如何添加T值。同时,编译器也不知道T的结果类型,例如在这种情况下的加法运算结果。 - JonH
这些参数是否被强制为“数值类型”? - El Ronnoco
14个回答

80

没有通用的约束条件可以强制执行运算符重载。您可以查看以下库。或者,如果您使用的是.NET 4.0,则可以使用dynamic关键字:

public static T Add<T>(T number1, T number2)
{
    dynamic a = number1;
    dynamic b = number2;
    return a + b;
}

显然,这不会提供任何编译时的安全性,这正是泛型的作用。唯一实现编译时安全性的方法是强制执行泛型约束。而对于您的情况,没有可用的约束条件。这只是欺骗编译器的技巧。如果调用Add方法的调用方未传递与+运算符兼容的类型,则代码将在运行时抛出异常。


我认为现在有一个约束条件.. 详情请参见此处 - Aamir Masood
3
“where”限制条件一直存在。关键是在该“where”子句中没有办法指定“T可以添加到其他Ts”的位置 :) - flytzen
1
或者简单地说,公共静态动态 Add(动态 a, 动态 b) { 返回 a + b; } - Sarath
但编译器知道传递给它的类型在哪里使用。在这种情况下,它将能够为所有类型创建一个专门化版本,然后检查所有正在使用的专门化版本是否具有+的重载,对吗? - Didier A.
如果number1或number2为空,会发生什么? - sameer

34
这里提供的解决方案很好,但我想再添加一个使用表达式的解决方案。
public static T Add<T>(T a, T b)
{
    // Declare the parameters
    var paramA = Expression.Parameter(typeof(T), "a");
    var paramB = Expression.Parameter(typeof(T), "b");

    // Add the parameters together
    BinaryExpression body = Expression.Add(paramA, paramB);

    // Compile it
    Func<T, T, T> add = Expression.Lambda<Func<T, T, T>>(body, paramA, paramB).Compile();

    // Call it
    return add(a, b);
}

这样,您就创建了一个执行加法的 Func<T, T, T>

完整的解释可以在这篇文章中找到。


13

编译器不认识类型T,因此无法找到任何定义过的重载+运算符...

目前最好的方法是将您的方法声明为如下形式(因为所有数值类型都可以转换为double):

public static double Add (double number1, double number2)
{
  return number1 + number2;
}

或者如果您确定适当的+运算符已定义:

public static T Add<T>(T number1, T number2)
{
  dynamic dynamic1 = number1;
  dynamic dynamic2 = number2;
  return dynamic1 + dynamic2;
}

更新

或者两者的组合:

public static T Add<T>(T in1, T in2)
{
    var d1 = Convert.ToDouble(in1);
    var d2 = Convert.ToDouble(in2);
    return (T)(dynamic)(d1 + d2);
}

2
十进制数不能隐式地转换为双精度。 - kol

6

我最近在查找通用方法的时候遇到了这个问题,这种方法可以适用于任意数字列表,例如:

public T Calculate<T>(IEnumerable<T> numbers);

我找到的解决方案是使用lambda表达式:
public T Calculate<T>(Func<T, T, T> add, IEnumerable<T> numbers);

然后您可以通过以下方式进行调用:

var sum = this.Calculate((a, b) => a + b, someListOfNumbers);

显然,在某些情况下,您可能会在代码中使用“+”代替lambda表达式,但如果您需要以通用方式执行数学运算,那么这是我能想到的最容易编译安全的方法。请注意保留HTML标记。

请注意,此函数已存在 - 它是 LINQ 聚合函数:learn.microsoft.com/en-us/dotnet/api/…,可以用作:var sum = enumerable.Aggregate((a, b) => a + b); - Rich O'Kelly

4

由于这是一个没有约束的通用类型,编译器不知道涉及的类型是否有“+”重载,因此会出现编译器错误。

以下是一些解决方法:

public static TResult Add<T1, T2, TResult>(T1 left, T2 right, Func<T1, T2, TResult> AddMethod)
{
    return AddMethod(left, right);
}

var finalLabel = Add("something", 3,(a,b) => a + b.ToString());

以下代码可以构建,但它在运行时被评估,因此不安全。
public static T AddExpression<T>(T left, T right)
{
    ParameterExpression leftOperand = Expression.Parameter(typeof(T), "left");
    ParameterExpression rightOperand = Expression.Parameter(typeof(T), "right");
    BinaryExpression body = Expression.Add(leftOperand, rightOperand);
    Expression<Func<T, T, T>> adder = Expression.Lambda<Func<T, T, T>>(
        body, leftOperand, rightOperand);
    Func<T, T, T> theDelegate = adder.Compile();
    return theDelegate(left, right);
}

这是LINQ的一部分,被称为Aggregate - Bananach

4

尝试这段代码。这是一个通用的加法代码。

public static dynamic AddAllType(dynamic x, dynamic y)
{
    return  x + y;
}

虽然这样做是可行的,但并不是一个好主意。泛型的目的是在编译时承担类型解析成本。使用动态类型,你将在运行时承担这个成本。它可能会对性能产生非常重要的影响。 - Siddhartha Gandhi

3

这可以防止开发人员编写有错误的代码。以下是一些问题,以更好地解释此问题:

  1. 编译器是否知道类型[不知道,因为它是一个通用类型]。
  2. 它能接受对象作为参数吗[能够,但由于不知道如何处理它们,可能会导致错误的代码]

由于您想要防止代码出错,C# 不允许您使用 '+' '-' 和其他运算符。

解决方案:您可以使用 "dynamic","var" 类型,并在需要时返回它们的总和。


3
        public class generic<T>
        {
            T result;
            public generic(T eval1, T eval2)
            {

                dynamic a = eval1;
                dynamic b = eval2;
                result = a + b;
                Console.WriteLine(result);
                Console.ReadLine();


            }

             static void Main(string[] args)
        {

             generic<int> genobj = new generic<int>(20,30);

        }

1
如果您不想(或无法)使用动态类型或在运行时生成代码,则还有另一种方法,我从EqualityComparer实现中借鉴了这种方法。它有点冗长,但是可以利用“经典”的.NET工具进行另一种可能性。
首先,您需要定义一个泛型接口,它类似于“trait”,定义您想要的附加方法:
/// <summary>
/// Represents an interface defining the addition calculation for generic types.
/// </summary>
public interface IAddition<T>
{
    /// <summary>
    /// Calculates the addition result of <paramref name="left"/> and <paramref name="right"/>.
    /// </summary>
    /// <param name="left">The left operand.</param>
    /// <param name="right">The right operand.</param>
    /// <returns>The calculation result.</returns>
    T Add(T left, T right);
}

现在,您需要为您感兴趣的类型实现此特质,在本例中,我为float执行此操作:
internal class SingleAddition : IAddition<Single>
{
    Single IAddition<Single>.Add(Single left, Single right) => left + right;
}

然后您创建一个通用的静态类,这将是您实际交互的类。它会自动创建并缓存符合其泛型类型的IAddition<T>接口的实现:

/// <summary>
/// Represents an implementation of addition calculations for generic types.
/// </summary>
/// <typeparam name="T">The type to support the addition calculation.</typeparam>
public static class Addition<T>
{
    private static readonly IAddition<T> _implementation = Implement();

    /// <summary>
    /// Calculates the addition result of <paramref name="left"/> and <paramref name="right"/>.
    /// </summary>
    /// <param name="left">The left operand.</param>
    /// <param name="right">The right operand.</param>
    /// <returns>The calculation result.</returns>
    public static T Add(T left, T right) => _implementation.Add(left, right);

    private static IAddition<T> Implement()
    {
        Type type = typeof(T);
        // If T implements IAddition<T> return a GenericAddition<T>.
        if (typeof(IAddition<T>).IsAssignableFrom(type))
            return (IAddition<T>)Activator.CreateInstance(typeof(GenericAddition<>).MakeGenericType(type));
        // Otherwise use a default implementation for primitive types.
        switch (type.IsEnum ? Type.GetTypeCode(Enum.GetUnderlyingType(type)) : Type.GetTypeCode(type))
        {
            case TypeCode.Single:
                return (IAddition<T>)new SingleAddition();
            default:
                throw new NotSupportedException($"Type {type.Name} does not support addition.");
        }
    }
}

internal sealed class GenericAddition<T> : IAddition<T> where T : IAddition<T>
{
    T IAddition<T>.Add(T left, T right) => left.Add(left, right);
}

当然,你需要扩展类型代码的开关以支持所有你感兴趣的类型。这些将是所有原始类型和那些你无法修改的类型,因为对于你可以修改的类型,你只需实现IAddition<T>接口 - GenericAddition实现将负责使用它。
请注意,由于我添加了底层类型的检查,所以已经支持Enum值。
正如你在静态Addition<T>类中看到的那样,它公开了一个Add方法,最终你将从外部调用它,如下所示:
public class SomeStuff<T>
{
    public T TestAddition(T left, T right) => Addition<T>.Add(left, right);
}

// Testing in C# interactive:
> SomeStuff<float> floatStuff = new SomeStuff<float>();
> floatStuff.TestAddition(12, 24.34f)
36.34

学校数学从未如此简单!也许吧。其实不是。这是机密。

关于看到 (T left, T right) 并且通过类型生成缓存方法,让我觉得这太复杂了。但是我认为这种方法是公平的,而且比使用动态方式要好得多,至少在性能和纯度方面。 - user1969177
是的,这很糟糕。我希望C# 8能够支持“形状”或它们被称为什么,以允许泛型像C++中的模板参数一样工作,只需使用operator +(如果可用),而不管实现的接口如何。 - Ray

1

对于 .NET 7 及以上版本,这比其他解决方案更有意义,在我第一眼看来。 - Prafulla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接