为什么我在运行时遇到泛型约束违规?

Laz*_*zlo 23 .net c# generics runtime type-constraints

我在尝试创建一个严重依赖于泛型的类的新实例时遇到以下异常:

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)
Run Code Online (Sandbox Code Playgroud)

我很困惑为什么会这样.编译时是否检查了通用约束?

我的谷歌搜索让我得出结论,这与这些原因中的任何一个有关,或者(有时?)两者:

我不准备牺牲的一件事是自我参照模式.我绝对需要它用于特定目的.

但是,我想帮助指出这个问题出现的地点和原因.由于库是庞大的并且产生了巨大的通用模式,我认为最好根据请求逐步提供代码位.

根据要求,再次声明.但我想强调的是,我宁愿一般都知道为什么会发生这样的异常,然后在我的特定代码中自行修复它,而不是为后代找到特定的修复.此外,这将是更长的人分析代码来回答,而不是给一般的解释,为什么泛型类型的限制可以在运行时被侵犯.

执行声明:

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
Run Code Online (Sandbox Code Playgroud)

图书馆声明:

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
}
Run Code Online (Sandbox Code Playgroud)

正如评论中所指出的,为我提供帮助的最简单方法是将代码最小化到一个小的,可重现的例子,其中bug仍然存在.然而,这是既硬又长了我,并且具有对错误一heisenbug,因为它的复杂性发生的几率较高.

我试图将它隔离到下面,但是当我这样做时我没有得到错误:

// 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
    }
}
Run Code Online (Sandbox Code Playgroud)

血腥细节

  1. 分析异常告诉我们TOutPacket违反TInPacketRelay<TInPacket, TOutPacket, TCryptograph, Tendian>.
  2. Relay我们的实例是TestClient,实现AwareClient,实现Client,实现Relay.
    • AwareClient结合使用,AwarePacket以便两端都知道哪种类型的客户端接收哪种类型的数据包.
  3. 因此,我们知道,TOutPacketTestClient违反TInPacketTestClient.
  4. 实现的类TOutPacketServerPacket.Out,它的衍生物OperationPacket.这种类型在泛型方面相对简单,因为它只提供枚举类型和字节序类型,不会对其他类进行交叉引用.结论:问题本身并非(很可能)不在本声明中.
  5. 实现的类TInPacketServerPacket.In,它的衍生物AwarePacket.这种类型比复杂得多TOutPacket,因为它交叉引用泛型以了解AwarePacket接收它的客户端().可能在这种通用混乱中出现问题.

然后,许多假设可以融合.在这一点上,我读到的内容是正确的,并被编译器接受,但显然有一些错误.

你能帮我找出为什么我在运行时使用我的代码得到通用约束违规吗?

Chr*_*ens 14

解:

因此,在对通用参数和约束进行了一些讨论后,我想我终于找到了问题/解决方案,我希望我不会过早地庆祝.

首先,我仍然认为这是动态运行时试图调用TestServer构造函数的错误(或至少是一个怪癖).它也可能是一个编译器错误,也就是说,如果它违反标准将类型化的类转换为动态(然后我再次假设),而不是将其转换为预期的类型.

那个,我的意思是这个代码:

 TestServer test = new TestServer(GetPort());
Run Code Online (Sandbox Code Playgroud)

转到Binder.InvokeConstructor下面,做了一大堆额外的转换,看起来就像你期望的那样代码(下面的代码在int cast之后生成)

在解决方案上,这一切都与泛型参数的顺序有关.据我所知,标准中没有任何内容可以说明你应该将你的泛型放入什么样的顺序.当你使用普通的int实例化类时,代码会起作用.看一下Server和Client如何命令他们的参数:

 Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
 Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
Run Code Online (Sandbox Code Playgroud)

完全相同的.如果从TestClient中删除所有其他类,并使TestClient的约束仅适用于基本Client和Server类,则一切都按预期工作,没有例外.我发现这个问题是AwareClientAwarePacket和添加TOperationCode

如果删除TOperationCode抽象类和继承类,则代码将再次按预期工作.这是不可取的,因为您可能希望在您的类中使用该泛型参数.我发现将它移动到参数的末尾可以解决问题.

 AwareClient<TOperationCode, TServer, TClient, 
             TInPacket, TOutPacket, TCryptograph, TEndian>
 AwarePacket<TOperationCode, TServer, TClient, TInPacket, 
             TOutPacket, TCryptograph, TEndian>
