È sicuro per gli struct implementare le interfacce?


94

Mi sembra di ricordare di aver letto qualcosa su come sia dannoso per le strutture implementare le interfacce in CLR tramite C #, ma non riesco a trovare nulla al riguardo. È male? Ci sono conseguenze impreviste nel farlo?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

Risposte:


45

Ci sono molte cose in corso in questa domanda ...

È possibile per una struttura implementare un'interfaccia, ma ci sono problemi che derivano dal casting, dalla mutabilità e dalle prestazioni. Vedi questo post per maggiori dettagli: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

In generale, le strutture dovrebbero essere utilizzate per oggetti che hanno semantica di tipo valore. Implementando un'interfaccia su una struttura puoi incappare in problemi di boxe poiché la struttura viene lanciata avanti e indietro tra la struttura e l'interfaccia. Come risultato del boxing, le operazioni che cambiano lo stato interno della struttura potrebbero non funzionare correttamente.


3
"Come risultato della boxe, le operazioni che cambiano lo stato interno della struttura potrebbero non comportarsi correttamente." Fai un esempio e ottieni la risposta.

2
@ Will: Non sono sicuro di cosa ti riferisci nel tuo commento. Il post del blog a cui ho fatto riferimento contiene un esempio che mostra dove la chiamata di un metodo di interfaccia sulla struttura non modifica effettivamente il valore interno.
Scott Dorman

12
@ScottDorman: in alcuni casi, avere strutture che implementano le interfacce può aiutare a evitare il pugilato. I primi esempi sono IComparable<T>e IEquatable<T>. Memorizzare una struttura Fooin una variabile di tipo IComparable<Foo>richiederebbe il boxing, ma se un tipo generico Tè vincolato a IComparable<T>uno può confrontarlo con un altro Tsenza dover inscatolare nessuno dei due, e senza dover sapere nulla di Tdiverso da quello che implementa il vincolo. Tale comportamento vantaggioso è reso possibile solo dalla capacità delle strutture di implementare le interfacce. Detto questo ...
supercat

3
... sarebbe stato carino se ci fosse un mezzo per dichiarare che una particolare interfaccia dovrebbe essere considerata applicabile solo a strutture unboxed, poiché ci sono alcuni contesti in cui non sarebbe possibile per un oggetto classe o una struttura boxed avere il desiderato comportamenti.
supercat

2
"Gli struct dovrebbero essere usati per oggetti che hanno una semantica di tipo valore. ... le operazioni che cambiano lo stato interno dello struct potrebbero non funzionare correttamente." Il vero problema non è il fatto che la semantica del tipo di valore e la mutatabilità non si mescolano bene?
jpmc26

185

Poiché nessun altro ha fornito esplicitamente questa risposta, aggiungerò quanto segue:

L'implementazione di un'interfaccia su una struttura non ha alcuna conseguenza negativa.

Qualsiasi variabile del tipo di interfaccia utilizzata per contenere una struttura risulterà in un valore boxed di quella struttura utilizzata. Se la struttura è immutabile (una buona cosa), questo è nel peggiore dei casi un problema di prestazioni a meno che tu non sia:

  • usando l'oggetto risultante per scopi di blocco (un'idea immensamente cattiva in ogni caso)
  • usando la semantica di uguaglianza dei riferimenti e aspettandosi che funzioni per due valori boxed dalla stessa struttura.

Entrambi questi eventi sarebbero improbabili, invece è probabile che tu stia facendo una delle seguenti:

Generici

Forse molte ragioni ragionevoli per le strutture che implementano le interfacce è che possono essere utilizzate all'interno di un contesto generico con vincoli . Quando viene utilizzata in questo modo, la variabile in questo modo:

class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
    private readonly T a;

    public bool Equals(Foo<T> other)
    {
         return this.a.Equals(other.a);
    }
}
  1. Abilita l'uso della struttura come parametro di tipo
    • purché non venga utilizzato nessun altro vincolo come new()o class.
  2. Consentire di evitare il pugilato su strutture usate in questo modo.

Quindi this.a NON è un riferimento all'interfaccia quindi non causa una casella di ciò che è inserito in esso. Inoltre, quando il compilatore c # compila le classi generiche e deve inserire invocazioni dei metodi di istanza definiti sulle istanze del parametro Type T, può utilizzare il codice operativo vincolato :

