Avatar ·

Как вызвать generic метод через Type переменную в C#?

Всем привет. Я пишу небольшой парсер, который должен обрабатывать разные типы данных, и столкнулся с проблемой, которую не могу решить уже второй день. У меня есть generic метод: ```csharp public T Deserialize(string data) where T : class { // какая-то логика десериализации return JsonConvert.DeserializeObject(data); } ``` В рантайме я получаю `Type` объекта, который нужно десериализовать. Например, через рефлексию я нахожу все классы, реализующие интерфейс `IEntity`, и для каждого хочу вызвать `Deserialize` с конкретным типом. Проблема в том, что я не могу просто написать `Deserialize(data)`, потому что `myType` — это переменная типа `Type`, а не имя типа на этапе компиляции. Я пытался использовать `typeof` и `MakeGenericMethod`, но у меня ничего не работает. Вот мой текущий код, который выдает ошибку: ```csharp Type targetType = typeof(MyClass); MethodInfo method = typeof(MyParser).GetMethod("Deserialize"); MethodInfo genericMethod = method.MakeGenericMethod(targetType); object result = genericMethod.Invoke(this, new object[] { data }); ``` Ругается на то, что метод не найден или параметры не совпадают. Подскажите, как правильно вызывать generic методы с типом, который известен только во время выполнения? Может быть, есть более простой способ без рефлексии? Заранее спасибо! <<>>

Правильный вызов generic метода через рефлексию

Ваш подход в целом верный, но есть несколько нюансов. Давайте разберем правильный способ шаг за шагом.

Шаг 1: Получение MethodInfo без указания generic-параметров

Когда вы вызываете GetMethod("Deserialize"), вы получаете неконкретизированный generic-метод. Но есть важный момент: если у вас есть перегрузки метода (например, Deserialize<T> и просто Deserialize), то нужно явно указать, что мы ищем generic-версию. Лучше использовать GetMethods() и фильтровать:

// Получаем все методы с именем "Deserialize"
MethodInfo method = typeof(MyParser)
    .GetMethods()
    .First(m => m.Name == "Deserialize" && m.IsGenericMethodDefinition);

Шаг 2: Создание конкретного generic-метода

Теперь, когда у нас есть MethodInfo для Deserialize<>, мы можем закрыть его нужным типом:

Type targetType = typeof(MyClass);
MethodInfo genericMethod = method.MakeGenericMethod(targetType);

Шаг 3: Вызов метода

При вызове Invoke нужно передать экземпляр класса (если метод нестатический) и массив параметров:

object result = genericMethod.Invoke(this, new object[] { dataString });

Полный рабочий пример

public class MyParser
{
    public T Deserialize<T>(string data) where T : class
    {
        return JsonConvert.DeserializeObject<T>(data);
    }

    public object DeserializeByType(Type targetType, string data)
    {
        // Шаг 1: находим generic-метод
        MethodInfo method = typeof(MyParser)
            .GetMethods()
            .First(m => m.Name == "Deserialize" && m.IsGenericMethodDefinition);
        
        // Шаг 2: закрываем generic-параметр
        MethodInfo genericMethod = method.MakeGenericMethod(targetType);
        
        // Шаг 3: вызываем
        return genericMethod.Invoke(this, new object[] { data });
    }
}

// Использование:
var parser = new MyParser();
Type type = typeof(MyClass);
object result = parser.DeserializeByType(type, "{\"name\":\"test\"}");
var myObj = (MyClass)result; // приведение к нужному типу

Важные моменты:

  • Проверка на null: всегда проверяйте, что method не равен null. Если метод не найден, вы получите исключение.
  • Производительность: рефлексия медленная. Если вы вызываете этот метод много раз, кэшируйте MethodInfo и genericMethod в словаре.
  • Generic-ограничения: если у вас есть ограничения (where T : class), убедитесь, что targetType им соответствует, иначе будет исключение.
  • Возвращаемое значение: Invoke возвращает object, поэтому вам придется привести результат к нужному типу.

Кэширование для производительности

private readonly Dictionary<Type, MethodInfo> _cache = new();

public object DeserializeByTypeCached(Type targetType, string data)
{
    if (!_cache.TryGetValue(targetType, out var method))
    {
        var methodDef = typeof(MyParser)
            .GetMethods()
            .First(m => m.Name == "Deserialize" && m.IsGenericMethodDefinition);
        method = methodDef.MakeGenericMethod(targetType);
        _cache[targetType] = method;
    }
    return method.Invoke(this, new object[] { data });
}

Этот подход сработает для любого generic-метода, а не только для десериализации. Главное — правильно получить неконкретизированный MethodInfo и закрыть его нужным типом.

<<>>

Альтернатива: использование интерфейсов или абстрактного базового класса

Если вы контролируете код и типы известны заранее (хотя и не на этапе компиляции), можно избежать рефлексии через паттерн "посетитель" или фабрику.

Создайте не-generic интерфейс:

public interface IDeserializer
{
    object Deserialize(string data);
}

public class Deserializer<T> : IDeserializer where T : class
{
    public object Deserialize(string data) 
        => JsonConvert.DeserializeObject<T>(data);
}

Затем используйте словарь для сопоставления типов с десериализаторами:

private readonly Dictionary<Type, IDeserializer> _deserializers = new()
{
    { typeof(MyClass), new Deserializer<MyClass>() },
    { typeof(OtherClass), new Deserializer<OtherClass>() }
};

public object Deserialize(Type type, string data)
{
    if (_deserializers.TryGetValue(type, out var deserializer))
        return deserializer.Deserialize(data);
    throw new NotSupportedException($"Type {type} is not supported");
}

Плюсы: без рефлексии, быстрее, типобезопасно на этапе регистрации.
Минусы: нужно заранее регистрировать все типы. Подходит, если количество типов ограничено и известно.