web-dev-qa-db-fra.com

Est-il sûr que les structures implémentent des interfaces?

Je semble me souvenir d'avoir lu quelque chose sur la façon dont il est mauvais pour les structures d'implémenter des interfaces dans CLR via C #, mais je n'arrive pas à trouver quoi que ce soit à ce sujet. Est-il mauvais? Y a-t-il des conséquences inattendues à le faire?

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

Il y a plusieurs choses qui se passent dans cette question ...

Il est possible pour une structure d'implémenter une interface, mais il existe des problèmes liés à la conversion, à la mutabilité et aux performances. Voir cet article pour plus de détails: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

En général, les structures doivent être utilisées pour les objets qui ont une sémantique de type valeur. En implémentant une interface sur une structure, vous pouvez rencontrer des problèmes de boxe lorsque la structure est échangée entre la structure et l'interface. En raison de la boxe, les opérations qui modifient l'état interne de la structure peuvent ne pas se comporter correctement.

45
Scott Dorman

Étant donné que personne d'autre n'a explicitement fourni cette réponse, j'ajouterai ce qui suit:

L'implémentation d'une interface sur une structure n'a aucune conséquence négative.

Toute variable du type d'interface utilisé pour contenir une structure entraînera l'utilisation d'une valeur encadrée de cette structure. Si la structure est immuable (une bonne chose), c'est au pire un problème de performances, sauf si vous êtes:

  • utiliser l'objet résultant à des fins de verrouillage (une très mauvaise idée de toute façon)
  • en utilisant la sémantique d'égalité de référence et en s'attendant à ce qu'elle fonctionne pour deux valeurs encadrées de la même structure.

Ces deux éléments seraient peu probables, mais vous effectuez probablement l'une des actions suivantes:

Génériques

De nombreuses raisons raisonnables peuvent expliquer l'implémentation d'interfaces par les structures afin qu'elles puissent être utilisées dans un contexte générique avec contraintes. Lorsqu'elle est utilisée de cette façon, la variable comme ceci:

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. Activez l'utilisation de la structure comme paramètre de type
    • tant qu'aucune autre contrainte comme new() ou class n'est utilisée.
  2. Permet d'éviter la boxe sur les structures utilisées de cette manière.

Alors this.a N'EST PAS une référence d'interface donc il ne provoque pas de boîte de ce qui y est placé. De plus, lorsque le compilateur c # compile les classes génériques et doit insérer des invocations des méthodes d'instance définies sur les instances du paramètre Type T, il peut utiliser l'opcode contraint :

Si thisType est un type valeur et que thisType implémente la méthode, ptr est passé tel quel comme pointeur 'this' vers une instruction de méthode d'appel, pour l'implémentation de method par thisType.

Cela évite la boxe et puisque le type de valeur implémente l'interface est doit implémenter la méthode, donc aucune boxe ne se produira. Dans l'exemple ci-dessus, l'invocation de Equals() se fait sans aucune case à ce sujet.1.

API à faible friction

La plupart des structures doivent avoir une sémantique de type primitive où les valeurs identiques au niveau du bit sont considérées comme égales2. Le runtime fournira un tel comportement dans la Equals() implicite mais cela peut être lent. Cette égalité implicite est également pas présentée comme une implémentation de IEquatable<T> Et empêche ainsi les structures d'être facilement utilisées comme clés pour les dictionnaires à moins qu'elles ne l'implémentent explicitement elles-mêmes. Il est donc courant pour de nombreux types de structures publiques de déclarer qu'ils implémentent IEquatable<T> (Où T est eux-mêmes) pour rendre cela plus facile et plus performant ainsi que cohérent avec le comportement de nombreuses valeurs existantes. types dans le CLR BCL.

Toutes les primitives de la BCL implémentent au minimum:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (Et donc IEquatable)

Beaucoup implémentent également IFormattable, et bon nombre des types de valeurs définis par le système comme DateTime, TimeSpan et Guid implémentent également plusieurs d'entre eux. Si vous implémentez un type similaire "largement utile" comme une structure de nombre complexe ou des valeurs textuelles de largeur fixe, l'implémentation de bon nombre de ces interfaces communes (correctement) rendra votre structure plus utile et utilisable.

Exclusions