Se thisType è un tipo di valore e thisType implementa il metodo, ptr viene passato senza modifiche come puntatore 'this' a un'istruzione del metodo di chiamata, per l'implementazione del metodo da thisType.

Ciò evita il boxing e poiché il tipo di valore sta implementando l'interfaccia deve implementare il metodo, quindi non si verificherà il boxing. Nell'esempio sopra, l' Equals()invocazione viene eseguita senza casella su questo a 1 .

API a basso attrito

La maggior parte delle strutture dovrebbe avere una semantica di tipo primitivo in cui valori identici bit per bit sono considerati uguali 2 . Il runtime fornirà tale comportamento in modo implicito Equals()ma può essere lento. Anche questa uguaglianza implicita non è esposta come un'implementazione di IEquatable<T>e quindi impedisce che le strutture vengano usate facilmente come chiavi per i dizionari a meno che non le implementino esplicitamente. È quindi comune per molti tipi di strutture pubbliche dichiarare di implementare IEquatable<T>(dove si Ttrovano autonomamente) per renderlo più semplice e con prestazioni migliori, nonché coerente con il comportamento di molti tipi di valore esistenti all'interno del CLR BCL.

Tutte le primitive nella BCL implementano come minimo:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T>(E quindi IEquatable)

Molti implementano anche IFormattable, inoltre molti dei tipi di valore definiti dal sistema come DateTime, TimeSpan e Guid implementano molti o tutti anche questi. Se stai implementando un tipo simile 'ampiamente utile' come una struttura numerica complessa o alcuni valori testuali a larghezza fissa, l'implementazione di molte di queste interfacce comuni (correttamente) renderà la tua struttura più utile e utilizzabile.

Esclusioni

Ovviamente se l'interfaccia implica fortemente la mutabilità (come ICollection) allora implementarlaèuna cattiva idea in quanto significherebbe che hai reso la struttura mutabile (portando al tipo di errori descritti già dove le modifiche si verificano sul valore boxed piuttosto che sull'originale ) o confondi gli utenti ignorando le implicazioni dei metodi come Add()o generando eccezioni.

Molte interfacce NON implicano mutabilità (come IFormattable) e servono come modo idiomatico per esporre determinate funzionalità in modo coerente. Spesso l'utente della struttura non si preoccuperà di alcun sovraccarico di boxe per tale comportamento.

Sommario

Se fatto in modo sensato, su tipi di valore immutabili, l'implementazione di interfacce utili è una buona idea


Appunti:

1: Nota che il compilatore può usarlo quando invoca metodi virtuali su variabili note per essere di un tipo di struttura specifico ma in cui è necessario invocare un metodo virtuale. Per esempio:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

L'enumeratore restituito dalla lista è una struttura, un'ottimizzazione per evitare un'allocazione durante l'enumerazione della lista (con alcune interessanti conseguenze ). Tuttavia la semantica di foreach specificano che se gli attrezzi enumeratore IDisposablepoi Dispose()saranno chiamati una volta che l'iterazione è completato. Ovviamente fare in modo che ciò avvenga tramite una chiamata boxed eliminerebbe qualsiasi vantaggio dell'enumeratore come struttura (in effetti sarebbe peggio). Peggio ancora, se la chiamata dispose modifica in qualche modo lo stato dell'enumeratore, ciò accadrebbe sull'istanza boxed e molti bug sottili potrebbero essere introdotti in casi complessi. Pertanto l'IL emesso in questo tipo di situazione è:

IL_0001: newobj System.Collections.Generic.List..ctor
IL_0006: stloc.0     
IL_0007: nop         
IL_0008: ldloc.0     
IL_0009: callvirt System.Collections.Generic.List.GetEnumerator
IL_000E: stloc.2     
IL_000F: br.s IL_0019
IL_0011: ldloca.s 02 
IL_0013: chiama System.Collections.Generic.List.get_Current
IL_0018: stloc.1     
IL_0019: ldloca.s 02 
IL_001B: chiama System.Collections.Generic.List.MoveNext
IL_0020: stloc.3     
IL_0021: ldloc.3     
IL_0022: brtrue.s IL_0011
IL_0024: leave.s IL_0035
IL_0026: ldloca.s 02 
IL_0028: vincolato. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: nop         
IL_0034: endfinally  

