2013-12-19 10 views
9

Hier ist ziemlich einfach generische Klasse. Der generische Parameter ist auf den Referenztyp beschränkt. IRepository und DbSet enthalten ebenfalls dieselbe Einschränkung.Warum wird die Box-Anweisung für generische ausgegeben?

public class Repository<TEntity> : IRepository<TEntity> 
    where TEntity : class, IEntity 
{ 
    protected readonly DbSet<TEntity> _dbSet; 
    public void Insert(TEntity entity) 
    { 
     if (entity == null) 
     throw new ArgumentNullException("entity", "Cannot add null entity."); 
     _dbSet.Add(entity); 
    } 
} 

Kompilierte IL enthält box Anweisung. Hier ist die Release-Version (die Debug-Version enthält sie aber auch).

.method public hidebysig newslot virtual final 
    instance void Insert(!TEntity entity) cil managed 
{ 
    // Code size  38 (0x26) 
    .maxstack 8 
    IL_0000: ldarg.1 
    >>>IL_0001: box  !TEntity 
    IL_0006: brtrue.s IL_0018 
    IL_0008: ldstr  "entity" 
    IL_000d: ldstr  "Cannot add null entity." 
    IL_0012: newobj  instance void [mscorlib]System.ArgumentNullException::.ctor(string, 
              string) 
    IL_0017: throw 
    IL_0018: ldarg.0 
    IL_0019: ldfld  class [EntityFramework]System.Data.Entity.DbSet`1<!0> class Repository`1<!TEntity>::_dbSet 
    IL_001e: ldarg.1 
    IL_001f: callvirt instance !0 class [EntityFramework]System.Data.Entity.DbSet`1<!TEntity>::Add(!0) 
    IL_0024: pop 
    IL_0025: ret 
} // end of method Repository`1::Insert 

UPDATE:

Mit object.Equals(entity, default(TEntity)) sieht es noch schlimmer:

.maxstack 2 
    .locals init ([0] !TEntity CS$0$0000) 
    IL_0000: ldarg.1 
    >>>IL_0001: box  !TEntity 
    IL_0006: ldloca.s CS$0$0000 
    IL_0008: initobj !TEntity 
    IL_000e: ldloc.0 
    >>>IL_000f: box  !TEntity 
    IL_0014: call  bool [mscorlib]System.Object::Equals(object, 
           object) 
    IL_0019: brfalse.s IL_002b 

UPDATE2:

Für diejenigen, die interessiert sind, hier ist der Code von JIT kompiliert im Debugger angezeigt:

0cd5af28 55    push ebp 
0cd5af29 8bec   mov  ebp,esp 
0cd5af2b 83ec18   sub  esp,18h 
0cd5af2e 33c0   xor  eax,eax 
0cd5af30 8945f0   mov  dword ptr [ebp-10h],eax 
0cd5af33 8945ec   mov  dword ptr [ebp-14h],eax 
0cd5af36 8945e8   mov  dword ptr [ebp-18h],eax 
0cd5af39 894df8   mov  dword ptr [ebp-8],ecx 
    //entity reference to [ebp-0Ch] 
0cd5af3c 8955f4   mov  dword ptr [ebp-0Ch],edx 
    //some debugger checks 
0cd5af3f 833d9424760300 cmp  dword ptr ds:[3762494h],0 
0cd5af46 7405   je  0cd5af4d Branch 
0cd5af48 e8e1cac25a  call clr!JIT_DbgIsJustMyCode (67987a2e) 
0cd5af4d c745fc00000000 mov  dword ptr [ebp-4],0 
0cd5af54 90    nop 

    //comparison or entity ref with zero 
0cd5af55 837df400  cmp  dword ptr [ebp-0Ch],0 
0cd5af59 0f95c0   setne al 
0cd5af5c 0fb6c0   movzx eax,al 
0cd5af5f 8945fc   mov  dword ptr [ebp-4],eax 
0cd5af62 837dfc00  cmp  dword ptr [ebp-4],0 
    //if not zero, jump further 
0cd5af66 7542   jne  0cd5afaa Branch 
    //throwing exception here  

Der Grund für diese Frage ist eigentlich, dass NDepend über Boxen/Unboxing warnt. Ich war neugierig, warum es Boxen in einigen generischen Klassen gefunden hat, und jetzt ist es klar.

+1

Ist es dasselbe, wenn Sie object.Equals (Entität, Standard (TEntity)) verwenden? –

+0

Ich werde hier einen Stich in die Dunkelheit machen. IIRC, Interfaces _kann Boxen von Werttypen verursachen, also frage ich mich, ob das etwas damit zu tun hat. Vielleicht denkt der Compiler, dass er es einkisten muss, falls ein Wert-Typ eingegeben wird, um es gegen die 'Null'-Referenz zu prüfen. Ich könnte jedoch voll davon sein. :) EDIT: Dies ist trotz der Einschränkung, dass es eine 'Klasse' (kein Werttyp) ist, aber ich bin nicht sicher, ob das vom Compiler und/oder CLR und/oder IL berücksichtigt wird. –