Évidemment, si l'interface implique fortement mutabilité (comme ICollection) alors l'implémenter est une mauvaise idée car cela signifierait que vous avez soit rendu la structure mutable (conduisant à des sortes de erreurs déjà décrites où les modifications se produisent sur la valeur encadrée plutôt que sur l'original) ou vous confondez les utilisateurs en ignorant les implications des méthodes comme Add() ou en lançant des exceptions.

De nombreuses interfaces n'impliquent PAS de mutabilité (comme IFormattable) et servent de moyen idiomatique d'exposer certaines fonctionnalités de manière cohérente. Souvent, l'utilisateur de la structure ne se souciera d'aucune surcharge de boxe pour un tel comportement.

Sommaire

Lorsqu'elle est effectuée de manière raisonnable, sur des types de valeur immuables, la mise en œuvre d'interfaces utiles est une bonne idée


Remarques:

1: Notez que le compilateur peut l'utiliser lors de l'appel de méthodes virtuelles sur des variables qui sont connues pour être d'un type de structure spécifique mais dans lesquelles il est nécessaire d'appeler une méthode virtuelle. Par exemple:

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

L'énumérateur renvoyé par la liste est une structure, une optimisation pour éviter une allocation lors de l'énumération de la liste (avec quelques --- conséquences ) intéressantes. Cependant, la sémantique de foreach spécifie que si l'énumérateur implémente IDisposable alors Dispose() sera appelé une fois l'itération terminée. Évidemment, le fait de se produire via un appel en boîte éliminerait tout avantage de l'énumérateur étant une structure (en fait, ce serait pire). Pire encore, si l'appel dispose modifie l'état de l'énumérateur d'une manière ou d'une autre, cela se produira sur l'instance encadrée et de nombreux bogues subtils pourraient être introduits dans des cas complexes. Par conséquent, l'IL émis dans ce genre de situation est:

 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: appeler System.Collections.Generic.List.get_Current 
 IL_0018: stloc.1 
 IL_0019: ldloca.s 02 
 IL_001B: appeler System.Collections.Generic.List.MoveNext 
 IL_0020: stloc.3 
 IL_0021: ldloc.3 
 IL_0022: brtrue.s IL_0011 
 IL_0024: congé.s IL_0035 
 IL_0026: ldloca .s 02 
 IL_0028: contraint. System.Collections.Generic.List.Enumerator 
 IL_002E: callvirt System.IDisposable.Dispose 
 IL_0033: nop 
 IL_0034: enfin 

Ainsi, l'implémentation d'IDisposable ne pose aucun problème de performances et l'aspect (regrettable) mutable de l'énumérateur est préservé si la méthode Dispose fait réellement quelque chose!

2: double et float sont des exceptions à cette règle où les valeurs NaN ne sont pas considérées comme égales.

161
ShuggyCoUk

Dans certains cas, il peut être bon pour une structure d'implémenter une interface (si cela n'a jamais été utile, il est douteux que les créateurs de .net l'auraient prévu). Si une structure implémente une interface en lecture seule comme IEquatable<T>, Le stockage de la structure dans un emplacement de stockage (variable, paramètre, élément de tableau, etc.) de type IEquatable<T> Nécessitera qu'elle soit encadrée ( chaque type de structure définit en fait deux types de choses: un type d'emplacement de stockage qui se comporte comme un type de valeur et un type d'objet de tas qui se comporte comme un type de classe; le premier est implicitement convertible en le second - "boxing" - et le le second peut être converti en premier via un cast explicite - "unboxing"). Il est possible d'exploiter l'implémentation d'une interface d'une structure sans boxe, cependant, en utilisant ce qu'on appelle des génériques contraints.

Par exemple, si l'on avait une méthode CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, une telle méthode pourrait appeler thing1.Compare(thing2) sans avoir à encadrer thing1 Ou thing2. Si thing1 Se trouve être, par exemple, un Int32, Le run-time sait que lorsqu'il génère le code pour CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Comme il connaîtra le type exact de la chose qui héberge la méthode et de la chose qui est passée en tant que paramètre, il n'aura pas à les encadrer.

Le plus gros problème avec les structures qui implémentent des interfaces est qu'une structure qui est stockée dans un emplacement de type interface, Object ou ValueType (par opposition à un emplacement de son propre type) se comportera comme un objet de classe. Pour les interfaces en lecture seule, ce n'est généralement pas un problème, mais pour une interface en mutation comme IEnumerator<T>, Cela peut produire une sémantique étrange.

Considérez, par exemple, le code suivant:

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'instruction marquée # 1 amorcera enumerator1 Pour lire le premier élément. L'état de cet énumérateur sera copié dans enumerator2. L'instruction marquée # 2 avancera cette copie pour lire le deuxième élément, mais n'affectera pas enumerator1. L'état de ce deuxième énumérateur sera ensuite copié dans enumerator3, Qui sera avancé par l'instruction marquée # 3. Ensuite, parce que enumerator3 Et enumerator4 Sont tous deux des types de référence, un RÉFÉRENCE à enumerator3 Sera alors copié dans enumerator4, donc la déclaration marquée avancera effectivement les deuxenumerator3 et enumerator4.

Certaines personnes essaient de prétendre que les types de valeur et les types de référence sont les deux types de Object, mais ce n'est pas vraiment vrai. Les types de valeur réelle sont convertibles en Object, mais n'en sont pas des instances. Une instance de List<String>.Enumerator Qui est stockée dans un emplacement de ce type est un type de valeur et se comporte comme un type de valeur; le copier à un emplacement de type IEnumerator<String> le convertira en un type de référence, et il se comportera comme un type de référence. Ce dernier est une sorte de Object, mais pas le premier.

BTW, quelques notes supplémentaires: (1) En général, les types de classe mutables devraient avoir leurs méthodes Equals pour tester l'égalité de référence, mais il n'y a aucun moyen décent pour une structure encadrée de le faire; (2) malgré son nom, ValueType est un type de classe, pas un type de valeur; tous les types dérivés de System.Enum sont des types valeur, tout comme tous les types dérivés de ValueType à l'exception de System.Enum, mais les deux ValueType et System.Enum Sont des types de classe.

8
supercat

(Eh bien, je n'ai rien de majeur à ajouter, mais je n'ai pas encore de prouesses d'édition, alors voilà.)
Parfaitement sûr. Rien d'illégal avec l'implémentation d'interfaces sur des structures. Cependant, vous devez vous demander pourquoi vous souhaitez le faire.

Cependant obtenir une référence d'interface à une structure le BOXERA . Donc, pénalité de performance et ainsi de suite.

Le seul scénario valable auquel je peux penser en ce moment est illustré dans mon article ici . Lorsque vous souhaitez modifier l'état d'une structure stockée dans une collection, vous devez le faire via une interface supplémentaire exposée sur la structure.

3
Gishu

Les structures sont implémentées en tant que types de valeur et les classes sont des types de référence. Si vous avez une variable de type Foo, et que vous y stockez une instance de Fubar, elle la "mettra en boîte" dans un type de référence, annulant ainsi l'avantage d'utiliser une structure en premier lieu.

La seule raison pour laquelle je vois utiliser une structure au lieu d'une classe est parce que ce sera un type de valeur et non un type de référence, mais la structure ne peut pas hériter d'une classe. Si la structure hérite d'une interface et que vous passez des interfaces, vous perdez cette nature de type valeur de la structure. Autant en faire une classe si vous avez besoin d'interfaces.

3
dotnetengineer

Je pense que le problème est qu'il provoque la boxe car les structures sont des types de valeur, donc il y a une légère pénalité de performance.

Ce lien suggère qu'il pourrait y avoir d'autres problèmes avec cela ...

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

1
Simon Keep

Il n'y a aucune conséquence à une structure implémentant une interface. Par exemple, les structures système intégrées implémentent des interfaces comme IComparable et IFormattable.

0
Joseph Daigle

Il y a très peu de raisons pour qu'un type de valeur implémente une interface. Comme vous ne pouvez pas sous-classer un type de valeur, vous pouvez toujours le désigner comme son type concret.

À moins bien sûr que vous ayez plusieurs structures implémentant toutes la même interface, cela pourrait être marginalement utile alors, mais à ce stade, je recommanderais d'utiliser une classe et de le faire correctement.

Bien sûr, en implémentant une interface, vous encadrez la structure, donc elle se trouve maintenant sur le tas, et vous ne pourrez plus la transmettre par valeur ... Cela renforce vraiment mon opinion que vous devriez simplement utiliser une classe dans cette situation.

0
FlySwat