Вывести универсальный тип на основе другого указанного универсального типа и использовать его - PullRequest
1 голос
/ 24 марта 2011

Прежде всего, я хочу отметить, что у меня уже есть работающее решение, но я пытаюсь выяснить, есть ли способ сделать код чище и менее громоздким.

Вот моя ситуация. Я на самом деле упростил ситуацию и создал поддельный пример, чтобы проиллюстрировать иллюстрацию. Я просто собираюсь выложить конкретный пример, показывающий, что я уже сделал, и это работает.

Предположим, у нас есть эти классы:

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 на списки вместо квадратов и кругов. Кроме того, предположим, что настройка 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 (), так что это не так уж и сложно, и я не знаю, может быть, это Хорошо, что оба типа указаны так, что это видно. Я просто подумал, что было бы здорово, если бы я мог найти какой-то способ сделать это, и я бы также узнал что-то новое. Я в основном хочу знать, можно ли это сделать, чтобы удовлетворить мое любопытство. Я не думаю, что это действительно так важно с точки зрения качества кода.

Ответы [ 2 ]

0 голосов
/ 29 июля 2014

Я не уверен, что это очень хороший способ сделать что-то, но у меня есть метод GetMockShapes, работающий так, как вы искали.Идея состоит в том, чтобы начать с MockShapeFactory, получить его метод CreateMockShape, преобразовать его в соответствующую универсальную версию и вызвать его для создания объекта правильного типа.

Это получает object хотяи метод mockShapes 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 известно во время компиляции, вы можете использовать простое статическое приведение вместо обхода системы ввода для передачи универсального объекта типизированному методу.Я думаю, что этот способ намного лучше, если вы можете его использовать.

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;
}
0 голосов
/ 24 марта 2011

Хорошо, теперь проблема в том, что вы не можете вызвать универсальный объект с экземпляром 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.

Этот шаблон может быть расширен для компиляции.делегаты, тем самым ускоряя все это, но для тестовой среды такая оптимизация, вероятно, не имеет смысла.

...