【问题标题】:Generic lists and co/contravariance通用列表和协/逆变
【发布时间】:2013-03-29 17:53:15
【问题描述】:

假设我有一个通用的List<ICalculation>,它用作我的应用程序中所有预定义计算的存储库...

我有一个名为ICalculation<T, U> 的通用接口,它实现了更基本的ICalculation

public interface ICalculation
{
    string Identifier { get; }
    object Calculate(object inputData);
}

public interface ICalculation<in TIn, out TOut> : ICalculation
{
    string Identifier { get; }
    TOut Calculate(TIn inputData)
}

我还有一个抽象类 CalculationBase 实现了这个接口

public abstract class CalculationBase<TIn, TOut> : ICalculation<in TIn, out TOut>, ICalculation
{
    public abstract string Identifier { get; }
    public abstract Func<TIn, TOut> Calculation { get; }
    public virtual TOut Calculate(TIn inputData)
    {
        return Calculate(inputData, Calculation);
    }
    virtual object ICalculation.Calculate(object inputData)
    {
        return (TOut)calculation((TIn)inputData);
    }
    public static TOut Calculate(TIn inputData, Func<TIn, TOut> calculation)
    {
        if (calculation == null || inputData == null)
            return default(TOut);

        return calculation(inputData);
    }
}

所以,现在我有了一大堆计算,它们通过一些输入实现了 CalculationBase 函数……一个例子:

public sealed class NumberOfBillableInvoices : CalculationBase<IClientAccount, int>
{
    public override string identifier { get { return "@BillableInvoiceCount"; } }
    public override Func<IClientAccount, int> Calculation
    {
        get { return inputData => inputData.Invoices.Count(i => i.IsBillable); }
    }
} 

每个计算都针对特定类型的对象,并根据计算的性质返回不同的输出。例如:货币计算可能返回小数,计数器可能返回整数或长整数等。

我有一个计算存储库,它会在应用程序负载时自行加载,当需要计算公式时,计算引擎会获取正在查询的对象 - 在本例中,如果我们有一些具体实例IClientAccount 类型的,我们希望针对它评估一些公式 - 例如,对前 5 张之后的每张发票征收 1.20 美元:"Math.Max(@BillableInvoiceCount - 5, 0) * $1.20"。引擎会抓取所有 TIn 类型为 IClientAccount 的计算,并将计算与公式中找到的令牌匹配(即@BillableInvoiceCount)。然后一些计算引擎,如 NCalc、FLEE 或其他计算引擎将评估最终方程。

所以,我的问题是我不希望遍历 每个 计算以寻找正确的标记 - 实际上,如果标记跨越多个对象类型,它们可能会发生冲突。例如,我可能想在不同的上下文中使用相同的标记来表示不同的事物。如果我可以将存储库中的计算范围缩小到 TIn 与我尝试计算的对象类型匹配的那些,那会更容易。

在这一点上我有一些想法-

1)。我可以创建一个仅编组对象的 TIn 部分的存储库吗?我认为这个问题的答案很可能,不......但如果有可能,我不知道如何实现这一点 - 有没有人有任何想法?

2)。有没有办法在我的存储库中查询 TIn 与我正在查询的对象的类型匹配的所有计算?如果有,怎么做?

3)。我是否有多个存储库基于我计算的所有 TIn/TOut 组合...如果是这样,我如何将正确的存储库与我正在查询的对象结合起来?因为我仍在尝试仅根据 TIn 部分匹配存储库...

4)。让我的所有计算都返回双精度而不是让它们返回不同的类型,然后我的存储库可以输入到输入类型,从而使它们更简单......但是虽然这很简单,但从语义上来说,它只是感觉不对。

想法?

提前干杯:)

【问题讨论】:

  • 考虑使用小数而不是双精度数进行货币计算。对长度或质量等物理量使用双精度数。
  • 谢谢埃里克...也许我对货币的引用应该是小数...现在我想起来了,我的代码库中的类实际上使用小数作为货币 - 我更正了我的引用,谢谢; )

标签: c# generics .net-4.0 covariance contravariance


【解决方案1】:

请记住,泛型是一个编译时工件,如果你想使用它们,你需要在编写时知道你想要什么类。如果您需要运行时检查,那么非泛型方法可能是最好的。

假设您知道您的对象类型正确 -> 对象重载应该可以正常工作。如果映射失败,.NET 框架将抛出​​异常,因此您不必担心那里的静默失败。

1)。我可以创建一个仅编组对象的 TIn 部分的存储库吗?我认为这个问题的答案很可能,不......但如果有可能,我不知道如何实现这一点 - 有没有人有任何想法?

