Подтвердить что ты не робот

Почему я получаю общее нарушение ограничений во время выполнения?

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

new TestServer(8888);

System.TypeLoadException

GenericArguments[0], 'TOutPacket', on     
'Library.Net.Relay`4[TInPacket,TOutPacket,TCryptograph,TEndian]' 
violates the constraint of type parameter 'TInPacket'.

at System.RuntimeTypeHandle.Instantiate(RuntimeTypeHandle handle, IntPtr* pInst, Int32 numGenericArgs, ObjectHandleOnStack type)
at System.RuntimeTypeHandle.Instantiate(Type[] inst)
at System.RuntimeType.MakeGenericType(Type[] instantiation)

Я озадачен, почему это происходит. Не проверяются ли общие ограничения во время компиляции?

Мой googling привел меня к выводу, что это имеет какое-то отношение к любой из этих причин или (иногда?):

  • Порядок, в котором общие классы (where) определены в классах;
  • Использование шаблона с саморегуляцией (conter-intuitive, но очень легально, см. сообщение в блоге Eric Lippert)

Одна вещь, которую я не готов жертвовать, - это шаблон саморегуляции. Мне это абсолютно необходимо для определенной цели.

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

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

Объявления о реализации:

class TestServer : Server<TestServer, TestClient, ServerPacket.In, ServerPacket.Out, BlankCryptograph, LittleEndianBitConverter>

class TestClient : AwareClient<TestOperationCode, TestServer, TestClient, ServerPacket.In, ServerPacket.Out, BlankCryptograph, LittleEndianBitConverter>

class ServerPacket
{
    public abstract class In : AwarePacket<TestOperationCode, TestServer, TestClient, ServerPacket.In, ServerPacket.Out, BlankCryptograph, LittleEndianBitConverter>.In
    public class Out : OperationPacket<TestOperationCode, LittleEndianBitConverter>.Out
}

public enum TestOperationCode : byte

Объявления библиотеки:

public abstract class Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian> : IDisposable
    where TServer : Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TClient : Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TInPacket : Packet<TEndian>.In
    where TOutPacket : Packet<TEndian>.Out
    where TCryptograph : Cryptograph, new()
    where TEndian : EndianBitConverter, new()

public abstract class Relay<TInPacket, TOutPacket, TCryptograph, TEndian> : IDisposable
    where TInPacket : Packet<TEndian>.In
    where TOutPacket : Packet<TEndian>.Out
    where TCryptograph : Cryptograph, new()
    where TEndian : EndianBitConverter, new()

public abstract class Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian> : Relay<TInPacket, TOutPacket, TCryptograph, TEndian>, IDisposable
    where TServer : Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TClient : Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TInPacket : Packet<TEndian>.In
    where TOutPacket : Packet<TEndian>.Out
    where TCryptograph : Cryptograph, new()
    where TEndian : EndianBitConverter, new()

public abstract class Packet<TEndian> : ByteBuffer<TEndian>, IDisposable 
    where TEndian : EndianBitConverter, new()
{
    public abstract class In : Packet<TEndian>
    public abstract class Out : Packet<TEndian>
}

public class OperationPacket<TOperationCode, TEndian> 
    where TEndian : EndianBitConverter, new()
{
    public class In : Packet<TEndian>.In
    public class Out : Packet<TEndian>.Out
}

public abstract class AwareClient<TOperationCode, TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian> : Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>, IDisposable
    where TCryptograph : Cryptograph, new()
    where TInPacket : AwarePacket<TOperationCode, TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>.In
    where TOutPacket : Packet<TEndian>.Out
    where TServer : Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TClient : AwareClient<TOperationCode, TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TEndian : EndianBitConverter, new()

public class AwarePacket<TOperationCode, TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TCryptograph : Cryptograph, new()
    where TInPacket : AwarePacket<TOperationCode, TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>.In
    where TOutPacket : Packet<TEndian>.Out
    where TServer : Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TClient : AwareClient<TOperationCode, TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
    where TEndian : EndianBitConverter, new()
{
    public abstract class In : OperationPacket<TOperationCode, TEndian>.In
}

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

