为什么我在运行时收到通用约束违规错误?

23

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

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)

我对这种情况感到困惑。难道泛型约束不是在编译时检查吗?

我的谷歌搜索得出的结论是,这与以下原因有关,或者(有时?)两者都有:

  • 类中定义泛型约束(where)的顺序;
  • 使用自引用泛型模式(违反直觉但合法,详见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
}

正如评论中所指出的那样,对我来说获得这个问题的帮助的最简单方法是将代码最小化为一个小而可重复的示例,在其中错误仍然存在。然而,这对我来说既困难又漫长,并且很有可能会使错误变成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
    }
}

血腥细节

  1. 分析异常告诉我们,在 Relay<TInPacket, TOutPacket, TCryptograph, Tendian> 上,TOutPacket 违反了 TInPacket
  2. 我们拥有的 Relay 实例是 TestClient,它实现了 AwareClientAwareClient 又实现了 ClientClient 又实现了 Relay
    • AwareClientAwarePacket 配合使用,这样两端就会知道哪种类型的客户端接收哪种类型的数据包。
  3. 因此,我们知道 TestClient 中的 TOutPacket 违反了 TestClient 中的 TInPacket
  4. 实现 TOutPacket 的类是 ServerPacket.Out,它是 OperationPacket 的一个派生类。就泛型而言,这种类型相对简单,它只提供枚举类型和字节序类型,没有与其他类的交叉引用。结论:问题很可能不在这个声明本身。
  5. 实现 TInPacket 的类是 ServerPacket.In,它是 AwarePacket 的一个派生类。这种类型比 TOutPacket 复杂得多,因为它交叉引用泛型以便于“感知”(AwarePacket)接收它的客户端。问题可能就出在这个泛型混乱的地方。

那么,许多假设可以融合在一起。此时,我所读到的代码与编译器接受的代码是正确的,但很明显有问题。

你能帮我找出为什么我的代码会在运行时出现泛型约束违规的问题吗?


7
顺便提一下:我相当确定你有点过度使用泛型。特别是TCryptographTEndian类型参数让我感到有些奇怪。我认为这些应该是Cryptograph类型和EndianBitConverter类型的普通属性,你可以将其分配给派生类的实例。 - CodesInChaos
1
你的汇编代码是否可验证?你的代码去哪了?你刚刚发布了Server类的声明。 - CodesInChaos
1
@Lazlo:你能发布堆栈跟踪吗? - LukeH
@Charles:今晚会尝试,但这是一个漫长的过程。特别是考虑到精简代码可能会产生Heisenbug,因为这个错误似乎源于复杂性。 - Lazlo
@Charles:问题已更新。简化代码确实不容易。 - Lazlo
显示剩余16条评论
4个回答

14

解决方案:

在对泛型参数和约束进行一些调整后,我认为我终于找到了问题/解决方案,希望这并不是我过早庆祝。

首先,我仍然认为这是一个bug(或至少是一个怪癖),与动态运行时尝试调用TestServer构造函数的方式有关。它也可能是编译器的bug,也就是说,如果将一个类型化类转换为动态类型(然后再转换回来)而不是将其强制转换为其预期类型,则违反了标准。

我的意思是,这段代码:

 TestServer test = new TestServer(GetPort());

转化为下面的Binder.InvokeConstructor,进行一大堆额外的转换,并且看起来完全不像你期望的代码(在int强制转换之后生成的代码应该是期望的)

解决方法与泛型参数的顺序有关。据我所知,在标准中没有规定您应该以何种顺序放置泛型。当您使用普通的int实例化类时,代码可以正常工作。看看Server和Client如何按照其参数的顺序排序:

 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的一个bug。不仅仅是两个类的泛型参数顺序不同,或者一个继承自另一个并添加了一个参数,这并不简单。我正在尝试使用一个更简单的例子来复现这个问题,但到目前为止,这个情况是唯一能让我得到异常的情况。


编辑/我的发现过程

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

我认为更有趣的是,至少在我的机器上,异常只会在第一次尝试创建TestClient时抛出(这些仍然是FirstChanceExceptions,显然由内部运行时处理,而不是用户代码)。

做这件事:

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

使用动态方法调用时,不会得到相同的调用结果,而是编译器在内部创建了三个不同的CallSite类、三个不同的声明。从实现的角度来看,这是有意义的。但我发现有趣的是,尽管从我所看到的代码来看(谁知道它是否在内部共享),异常仅在第一次调用构造函数时才被抛出。

我希望能够调试这个问题,但符号服务器不会下载动态生成器的源代码,并且局部变量窗口也没有太多帮助。我希望微软的某位可以帮忙解答这个迷团。


认为我已经理解了这个问题,但我并不确定。我需要一位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();
}

基本上,发生的情况是RuntimeBinder创建了一个函数并尝试创建的不是要传递给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();
}

注意,它创建的是Convert绑定而不是InvokeConstructor绑定,并带有一个显式标志。它并没有尝试动态调用构造函数,而是调用一个将动态类型转换为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 源代码浏览并在任何抛出异常的地方启用断点,则可以捕获 TypeLoadException。并查看整个 .net 堆栈跟踪。此外,您可以使用 WinDbg 来复现它。


