【问题标题】:Infer a generic type based on another specified generic type and use it根据另一个指定的泛型类型推断泛型类型并使用它
【发布时间】:2011-03-24 08:12:25
【问题描述】:

首先,我想指出,我已经有了一个可行的解决方案,但我正在尝试看看是否有办法让代码更简洁、更简洁。

这是我的情况。我实际上已经简化了情况并创建了一个假示例以使插图清晰。我只是要展示一个具体的例子来展示我已经完成的工作,并且它是有效的。

假设我们有这些类:

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() 指定了所需的列表输出类型和您实际希望列表包含的模拟类型。实际上,我已经知道如果我向 GetMockShapes() 请求 List,那么它实际上应该被 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 而不是 List,那么他可以利用 C# 4.0 中的协方差,而他不必做任何这些废话” 这是真的,但是在我们的真实代码中,我们没有使用 List,而是使用自定义的具体类型,Something(而且它不是 IEnumerable 样式的集合),而我没有有能力改变 Something 的用法并立即引入协变接口 ISomething

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

【问题讨论】:

  • 好吧,在你的例子中它不会编译,因为getAppropriateMockType 没有被指定为通用的。将其指定为具有与第一种方法相同的约束的泛型,它会起作用吗?
  • 啊,谢谢。这修复了两个编译器错误之一,但还有另一个。我更正了该代码,还添加了注释以显示仍然导致问题的部分。

标签: c# generics c#-4.0 covariance


【解决方案1】:

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

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

  • 修改MockShapeFactory.CreateMockShape&lt;T&gt; 方法以采用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

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

【讨论】:

    【解决方案2】:

    我不确定这是否是一种很好的做事方式,但我得到了 GetMockShapes 方法,它可以按照您正在寻找的方式工作。这个想法是从MockShapeFactory 开始,获取它的CreateMockShape 方法,将其转换为适当的泛型版本并调用它来创建正确类型的对象。

    这会得到一个object,而mockShapesAdd 方法只接受正确输入的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&lt;TBase&gt; 并用TMock 填充它。 TMock 的全部意义在于冒充TBase,所以TMock 是一个 TBase。事实上,List 甚至使用TBase 作为其类型参数。

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

    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<mockType>() 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);
    
            //
            // Changes start here
            //
    
            // Static cast the mock shape to the type it's impersonating
            TBase mockBase = (TBase)mockShape;
    
            // Now this works because typeof(mockBase) is known at compile time.
            mockShapes.Add(mockBase);
        }
    
        return mockShapes;
    }
    

    【讨论】:

      猜你喜欢
      • 2021-06-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-25
      相关资源
      最近更新 更多