根据另一个指定的泛型类型推断泛型类型并使用它

时间:2011-03-24 08:12:25

标签: c# generics c#-4.0 covariance

首先,我想指出,我已经有了一个可行的解决方案,但我想看看是否有办法让代码更清晰,更轻松。

这是我的情况。我实际上已经简化了这种情况,并创建了一个虚假的例子来清楚说明。我将展示一个具体的例子,展示我已经完成的工作,并且它有效。

假设我们有这些类:

public abstract class Shape{ //...elided... }
public class Square : Shape { //...elided... }
public class Circle : Shape { //...elided... }

并且假设有某种类与他们一起做这样的事情:

public class ShapeThingy
{
     public static void MakeSquaresDance(List<Squares> squares){ //...elided... }
     public static void RollCircles(List<Circles> circles){ //...elided... }
}

现在假设我想测试ShapeThingy类。假设对于某些测试,我想将MockSquares和MockCircles替换为代替Squares和Circles的列表。另外,假设设置MockCircles和MockSquares是非常相似的,这样我想有一个方法来创建模拟形状列表,我告诉这个方法我需要的形状类型。以下是我实施它的方法:

public class Tests
{
      [Test]
      public void TestDancingSquares()
      {
          List<Squares> mockSquares = GetMockShapes<Square, MockSquare>();
          ShapeThingy.MakeSquaresDance(mockSquares);

          Assert.Something();
      }

      [Test]
      public void TestRollingCircles()
      {
          List<Circles> mockCircles = GetMockShapes<Circle, MockCircle>();
          ShapeThingy.RollCircles(mockCircles );

          Assert.Something();
      }


      private List<TBase> GetMockShapes<TBase, TMock>()
         where TBase : Shape
         where TMock : TBase, new()
      {
         List<TBase> mockShapes = new List<TBase>();

         for (int i = 0; i < 5; i++)
         {
            mockShapes.Add(MockShapeFactory.CreateMockShape<TMock>());
         }
      }

}

public class MockSquare : Square { //...elided... }
public class MockCircle : Circle { //...elided... }

public class MockShapeFactory
{
    public static T CreateMockShape<T>()
        where T : Shape, new()
    {
        T mockShape = new T();
        //do some kind of set up
        return mockShape;
    }
}

现在这很好用。我遇到的问题是你已经指定GetMockShapes()列表的所需输出类型,以及实际上希望列表包含的模拟类型。实际上,我已经知道如果我向List&lt; Square&gt;询问GetMockShapes(),那么它实际上应该用MockSquare填充。必须一遍又一遍地指定这两件事是很麻烦的。

我想做的是这样的事情:

      private List<TBase> GetMockShapes<TBase>()
         where TBase : Shape
      {
         List<TBase> mockShapes = new List<TBase>();

         Type mockType = getAppropriateMockType<TBase>();

         for (int i = 0; i < 5; i++)
         {
            //compiler error: typeof(mockType) doesn't work here
            mockShapes.Add(MockShapeFactory.CreateMockShape<typeof(mockType)>());
         }
      }

      private Type getAppropriateMockType<TBase>()
      {
         if(typeof(TBase).Equals(typeof(Square)))
         {
            return typeof(MockSquare);
         }

         if(typeof(TBase).Equals(typeof(Circle)))
         {
            return typeof(MockCircle);
         }

         //else
         throw new ArgumentException(typeof(TBase).ToString() + " cannot be converted to a mock shape type.");
      }

      //add then a test would look like this
      //(one less word, one less chance to screw up)
      [Test]
      public void TestDancingSquares()
      {
          List<Squares> mockSquares = GetMockShapes<Square>();
          ShapeThingy.MakeSquaresDance(mockSquares);

          Assert.Something();
      }

问题是版本无法编译,我无法找到解决方法。也许我想做的事情是不可能的。

现在,你可能会想,“如果他只使用IEnumerable&lt; T&gt;而不是List&lt; T&gt;,那么他可以利用C#4.0中的协方差,他不必做任何这个废话,“这是真的,但在我们的实际代码中,我们没有使用List&lt; T&gt;,而是一个自定义的具体类型,Something&lt; T&gt; (并且它不是IEnumerable风格的集合),并且我无法更改Something&lt; T&gt;的使用情况。并引入协变接口ISomething&lt; out T&gt;现在。

无论如何,我想,我试图做的就是试图让自己不必在每次调用GetMockShapes()时输入一个额外的单词,所以它不是那么大的交易,我不知道,也许它是很好,两种类型都被指定,所以很明显。我只是觉得如果我能找到一些方法来做这件事会很酷,我也会学到一些新东西。我大多想知道是否可以这样做以满足我的好奇心。我不认为它在代码质量方面真的那么重要。

2 个答案:

答案 0 :(得分:0)

现在问题是你不能用Type实例调用泛型,你需要一个编译时类型句柄。

要解决这个问题,你可以:

  • 修改MockShapeFactory.CreateMockShape<T>方法以获取Type实例,而不是将其写为通用实例 - 但实际创建的实例可能会更难。

  • 使用反射动态绑定到CreateMockShape方法的“正确”版本(基于从getAppropriateMockType返回的类型)。

对于第二个 - 这个测试代码可能会有用:

#region some stubs (replaced with your types)

public class Shape { }
public class MockSquare : Shape { }
public class MockCircle : Shape { }

public class MockShapeFactory
{
  //I've added a constraint so I can new the instance
  public static T CreateMockShape<T>()
    where T : Shape, new()
  {
    Console.WriteLine("Creating instance of {0}", typeof(T).FullName);
    return new T();
  }
}

#endregion

//you can cache the reflected generic method
System.Reflection.MethodInfo CreateMethodBase =
  typeof(MockShapeFactory).GetMethod(
    "CreateMockShape", 
    System.Reflection.BindingFlags.Public 
    | System.Reflection.BindingFlags.Static
  );

[TestMethod]
public void TestDynamicGenericBind()
{
  //the DynamicBindAndInvoke method becomes your replacement for the 
  //MockShapeFactory.CreateMockShape<typeof(mockType)>() call
  //And you would pass the 'mockType' parameter that you get from
  //getAppropriateMockType<TBase>();
  Assert.IsInstanceOfType
    (DynamicBindAndInvoke(typeof(MockCircle)), typeof(MockCircle));

  Assert.IsInstanceOfType
    (DynamicBindAndInvoke(typeof(MockSquare)), typeof(MockSquare));
}
//can change the base type here according to your generic
//but you will need to do a cast e.g. <
public Shape DynamicBindAndInvoke(Type runtimeType)
{
  //make a version of the generic, strongly typed for runtimeType
  var toInvoke = CreateMethodBase.MakeGenericMethod(runtimeType);
  //should actually throw an exception here.
  return (Shape)toInvoke.Invoke(null, null);
}

它看起来比它更糟糕 - 目标是用一个接受Type实例的方法替换对工厂通用方法的调用 - 这是DynamicBindAndInvoke(Type)在这个例子中的作用。在这个测试中它可能看起来毫无意义 - 但这只是因为我在编译时提供已知的类型 - 在你的情况下,传递的类型将是从你的getAppropriateMockType方法中检索的类型。

请注意,我假设您的工厂方法在MockShapeFactory处是静态的。如果不是,则反射和调用代码必须更改为搜索实例方法并将工厂实例作为第一个参数传递给Invoke

这种模式可以扩展到编译代理,从而加快了速度,但对于测试环境而言,这种优化可能毫无意义。

答案 1 :(得分:0)

我不确定这是一种非常好的做事方式,但我得到了GetMockShapes方法,正如您所寻找的那样。我们的想法是从MockShapeFactory开始,获取其CreateMockShape方法,将其转换为适当的通用版本并调用它以创建正确类型的对象。

虽然获得了object,但mockShapes的{​​{1}}方法只接受正确输入的Add。我无法弄清楚如何动态地将新的Shape转换为适当的类型。我认为这样就无需通过反射调用构建器了。

我绕过了类型检查系统(就像我说的那样,“不确定这是一种非常好的做事方式”)。我从mockShape列表开始,得到了它的运行时类型,得到了它的mockShapes方法,并用新创建的对象调用了它。编译器期望该方法的对象并允许这个;反射在运行时强制正确键入。如果Add返回不合适的类型,可能会发生错误。

GetAppropriateMockType

更好(但特定情况)的方式

经过一番思考之后,我意识到这里有一个未说明的假设,你可以滥用。您正在尝试制作using System.Collections.Generic; using System.Reflection; using System; private List<TBase> GetMockShapes<TBase>() where TBase : Shape { Type TMock = getAppropriateMockType<TBase>(); // Sanity check -- if this fails, bad things might happen. Assert(typeof(TBase).IsAssignableFrom(TMock)); List<TBase> mockShapes = new List<TBase>(); // Find MockShapeFactory.CreateMockShape() method MethodInfo shapeCreator = typeof(MockShapeFactory).GetMethod("CreateMockShape"); // Convert to CreateMockShape<TMock>() method shapeCreator = shapeCreator.MakeGenericMethod(new Type[] { TMock }); for (int i = 0; i < 5; i++) { // Invoke the method to get a generic object // The object to invoke on is null because the method is static // The parameter array is null because the method expects no parameters object mockShape = shapeCreator.Invoke(null, null); mockShapes.GetType() // Get the type of mockShapes .GetMethod("Add") // Get its Add method .Invoke( // Invoke the method mockShapes, // on mockShapes list new object[] { mockShape }); // with mockShape as argument. } return mockShapes; } 并将其填入List<TBase>TMock的重点是冒充TMock,因此TBase TMock。实际上,TBase甚至使用List作为其类型参数。

这很重要,因为这意味着您不必将通用对象强制转换为TBase,您只需将其转换为TMock即可。由于TBase在编译时是已知的,因此您可以使用简单的静态强制转换而不是绕过类型系统将通用对象传递给类型化方法。我认为如果你能使用它,这种方式会好很多。

TBase