这是个惊人的故事,而我的投票今天已经用完了 :-( - Dan Abramov
我认为你真的找到了一些东西。我注意到当我在调试时,从new TestServer(<dynamic method>)得到的类型实际上也是动态的,而不是TestServer。由于testServer本身在其他类中用作类型参数,这可能是问题所在,因为它可能是某个RuntimeType而不是实际的TestServer..现在最大的问题是为什么它似乎对大多数人都有效,也许是运行时中已经修复的微妙错误,也许与类型缓存有关,谁知道呢。 - aL3891
这是一个很好的问题。我知道抛出的异常是被运行时成功捕获的FirstChanceExceptions。@Lazlo说他在调试器中遇到了这些异常,所以可能是它设置为在所有异常(已抛出和/或未处理)上中断执行。 - Christopher Currens
嗯,你的改进答案更有趣了。你有没有想过为什么在这种情况下泛型的顺序很重要?根据编译器代码,我一时也看不出来。 - Lazlo
我给了你赏金以表彰你的深入分析,但我仍然会等待看看是否有@Microsoft的人能够对这个bug做出解释,这才是真正的“答案”。顺便说一下,非常感谢。 :) - Lazlo
显示剩余3条评论

8

这与所有通用结构体都无关。信不信由你,我的设计是稳定和功能齐备的。

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

这个int实际上是通过一个与此无关的动态表达式获取的。我们假设它是

dynamic GetPort() { return 8888; }

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

对于我没有使用反射这件事,向CodeInChaos道歉,我想那只是半真的。

现在赏金已经开始了,但这个漏洞依旧存在(我想使用我的动态方法)。所以,请问有人能 a) 解释为什么会发生这种情况(毕竟类型是有效的),并且 b) 提出解决方法吗?悬赏和采纳答案将会给予那个人。

如果您想要实验,我得到了这些可以重现和崩溃的代码: http://pastie.org/2277415

如果您想要实际可执行的崩溃程序,连同解决方案和项目: http://localhostr.com/file/zKKGU74/CrashPlz.7z


1
我不确定,对我来说它仍然没有崩溃。我将你的所有代码转换为一个独立的控制台应用程序。运行得非常完美。所以我想这仍然很难隔离。 - Kirk Woll
1
你发布的代码(http://pastie.org/2277415)在我的电脑上没有崩溃!它成功地继续执行到“Console.ReadLine()”。问题仍然隐藏在其他地方。 - Jalal Said
@Jalal,我也是。(我使用的是Win7,64位,.Net 4.0) - Kirk Woll
1
它对我来说确实崩溃了...这真是一个令人困惑的问题:P特别是因为TestServer没有任何开放的泛型。 - aL3891
2
在我的Win7 32位系统上,没有安装服务包,在VS2010的调试和发布模式下都没有崩溃。不过,阅读这段代码真让我头疼! - Igby Largeman
显示剩余13条评论

3

我的猜测是一些旧的编译代码仍然存在于某个地方..特别是如果问题突然消失了。

  • 您最近是否移动了任何类型参数?
  • 您是否在构建时递增程序集版本?(可能会引起问题,因为类型的完全限定名称发生变化)
  • 这个异常发生的场景是什么?是客户端使用不同的二进制文件调用服务器吗?

如果这些问题中有任何一个是真的,我会删除我能找到的所有二进制文件,并从头开始重新构建所有内容 :)

-编辑-

此外,请确保您不会意外直接引用二进制文件,除非您确实需要。您应该始终使用项目引用来确保所有内容都得到正确重新构建。

-edit2-

好的,这太奇怪了...我将您的代码粘贴到我的 Playground 解决方案中,出现了异常。但现在我尝试了您的已编译版本,它却工作了!

我将代码与我的旧版本进行了比较,完全相同...

我比较了 proj 文件,不完全相同,但我复制了所有细节,以便它们仍然可以正常工作,仍然您的项目可以工作,而我的不能!

因此,我检查了解决方案文件..除了项目 GUID 外没有任何不同..仍然处于相同的情况..

因此,我删除了我能想到的唯一其他东西,即我的 Playground 解决方案的.suo 文件.. 然后它们都可以工作了..

suo 文件似乎是二进制文件,所以我不太确定其中设置了什么。我确实知道,在安装 .net/vs2010 sp1 之前,我拥有那个 suo 文件,也许里面有一些旧内容,谁知道呢。我会尝试更多地调查。

-edit4-

好吧,我不知道发生了什么..现在我无法使代码再次崩溃。甚至复制旧的 .suo 文件也不起作用了..


我已经移动了类型参数,我在构建时进行了递增,但我没有从客户端远程调用服务器。网络存在是无关紧要的,因为异常甚至在套接字创建之前就发生了。稍后我会考虑重新构建整个东西。 - Lazlo
将类型参数移动是“危险的”,因为这通常不会产生编译器错误,但可能会显著改变代码的语义。特别是如果您在调用现场推断泛型类型,也就是在未明确给出类型参数的情况下传入对象。尽管这是一种非常普遍的做法(我几乎总是这样做),但它仍然有潜在的风险。我打赌您的问题可能是装配版本号不匹配,您可能已经重新构建了基类装配,但没有重新构建所有派生的装配。 - aL3891
嗯,实际上可能不是版本号的问题,而是当您遇到异常时正在创建类型的程序集的旧版本。尝试在该特定dll上使用反编译器(reflector),看看它是否实际上正在使用正确的类型参数顺序。(TInPacket和TOutpacket最近是否交换了位置?) - aL3891
嗯,试图重现这个错误似乎是不可能的,可能真的与你所说的程序集有关。我正在研究它。 - Lazlo
问题已更新,原因已找到。请阅读我的答案。 - Lazlo
嗯,关于SUO的信息很有趣,但正如你所指出的,在尝试了一些之后它变得无关紧要了。它不会影响崩溃行为。 - Lazlo

2
如果您确实没有使用任何反射,则似乎表明C#编译器或运行时存在错误。通常,这会导致代码无法验证。
似乎您创建了一个运行时认为是非法的结构,但是C#编译器未能将其识别为非法。哪个有错误很难说,因为您省略了关键类型声明。

我再次添加了类型声明。 - Lazlo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接