Pertanto l'implementazione di IDisposable non causa problemi di prestazioni e l'aspetto (deplorevole) mutevole dell'enumeratore viene preservato se il metodo Dispose fa effettivamente qualcosa!

2: double e float sono eccezioni a questa regola in cui i valori NaN non sono considerati uguali.


1
Il sito egheadcafe.com è stato spostato, ma non ha fatto un buon lavoro conservando i suoi contenuti. Ho provato, ma non riesco a trovare il documento originale di eggheadcafe.com/software/aspnet/31702392/… , mancando della conoscenza dell'OP. (PS +1 per un ottimo riassunto).
Abel

2
Questa è un'ottima risposta, ma penso che tu possa migliorarla spostando il "Riepilogo" in alto come "TL; DR". Fornire prima la conclusione aiuta il lettore a sapere dove stai andando con le cose.
Hans,

Dovrebbe essere visualizzato un avviso del compilatore durante il cast di un file structa un file interface.
Jalal

8

In alcuni casi può essere utile per una struttura implementare un'interfaccia (se non fosse mai stata utile, è dubbio che i creatori di .net l'avrebbero fornita). Se uno struct implementa un'interfaccia di sola lettura come IEquatable<T>, memorizzare lo struct in una posizione di archiviazione (variabile, parametro, elemento array, ecc.) Di tipo IEquatable<T>richiederà che sia boxed (ogni tipo di struct definisce effettivamente due tipi di cose: tipo di posizione che si comporta come un tipo di valore e un tipo di oggetto heap che si comporta come un tipo di classe; il primo è convertibile implicitamente nel secondo - "boxing" - e il secondo può essere convertito nel primo tramite cast esplicito-- "spacchettamento"). È possibile sfruttare l'implementazione di un'interfaccia di una struttura senza boxing, tuttavia, utilizzando quelli che vengono chiamati generici vincolati.

Ad esempio, se si avesse un metodo CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, tale metodo potrebbe chiamare thing1.Compare(thing2)senza dover box thing1o thing2. Se thing1accade, ad esempio, an Int32, il runtime lo saprà quando genera il codice per CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Poiché conoscerà il tipo esatto sia della cosa che ospita il metodo sia della cosa che viene passata come parametro, non dovrà boxare nessuno dei due.

Il problema più grande con le strutture che implementano le interfacce è che una struttura che viene memorizzata in una posizione del tipo di interfaccia Object, o ValueType(al contrario di una posizione del proprio tipo) si comporterà come un oggetto di classe. Per le interfacce di sola lettura questo non è generalmente un problema, ma per un'interfaccia mutevole come IEnumerator<T>questa può produrre alcune strane semantiche.

Considera, ad esempio, il codice seguente:

List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4

L'istruzione contrassegnata # 1 si aprirà enumerator1per leggere il primo elemento. Lo stato di quell'enumeratore verrà copiato enumerator2. L'istruzione # 2 contrassegnata farà avanzare quella copia per leggere il secondo elemento, ma non avrà effetto enumerator1. Lo stato di quel secondo enumeratore verrà quindi copiato enumerator3, che verrà avanzato dall'istruzione contrassegnata n. 3. Poi, a causa enumerator3e enumerator4sono entrambi i tipi di riferimento, un Avente per enumerator3poi essere copiato enumerator4, in modo marcato dichiarazione sarà efficace avanzare entrambi enumerator3 e enumerator4.

Alcune persone cercano di fingere che i tipi di valore ei tipi di riferimento siano entrambi tipi di Object, ma non è proprio vero. I tipi di valore reale sono convertibili in Object, ma non ne sono istanze. Un'istanza di List<String>.Enumeratorcui è memorizzata in una posizione di quel tipo è un tipo di valore e si comporta come un tipo di valore; copiandolo in una posizione di tipo, IEnumerator<String>verrà convertito in un tipo di riferimento e si comporterà come un tipo di riferimento . Quest'ultimo è una specie di Object, ma il primo non lo è.