Я попытался выделить его следующим образом, но я не получаю ошибку, когда я это делаю:

// Equivalent of library
class A<TA, TB, TI, TO> // Client
    where TA : A<TA, TB, TI, TO>
    where TB : B<TA, TB, TI, TO>
    where TI : I
    where TO : O
{ }

class B<TA, TB, TI, TO> // Server
    where TA : A<TA, TB, TI, TO>
    where TB : B<TA, TB, TI, TO>
    where TI : I
    where TO : O
{ }

class I { } // Input packet

class O { } // Output packet

// Equivalent of Aware

class Ii<TA, TB, TI, TO> : I { } // Aware input packet

class Ai<TA, TB, TI, TO> : A<TA, TB, TI, TO> // Aware capable client
    where TA : Ai<TA, TB, TI, TO>
    where TB : B<TA, TB, TI, TO>
    where TI : Ii<TA, TB, TI, TO>
    where TO : O
{ }

// Equivalent of implementation

class XI : Ii<XA, XB, XI, XO> { }
class XO : O { }

class XA : Ai<XA, XB, XI, XO> { }
class XB : B<XA, XB, XI, XO> { }

class Program
{
    static void Main(string[] args)
    {
        new XB(); // Works, so bad isolation
    }
}

Детали Gory

  • Анализ исключения указывает нам, что TOutPacket нарушает TInPacket на Relay<TInPacket, TOutPacket, TCryptograph, Tendian>.
  • У экземпляра Relay есть TestClient, который реализует AwareClient, который реализует Client, который реализует Relay.
    • AwareClient используется в сочетании с AwarePacket, чтобы оба конца знали, какой тип клиента получает тот тип пакетов.
  • Поэтому мы знаем, что TOutPacket в TestClient нарушает TInPacket в TestClient.
  • Класс, реализующий TOutPacket, ServerPacket.Out, который является производной от OperationPacket. Этот тип относительно прост с точки зрения дженериков, поскольку он предоставляет только тип перечисления и тип endian, не делая перекрестной ссылки на другие классы. Вывод: проблема не (скорее всего) не в этом объявлении сама по себе.
  • Класс, реализующий TInPacket, ServerPacket.In, который является производной от AwarePacket. Этот тип намного сложнее, чем TOutPacket, так как он перекрестно ссылается на генерики, чтобы знать (AwarePacket) клиента, который его получил. Вероятно, в этой общей проблеме возникает проблема.

Тогда многие гипотезы могут слиться. На этом этапе, что я читаю, правильно и принято компилятором, но, очевидно, что-то не так.

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

4b9b3361

Ответ 1

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

Фактическая причина была единственной вещью, которую я не подозревал: параметр int port передан new TestServer(int port).

Этот int был фактически получен с помощью динамического выражения, которое не имеет значения. Скажем, это было

dynamic GetPort() { return 8888; }

new TestServer(GetPort()); // Crash
new TestServer((int)GetPort()); // Works

Извините за CodeInChaos за то, что я не использовал никакого отражения, я думаю, это было только наполовину.

Теперь, щебет начинается и ошибка все еще там (я хочу использовать мой динамический метод). Итак, может ли кто-нибудь объяснить, почему это происходит (в конце концов, тип действительно) и б) предложить способ его исправить? Баунти и принятый ответ пойдут к этому человеку.

Если вы хотите поэкспериментировать, я получил этот код для воспроизведения и сбоя: http://pastie.org/2277415

Если вы хотите, чтобы фактический исполняемый файл сработал вместе с решением и проектом: http://localhostr.com/file/zKKGU74/CrashPlz.7z

Ответ 2

Решение:

Итак, после некоторого финализации с общими параметрами и ограничениями, я думаю, что я наконец нашел проблему/решение, и я надеюсь, что я не праздную слишком рано.

Прежде всего, я все же думаю, что это ошибка (или, по крайней мере, причуда) в том, как динамическая среда выполнения пытается вызвать конструктор TestServer. Это также может быть ошибкой компилятора, т.е. Если он против стандарта, чтобы преобразовать типизированный класс в динамический (тогда я полагаю, снова) вместо того, чтобы отнести его к ожидаемому типу.

