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

发布于 2024-10-25 22:37:45 字数 3848 浏览 6 评论 0原文

首先,我想指出,我已经有了一个可行的解决方案,但我正在尝试看看是否有一种方法可以使代码更干净、不那么麻烦。

这是我的情况。我实际上已经简化了情况并创建了一个假例子来使说明更清楚。我只是想举一个具体的例子来展示我已经做了什么,而且它是有效的。

假设我们有这些类:

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() 时额外输入一个单词,所以这并不是什么大不了的事,而且我不知道,也许两者都很好类型已指定,以便一目了然。我只是觉得如果我能找到一些方法来做到这一点那就太酷了,而且我也能学到一些新东西。我主要想知道是否可以这样做来满足我的好奇心。我认为就代码质量而言这并不是那么重要。

First of all, I want to point out, that I already have a working solution, but I am trying to see if there is a way to make the code cleaner and less cumbersome.

Here is my situation. I have actually simplified the situation and created a fake example to make the illustration clear. I am just going to lay out a concrete example showing what I have already done, and it works.

Suppose we have these classes:

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

And suppose there's some kind of class that does something with them like this:

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

Now suppose I want to test the ShapeThingy class. Suppose that for some of the tests, I want to substitute MockSquares and MockCircles into the lists in place of Squares and Circles. Also, suppose that setting up the MockCircles and the MockSquares is very similar, such that I want to have one method to create the lists of mock shapes, and I tell this method the type of shape that I need. Here is how I have implemented it:

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;
    }
}

Now this works fine. The problem I have with it is that you have specify to GetMockShapes() both the desired output type of the list, and the mock type that you actually want the list to contain. When in reality, I already know that if I ask GetMockShapes() for List<Square>, then it should actually be filled with MockSquare. It's kind of cumbersome to have to specify both things over and over.

What I want to do is something like this:

      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();
      }

The problem is that version won't compile, and I can't figure out a way around it. Maybe what I want to do is not possible.

Now at this point you may be thinking, "If he just uses IEnumerable<T> instead of List<T>, then he can take advantage of covariance in C# 4.0 and he won't have to do any of this crap," which is true, but but in our real code, we are not using List<T>, but rather a custom concrete type, Something<T> (and it is not an IEnumerable-style collection), and I don't have the ability to change the usage of Something<T> and introduce a covariant interface ISomething<out T> right now.

Anyways, all I am trying to do, I guess, is trying to save myself from having to type one extra word whenever I call GetMockShapes(), so it's not really that big of a deal, and I dunno, maybe it's good that both types are specified so that it's plain to see. I just thought it would be cool if I could figure out some way to do this, and I would learn something new as well. I mostly want to know if this can be done to satisfy my curiosity. I don't think it's really that important in terms of code quality.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

人间☆小暴躁 2024-11-01 22:37:45

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

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

  • 修改 MockShapeFactory.CreateMockShape 方法以获取 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

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

Okay the problem now is that you can't invoke a generic with a Type instance, you need a compile-time type handle for that.

To get around this you can:

  • Modify the MockShapeFactory.CreateMockShape<T> method to take a Type instance rather than write it as generic - but the actual creation of the instance would probably be harder then.

  • Dynamically bind to the 'correct' version of the CreateMockShape method (based on the type returned from getAppropriateMockType) using reflection.

For the second - this test code might prove helpful:

#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);
}

It looks worse than it is - the goal is to replace the call to the factory's generic method with one that accepts a Type instance - which is what DynamicBindAndInvoke(Type) does in this example. It might look pointless in this test - but that's only because I'm feeding in types known at compile time - in your case the type being passed would be the one retrieved from your getAppropriateMockType method.

Note that I've assumed that your factory method is a static on MockShapeFactory here. If it's not, then the reflection and invoke code would have to change to search for an instance method and to pass the instance of the factory as the first parameter to Invoke.

This pattern can be extended to compile delegates, thus speeding it all up, but for a test environment that kind of optimisiation is probably pointless.

神也荒唐 2024-11-01 22:37:45

我不确定这是一种很好的方法,但我得到了 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 并用 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;
}

I'm not sure it's a very good way to do things, but I got the GetMockShapes method working the way you were looking for. The idea is to start with the MockShapeFactory, get its CreateMockShape method, convert it to the appropriate generic version and invoke it to create an object of the correct type.

That gets an object though, and mockShapes's Add method only accepts the correctly typed Shape. I couldn't figure out how to dynamically cast the new mockShape to its appropriate type. That would have avoided the need to invoke the builder through reflection anyway, I think.

I circumvented the type checking system instead (like I said, "not sure it's a very good way to do things"). I started with the mockShapes list, got its runtime type, got its Add method, and invoked that with the newly created object. The compiler expects objects for the method and allows this; reflection enforces proper typing at runtime. Bad things might happen if GetAppropriateMockType ever returns an inappropriate type.

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;
}

A better (but situation-specific) way

After some more thinking I realized there's an unstated assumption here you can abuse. You're trying to make a List<TBase> and fill it with TMock. The whole point of TMock is to impersonate TBase, so TMock is a TBase. In fact, the List even uses TBase as its type parameter.

That's important because it means you don't have to cast the generic object to a TMock, you can just cast it to a TBase. Since TBase is known at compile time, you can use a simple static cast instead of circumventing the typing system to pass a generic object to a typed method. I think this way is a lot better if you can use it.

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;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文