2014-11-04 18 views
7

Se si cerca di fare una struct generico con l'attributo [ StructLayout (LayoutKind .Explicit)], usando l'struct genera un'eccezione in fase di esecuzione:Perché i tipi generici non possono avere un layout esplicito?

System.TypeLoadException: Impossibile caricare il tipo 'foo' da 'bar' assemblaggio perché tipi generici non può avere un layout esplicito.

Ho avuto difficoltà a trovare alcuna prova che questa limitazione esiste anche. I documenti Type.IsExplicitLayout implicano fortemente che è consentito e supportato. Qualcuno sa perché questo non è permesso? Non riesco a pensare a nessun motivo per cui i tipi generici potrebbero renderlo meno verificabile. Mi sembra un caso limite che semplicemente non si sono preoccupati di implementare.

Ecco an example del motivo per cui il layout generico esplicita sarebbe utile:

public struct TaggedUnion<T1,T2> 
{ 
    public TaggedUnion(T1 value) { _union=new _Union{Type1=value}; _id=1; } 
    public TaggedUnion(T2 value) { _union=new _Union{Type2=value}; _id=2; } 

    public T1 Type1 { get{ if(_id!=1)_TypeError(1); return _union.Type1; } set{ _union.Type1=value; _id=1; } } 
    public T2 Type2 { get{ if(_id!=2)_TypeError(2); return _union.Type2; } set{ _union.Type2=value; _id=2; } } 

    public static explicit operator T1(TaggedUnion<T1,T2> value) { return value.Type1; } 
    public static explicit operator T2(TaggedUnion<T1,T2> value) { return value.Type2; } 
    public static implicit operator TaggedUnion<T1,T2>(T1 value) { return new TaggedUnion<T1,T2>(value); } 
    public static implicit operator TaggedUnion<T1,T2>(T2 value) { return new TaggedUnion<T1,T2>(value); } 

    public byte Tag {get{ return _id; }} 
    public Type GetUnionType() {switch(_id){ case 1:return typeof(T1); case 2:return typeof(T2); default:return typeof(void); }} 

    _Union _union; 
    byte _id; 
    void _TypeError(byte id) { throw new InvalidCastException(/* todo */); } 

    [StructLayout(LayoutKind.Explicit)] 
    struct _Union 
    { 
     [FieldOffset(0)] public T1 Type1; 
     [FieldOffset(0)] public T2 Type2; 
    } 
} 

utilizzo:

TaggedUnion<int, double> foo = 1; 
Debug.Assert(foo.GetUnionType() == typeof(int)); 
foo = 1.0; 
Debug.Assert(foo.GetUnionType() == typeof(double)); 
double bar = (double) foo; 

Edit:

per essere chiari, si noti che i layout non sono verificate al momento della compilazione anche se la struttura non è generica. Le differenze di sovrapposizione e x64 di riferimento sono rilevate in fase di esecuzione dal CLR: http://pastebin.com/4RZ6dZ3S Mi chiedo perché i generici sono limitati quando i controlli vengono eseguiti in runtime in entrambi i casi.

risposta

4

La radice del problema è genericità e verificabilità e un progetto basato su vincoli di tipo. La regola che non possiamo sovrapporre i riferimenti (puntatore) con i tipi di valore è un vincolo implicito, multiparametrico. Quindi, sappiamo che il CLR è abbastanza intelligente da verificarlo in casi non generici ... perché non generico? Sembra attraente.

