【问题标题】:Why am I getting a generic constraint violation at runtime? [closed]为什么我在运行时会违反一般约束? [关闭]
【发布时间】:2014-06-17 00:46:57
【问题描述】:

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

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's blog post

我还没有准备好牺牲的一件事是自我引用模式。我绝对需要它用于特定目的。

但是,我需要一些帮助来指出此问题发生的位置和原因。由于库非常庞大,并且生成了大量的通用模式,我认为最好根据请求逐步提供代码位。

应要求,再次声明。但我想强调一个事实,即我宁愿一般地知道为什么会发生这样的异常,然后继续在我的特定代码中自己修复它,而不是为后代找到特定的修复程序。此外,任何分析代码的人都比给出一般解释为什么在运行时会违反泛型类型约束要长得多。

实现声明:

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
}

正如 cmets 中所述,对我来说,在这个问题上获得帮助的最简单方法是将代码最小化为仍然存在错误的小型且可重现的示例。但是,这对我来说既困难又漫长,并且很有可能使该错误成为海森错误,因为它是由复杂性引起的。

我尝试将其隔离为以下内容,但这样做时我没有得到错误:

// 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. 分析异常告诉我们TOutPacketRelay&lt;TInPacket, TOutPacket, TCryptograph, Tendian&gt; 上违反了TInPacket
  2. 我们拥有的Relay的实例是TestClient,它实现了AwareClient,它实现了Client,它实现了Relay
    • AwareClientAwarePacket配合使用,让两端都知道哪种类型的客户端接收哪种类型的数据包。
  3. 因此,我们知道TestClient 中的TOutPacket 违反了TestClient 中的TInPacket
  4. 实现TOutPacket 的类是ServerPacket.Out,它是OperationPacket 的派生类。这种类型在泛型方面相对简单,因为它只提供了一个枚举类型和一个字节序类型,没有对其他类的交叉引用。结论:问题本身并不(很可能)不在此声明中。
  5. 实现TInPacket 的类是ServerPacket.In,它是AwarePacket 的派生类。这种类型比TOutPacket 复杂得多,因为它交叉引用泛型以意识到 (AwarePacket) 接收它的客户端。问题可能就是在这种普遍的混乱中发生的。

然后,许多假设可以融合。在这一点上,我读到的内容是正确的并被编译器接受,但显然那里有问题。

您能帮我找出为什么我的代码在运行时会违反一般约束吗?

【问题讨论】:

  • 您是否使用反射来专门化泛型类型?或者也许使用一个库?
  • 附带说明:我很确定您有点过度使用泛型。特别是TCryptographTEndian 类型参数让我觉得很奇怪。我认为这些应该是 CryptographEndianBitConverter 类型的普通属性,您可以为其分配派生类的实例。
  • 您的程序集是否可验证?你的代码去哪儿了?您刚刚发布了 Server 课程的声明。
  • 我宁愿远离设计问题。例如,需要TEndian,但我可以使用 TCryptograph。只有我发现这样更方便。
  • @Lazlo:你能发布堆栈跟踪吗?

标签: c# .net generics runtime type-constraints


【解决方案1】:

解决方案:

所以,在对通用参数和约束进行了一番摸索之后,我想我终于找到了问题/解决方案,我希望我不会庆祝得太早。

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

我的意思是这段代码:

 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 类,则一切都按预期工作,没有例外。我发现问题在于AwareClientAwarePacket 以及添加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 个类的泛型参数乱序,或者一个从另一个类继承并添加一个参数那么简单。我正在努力尝试用一个更简单的例子来重现这一点,但到目前为止,这个案例是我唯一能够获得例外的案例。


编辑/我的发现过程

如果您删除 Relay&lt;TInPacket, TOutPacket, TCryptograph, TEndian&gt; 类中的约束,则不会引发异常。

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

这样做:

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

不会通过动态方法导致相同的调用,而是编译器在内部创建三个单独的CallSite 类,三个单独的声明。从实现的角度来看,这是有道理的。不过,我发现特别有趣的是,尽管从我所见,他们的代码没有共享(谁知道它是否在内部),但仅在第一次调用构造函数时才抛出异常。

我希望我有能力对此进行调试,但是 Symbol Servers 不会下载动态构建器的源代码,并且本地窗口也不是很有帮助。我希望微软的人能帮助解答这个谜团。


认为我有,但我不确定。我肯定需要 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() 的 int,而是一个新的 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 绑定,而是创建一个带有 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));

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

看起来这可能是一个错误......


如果您启用 .NET 源代码浏览并在任何引发的异常上启用断点,您将捕获 TypeLoadException。并且可以查看整个 .net 堆栈跟踪。另外,你可以用 WinDbg 重现它。