Run Code Online (Sandbox Code Playgroud)

 AwareClient<TServer, TClient, TInPacket, TOutPacket, 
                   TCryptograph, TEndian, TOperationCode>
 AwarePacket<TServer, TClient, TInPacket, TOutPacket, 
                   TCryptograph, TEndian, TOperationCode>
Run Code Online (Sandbox Code Playgroud)

当然,您必须使用通用约束的顺序进行一些更改才能使其编译,但这似乎可以解决您的问题.

那就是说,我的直觉告诉我这是clr中的一个错误.现在,它不仅仅是有两个带有通用参数乱序的类,或者一个通过添加参数从另一个继承的类.我正在尝试用一个更简单的例子来重现这个,但到目前为止,这一个案例是我唯一能够获得异常的案例.


编辑/我的发现过程

如果删除Relay<TInPacket, TOutPacket, TCryptograph, TEndian>类中的约束,则不会抛出异常.

我认为我发现更有趣的是,只有在您一次尝试创建TestClient时才会抛出异常,至少在我的机器上(这些仍然是FirstChanceExceptions,显然由内部运行时处理,它们未被用户代码处理) .

这样做:

new TestServer(GetPort());
new TestServer(GetPort());
new TestServer(GetPort());
Run Code Online (Sandbox Code Playgroud)

不会通过动态方法导致相同的调用,而是编译器在CallSite内部创建三个单独的类,三个单独的声明.从实现的角度来看,这是有道理的.我觉得特别有趣,虽然是,即使从我所看到的,他们的代码是不能共享(谁知道它是内部),例外只抛出的第一个调用构造函数.

我希望我能够调试它,但是Symbol Servers不会下载动态构建器的源代码,而locals窗口也不是很有用.我希望微软的某个人可以帮助回答这个谜团.


我拥有它,但我不确定.我肯定需要一位C#动力学专家来证实这一点.

所以,我做了一些测试,以确定为什么在将它传递给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();
}
Run Code Online (Sandbox Code Playgroud)

本质上,正在发生的事情是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();
}
Run Code Online (Sandbox Code Playgroud)

请注意,它不是创建InvokeConstructor绑定,而是使用Explicit标志创建Convert绑定.它不是试图动态调用构造函数,而是调用一个将动态转换为TestServer构造函数的函数,从而将其传递给实际的int而不是通用对象.

我想我的观点是,肯定是有什么不对您的仿制药(除了他们是相当难以辨认,IMO滥用的事实),而是一个问题与编译器是如何试图动态调用构造函数.

此外,它看起来与实际将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));
Run Code Online (Sandbox Code Playgroud)

'ConsoleApplication1.Relay`4 [TInPacket,TOutPacket,TCryptograph,TEndian]上的相同TypeLoadException,GenericArguments [0],'TOutPacket'违反了类型参数'TInPacket'的约束.发生了.显然,运行时很难在泛型类型上调用构造函数.

似乎这可能是一个错误......


如果启用.NET源浏览并在任何抛出的异常上启用断点,您将捕获TypeLoadException.并且可以查看整个.net堆栈跟踪.此外,您可以使用WinDbg重现它.


Laz*_*zlo 8

它与所有通用结构无关.信不信由你,我的设计稳定而实用.

实际原因是我唯一没有怀疑的:int port传递给的参数new TestServer(int port).

int实际上是通过动态表达获得的,这是无关紧要的.让我们说是的

dynamic GetPort() { return 8888; }

new TestServer(GetPort()); // Crash
new TestServer((int)GetPort()); // Works
Run Code Online (Sandbox Code Playgroud)

向CodeInChaos道歉,因为我没有反思,我想这只是半真的.

现在,赏金开始了,bug仍然存在(我想使用我的动态方法).那么,任何人都可以a)解释为什么会发生这种情况(毕竟,类型是有效的)和b)提出一种解决方法吗?赏金和接受的答案将归给那个人.

如果您想进行实验,我会将此代码重现并崩溃:http://pastie.org/2277415

如果你想要崩溃的实际可执行文件,以及解决方案和项目:http://localhostr.com/file/zKKGU74/CrashPlz.7z

  • 在Win7 32bit上没有崩溃,没有服务包,在VS2010调试和发布模式下运行.阅读代码会伤害我的大脑! (2认同)