Под этим я имею в виду, что этот код:

 TestServer test = new TestServer(GetPort());

превращается в Binder.InvokeConstructor ниже, выполняя целую кучу дополнительного кастинга и не похож на код, который вы ожидаете от него (код, созданный после того, как будет выполняться int cast)

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

 Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
 Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>

Точно так же. Если удалить все остальные классы из TestClient и заставить ограничения TestClient работать только с базовым классом Client и Server, все работает так, как ожидалось, никаких исключений. Я нашел, что проблема связана с AwareClient и AwarePacket и добавлением TOperationCode

Если вы удаляете TOperationCode, а в абстрактных классах и наследующих классах код снова работает, как ожидается. Это нежелательно, так как вам, вероятно, нужен этот общий аргумент в вашем классе. Я обнаружил, что перемещение до конца аргументов решает проблему.

 AwareClient<TOperationCode, TServer, TClient, 
             TInPacket, TOutPacket, TCryptograph, TEndian>
 AwarePacket<TOperationCode, TServer, TClient, TInPacket, 
             TOutPacket, TCryptograph, TEndian>

становится

 AwareClient<TServer, TClient, TInPacket, TOutPacket, 
                   TCryptograph, TEndian, TOperationCode>
 AwarePacket<TServer, TClient, TInPacket, TOutPacket, 
                   TCryptograph, TEndian, TOperationCode>

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

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


EDIT (s)/Мой процесс обнаружения

Если вы удалите ограничения в классе Relay<TInPacket, TOutPacket, TCryptograph, TEndian>, исключения не будут выбрасываться.

Я думаю, что более интересным является то, что исключения вызывается только при первом создании TestClient, по крайней мере на моей машине (это все еще FirstChanceExceptions, которые, по-видимому, обрабатываются внутренней средой выполнения, они не обрабатываются код пользователя).

Выполнение этого действия:

new TestServer(GetPort());
new TestServer(GetPort());
new TestServer(GetPort());

не приводит к тому же вызову через динамический метод, но компилятор делает три отдельных класса CallSite внутри, три отдельных объявления. Это имеет смысл с точки зрения внедрения. Тем не менее, я считаю особенно интересным то, что, хотя, как я вижу, их код не используется (кто знает, является ли он внутренне), исключения только генерируются при первом вызове конструктора.

Мне жаль, что у меня не было возможности отладить это, но серверы Symbol не будут загружать источник для динамических сборщиков, а окно locals не очень полезно. Я надеюсь, что кто-то из Microsoft может помочь ответить на эту тайну.


Я думаю, что у меня это есть, но я не уверен. Я бы определенно нуждался в эксперте по динамике С#, чтобы подтвердить это.

Итак, я сделал несколько тестов, чтобы выяснить, почему он сработает с явным приведением против неявного приведения, передав его конструктору TestServer.

Это основной код для вашей версии, скомпилированный:

private static void Main(string[] args)
{
    if (<Main>o__SiteContainer0.<>p__Site1 == null)
    {
        <Main>o__SiteContainer0.<>p__Site1 = 
        CallSite<Func<CallSite, Type, object, TestServer>>.Create(
        Binder.InvokeConstructor(CSharpBinderFlags.None, typeof(Program),
        new CSharpArgumentInfo[] {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.IsStaticType | 
            CSharpArgumentInfoFlags.UseCompileTimeType, null), 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
    }

    TestServer server = <Main>o__SiteContainer0.<>p__Site1.Target.Invoke(
    <Main>o__SiteContainer0.<>p__Site1, typeof(TestServer), GetPort());
    Console.ReadLine();
}

По существу, происходит то, что RuntimeBinder создал функцию, которую пытается создать, а не int, чтобы перейти к GetPort(), а вместо этого новый TestServer, динамически вызывающий его конструктор.

Посмотрите на разницу, когда вы передаете его в int и передаете его конструктору:

private static void Main(string[] args)
{
    if (<Main>o__SiteContainer0.<>p__Site1 == null)
    {
        <Main>o__SiteContainer0.<>p__Site1 = 
        CallSite<Func<CallSite, object, int>>.Create(Binder.Convert(
        CSharpBinderFlags.ConvertExplicit, typeof(int), typeof(Program)));
    }

    TestServer server = new TestServer(
    <Main>o__SiteContainer0.<>p__Site1.Target.Invoke(
    <Main>o__SiteContainer0.<>p__Site1, GetPort()));

    Console.ReadLine();
}

Обратите внимание, что вместо создания привязки InvokeConstructor он создает привязку Convert с явным знаком. Вместо того, чтобы динамически вызывать конструктор, он вызывает функцию, которая преобразует динамический объект в конструктор TestServer, передавая ему фактический int вместо общего объекта.

Я предполагаю, что, судя по всему, нет ничего плохого в ваших дженериках (кроме того, что они довольно неразборчивы и чрезмерное использование ИМО), а скорее проблема с тем, как компилятор пытается динамически вызывать конструктор.

Кроме того, похоже, что это не имеет никакого отношения к фактическому прохождению по int к конструктору. Я удалил конструктор из TestClient и сделал этот CallSite (по существу такой же, как ошибка, минус параметр int)

        var lawl = CallSite<Func<CallSite, Type, TestServer>>.Create(
        Binder.InvokeConstructor(CSharpBinderFlags.None, typeof(Program), 
        new CSharpArgumentInfo[] { 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.IsStaticType | 
            CSharpArgumentInfoFlags.UseCompileTimeType, null), 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));

        TestServer lol = lawl.Target.Invoke(lawl, typeof(TestServer));

И те же TypeLoadException, GenericArguments [0], 'TOutPacket', на 'ConsoleApplication1.Relay`4 [TInPacket, TOutPacket, TCryptograph, TEndian]' нарушают ограничение параметра типа TInPacket. произошло. По-видимому, во время выполнения сложно использовать конструкторы для вашего общего типа.

Похоже, это может быть ошибка...


Если вы включите просмотр в .NET Source и включите точки останова при любом запущенном исключении, вы поймаете исключение TypeLoadException. и может просматривать всю трассировку стека .net. Кроме того, вы можете воспроизвести его с помощью WinDbg.

Ответ 3

Я предполагаю, что какой-то старый скомпилированный код висит где-то где-то.. особенно если проблема внезапно исчезла.

  • В последнее время вы перемещали какие-либо аргументы типа?
  • Вы увеличиваете версии сборки при сборке? (может вызвать проблемы потому что меняются полностью квалифицированные имена типов)
  • Что такое сценарий, в котором происходит это исключение, является ли он клиентом вызов сервера с использованием разных копий двоичных файлов?

Если любой из этих вопросов верен, я бы удалял каждый бинарный файл, который мог бы найти и перестроить все с нуля:)

-edit -

Кроме того, убедитесь, что вы не случайно ссылаетесь на двоичные файлы напрямую, если вам действительно не нужно. Вы всегда должны использовать ссылки на проект, чтобы убедиться, что все правильно восстановлено.

-edit2 -

Хорошо, это странно странно.. Я вставил в свой код решение для игровых площадок, которое у меня есть, получило исключение. но теперь я попробовал вашу скомпилированную версию, и это сработало!

Я изменил код своей старой версией, точно так же...

Я различал профайлы, не совсем то же самое, но я скопировал все детали, чтобы они, где еще работал ваш проект, мой не сделал!

Итак, я проверил файлы решений.. нет разного appart из проекта guids.., все та же ситуация..

Итак, я удалил единственную вещь, о которой мог подумать, файл .suo для моего игрового решения.. и они оба работали..

Файлы suo кажутся двоичными, поэтому я не уверен, что именно там установлено. Я знаю, что у меня был этот файл suo до установки .net/vs2010 sp1, хотя, может быть, там есть какой-то старый материал, который знает. я попробую и исследую больше.

-edit4 -

Ну, я не знаю, что происходит. Теперь я не могу снова получить код. Даже копирование старого файла .suo обратно не работает.

Ответ 4

Если вы действительно не используете никакого отражения, это, по-видимому, указывает на ошибку в компиляторе С# или во время выполнения. Обычно это приводит к непроверяемому коду.

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