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)
我很困惑为什么会这样.编译时是否检查了通用约束?
我的谷歌搜索让我得出结论,这与这些原因中的任何一个有关,或者(有时?)两者:
where
在类中定义泛型约束()的顺序;我不准备牺牲的一件事是自我参照模式.我绝对需要它用于特定目的.
但是,我想帮助指出这个问题出现的地点和原因.由于库是庞大的并且产生了巨大的通用模式,我认为最好根据请求逐步提供代码位.
根据要求,再次声明.但我想强调的是,我宁愿一般都知道为什么会发生这样的异常,然后在我的特定代码中自行修复它,而不是为后代找到特定的修复.此外,这将是更长的人分析代码来回答,而不是给一般的解释,为什么泛型类型的限制可以在运行时被侵犯.
执行声明:
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)
血腥细节
TOutPacket
违反TInPacket
了Relay<TInPacket, TOutPacket, TCryptograph, Tendian>
.Relay
我们的实例是TestClient
,实现AwareClient
,实现Client
,实现Relay
.
AwareClient
结合使用,AwarePacket
以便两端都知道哪种类型的客户端接收哪种类型的数据包.TOutPacket
在TestClient
违反TInPacket
的TestClient
.TOutPacket
是ServerPacket.Out
,它的衍生物OperationPacket
.这种类型在泛型方面相对简单,因为它只提供枚举类型和字节序类型,不会对其他类进行交叉引用.结论:问题本身并非(很可能)不在本声明中.TInPacket
是ServerPacket.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类,则一切都按预期工作,没有例外.我发现这个问题是AwareClient
和AwarePacket
和添加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重现它.
它与所有通用结构无关.信不信由你,我的设计稳定而实用.
实际原因是我唯一没有怀疑的: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