Другое

Исправление TypeLoadException в .NET Reflection Emit для универсальных интерфейсов

Узнайте, как исправлять ошибки TypeLoadException при создании универсальных интерфейсов с помощью .NET Reflection.Emit. Узнайте о ключевых проблемах в вашей реализации и как правильно связывать методы интерфейсов.

Как использовать .NET reflection для создания универсальных интерфейсов и класса, который их реализует? Я encountering TypeLoadException ошибки при попытке реализации универсальных интерфейсов с конкретными аргументами типов с помощью System.Reflection.Emit. Сообщения об ошибках указывают на проблемы с сигнатурами методов и их реализациями. Вот мой код, который пытается создать интерфейсы и класс, эквивалентные этому C#:

csharp
public interface MyInterface1
{
    void IfaceMethod(int arg);
}

public interface MyInterface2<T>
{
    void IfaceMethod(int arg);
}

public interface MyInterface3<T>
{
    void IfaceMethod(T arg);
}

public class MyClass : MyInterface1, MyInterface2<double>, MyInterface3<int>
{
    void MyInterface1.IfaceMethod(int arg)
    {
        Console.WriteLine(...);
    }

    void MyInterface2<double>.IfaceMethod(int arg)
    {
        Console.WriteLine(...);
    }

    void MyInterface3<int>.IfaceMethod(int arg)
    {
        Console.WriteLine(...);
    }
}

Когда я использую reflection для создания этих типов, я получаю ошибки вроде:

  • ‘Type ‘MyClass’ tried to override method ‘fn_0’ but does not implement or inherit that method.’
  • ‘Signature of the body and declaration in a method implementation do not match.’

Вот исправленная версия моего кода, которая работает:

csharp
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;
using System.Linq;

class Test
{
    static void Main()
    {
        string example_name = "Interface example";
        AssemblyName asm_name = new AssemblyName(example_name);
        AssemblyBuilder asm_builder = AssemblyBuilder.DefineDynamicAssembly(asm_name, AssemblyBuilderAccess.Run);
        ModuleBuilder module_builder = asm_builder.DefineDynamicModule(example_name);

        TypeBuilder class_builder = module_builder.DefineType("MyClass", TypeAttributes.Public | TypeAttributes.Class);

        TypeBuilder interface1_builder = module_builder.DefineType("MyInterface1", TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);
        TypeBuilder interface2_builder = module_builder.DefineType("MyInterface2", TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);
        TypeBuilder interface3_builder = module_builder.DefineType("MyInterface3", TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);
        Type[] interface2_generics = interface2_builder.DefineGenericParameters(["T"]);
        Type[] interface3_generics = interface3_builder.DefineGenericParameters(["T"]);

        MethodAttributes method_attributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Abstract;
        List<MethodBuilder> interface_method_builders = new List<MethodBuilder>();
        interface_method_builders.Add(interface1_builder.DefineMethod("IfaceMethod", method_attributes, null, [typeof(int)]));
        interface_method_builders.Add(interface2_builder.DefineMethod("IfaceMethod", method_attributes, null, [typeof(int)]));
        interface_method_builders.Add(interface3_builder.DefineMethod("IfaceMethod", method_attributes, null, interface3_generics));
        
        interface1_builder.CreateType();
        interface2_builder.CreateType();
        interface3_builder.CreateType();
        
        List<Type> interfaces = new List<Type>();
        interfaces.Add(interface1_builder);
        interfaces.Add(interface2_builder.MakeGenericType([typeof(double)]));
        interfaces.Add(interface3_builder.MakeGenericType([typeof(int)]));
        
        for(int i = 0; i < interfaces.Count; i++){
            Type interface_type = interfaces[i];
            MethodInfo interface_method = interface_method_builders[i];
            try
            {
                interface_method = TypeBuilder.GetMethod(interface_type, interface_method); /* на случай, если это метод из универсального интерфейса */
            }
            catch(Exception)
            {
            }
            
            class_builder.AddInterfaceImplementation(interface_type);

            string method_name = "fn_"+i;
            MethodBuilder class_method = class_builder.DefineMethod(method_name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, null, [typeof(int)]);
            ILGenerator il = class_method.GetILGenerator();
            il.EmitWriteLine("Hello from " + method_name + " implementing IfaceMethod from " + interface_type.Name);
            il.Emit(OpCodes.Ret);

            class_builder.DefineMethodOverride(class_method, interface_method);
        }

        Type class_type = class_builder.CreateType();
        object obj = Activator.CreateInstance(class_type);
        List<MethodInfo> methods = class_type.GetInterfaces().Select(i => i.GetMethod("IfaceMethod")).ToList();
        Object[] args = [123];
        foreach(MethodInfo method in methods){
            method.Invoke(obj, args);
        }
    }
}

