C#通用和特定扩展方法组合歧义

时间:2011-02-02 04:41:02

标签: c# generics extension-methods type-parameter

我有一个名为 Fruit。抽象类 然后我有一个名为 Apple 派生类

我有这两种扩展方法:

public static IQueryable<TFruit> WithEagerLoading<TFruit>(this IQueryable<TFruit> query) where TFruit : Fruit
{
   return query.EagerLoad(x => x.Distributors); // Fruit.Distributors
}

public static IQueryable<Apple> WithEagerLoading(this IQueryable<Apple> query)
{
   query = query.EagerLoad(x => x.AppleBrands); // Apple.AppleBrands

   // now bubble up to base extension method
   return query.WithEagerLoading<Apple>();
}

现在,这是我在存储库中的通用方法:

public TFruit FindById<TFruit>(int fruitId) where TFruit : Fruit
{
   var query = _ctx.Fruits // IQueryable<Fruit>
                   .OfType<TFruit>(); // IQueryable<TFruit>

   query = query.WithEagerLoading();

   return query.SingleOrDefault(x => x.FruitId == fruitId);
}

我遇到的问题是,当我这样做时:

var apple = repository.FindById<Apple>(1);

它进入IQueryable<Fruit>扩展方法。

我希望它进入IQueryable<Apple>扩展方法。对于其他类型的Fruit,它应该进入IQueryable<TFruit>扩展方法。

我认为编译器会选择最具体的扩展方法。

有什么想法吗?

修改

感谢您的评论/回答。我现在看到为什么这不起作用。

那么有什么方法可以解决这个问题?如果我创建一个方法:

public static IQueryable<Apple> WithAppleEagerLoading(this IQueryable<Apple> query)

我如何从我的通用存储库中调用它?我必须检查TFruit

的类型
public TFruit FindById<TFruit>(int fruitId) where TFruit : Fruit
{
   var query = _ctx.Fruits // IQueryable<Fruit>
                   .OfType<TFruit>(); // IQueryable<TFruit>

   if (typeof(TFruit) == typeof(Apple))
       query = query.WithAppleEagerLoading();
   else 
       query = query.WithEagerLoading();

   return query.SingleOrDefault(x => x.FruitId == fruitId);
}

哪个不好 - 考虑到我有大约20种派生类型。

任何人都可以提供替代我试图做的事情吗?

3 个答案:

答案 0 :(得分:2)

我遇到了类似的问题,我希望有效地更改方法的优先级,以便首先解析“专用”版本。

您可以在不更改调用代码的情况下实现此目的,但该解决方案可能不受欢迎,因为它使用运行时反射和代码生成。无论如何我只是想把它扔出去(我试着每天坚持至少一个答案!)。

请注意,此代码表示需要根据您的方案进行调整的抽象模式。

public class Base
{
  public string BaseString { get; set; }
}

public class Derived : Base
{
  public string DerivedString { get; set; }
}

public static class SO4870831Extensions
{
  private static Dictionary<Type, Action<Base>> _helpers = 
    new Dictionary<Type,Action<Base>>();

  public static void Extension<TBase>(this TBase instance) 
    where TBase :Base
  {
    //see if we have a helper for the absolute type of the instance
    var derivedhelper = ResolveHelper<TBase>(instance);

    if (derivedhelper != null)
      derivedhelper(instance);
    else
      ExtensionHelper(instance);
  }

  public static void ExtensionHelper(this Base instance)
  {
    Console.WriteLine("Base string: {0}", 
      instance.BaseString ?? "[null]");
  }

  /// <summary>
  /// By Default this method is resolved dynamically, but is also 
  /// available explicitly.
  /// </summary>
  /// <param name="instance"></param>
  public static void ExtensionHelper(this Derived instance)
  {
    Console.WriteLine("Derived string: {0}", 
      instance.DerivedString ?? "[null]");
    //call the 'base' version - need the cast to avoid Stack Overflow(!)
    ((Base)instance).ExtensionHelper();
  }