编组是指基础数据的转换,在这种情况下,您可以简单地传递原始对象,因为您的 ICalculation.Calculate 方法会为您进行转换。您可能遇到的唯一问题是如果TIn 是一个值类型并且您传入一个空值(在这种情况下您的Calculate 空值处理将不会发生)

2)。有没有办法在我的存储库中查询 TIn 与我正在查询的对象的类型匹配的所有计算?如果有,怎么做?

我会尝试使用非通用版本,除非您想要更清晰的异常树,否则它应该在这种情况下完成这项工作。

3)。我是否有多个存储库基于我计算的所有 TIn/TOut 组合...如果是这样,我如何将正确的存储库与我正在查询的对象结合起来?因为我仍在尝试仅根据 TIn 部分匹配存储库...

如果你想这样做,诀窍是让你的保存方法只保存TIn,而不是两者都保存。例如Dictionary&lt;Type,ICalculation&gt;,其中TypeTIn

4)。让我的所有计算都返回双精度而不是让它们返回不同的类型,然后我的存储库可以输入到输入类型,从而使它们更简单......但是虽然这很简单,但从语义上来说,它只是感觉不对。

这里需要注意的一点是,我的建议仅在您没有在方法调用之间进行任何转换的情况下才有效。如果您在object 中有一个int,并且您尝试将其转换为double,它将失败。

您可以通过调用Convert.ChangeType 而不是直接转换来避免这种情况。它会像这样工作:

object ICalculation.Calculate(object inputData)
{
    if (inputData == null && typeof(TIn).IsValueType)
        return default(TOut);
    return Calculate((TIn)Convert.ChangeType(inputData, typeof(TIn));
}

注意对方法的一些更改:

  • 我为值类型添加了显式的 null 处理程序,因为 Convert.ChangeType 只会引发异常。
  • 我调用Calculate 的通用形式以防它被重载。
  • 我把它设为非虚拟的,除非你真的有充分的理由,否则你真的不应该重载这个方法,因为它只是提供了两个接口的对称性。
  • 我不转换结果。 calculationCalculate 都保证返回 TOut,因此转换是多余的。
  • 添加了ChangeType,这将允许您静默处理将int 传递到decimal

注意ChangeType 存在危险,它类似于显式强制转换。无论您的数据发生什么变化,它都会尽最大努力进行转换。看起来溢出会按预期处理,但截断会静默发生。

如果你有类似的情况,主要的一点是测试你的边缘情况。

【讨论】:

  • 哇!感谢您的详细回复。 +1 只是为了努力。我需要向接口添加一些特殊性以防止 TIn 成为值类型...并且还需要一些运行时检查以防止 TIn 为空...我绝对喜欢使用 Convert.ChangeTo 进行数据类型静默转换的方法所以这绝对值得考虑。
  • where TIn : class 应将其限制为非值类型。
【解决方案2】:

如果你知道一切都源自CalculationBase&lt;,&gt;,我想你可以这样做:

// can also be made an extension method
static bool HasCorrectInType(ICalculation calc, Type desiredInType)
{
  var t = calc.GetType();
  do
  {
    if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(CalculationBase<,>))
      return t.GetGenericArguments()[0].IsAssignableFrom(desiredInType);
    t = t.BaseType;
  } while (t != null)

  throw new Exception("The type " + calc.GetType() + " not supported");
}

然后像这样使用它:

List<ICalculation> repository = XXX;
var matches = repository.Where(c => HasCorrectInType(c, type));

编辑:新想法:如果你放一个新属性:

public Type InType
{
  get { return typeof(TIn); }
}

到你的抽象类CalculationBase&lt;TIn, TOut&gt;,并将这个属性添加到你的非泛型接口ICalculation,这样你就不必遍历基类,但可以直接说calc.InType.IsAssignableFrom(desiredInType)

【讨论】:

  • 不理想,但我喜欢它...我可能会使用它,这取决于它的性能。谢谢:)
  • @BenAlabaster 我有了一个更理想的新想法,也许(见编辑)。
  • 响应您的编辑,我实际上一直在独立考虑这种方法。它适合尝试在运行时评估它的目的,但我不确定它是否会玷污我的基类......虽然它值得考虑。我认为,如果我的计算引擎最终将完成所有公式的编组和对象处理评估要使用的计算,那将是合适的。
【解决方案3】:

我认为应该选择最简单的解决方案。您可以使用反射来获取特定类型,或者在所有情况下都返回双精度。如果是密集型数据处理,尝试查找特定类型可能会减慢速度,因此返回双精度或整数完全没问题。

【讨论】:

    猜你喜欢
    • 2011-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-14
    • 2016-11-03
    • 2015-02-09
    • 2012-04-06
    • 1970-01-01
    相关资源
    最近更新 更多