【讨论】:

  • 多么精彩的故事,我今天的投票结束了:-(
  • 绝对+1,不过要重读几次。编辑:添加换行符以提高可读性,请有足够代表的人批准。
  • 我认为你真的很感兴趣。我在调试时注意到你从 new TestServer() 获得的东西的类型实际上也是动态的,而不是 TestServer。因为 testServer 是在其他类中自己用作类型争论,这可能是问题所在,因为它可能是一些 RuntimeType 而不是实际的 TestServer .. 现在最大的问题是为什么它似乎对大多数人有用,也许是已经在运行时修复的细微错误,可能与类型缓存有关,谁知道
  • 这是个好问题。好吧,我知道抛出的异常是运行时成功捕获的 FirstChanceExceptions。 @Lazlo 说他在调试器中得到了这些异常,所以它可能设置为中断所有异常的执行,抛出和/或未处理。
  • 嗯,你改进的答案更有趣。您知道为什么在这种情况下泛型的顺序很重要吗?根据编译器代码,我看不到任何副手。
【解决方案2】:

它与所有通用构造无关。信不信由你,我的设计既稳定又实用。

真正的原因是我唯一没有怀疑的:int port 参数传递给了new TestServer(int port)

这个int实际上是通过一个无关紧要的动态表达式获得的。假设是

dynamic GetPort() { return 8888; }

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

向 CodeInChaos 道歉说我没有使用反射,我想这只是对了一半。

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

如果你想实验,我得到这个代码来重现和崩溃:http://pastie.org/2277415

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

【讨论】:

  • 我不确定,它仍然不会为我崩溃。我将您的所有代码都转换为standalone console app。完美运行。所以我想它仍然很难隔离。
  • @Kirk Woll:我让这个崩溃了:pastie.org/2277415
  • posted 的代码不会在我的机器上崩溃!。它成功地继续到Console.ReadLine()。问题仍然隐藏在其他地方。
  • 确实对我来说崩溃了..这是一个令人头疼的问题:P 特别是因为 TestServer 没有任何开放的泛型
  • 在 Win7 32 位、无服务包、在 VS2010 调试和发布模式下运行时不会崩溃。阅读代码虽然伤了我的大脑!
【解决方案3】:

我的猜测是一些旧的编译代码在某处徘徊..特别是如果问题突然消失了

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

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

-编辑-

此外,请确保您不会意外地直接引用二进制文件,除非您确实必须这样做。您应该始终使用项目引用来确保正确重建所有内容。

-edit2-

好的,这太奇怪了..我将您的代码粘贴到我拥有的游乐场解决方案中,但出现异常。但现在我尝试了你的编译版本,它成功了!

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

我对 projfiles 进行了比较,不完全相同,但我复制了所有细节,以便它们在哪里,仍然你的项目工作,我的没有!

所以我检查了解决方案文件..与项目 guid 没有不同的 appart..,仍然是同样的情况..

所以我删除了我唯一能想到的其他东西,我的游乐场解决方案的 .suo 文件..它们都工作了..

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

-edit4-

嗯,我不知道发生了什么.. 现在我无法让代码再次崩溃。即使将旧的 .suo 文件复制回来也不起作用..

【讨论】:

  • 我已经移动了类型参数,我在构建时递增,但我没有从客户端远程调用到服务器。存在网络的事实是无关紧要的,因为异常发生在甚至可以创建套接字之前。稍后我会看到重建整个沙班。
  • 移动类型参数是“危险的”,因为这通常不会产生编译器错误,但可能会显着改变代码的语义。如果您在调用站点推断泛型,则尤其如此,即传入对象而不显式给出类型参数。这是一件非常常见的事情(我几乎总是这样做)。我的赌注是不匹配的程序集版本号,但您可能已经重建了基类程序集,但并非所有派生程序集。
  • hm,实际上它可能不是版本号,而是旧版本的程序集包含您在遇到异常时尝试创建的类型。尝试在该特定 dll 上使用反射器,看看它是否实际上使用了类型参数的正确顺序。 (TInPacket 和 TOutpacket 最近互换了位置吧?)
  • 嗯,试图重现似乎不可能的错误,它可能真的与你所说的程序集有关。现在正在调查。
  • 问题已更新,原因已找到。请阅读我的回答。
【解决方案4】:

如果您确实没有使用反射,这似乎表明 C# 编译器或运行时存在错误。通常这会导致无法验证的代码。

您似乎创建了一个运行时认为非法的构造,但 C# 编译器并未将其识别为非法。由于您省略了基本类型声明,因此很难说哪个有错误。

【讨论】:

  • 我又添加了类型声明。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-13
  • 2012-05-03
  • 2020-04-01
  • 2014-10-16
相关资源
最近更新 更多