  private static Action<Base> ResolveHelper<TBase>(TBase instance) 
    where TBase : Base
  {
    Action<Base> toReturn = null;
    Type instanceType = instance.GetType();
    if (_helpers.TryGetValue(instance.GetType(), out toReturn))
      return toReturn;  //could be null - that's fine

    //see if we can find a method in this class for that type
    //this could become more complicated, for example, reflecting
    //the type itself, or using attributes for richer metadata
    MethodInfo helperInfo = typeof(SO4870831Extensions).GetMethod(
      "BaseExtensionHelper", 
      BindingFlags.Public | BindingFlags.Static, 
      null, 
      new Type[] { instanceType }, 
      null);

    if (helperInfo != null)
    {
      ParameterExpression p1 = Expression.Parameter(typeof(Base), "p1");
      toReturn =
        Expression.Lambda<Action<Base>>(
        /* body */
          Expression.Call(
            helperInfo,
            Expression.Convert(p1, instanceType)),
        /* param */
          p1).Compile();
      _helpers.Add(instanceType, toReturn);
    }
    else
      //cache the null lookup so we don't expend energy doing it again
      _helpers.Add(instanceType, null);
    return toReturn;
  }
}
/// <summary>
/// Summary description for UnitTest1
/// </summary>
[TestClass]
public class UnitTest1
{
  [TestMethod]
  public void TestMethod1()
  {
    var a = new Base() { BaseString = "Base Only" };
    var b = new Derived() { DerivedString = "Derived", BaseString = "Base" };

    a.Extension();
    //Console output reads:
    //"Base String: Base Only"
    b.Extension();
    //Console output reads:
    //"Derived String: Derived"
    //"Base String: Base"
  }

我并不是说这种模式比找到使用该语言提供的更传统模式的多态解决方案更好 - 但它是一种解决方案:)

此模式可以应用于大多数扩展方法 - 但是在当前形式中,您必须为要编写的每个扩展方法重复该模式。同样,如果这些扩展需要ref / out参数,那将会更加棘手。

正如我在评论中所说,您可以考虑更改方法的查找以支持在实例类型本身中定义(将相关代码保存在一起)。

你必须做一些工作才能使你的IQueryable正常工作 - 对不起,我没有让这个解决方案与你的场景更直接相关,只是更容易模拟一个测试解决方案把大部分通用地狱带走了!

答案 1 :(得分:1)

最终你需要找到一种方法来引入一些多态性 - 你想要加载苹果的特殊行为,这扩展了加载水果的基本行为。有时,最简单的方法是创建存储库类,例如:

class Repository<T> : IRepository<T>
{

   public virtual T FindById(int id)
   { ... }
}

class FruitRepository<T> : Repository<T> where T : Fruit
{
   public override T FindById(int id)
   {  ... }
}

class AppleRepository : FruitRepository<Apple>
{
   public override T FindById(int id)
   {  ... }
}

现在,FindByID不需要在方法级别拥有泛型参数,它只是在类级别使用泛型参数。然后,您可以根据需要使FruitRepository和AppleRepository覆盖FindByID。您的使用代码会有所不同,因为您必须确保您拥有的存储库实例适合查找苹果。

如果您正在使用IoC容器,则可以在请求IRepository<Apple>请求它返回AppleRepository实例时注册。然后你会这样使用它:

// ideally you would resolve this via constructor injection, but whatever.
var repository = container.Resolve<IRepository<Apple>>(); 
var apple = repository.FindByID(1);

如果你没有使用IoC容器......那么......你应该是:)

答案 2 :(得分:0)

扩展方法解析在编译时发生。查询变量的类型为IQueryable<TFruit>,因此编译器会选择与WithEagerLoading<Fruit>匹配的最具体的方法。它不能选择苹果,因为它只知道TFruit是某种Fruit。它选择一次,永久。

您所建议的是要求它根据运行时的类型动态决定使用哪种扩展方法,或者根据{{1}的特定值编译以IQueryable<TFruit>为单位的不同版本的解析方法}。

编辑以回答其他问题

嗯,特殊的外壳并不是非常可怕,因为你可以使用switch语句。但我同意,如果你有很多类型的话,这并不理想。在委托子类方面,我会稍微调整保罗的答案:

TFruit

然后

abstract class FruitRepository : IRepository<T> where TFruit : Fruit
{
   public TFruit FindByID(int fruitID)
   {
       //query stuff here

       query = AddEagerLoading(query)
                  .WithEagerLoading();
    }

    //this could also be abstract to prevent you from forgetting
    public virtual IQueryable<TFruit> AddEagerLoading(IQueryable<TFruit> query)
    {
        return query;
    }
}

这样你就可以获得每个子类的最小代码。