A proposito, un altro paio di note: (1) In generale, i tipi di classi mutabili dovrebbero avere i loro Equalsmetodi che testano l'uguaglianza dei riferimenti, ma non c'è un modo decente per una struttura in scatola di farlo; (2) nonostante il nome, ValueTypeè un tipo di classe, non un tipo di valore; tutti i tipi derivati ​​da System.Enumsono tipi valore, così come tutti i tipi che derivano da ValueTypead eccezione di System.Enum, ma entrambi ValueTypee System.Enumsono tipi di classe.


3

Le strutture vengono implementate come tipi di valore e le classi sono tipi di riferimento. Se hai una variabile di tipo Foo e memorizzi un'istanza di Fubar in essa, la "inscatolerà" in un tipo di riferimento, vanificando così il vantaggio di usare una struttura in primo luogo.

L'unico motivo per cui vedo di usare una struttura invece di una classe è perché sarà un tipo di valore e non un tipo di riferimento, ma la struttura non può ereditare da una classe. Se la struttura eredita un'interfaccia e si passano le interfacce, si perde la natura del tipo di valore della struttura. Tanto vale renderlo una classe se hai bisogno di interfacce.


Funziona in questo modo anche per le primitive che implementano le interfacce?
aoetalks

3

(Beh, non ho niente di importante da aggiungere ma non hai ancora capacità di editing, quindi eccolo qui ..)
Perfettamente sicuro. Niente di illegale con l'implementazione di interfacce su struct. Tuttavia dovresti chiederti perché vorresti farlo.

Tuttavia, l' ottenimento di un riferimento di interfaccia a una struttura lo farà BOX . Quindi penalizzazione delle prestazioni e così via.

L'unico scenario valido a cui posso pensare in questo momento è illustrato nel mio post qui . Quando si desidera modificare lo stato di una struttura archiviata in una raccolta, è necessario farlo tramite un'interfaccia aggiuntiva esposta sulla struttura.


Se si passa un Int32a un metodo che accetta un tipo generico T:IComparable<Int32>(che può essere un parametro di tipo generico del metodo o la classe del metodo), quel metodo sarà in grado di utilizzare il Comparemetodo sull'oggetto passato senza boxarlo.
supercat


0

Non ci sono conseguenze per una struttura che implementa un'interfaccia. Ad esempio, le strutture di sistema integrate implementano interfacce come IComparablee IFormattable.


0

Ci sono pochi motivi per cui un tipo di valore implementa un'interfaccia. Poiché non è possibile creare una sottoclasse di un tipo di valore, è sempre possibile fare riferimento ad esso come al suo tipo concreto.

A meno che, naturalmente, non abbiate più strutture che implementano la stessa interfaccia, potrebbe essere marginalmente utile, ma a quel punto consiglierei di usare una classe e di farlo bene.

Ovviamente, implementando un'interfaccia, stai inscatolando la struttura, quindi ora si trova sull'heap e non sarai più in grado di passarla per valore ... Questo rafforza davvero la mia opinione che dovresti usare solo una classe in questa situazione.


Quante volte passi IComparable invece dell'implementazione concreta?
FlySwat

Non è necessario passare in IComparablegiro per inscatolare il valore. Chiamando semplicemente un metodo che si aspetta IComparablecon un tipo di valore che lo implementa, si boxerà implicitamente il tipo di valore.
Andrew Hare

1
@AndrewHare: i generici vincolati consentono IComparable<T>di invocare metodi su strutture di tipo Tsenza boxing.
supercat

-10

Le strutture sono proprio come le classi che vivono nello stack. Non vedo motivo per cui dovrebbero essere "insicuri".


Tranne che mancano di eredità.
FlySwat

7
Sono in disaccordo con ogni parte di questa risposta; essi non necessariamente vivono sullo stack, e la copia-semantica è molto diverso alle classi.
Marc Gravell

1
Sono immutabili, l'uso eccessivo di struct renderà triste la tua memoria :(
Teoman shipahi

1
@Teomanshipahi L'utilizzo eccessivo di istanze di classe farà impazzire il tuo garbage collector.
IllidanS4 supporta Monica

4
Per qualcuno che ha più di 20.000 ripetizioni, questa risposta è semplicemente inaccettabile.
Krythic
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.