it-swarm.it

È sicuro per le strutture implementare interfacce?

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

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

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

È possibile per una struttura implementare un'interfaccia, ma ci sono preoccupazioni che derivano dal casting, dalla mutabilità e dalle prestazioni. Vedi questo post per maggiori dettagli: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

In generale, le strutture dovrebbero essere usate per oggetti che hanno una semantica di tipo valore. Implementando un'interfaccia su una struttura è possibile imbattersi in problemi di boxe mentre la struttura viene proiettata avanti e indietro tra la struttura e l'interfaccia. Come risultato del pugilato, le operazioni che cambiano lo stato interno della struttura potrebbero non comportarsi correttamente.

45
Scott Dorman

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

L'implementazione di un'interfaccia su una struttura non ha conseguenze negative di sorta.

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

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

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

Generics

Forse molte ragioni ragionevoli per le strutture che implementano le interfacce è che possono essere usate in un contesto generico con vincoli. Se usata 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 di struct come parametro di tipo
    • purché non venga utilizzato nessun altro vincolo come new() o class.
  2. Consentire di evitare il pugilato su strutture utilizzate in questo modo.

Quindi this.a NON è un riferimento all'interfaccia, quindi non causa una scatola di ciò che vi è inserito. 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 limitato :

Se thisType è un tipo di valore e thisType implementa il metodo, allora ptr viene passato non modificato come puntatore 'this' a un'istruzione del metodo call, per l'implementazione del metodo da thisType.

Ciò evita il pugilato e poiché il tipo di valore sta implementando l'interfaccia è must implementare il metodo, quindi non si verificherà alcun pugilato. Nell'esempio sopra, l'invocazione Equals() viene eseguita senza box su this.a1.

API a basso attrito

La maggior parte delle strutture dovrebbe avere una semantica di tipo primitivo in cui valori identici bit a bit sono considerati uguali2. Il runtime fornirà tale comportamento nell'implicito Equals() ma questo può essere lento. Anche questa uguaglianza implicita è no 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 lo implementino esplicitamente da soli. È quindi comune per molti tipi di strutture pubbliche dichiarare che implementano IEquatable<T> (Dove T sono loro stessi) per rendere questo più facile e meglio performante oltre che coerente con il comportamento di molti valori esistenti tipi all'interno del CLR BCL.

Tutte le primitive nel BCL implementano come minimo:

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

Molti implementano anche IFormattable, inoltre molti dei tipi di valori definiti dal sistema come DateTime, TimeSpan e Guid implementano anche molti o tutti questi. Se stai implementando un tipo altrettanto "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 mutabilità (come ICollection) l'implementazione è una cattiva idea in quanto significherebbe che hai reso la struttura mutabile (portando al tipo di errori già descritti in cui si verificano modifiche sul valore inscatolato anziché sull'originale) o si confondono gli utenti ignorando le implicazioni di metodi come Add() o generando eccezioni.

Molte interfacce NON implicano mutabilità (come IFormattable) e fungono da modo idiomatico per esporre determinate funzionalità in modo coerente. Spesso l'utente della struttura non si preoccuperà delle spese generali 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ò usare questo quando invoca metodi virtuali su variabili che sono conosciuti per essere di un tipo di struttura specifico ma in cui è richiesto per invocare un metodo virtuale. Per esempio:

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

L'enumeratore restituito dall'elenco è una struttura, un'ottimizzazione per evitare un'allocazione durante l'enumerazione dell'elenco (con alcune interessanti conseguenze ). Tuttavia, la semantica di foreach specifica che se l'enumeratore implementa IDisposable, verrà chiamato Dispose() una volta completata l'iterazione. Ovviamente il fatto che ciò avvenga attraverso una chiamata in scatola eliminerebbe qualsiasi beneficio dell'enumeratore essendo una struttura (in effetti sarebbe peggio). Peggio ancora, se dispose call modifica in qualche modo lo stato dell'enumeratore, ciò si verificherebbe sull'istanza in scatola e molti casi 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 alcun problema di prestazioni e l'aspetto (deplorevole) mutabile dell'enumeratore viene preservato se il metodo Dispose effettivamente fa qualcosa!

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

161
ShuggyCoUk