В чем была ключевая проблема в моей первоначальной реализации, и как исправленная версия ее решает?

Основная проблема в вашей исходной реализации была связана с несоответствиями сигнатур методов и неправильным связыванием методов интерфейса при работе с универсальными интерфейсами в System.Reflection.Emit. Исправленная версия решает эту проблему за счет нескольких критических улучшений в том, как методы интерфейса определяются, связываются и реализуются.

Содержание

Анализ основной проблемы

Ошибки TypeLoadException, с которыми вы столкнулись, возникают потому, что загрузчик типов CLR проверяет, что реализации методов точно соответствуют их объявленным сигнатурам. При работе с универсальными интерфейсами эта проверка становится еще более строгой из-за ограничений параметров типа и требований к созданию экземпляров.

Сообщения об ошибках указывают на:

  • "Тип 'MyClass' попытался переопределить метод 'fn_0', но не реализует или не наследует этот метод" - предполагает, что метод не был правильно связан с интерфейсом
  • "Сигнатура тела и объявления в реализации метода не совпадают" - указывает на несоответствия параметров или типов возвращаемого значения

Ключевые проблемы в исходной реализации

На основе результатов исследований и общих шаблонов, ваша исходная реализация, вероятно, страдала от одной или нескольких из этих проблем:

  1. Разрешение методов универсального интерфейса: При создании универсальных интерфейсов, таких как MyInterface2<T> и MyInterface3<T>, методы интерфейса не были правильно разрешены после создания экземпляров интерфейсов с конкретными аргументами типов.

  2. Несоответствия сигнатур методов: Сигнатура тела emitted метода не точно соответствовала объявлению метода интерфейса, особенно с универсальными параметрами типа.

  3. Неправильное связывание интерфейса: Методы класса не были правильно связаны с соответствующими методами интерфейса с помощью DefineMethodOverride.

  4. Проблемы с таймингом: Создание типов интерфейса после определения методов на них может вызвать проблемы связывания, как отмечено в обсуждениях StackOverflow.

  5. Обработка универсальных параметров: Проблемы с обработкой универсальных параметров типа в сигнатурах методов, особенно при работе с дисперсией (такой как параметры in), как упоминается в этом посте StackOverflow.

Как исправленная версия решает эти проблемы

Исправленная версия решает эти проблемы за счет нескольких стратегических улучшений:

1. Правильный порядок создания типов

csharp
// Создавайте типы интерфейса ПЕРЕД их использованием
interface1_builder.CreateType();
interface2_builder.CreateType();
interface3_builder.CreateType();

Это гарантирует, что при последующем вызове MakeGenericType() и GetMethod() типы интерфейса полностью сконструированы и доступны для разрешения.

2. Правильная обработка универсальных интерфейсов

csharp
// Определяйте универсальные параметры перед созданием методов
Type[] interface2_generics = interface2_builder.DefineGenericParameters(["T"]);
Type[] interface3_generics = interface3_builder.DefineGenericParameters(["T"]);

Это правильно устанавливает универсальные параметры типа перед определением методов, которые их используют.

3. Разрешение методов интерфейса после создания экземпляра

csharp
// Обрабатывайте случай, когда метод может быть из универсального интерфейса
interface_method = TypeBuilder.GetMethod(interface_type, interface_method);

Этот критически важный шаг гарантирует, что при создании экземпляра универсального интерфейса (такого как MyInterface2<double>) ссылка на метод правильно разрешается для конкретного созданного метода.

4. Определение переопределения метода

csharp
class_builder.DefineMethodOverride(class_method, interface_method);

Это явно связывает реализацию метода класса с объявлением метода интерфейса, удовлетворяя требованию CLR, что реализации интерфейса должны быть правильно объявлены.

5. Уникальные имена методов с правильным связыванием

csharp
string method_name = "fn_"+i;
MethodBuilder class_method = class_builder.DefineMethod(method_name, ...);
class_builder.DefineMethodOverride(class_method, interface_method);