Una definizione di tipo generico corretta è quella che è verificabile per funzionare oggi per qualsiasi tipo esistente (all'interno dei vincoli) e qualsiasi che verrà definito in futuro. [1] CLR via C#, Richter Il compilatore verifica la definizione del tipo generico aperto da solo, considerando qualsiasi tipo di vincoli specificati per restringere i possibili argomenti del tipo.

In assenza di un vincolo di tipo più specifico, per Foo<T,U>, T e U rappresentano ciascuno sia l'unione di tutti i possibili tipi di valore e di riferimento, e la interface comune a tutti i tipi (la base System.Object). Se vogliamo rendere T o U più specifico, possiamo aggiungere vincoli di tipo primario e secondario. Nell'ultima versione di C#, il più specifico possibile è una classe o un'interfaccia. i vincoli di tipo struct o primitive non sono supportati.

Smussiamo Attualmente dire:

  1. cui solo struct o value type
  2. dove T se T è un tipo sigillato

Es:

public struct TaggedUnion<T1, T2> 
    where T1 : SealedThing // illegal 

quindi non abbiamo modo di definire un tipo generico verificabile per non violare mai la regola di sovrapposizione per tutti i tipi all'interno di T e U. Anche se potessimo vincolare con struct, è ancora possibile ricavare una struttura con campi di riferimento tale che per alcuni tipi in futuro, T<,> non sarebbe corretto.

Quindi quello che stiamo veramente chiedendo qui è why don't generic types allow implicit type constraints based on code within the class?; layout esplicito è un dettaglio di implementazione interna che impone restrizioni su quali combinazioni di T1 e T2 sono legali. A mio parere, ciò non è coerente con il design che dipende dai vincoli di tipo. Si viola il contratto pulito del sistema di tipo generico come progettato. Quindi, perché persino passare attraverso il problema di imporre un sistema di vincoli di tipo nella progettazione, in primo luogo, se intendiamo romperlo? Potremmo anche buttarlo e sostituirlo con delle eccezioni.

Allo stato attuale delle cose:

  1. Tipo vincoli sono metadati visibile del tipo generico aperto
  2. Verifica del tipo generico Foo<T,U> viene eseguita sulla definizione aperta F<,> volta. Per ogni istanza di tipo vincolato di Foo<t1,u1>, t1 e u1 vengono controllati per la correttezza del tipo rispetto ai vincoli. Non è necessario reverificare il codice per la classe e i metodi per Foo<t1,u1>.

Tutto questo è "Per quanto ne so"

Non v'è alcuna ragione tecnica difficile per cui ogni tipo di istanza generico non poteva essere semanticamente analizzato per correttezza (C++ è la prova di questo), ma sembrerebbe per rompere il design in atto.

TL; DR

Senza rompere o completare il disegno tipo di vincolo esistente non esiste alcun modo per questo di essere verificabile.

Forse, combinato con opportuni nuovi vincoli di tipo, potremmo vederlo in futuro.

+0

Per la cronologia, lo stesso tipo di "restrizioni imposte" potrebbe essere implementato lanciando dal costruttore statico. (E non sarei sorpreso di vederlo fatto in pratica poiché i limiti generici sono assurdamente sottodimensionati.) Ma immagino che non sarei sorpreso se quello fosse davvero il loro fondamento logico per questa limitazione. – DBN

+0

@DBN - La mia ipotesi è che sia davvero la logica, altrimenti si potrebbe sostenere che il sistema di restrizione dei tipi potrebbe essere sballato al posto delle eccezioni. Sono anche d'accordo con te, sono sottodimensionati. Sul lato positivo, le implementazioni C#/CTS sono molto più facili da scrivere rispetto a C++. Come scrittore di compilatori, posso realisticamente avere la possibilità di implementare la mia lingua con i generici CTS senza assistenza. Con i modelli C++, è meglio avere una squadra con te. Devo ammettere, C# è un design relativamente pulito. – codenheim

+0

Non vedo alcun bel percorso per aggiungere ulteriori tipi di vincoli generici al sistema di tipi, che presuppone un insieme di elementi non ereditabili molto solidi [nuovo, struct e classe]. Mentre potrebbe essere utile avere un vincolo per es. "copiabile come valore" (che consente di definire strutture che * non erano *), attualmente non richiede alcun vincolo generico e quindi nessun tipo generico esistente sarebbe in grado di soddisfarlo. Quello che sarebbe probabilmente necessario sarebbe avere un anti-vincolo, tale che le strutture che non erano copiabili come valore potrebbero * solo * essere passate a parametri generici ... – supercat

9

è specificato in ECMA 335 (CLI), partizione II, sezione II.10.1.2:

esplicito: il layout dei campi è esplicitamente previsto (§II.10.7). Tuttavia, un tipo generico non deve avere un layout esplicito.

Potete immaginare come potrebbe essere imbarazzante - dato che la dimensione di un parametro di tipo dipende dal parametro tipo, si potrebbe ottenere alcuni effetti decisamente strano ... un campo di riferimento non è consentito a sovrapporsi con una tipo di valore integrato o altro riferimento, ad esempio, che sarebbe difficile garantire non appena vengono coinvolte dimensioni sconosciute. (Non ho esaminato come funziona per i riferimenti a 32 bit vs 64 bit, che hanno un problema simile ma leggermente diverso ...)

Sospetto che la specifica potrebbe essere stata scritta per rendere più dettagliato restrizioni - ma renderlo una semplice restrizione generale su tutti i tipi generici è notevolmente più semplice.

+1

Le strutture normali possono già variare in fase di esecuzione. (Vedi: IntPtr) Anche se non era generico, la verifica che non vi sia alcuna sovrapposizione tra i tipi di riferimento e di valore è già gestita esclusivamente dal CLR: http://pastebin.com/4RZ6dZ3S Che "restrizioni più dettagliate" avrebbero bisogno di essere fatto per i generici se la regola è stata revocata? – DBN

+0

@DBN Mi chiedo perché esiste una regola sui tipi generici, quindi, poiché il CLR verifica ogni tipo quando caricato. – IllidanS4

+0

@ IllidanS4 Sì, è quello che sto chiedendo. – DBN

2

Il design del.NET framework rende certe ipotesi sui tipi generici renderebbe essenzialmente impossibile consentire a una struttura di layout esplicito di avere campi la cui dimensione potrebbe variare in base ai parametri di tipo generico. Forse il più fondamentale è questo:

  • Se esiste una qualsiasi combinazione delle argomenti di tipo per il quale una definizione di tipo generico sarebbe valida, si può presumere valida per tutte le combinazioni di argomenti che soddisfano i suoi vincoli specificati.

conseguenza, sarebbe impossibile per NET per consentire una struttura esplicita-layout per utilizzare passata in tipo generico che non è stato vincolato al class come il tipo di qualsiasi campo diverso l'ultimissimo. Inoltre, l'uso del tipo di valore generico che ha utilizzato il tipo generico passato come argomento deve essere limitato nello stesso modo del tipo passato.

Non credo ci sarebbe alcun problema particolare che consente una struttura esplicita-layout per avere un classe vincolata parametro di tipo generico se nulla è stato permesso di sovrapporre qualsiasi campo di quel tipo o di qualsiasi valore tipo che lo usava come argomento di tipo, e un tale campo, se non si sa che fosse di tipo di riferimento, doveva essere l'ultima cosa nella struttura.

D'altra parte, la maggior parte dei casi di utilizzo "sicuri" potrebbe essere gestita meglio avendo una struttura di layout non esplicita che conteneva il tipo generico e hanno una o più strutture di layout esplicito annidate al suo interno. Un tale approccio potrebbe semanticamente fare tutto ciò che potrebbe essere fatto con le strutture di layout esplicito. L'unico fastidio sarebbe dover aggiungere un livello aggiuntivo di riferimento indiretto nel codice sorgente quando si accede ai membri annidati, e il rimedio non sarebbe quello di consentire strutture generiche di layout esplicito, ma piuttosto di fornire un mezzo attraverso il quale una struttura che contiene un'altra struttura potrebbe creare alias per i membri della struttura interna.

Per fare un esempio:

[StructLayout(LayoutKind.Explicit)] 
public struct _UnionLongAnd2Ints 
{ 
    [FieldOffset(0)] public int LowerWord; 
    [FieldOffset(4)] public int UpperWord; 
    [FieldOffset(0)] public long Value; 
} 
public struct LongTwoIntsUnionAndSomethingElse<T> 
{ 
    UnionLongAnd2Ints UnionPart; 
    T OtherPart; 
} 

Qui la struttura generica contiene un valore a 64 bit che si sovrappone su due valori a 32 bit, ma dovrebbe essere verificabili perché la parte in modo esplicito-posto-fuori doesn' Ho qualcosa di generico.

+0

Non penso di capire cosa intendi per le strutture esplicite nidificate. Cosa contengono? È diverso dalla struttura di layout esplicita nidificata nel mio esempio? (_Union) – DBN

+0

@DBN: vedere l'esempio sopra. – supercat