In alcuni casi può essere utile per una struttura implementare un'interfaccia (se non fosse mai utile, è dubbio che i creatori di .net l'avrebbero fornita). Se una struttura implementa un'interfaccia di sola lettura come IEquatable<T>, La memorizzazione della struttura in una posizione di archiviazione (variabile, parametro, elemento dell'array, ecc.) Di tipo IEquatable<T> Richiederà che sia boxata ( ogni tipo di struttura in realtà definisce due tipi di cose: un tipo di posizione di memoria che si comporta come un tipo di valore e un tipo di oggetto heap che si comporta come un tipo di classe; il primo è implicitamente convertibile nel secondo - "boxing" - e il il secondo può essere convertito nel primo tramite cast esplicito - "unboxing"). È possibile sfruttare l'implementazione di una struttura di un'interfaccia senza boxe, 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 digitare thing1 O thing2. Se thing1 Sembra essere, ad esempio, un Int32, Il runtime saprà che quando genera il codice per CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Dal momento che conoscerà il tipo esatto sia dell'elemento che ospita il metodo sia dell'elemento che viene passato 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 mutante come IEnumerator<T> Può produrre qualche strana semantica.

Si consideri, ad esempio, il seguente codice:

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 marcata n. 1 consentirà a enumerator1 Di leggere il primo elemento. Lo stato di quell'enumeratore verrà copiato in enumerator2. L'istruzione marcata n. 2 farà avanzare quella copia per leggere il secondo elemento, ma non influisce su enumerator1. Lo stato di quel secondo enumeratore verrà quindi copiato in enumerator3, Che verrà avanzato dall'istruzione n. 3 contrassegnata. Quindi, poiché enumerator3 E enumerator4 Sono entrambi tipi di riferimento, un RIFERIMENTO a enumerator3 Sarà quindi copiato in enumerator4, quindi l'istruzione contrassegnata avanzerà efficacemente entrambienumerator3 e enumerator4.

Alcune persone cercano di fingere che i tipi di valore e i 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>.Enumerator Che è 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> lo convertirà in un tipo di riferimento e si comporterà come un tipo di riferimento. Quest'ultimo è una specie di Object, ma il primo no.

A proposito, un altro paio di note: (1) In generale, i tipi di classe mutabili dovrebbero avere i loro metodi Equals per testare l'uguaglianza di riferimento, ma non esiste un modo decente per una struttura scatolata per farlo; (2) nonostante il suo nome, ValueType è un tipo di classe, non un tipo di valore; tutti i tipi derivati ​​da System.Enum sono tipi di valore, così come tutti i tipi che derivano da ValueType ad eccezione di System.Enum, ma entrambi ValueType e System.Enum Sono tipi di classe.

8
supercat

(Beh, non ho niente di importante da aggiungere, ma non hai ancora la capacità di modifica, quindi ecco qui ..)
Perfettamente sicuro. Niente di illegale nell'implementazione di interfacce su strutture. Tuttavia dovresti chiederti perché dovresti farlo.

Tuttavia l'ottenimento di un riferimento all'interfaccia di una struttura lo BOX . Quindi penalità per le prestazioni e così via.

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

3
Gishu

Le strutture sono implementate come tipi di valore e le classi sono tipi di riferimento. Se si dispone di una variabile di tipo Foo e in essa si memorizza un'istanza di Fubar, verrà "inscatolata" in un tipo di riferimento, vanificando così il vantaggio di utilizzare una struttura in primo luogo.

L'unico motivo per cui vedo di usare una struttura anziché 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 strutt eredita un'interfaccia e si passa attraverso le interfacce, si perde quel tipo di valore natura della struct. Potrebbe anche renderlo una classe se hai bisogno di interfacce.

3
dotnetengineer

Penso che il problema sia che causa il pugilato perché le strutture sono tipi di valore, quindi c'è una leggera penalità di prestazione.

Questo link suggerisce che potrebbero esserci altri problemi con esso ...

http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

1
Simon Keep

Non ci sono conseguenze per una struttura che implementa un'interfaccia. Ad esempio il sistema integrato costruisce interfacce come IComparable e IFormattable.

0
Joseph Daigle

Ci sono pochissime ragioni per un tipo di valore per implementare un'interfaccia. Poiché non è possibile sottoclassare un tipo di valore, è sempre possibile fare riferimento ad esso come tipo concreto.

A meno che, naturalmente, non ci siano più strutture che implementano tutte la stessa interfaccia, allora potrebbe essere marginalmente utile, ma a quel punto consiglierei di usare una classe e farlo nel modo giusto.

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

0
FlySwat