Использование уникальных имен методов и последующее связывание их с правильными методами интерфейса предотвращает конфликты сигнатур и обеспечивает правильное отображение реализации.

Критические детали реализации

Определение метода интерфейса

csharp
// Определяйте методы интерфейса с правильными сигнатурами
interface_method_builders.Add(interface1_builder.DefineMethod("IfaceMethod", method_attributes, null, [typeof(int)]));
interface_method_builders.Add(interface2_builder.DefineMethod("IfaceMethod", method_attributes, null, [typeof(int)]));
interface_method_builders.Add(interface3_builder.DefineMethod("IfaceMethod", method_attributes, null, interface3_generics));

Обратите внимание, как третий метод использует interface3_generics для типа параметра, в то время как первые два используют конкретные типы, такие как typeof(int).

Создание экземпляра универсального интерфейса

csharp
// Создавайте конкретные универсальные типы интерфейса
interfaces.Add(interface2_builder.MakeGenericType([typeof(double)]));
interfaces.Add(interface3_builder.MakeGenericType([typeof(int)]));

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

Шаблон реализации метода

Исправленная версия следует этому шаблону для каждого интерфейса:

  1. Получить метод интерфейса (с правильным разрешением)
  2. Добавить реализацию интерфейса в класс
  3. Определить уникальную реализацию метода класса
  4. Emit тело метода
  5. Связать метод класса с методом интерфейса с помощью DefineMethodOverride

Лучшие практики для Reflection.Emit с универсальными интерфейсами

  1. Создавайте типы перед их использованием: Всегда создавайте типы интерфейса перед ссылкой на них в определениях методов или создании экземпляров.

  2. Обрабатывайте разрешение универсальных параметров: Используйте TypeBuilder.GetMethod() для разрешения методов из универсальных интерфейсов после их создания экземпляра.

  3. Используйте уникальные имена методов: Избегайте конфликтов имен, используя уникальные имена методов для каждой реализации интерфейса.

  4. Явное переопределение метода: Всегда вызывайте DefineMethodOverride(), чтобы связать реализации с объявлениями интерфейса.

  5. Проверяйте сигнатуры: Дважды проверяйте, что сигнатуры методов точно совпадают между объявлениями интерфейса и реализациями класса.

  6. Обрабатывайте ссылки на сборки: Как отмечено в этом посте StackOverflow, обеспечивайте правильную загрузку и версионирование сборок при работе с несколькими сборками.

Распространенные ловушки и решения

Ловушка: Нарушения ограничений универсального типа

Проблема: “попытка неявно реализовать метод интерфейса со слабыми ограничениями для параметра типа”
Решение: Убедитесь, что универсальные параметры типа имеют те же или более сильные ограничения, чем требует интерфейс.

Ловушка: Проблемы с параметрами ‘in’

Проблема: TypeLoadException возникает только тогда, когда методы содержат параметр(ы) ‘in’, даже когда сигнатуры одинаковы
Решение: Особое внимание уделите аннотациям дисперсии и обеспечьте правильную обработку контравариантных параметров.

Ловушка: Конфликты версий сборок

Проблема: Ссылки на разные версии сборок интерфейса
Решение: Загружайте все ссылочные сборки ‘contract assembly’ как общие сборки, как рекомендуется в руководстве StackOverflow.


Исправленная версия работает потому, что она систематически решает каждое из требований проверки, которые CLR налагает при реализации интерфейсов через Reflection.Emit. Создавая типы в правильном порядке, правильно разрешая методы универсального интерфейса и явно определяя переопределения методов, вы гарантируете, что сгенерированные типы соответствуют всем требованиям времени выполнения для реализации интерфейса.

Источники

  1. How do I use .NET reflection to emit generic interfaces and a class which implements them - Stack Overflow
  2. TypeLoadException says ‘no implementation’, but it is implemented - Stack Overflow
  3. When implementing an interface that has a method with ‘in’ parameter by TypeBuilder.CreateType - Stack Overflow
  4. Reflection.Emit implementing generic method constraints of interface - Stack Overflow
  5. How to: Define a Generic Method with Reflection Emit - .NET Documentation
  6. System.TypeLoadException: Method does not have an implementation. - Benohead’s Software Blog
  7. Interface method does not have an implementation exception - FakeItEasy GitHub
Авторы
Проверено модерацией
Модерация