c# - tutorial - ¿Por qué obtengo una violación de restricción genérica en tiempo de ejecución?
generics c# tutorial (4)
Obtengo la siguiente excepción al intentar crear una nueva instancia de una clase que depende en gran medida de los genéricos:
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)
Estoy sorprendido de por qué sucede esto. ¿No se verifican las restricciones genéricas en el momento de la compilación?
Mi búsqueda en Google me llevó a la conclusión de que esto tiene algo que ver con cualquiera de estas causas, o (a veces?) Ambas:
- El orden en que se definen las restricciones genéricas (
where
) en las clases; - El uso del patrón genérico de autorreferencia (conter-intuitivo pero muy legal, consulte la publicación del blog de Eric Lippert )
Una cosa que no estoy dispuesto a sacrificar es el patrón de autorreferencia. Absolutamente lo necesito para un propósito específico.
Sin embargo, me gustaría un poco de ayuda para señalar dónde y por qué ocurre este problema. Como la biblioteca es masiva y crea enormes patrones genéricos, creo que sería mejor dar progresivamente bits de código a pedido.
A petición, declaraciones de nuevo. Pero me gustaría enfatizar el hecho de que preferiría saber en general por qué puede ocurrir una excepción como esta y luego proceder a corregirlo en mi código específico en lugar de encontrar una solución específica, para la posteridad. Además, será mucho más largo para cualquier persona que analice el código responder que dar una explicación general de por qué las restricciones de tipo genérico pueden violarse en tiempo de ejecución.
Declaraciones de implementación:
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
Declaraciones de la biblioteca:
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
}
Como se señaló en los comentarios, la forma más sencilla de obtener ayuda sobre esta pregunta para mí sería minimizar el código a un ejemplo pequeño y reproducible en el que el error todavía está presente. Sin embargo, esto es difícil y largo para mí, y tiene las altas posibilidades de hacer que el error sea un virus, ya que se debe a la complejidad.
Intenté aislarlo de lo siguiente, pero no me sale el error cuando lo hago:
// 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
}
}
Detalles sangrientos
- El análisis de la excepción nos dice que
TOutPacket
infringeTInPacket
enRelay<TInPacket, TOutPacket, TCryptograph, Tendian>
. - La instancia de
Relay
que tenemos esTestClient
, que implementaAwareClient
, que implementaClient
, que implementaRelay
.-
AwareClient
se usa junto conAwarePacket
para que ambos extremos sepan qué tipo de cliente recibe qué tipo de paquetes.
-
- Por lo tanto, sabemos que
TOutPacket
enTestClient
violaTInPacket
enTestClient
. - La clase que implementa
TOutPacket
esServerPacket.Out
, que es un derivado deOperationPacket
. Este tipo es relativamente simple en términos de genéricos, ya que solo proporciona un tipo de enumeración y un tipo endiano, sin hacer referencia cruzada a otras clases. Conclusión: El problema no está (lo más probable) no en esta declaración por sí misma. - La clase que implementa
TInPacket
esServerPacket.In
, que es un derivado deAwarePacket
. Este tipo es mucho más complejo queTOutPacket
, ya que hace referencia a los genéricos para estar al tanto (AwarePacket
) del cliente que lo recibió. Probablemente es en este lío genérico donde ocurre el problema.
Entonces, muchas hipótesis pueden fundirse. En este punto, lo que leo es correcto y aceptado por el compilador, pero evidentemente hay algo mal allí.
¿Puede ayudarme a descubrir por qué obtengo una violación de restricción genérica en tiempo de ejecución con mi código?
Solución:
Entonces, después de un poco de trabajo con los parámetros y restricciones genéricos, creo que finalmente encontré el problema / la solución, y espero que no esté celebrando demasiado pronto.
Lo primero es lo primero, aún creo que esto es un error (o al menos una peculiaridad) sobre cómo el tiempo de ejecución dinámico está intentando invocar al constructor de TestServer. También podría ser un error del compilador, es decir, si está en contra de la norma convertir una clase escrita en una dinámica (entonces supongo que de nuevo) en lugar de convertirla a su tipo esperado.
Con eso quiero decir que este código:
TestServer test = new TestServer(GetPort());
se convierte en el Binder.InvokeConstructor
continuación, hace un montón de lanzamientos adicionales y no se parece en nada al código que se esperaría (el código de abajo generado después de un lanzamiento int sería esperado)
En cuanto a la solución, todo tiene que ver con el orden de los argumentos genéricos. Por lo que sé, no hay nada en el estándar que tenga voz en el orden en que debe colocar sus genéricos. El código funciona cuando se crea una instancia de la clase con un int normal. Eche un vistazo a cómo el servidor y el cliente tienen sus argumentos ordenados:
Client<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
Server<TServer, TClient, TInPacket, TOutPacket, TCryptograph, TEndian>
Exactamente lo mismo. Si elimina todas las demás clases de TestClient y hace que las restricciones de TestClient solo funcionen con la clase de Cliente y Servidor base, todo funciona como se espera, sin excepciones. He encontrado que el problema es con AwareClient
y AwarePacket
y la adición de TOperationCode
Si elimina TOperationCode
y en las clases abstractas y las clases TOperationCode
, el código nuevamente funciona como se esperaba. Esto es indeseable, ya que probablemente desee ese argumento genérico en su clase. He encontrado que moverlo al final de los argumentos resuelve el problema.
AwareClient<TOperationCode, TServer, TClient,
TInPacket, TOutPacket, TCryptograph, TEndian>
AwarePacket<TOperationCode, TServer, TClient, TInPacket,
TOutPacket, TCryptograph, TEndian>
se convierte en
AwareClient<TServer, TClient, TInPacket, TOutPacket,
TCryptograph, TEndian, TOperationCode>
AwarePacket<TServer, TClient, TInPacket, TOutPacket,
TCryptograph, TEndian, TOperationCode>
Por supuesto, tiene que hacer algunos cambios más con el orden de las restricciones genéricas para que se compile, pero eso parece resolver su problema.
Dicho esto, mi intuición me dice que esto es un error en el clr. Ahora, no es tan simple como tener 2 clases con los argumentos genéricos fuera de orden, o una que hereda de la otra con un argumento agregado. Estoy trabajando para intentar reproducir esto con un ejemplo más simple, pero hasta ahora, este caso es el único con el que he podido obtener una excepción.
EDIT (s) / Mi proceso de descubrimiento
Si elimina las restricciones en la Relay<TInPacket, TOutPacket, TCryptograph, TEndian>
, las excepciones no se lanzan.
Creo que lo que me parece más interesante es que las excepciones solo se lanzan la primera vez que intenta crear el TestClient, al menos en mi máquina (estas aún son FirstChanceExceptions que aparentemente son manejadas por el tiempo de ejecución interno, no están manejadas por el código de usuario) .
Haciendo esto:
new TestServer(GetPort());
new TestServer(GetPort());
new TestServer(GetPort());
no produce la misma llamada a través del método dinámico, sino que el compilador CallSite
internamente tres clases separadas de CallSite
, tres declaraciones separadas. Esto tiene sentido desde el punto de vista de la implementación. Sin embargo, lo que me parece especialmente interesante es que aunque, por lo que puedo ver, su código no se comparte (quién sabe si es internamente), las excepciones solo se emiten en la primera llamada al constructor.
Desearía tener la capacidad de depurar esto, pero los Servidores de Símbolos no descargarán la fuente para los constructores dinámicos, y la ventana de locals no es muy útil. Espero que alguien de Microsoft pueda ayudar a responder este misterio.
Creo que lo tengo, pero no estoy seguro. Definitivamente necesitaría un experto en dinámica de C # para confirmarlo.
Entonces, hice algunas pruebas para descubrir por qué fallaría con un lanzamiento explícito frente a un lanzamiento implícito cuando se lo pasara al constructor TestServer
.
Este es el código principal para su versión como compilado:
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();
}
Esencialmente, lo que está sucediendo es que RuntimeBinder ha creado una función que intenta crear, no el int para pasar a GetPort()
, sino un nuevo TestServer, invocando dinámicamente su constructor.
Observe la diferencia cuando lo convierta en un int y se lo pase al constructor:
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();
}
Tenga en cuenta que en lugar de crear un enlace InvokeConstructor, crea un enlace Convert, con un indicador explícito. En lugar de intentar invocar dinámicamente el constructor, invoca una función que convierte la dinámica en el constructor TestServer, por lo que le pasa un int real en lugar de un objeto genérico.
Supongo que mi punto es que, definitivamente, no hay nada de malo en sus genéricos (aparte del hecho de que son bastante ilegibles e IMO están sobreutilizados), sino más bien un problema con la forma en que el compilador intenta invocar dinámicamente al constructor.
Además, parece que no tiene nada que ver con el hecho de pasar el int al constructor. Quité el constructor de TestClient e hice este CallSite, (esencialmente el mismo que el error menos el parámetro 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));
Y la misma excepción TypeLoadException, GenericArguments [0], ''TOutPacket'', en ''ConsoleApplication1.Relay`4 [TInPacket, TOutPacket, TCryptograph, TEndian]'' viola la restricción del parámetro de tipo ''TInPacket''. ocurrió. Al parecer, el tiempo de ejecución está teniendo dificultades para invocar constructores en su tipo genérico.
Parece que esto podría ser un error ...
Si habilita la exploración de .NET Source y habilita los puntos de interrupción en cualquier excepción lanzada, detectará la excepción TypeLoadException. y puede ver todo el seguimiento de la pila .net. Además, es que puedes reproducirlo con WinDbg.
No tenía nada que ver con todas las construcciones genéricas. Lo creas o no, mi diseño era estable y funcional.
La causa real fue lo único que no sospeché: el parámetro int port
pasó al new TestServer(int port)
.
Este int
fue realmente obtenido a través de una expresión dinámica que es irrelevante. Digamos que fue
dynamic GetPort() { return 8888; }
new TestServer(GetPort()); // Crash
new TestServer((int)GetPort()); // Works
Disculpas a CodeInChaos por decir que no utilicé ninguna reflexión, supongo que eso fue solo una verdad a medias.
Ahora, la recompensa ha comenzado y el error sigue ahí (quiero usar mi método dinámico). Entonces, ¿podría alguien a) explicar por qué sucede esto (después de todo, el tipo es válido) yb) proponer una forma de solucionarlo? La recompensa y la respuesta aceptada irán a esa persona.
Si quieres experimentar, tengo este código para reproducirlo y bloquearlo: http://pastie.org/2277415
Si desea el archivo ejecutable real que falla, junto con la solución y el proyecto: http://localhostr.com/file/zKKGU74/CrashPlz.7z
Si de hecho no usa ninguna reflexión, esto parece indicar un error en el compilador de C # o el tiempo de ejecución. Por lo general, esto resulta en un código no verificable.
Parece que creaste una construcción que el tiempo de ejecución considera ilegal, pero el compilador de C # no reconoció como ilegal. Es difícil decir cuál tiene el error ya que omitió las declaraciones de tipos esenciales.
mi conjetura es que algún código compilado antiguo está colgando en algún lugar ... especialmente si el problema desapareció repentinamente
- ¿Has movido algún tipo de argumentos últimamente?
- ¿Estás incrementando las versiones de ensamblado en la compilación? (Puede causar problemas porque cambian los nombres completos de los tipos)
- ¿Cuál es el escenario en el que ocurre esta excepción? ¿Es un cliente que llama a un servidor que usa diferentes copias de los binarios?
Si alguna de estas preguntas es cierta, eliminaría cada binario que pudiera encontrar y reconstruir todo desde cero :)
-editar-
Además, asegúrese de que no está haciendo referencia accidental a los archivos binarios directamente, a menos que realmente tenga que hacerlo. Siempre debe usar las referencias del proyecto para asegurarse de que todo se reconstruya correctamente.
-editar2-
Bueno, esto es muy extraño ... pegué su código en una solución de patio de juegos que tengo, tengo la excepción. Pero ahora probé tu versión compilada, ¡y funcionó!
Difundí el código con mi versión anterior, exactamente igual ...
Difundí los archivos de proyecto, no exactamente iguales, pero copié todos los detalles para que funcionen, aún funciona tu proyecto, ¡el mío no!
Así que revisé los archivos de la solución ... no es diferente de las guías de proyecto ..., sigue siendo la misma situación ...
Así que eliminé la única otra cosa en la que podía pensar, el archivo .suo para mi solución en el área de juegos ... y ambos funcionaron ...
Los archivos suo parecen ser binarios, así que no estoy realmente seguro de qué es exactamente lo que está configurado allí. Sé que tenía ese archivo suo antes de instalar .net / vs2010 sp1, aunque quizás haya algunas cosas viejas ahí, quién sabe. Voy a tratar de investigar más.
-editar4-
Bueno, no sé qué está pasando ... ahora no puedo hacer que el código vuelva a fallar. Incluso copiar el archivo .suo anterior no funciona ...