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);
}
}
- Abilita l'uso della struttura come parametro di tipo
- purché non venga utilizzato nessun altro vincolo come
new()
o class
.
- 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 T
trovano 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)
;
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 IDisposable
poi 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.