+1

Ich würde auch vermuten, dass sowohl "object.Equals" als auch "==" operator so _general_ ist, dass es standardmäßig boxt. Versuchen Sie 'EqualityComparer .Default.Equals (Entität, Standard (T))' wie es generische Methode verwenden sollte. –

Antwort

12

Die Zustände ECMA-Spezifikation diese über die box Anweisung:

Stapel Übergang: ..., val -> ..., obj

...

Wenn typeTok ein generischer Parameter ist, auf das das Verhalten der Box-Befehl hängt Aktueller Typ zur Laufzeit. Wenn dieser Typ [...] ein Referenztyp ist, wird val nicht geändert.

Was es sagt ist, dass der Compiler kann annehmen, dass es zu box ein Referenztyp sicher ist. Bei Generika hat der Compiler also zwei Möglichkeiten: Den Code ausgeben, der garantiert funktioniert, unabhängig davon, wie der generische Typ eingeschränkt ist, oder den Code optimieren und redundante Anweisungen weglassen, wo sie sich als unnötig erweisen können.

Der Microsoft C# -Compiler neigt im Allgemeinen dazu, den einfacheren Ansatz zu wählen und alle Optimierungen der JIT-Stufe zu überlassen. Für mich sieht Ihr Beispiel genau so aus: Sie optimieren nichts, weil das Implementieren einer Optimierung Zeit kostet und das Speichern dieser box-Anweisung in der Praxis wahrscheinlich sehr wenig Wert hat.

In C# kann sogar ein unbeschränkter generischer typisierter Wert mit null verglichen werden, daher muss der Compiler diesen allgemeinen Fall unterstützen. Der einfachste Weg, diesen allgemeinen Fall zu implementieren, besteht darin, die Anweisung box zu verwenden, die alle schwerfälligen Typen wie Referenz-, Wert- und NULL-Werte verarbeitet und entweder einen Referenzwert oder einen Nullwert korrekt auf den Stapel verschiebt. Daher ist es für den Compiler am Einfachsten, unabhängig von den Einschränkungen box auszugeben und dann den Wert mit Null zu vergleichen (brtrue).

+0

Notieren Sie die Antwort von Hans, die wie der letzte Grund für den Unterricht klingt und wahrscheinlich die akzeptierte Antwort sein sollte. PEVerify ist wie die Autorität in Bezug auf die richtige IL, und wenn PEVerify etwas aus welchem ​​Grund auch immer zurückweist, ist es so gut wie ungültig (wichtig in Szenarien mit geringem Vertrauen). –

14

Ich stieß auf einen sehr relevanten Kommentar beim Überprüfen des C# -Compiler-Quellcodes, der BOX-Anweisungen generiert. Die Fncbind.CPP-Quelldatei hat diesen Kommentar, soweit nicht anderweitig direkt auf diesen speziellen Code bezogen werden:

// HINWEIS: Für die Flaggen, haben wir EXF_FORCE_UNBOX verwenden (nicht EXF_REFCHECK), auch wenn
// wir wissen, dass die Art ist ein Referenztyp. Der Prüfer erwartet den gesamten Code für
// Typ-Parameter, die sich so verhalten, als wäre der Typparameter ein Werttyp.
// Der Jitter sollte darüber ....

So ist es es klug sein, weil der Prüfer es erfordert.

Und ja, der Jitter ist schlau darüber. Es gibt einfach keinen Code für die BOX-Anweisung aus.

+0

Das ist ziemlich interessant - also wurde beschlossen, den IL-Ansatz zu vereinfachen, sobald (wahrscheinlich) die Unboxing-Referenztypüberprüfung bereits im JIT-Compiler existierte. – mikalai

+0

Ah, guter Fund. Ich habe mich sehr bemüht, die ECMA-Spezifikation auf diese Weise zu interpretieren, aber es sah einfach nicht so aus, als ob die Spezifikation dies erfordert.Relevantes Zitat: _ "Der durch Verifizierung verfolgte Typ ist immer" boxed "" typeTok "für generische Parameter, unabhängig davon, ob der tatsächliche Typ zur Laufzeit ein Wert oder Referenztyp ist." _ –

Verwandte Themen