Почему я получаю это исключение при выпуске классов, которые ссылаются друг на друга с помощью дженериков типа значений?

Этот фрагмент кода является упрощенным извлечением моего кода генерации кода, который создает два класса, которые ссылаются друг на друга как аргументы в родовом типе:

namespace Sandbox
{
    using System;
    using System.Reflection;
    using System.Reflection.Emit;

    internal class Program
    {
        private static void Main(string[] args)
        {
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run);
            var module = assembly.DefineDynamicModule("Test");

            var typeOne = module.DefineType("TypeOne", TypeAttributes.Public);
            var typeTwo = module.DefineType("TypeTwo", TypeAttributes.Public);

            typeOne.DefineField("Two", typeof(TestGeneric<>).MakeGenericType(typeTwo), FieldAttributes.Public);
            typeTwo.DefineField("One", typeof(TestGeneric<>).MakeGenericType(typeOne), FieldAttributes.Public);

            typeOne.CreateType();
            typeTwo.CreateType();

            Console.WriteLine("Done");
            Console.ReadLine();
        }
    }

    public struct TestGeneric<T>
    {
    }
}

Что должно привести к эквиваленту MSIL для следующего:

public class TypeOne
{
    public Program.TestGeneric<TypeTwo> Two;
}

public class TypeTwo
{
    public Program.TestGeneric<TypeOne> One;
}

Но вместо этого выдает это исключение в строке typeOne.CreateType():

System.TypeLoadException was unhandled
  Message=Could not load type 'TypeTwo' from assembly 'Test, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
  Source=mscorlib
  TypeName=TypeTwo
  StackTrace:
       at System.Reflection.Emit.TypeBuilder.TermCreateClass(RuntimeModule module, Int32 tk, ObjectHandleOnStack type)
       at System.Reflection.Emit.TypeBuilder.CreateTypeNoLock()
       at System.Reflection.Emit.TypeBuilder.CreateType()
       at Sandbox.Program.Main(String[] args) in C:\Users\aca1\Code\Sandbox\Program.cs:line 20

Интересно отметить:

  • Циркулярная ссылка не требуется для возникновения исключения; если я не определяю поле One на TypeTwo, создание TypeOne до TypeTwo все еще не выполняется, но создание TypeTwo до TypeOne завершается успешно. Поэтому исключение специально вызвано использованием типа, который еще не был создан в качестве аргумента в родовом типе поля; однако, поскольку мне нужно использовать циклическую ссылку, я не могу избежать этой ситуации, создав типы в определенном порядке.
  • Да, мне нужно использовать круговую ссылку.
  • Удаление оболочки TestGeneric<> и объявление полей как TypeOne и TypeTwo напрямую не вызывает эту ошибку; поэтому я могу использовать динамические типы, которые были определены, но не созданы.
  • Изменение TestGeneric<> от struct до class не вызывает этой ошибки; поэтому этот шаблон работает с большинством дженериков, а не с типичными типами значений.
  • Я не могу изменить объявление TestGeneric<> в моем случае, поскольку он объявлен в другой сборке, а именно System.Data.Linq.EntityRef<>, объявленный в System.Data.Linq.dll.
  • Моя круговая ссылка вызвана представлением двух таблиц с ссылками на внешние ключи друг к другу; следовательно, потребность в этом конкретном родовом типе и этой конкретной модели.
  • Успешная смена круговой ссылки на самоописание edit. Это произошло неудачно из-за того, что у меня был TestGeneric<> как вложенный тип в программе, поэтому он унаследовал видимость internal. Я исправил это сейчас в примере кода выше, и он действительно работает.
  • Также выполняется компиляция сгенерированного кода вручную (как код С#), поэтому это не проблема с неясным компилятором.

Любые идеи по a) почему это происходит, б) как я могу исправить это и/или c) как я могу обойти это?

Спасибо.

Ответы

Ответ 1

Я точно не знаю, почему это происходит. У меня есть хорошее предположение.

Как вы заметили, создание универсального класса рассматривается иначе, чем создание общей структуры. Когда вы создаете тип "TypeOne", эмиттеру необходимо создать общий тип "TestGeneric", и по какой-то причине необходим правильный тип, а не TypeBuilder. Возможно, это происходит при попытке определить размер новой родовой структуры? Я не уверен. Возможно, TypeBuilder не может определить его размер, поэтому необходим тип TypeTwo.

Если TypeTwo не может быть найден (поскольку он существует только как TypeBuilder), событие AppDomain TypeResolve будет запущено. Это дает вам возможность исправить проблему. При обработке события TypeResolve вы можете создать тип TypeTwo и решить проблему.

Вот грубая реализация:

namespace Sandbox
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Reflection.Emit;

    internal class Program
    {
        private static void Main(string[] args)
        {
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run);
            var module = assembly.DefineDynamicModule("Test");

            var typeOne = module.DefineType("TypeOne", TypeAttributes.Public);
            var typeTwo = module.DefineType("TypeTwo", TypeAttributes.Public);

            typeOne.DefineField("Two", typeof(TestGeneric<>).MakeGenericType(typeTwo), FieldAttributes.Public);
            typeTwo.DefineField("One", typeof(TestGeneric<>).MakeGenericType(typeOne), FieldAttributes.Public);

            TypeConflictResolver resolver = new TypeConflictResolver();
            resolver.AddTypeBuilder(typeTwo);
            resolver.Bind(AppDomain.CurrentDomain);

            typeOne.CreateType();
            typeTwo.CreateType();

            resolver.Release();

            Console.WriteLine("Done");
            Console.ReadLine();
        }
    }

    public struct TestGeneric<T>
    {
    }

    internal class TypeConflictResolver
    {
        private AppDomain _domain;
        private Dictionary<string, TypeBuilder> _builders = new Dictionary<string, TypeBuilder>();

        public void Bind(AppDomain domain)
        {
            domain.TypeResolve += Domain_TypeResolve;
        }

        public void Release()
        {
            if (_domain != null)
            {
                _domain.TypeResolve -= Domain_TypeResolve;
                _domain = null;
            }
        }

        public void AddTypeBuilder(TypeBuilder builder)
        {
            _builders.Add(builder.Name, builder);
        }

        Assembly Domain_TypeResolve(object sender, ResolveEventArgs args)
        {
            if (_builders.ContainsKey(args.Name))
            {
                return _builders[args.Name].CreateType().Assembly;
            }
            else
            {
                return null;
            }
        }
    }
}