In che modo un vincolo generico impedisce il pugilato di un tipo di valore con un’interfaccia implicitamente implementata?

La mia domanda è in qualche modo correlata a questa: interfaccia implementata esplicitamente e vincolo generico .

La mia domanda, tuttavia, è come il compilatore abilita un vincolo generico per eliminare la necessità del pugilato di un tipo di valore che implementa esplicitamente un’interfaccia.

Immagino che la mia domanda si riassuma in due parti:

  1. Che cosa sta succedendo con l’implementazione CLR dietro le quinte che richiede il boxing di un tipo di valore quando si accede a un membro dell’interfaccia esplicitamente implementato, e

  2. Cosa succede con un vincolo generico che rimuove questo requisito?

Qualche esempio di codice:

internal struct TestStruct : IEquatable { bool IEquatable.Equals(TestStruct other) { return true; } } internal class TesterClass { // Methods public static bool AreEqual(T arg1, T arg2) where T: IEquatable { return arg1.Equals(arg2); } public static void Run() { TestStruct t1 = new TestStruct(); TestStruct t2 = new TestStruct(); Debug.Assert(((IEquatable) t1).Equals(t2)); Debug.Assert(AreEqual(t1, t2)); } } 

E il risultante IL:

 .class private sequential ansi sealed beforefieldinit TestStruct extends [mscorlib]System.ValueType implements [mscorlib]System.IEquatable`1 { .method private hidebysig newslot virtual final instance bool System.IEquatable.Equals(valuetype TestStruct other) cil managed { .override [mscorlib]System.IEquatable`1::Equals .maxstack 1 .locals init ( [0] bool CS$1$0000) L_0000: nop L_0001: ldc.i4.1 L_0002: stloc.0 L_0003: br.s L_0005 L_0005: ldloc.0 L_0006: ret } } .class private auto ansi beforefieldinit TesterClass extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 L_0000: ldarg.0 L_0001: call instance void [mscorlib]System.Object::.ctor() L_0006: ret } .method public hidebysig static bool AreEqual<([mscorlib]System.IEquatable`1) T>(!!T arg1, !!T arg2) cil managed { .maxstack 2 .locals init ( [0] bool CS$1$0000) L_0000: nop L_0001: ldarga.s arg1 L_0003: ldarg.1 L_0004: constrained !!T L_000a: callvirt instance bool [mscorlib]System.IEquatable`1::Equals(!0) L_000f: stloc.0 L_0010: br.s L_0012 L_0012: ldloc.0 L_0013: ret } .method public hidebysig static void Run() cil managed { .maxstack 2 .locals init ( [0] valuetype TestStruct t1, [1] valuetype TestStruct t2, [2] bool areEqual) L_0000: nop L_0001: ldloca.s t1 L_0003: initobj TestStruct L_0009: ldloca.s t2 L_000b: initobj TestStruct L_0011: ldloc.0 L_0012: box TestStruct L_0017: ldloc.1 L_0018: callvirt instance bool [mscorlib]System.IEquatable`1::Equals(!0) L_001d: stloc.2 L_001e: ldloc.2 L_001f: call void [System]System.Diagnostics.Debug::Assert(bool) L_0024: nop L_0025: ldloc.0 L_0026: ldloc.1 L_0027: call bool TesterClass::AreEqual(!!0, !!0) L_002c: stloc.2 L_002d: ldloc.2 L_002e: call void [System]System.Diagnostics.Debug::Assert(bool) L_0033: nop L_0034: ret } } 

La chiamata chiave è constrained !!T invece di box TestStruct , ma la chiamata successiva è ancora callvirt in entrambi i casi.

Quindi non so cosa sia con il boxing richiesto per effettuare una chiamata virtuale, e soprattutto non capisco come l’utilizzo di un generico vincolato a un tipo di valore rimuova la necessità dell’operazione di boxe.

Ringrazio tutti in anticipo …

La mia domanda, tuttavia, è come il compilatore abilita un vincolo generico per eliminare la necessità del pugilato di un tipo di valore che implementa esplicitamente un’interfaccia.

Con “il compilatore” non è chiaro se si intende il jitter o il compilatore C #. Il compilatore C # lo fa emettendo il prefisso vincolato sulla chiamata virtuale. Vedere la documentazione del prefisso vincolato per i dettagli.

Cosa succede con l’implementazione CLR dietro le quinte che richiede il boxing di un tipo di valore quando si accede a un membro dell’interfaccia esplicitamente implementato

Se il metodo invocato è un membro dell’interfaccia esplicitamente implementato o meno non è particolarmente rilevante. Una domanda più generale sarebbe perché una chiamata virtuale richiede che il tipo di valore sia inserito in una scatola?

Per tradizione si intende una chiamata virtuale come invocazione indiretta di un puntatore del metodo in una tabella di funzioni virtuale. Questo non è esattamente il modo in cui le invocazioni dell’interfaccia funzionano nel CLR, ma è un modello mentale ragionevole ai fini di questa discussione.

Se è così che si invoca un metodo virtuale, da dove viene il vtable ? Il tipo di valore non ha un vtable in esso. Il tipo di valore ha appena il suo valore nella sua memoria. La boxe crea un riferimento a un object che ha una configurazione vtable per puntare a tutti i metodi virtuali del tipo di valore. (Ancora una volta, ti avverto che questo non è esattamente il modo in cui funzionano le invocazioni dell’interfaccia, ma è un buon modo per pensarci.)

Cosa succede con un vincolo generico che rimuove questo requisito?

Il jitter genererà un nuovo codice per ogni diversa costruzione di argomento del tipo valore del metodo generico. Se si sta generando codice nuovo per ogni tipo di valore diverso, è ansible adattare tale codice a quel tipo di valore specifico. Il che significa che non devi build un vtable e quindi cercare quali sono i contenuti del vtable! Sapete quale sarà il contenuto del vtable, quindi generate il codice per invocare direttamente il metodo.

L’objective finale è ottenere un puntatore alla tabella dei metodi della class in modo da poter chiamare il metodo corretto. Questo non può accadere direttamente su un tipo di valore, è solo un blob di byte. Ci sono due modi per arrivarci:

  • Opcodes.Box, implementa la conversione di boxe e trasforma il valore del valore in un object. L’object ha il puntatore della tabella dei metodi all’offset 0.
  • Opcodes.Contrained, passa il jitter al puntatore della tabella dei metodi direttamente senza necessità di boxe. Abilitato dal vincolo generico.

Quest’ultimo è chiaramente più efficiente.

La boxe è necessaria quando un object di tipo valore viene passato a una routine che si aspetta di ricevere un object di tipo class. Una dichiarazione di metodo come string ReadAndAdvanceEnumerator(ref T thing) where T:IEnumerator dichiara in realtà un’intera famiglia di funzioni, ognuna delle quali si aspetta un diverso tipo T Se T sembra essere un tipo di valore (ad es. List.Enumerator ), il compilatore Just-In-Time genererà effettivamente il codice macchina esclusivamente per eseguire ReadAndAdvanceEnumerator.Enumerator>() . A proposito, nota l’uso di ref ; se T fosse un tipo di class (i tipi di interfaccia usati in un contesto diverso dai vincoli contano come tipi di class) l’uso di ref sarebbe un inutile impedimento all’efficienza. Se, tuttavia, esiste una possibilità che T possa essere una this struttura di configurazione (ad esempio List.Enumerator ), sarà necessario l’uso di ref per garantire che vengano eseguite le mutazioni eseguite dalla struct durante l’esecuzione di ReadAndAdvanceEnumerator sulla copia del chiamante.

Penso che tu debba usare

  • riflettore
  • ildasm / monodess

per ottenere davvero la risposta che desideri

Puoi ovviamente esaminare le specifiche del CLR (ECMA) e / o il sorgente di un compilatore C # ( mono )

Il vincolo generico fornisce solo un tempo di compilazione per verificare che il tipo corretto sia passato nel metodo. Il risultato finale è sempre che il compilatore genera un metodo appropriato che accetta il tipo di runtime:

 public struct Foo : IFoo { } public void DoSomething(TFoo foo) where TFoo : IFoo { // No boxing will occur here because the compiler has generated a // statically typed DoSomething(Foo foo) method. } 

In questo senso, ignora la necessità del pugilato dei tipi di valore, poiché viene creata un’istanza di metodo esplicita che accetta direttamente quel tipo di valore.

Mentre quando un cast di un tipo di valore viene eseguito su un’interfaccia implementata, l’istanza è un tipo di riferimento, che si trova nell’heap. Poiché non sfruttiamo i generici in questo senso, stiamo forzando un cast a un’interfaccia (e al successivo inscatolamento) se il tipo di runtime è un tipo di valore.

 public void DoSomething(IFoo foo) { // Boxing occurs here as Foo is cast to a reference type of IFoo. } 

La rimozione del vincolo generico interrompe solo il tempo di compilazione controllando che si passi il tipo corretto nel metodo.