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

Je me souviens avoir lu quelque chose sur la façon dont il est mauvais pour les structs 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 at-il des conséquences imprévues?

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

Il y a plusieurs choses dans cette question …

Il est possible pour une structure d’implémenter une interface, mais il existe des problèmes liés au casting, à la mutabilité et aux performances. Voir ce post 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 objects dotés d’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 diffusé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.

Étant donné que personne d’autre n’a explicitement fourni cette réponse, j’appendai 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 performance, sauf si vous êtes:

  • utiliser l’object résultant à des fins de locking (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.

Les deux seraient peu probables, au lieu de cela, vous faites probablement l’une des actions suivantes:

Génériques

Peut-être que de nombreuses raisons raisonnables expliquent l’implémentation d’interfaces structurelles afin qu’elles puissent être utilisées dans un contexte générique avec des contraintes . Lorsqu’elle est utilisée de cette manière, la variable comme ceci:

 class Foo : IEquatable> where T : IEquatable { private readonly T a; public bool Equals(Foo other) { return this.a.Equals(other.a); } } 
  1. Activer l’utilisation de la structure en tant que paramètre de type
    • tant qu’aucune autre contrainte comme new() ou class n’est utilisée.
  2. Permettre d’éviter la boxe sur les structures utilisées de cette manière.

Alors ceci.a n’est PAS une référence d’interface et ne provoque donc pas de boîte contenant tout ce qui y est placé. De plus, lorsque le compilateur c # comstack 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, il peut utiliser l’opcode contraint :

Si thisType est un type valeur et que thisType implémente la méthode, alors ptr est transmis sans modification en tant que pointeur ‘this’ à une instruction de méthode d’appel, pour l’implémentation de la méthode par thisType.

Cela évite la boxe et comme le type de valeur qui implémente l’interface doit implémenter la méthode, aucune boxe ne se produira. Dans l’exemple ci-dessus, l’invocation Equals() est effectuée sans boîte sur ce point. 1 .

API à faible friction

La plupart des structures doivent avoir une sémantique de type primitif où les valeurs identiques au niveau du bit sont considérées égales à 2 . Le runtime fournira un tel comportement dans Equals() implicite mais cela peut être lent. En outre, cette égalité implicite n’est pas exposée en tant IEquatable d’ IEquatable et empêche donc l’utilisation des structures comme clés pour les dictionnaires, à moins qu’elles ne les implémentent explicitement elles-mêmes. Il est donc fréquent que de nombreux types de structures publiques déclarent implémenter IEquatable (où T est eux-mêmes) pour rendre cela plus facile et plus performant, et cohérent avec le comportement de nombreux types de valeurs existants dans la BCL CLR.

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

  • IComparable
  • IConvertible
  • IComparable
  • IEquatable (Et donc IEquatable )

Beaucoup implémentent également IFormattable . En outre, de nombreux types de valeurs définis par le système, tels que DateTime, TimeSpan et Guid, implémentent également la plupart d’entre eux. Si vous implémentez un type similaire “largement utile” comme une structure de nombres complexe ou des valeurs textuelles de largeur fixe, la mise en œuvre de plusieurs de ces interfaces communes (correctement) rendra votre structure plus utile et utilisable.

Exclusions

Évidemment, si l’interface implique fortement la mutabilité (telle que ICollection ), alors la mettre en œuvre est une mauvaise idée car cela signifierait que vous avez soit rendu la structure mutable (menant aux types d’erreurs déjà décrites où les modifications 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 la mutabilité (comme IFormattable ) et servent de moyen idiomatique pour exposer certaines fonctionnalités de manière cohérente. Souvent, l’utilisateur de la structure ne se soucie d’aucune surcharge de boxe pour un tel comportement.

Résumé

Lorsque cela est fait judicieusement, sur des types de valeurs 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 connues pour être d’un type struct spécifique mais dans lesquelles il est nécessaire d’appeler une méthode virtuelle. Par exemple:

 List l = new List(); 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 Dispose() sera appelé une fois l’itération terminée. De toute évidence, le fait d’avoir cela à travers un appel en boîte éliminerait tout avantage que l’énumérateur soit une structure (en fait, ce serait pire). Pire encore, si dispos call modifie de quelque manière que ce soit l’état de l’énumérateur, cela se produit sur l’instance encadrée et de nombreux bogues subtils peuvent être introduits dans des cas complexes. Par conséquent, l’IL émise dans ce type de situation est la suivante:

 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: appelez System.Collections.Generic.List.get_Current
 IL_0018: stloc.1     
 IL_0019: ldloca.s 02 
 IL_001B: appelez 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: contraint.  System.Collections.Generic.List.Enumerator
 IL_002E: callvirt System.IDisposable.Dispose
 IL_0033: nop         
 IL_0034: à la fin  

Ainsi, l’implémentation d’IDisposable ne pose aucun problème de performance et l’aspect (regrettable) mutable de l’énumérateur est préservé si la méthode Dispose fait quoi que ce soit!

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

Dans certains cas, il peut être intéressant pour une structure d’implémenter une interface (si cela n’était jamais utile, il est douteux que les créateurs de .net l’aient prévu). Si une structure implémente une interface en lecture seule telle que IEquatable , le stockage de la structure dans un emplacement de stockage (variable, paramètre, élément de tableau, etc.) de type IEquatable nécessitera sa mise en boîte définit deux types d’éléments: un type d’emplacement de stockage qui se comporte comme un type de valeur et un type d’object de tas qui se comporte comme un type de classe, le premier pouvant être converti implicitement en second – “boxing” – au premier par cast explicite – “unboxing”). Il est possible d’exploiter l’implémentation d’une interface sans boxer, en utilisant ce qu’on appelle des génériques contraints.

Par exemple, si on avait une méthode CompareTwoThings(T thing1, T thing2) where T:IComparable , une telle méthode pourrait appeler thing1.Compare(thing2) sans avoir à boxer thing1 ou thing2 . Si thing1 se trouve être, par exemple, un Int32 , le thing1 exécution le saura quand il générera le code pour CompareTwoThings(Int32 thing1, Int32 thing2) . Comme il connaîtra le type exact de la chose hébergeant la méthode et de ce qui est passé en paramètre, il ne sera pas obligé de les cocher.

Le plus gros problème avec les structures implémentant 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 object de classe. Pour les interfaces en lecture seule, cela ne pose généralement pas de problème, mais pour une interface en mutation telle que IEnumerator elle peut générer une sémantique étrange.

Considérez, par exemple, le code suivant:

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

L’instruction marquée # 1 amorcera l’ enumerator1 pour lire le premier élément. L’état de cet énumérateur sera copié dans enumerator2 . L’instruction marquée n ° 2 fera avancer cette copie pour lire le second élément, mais n’affectera pas l’ enumerator1 . L’état de ce deuxième énumérateur sera alors copié dans l’ enumerator3 , qui sera avancé par la déclaration n ° 3. Ensuite, étant donné que enumerator3 et enumerator4 sont tous deux des types de référence, une REFERENCE à enumerator3 sera alors copiée dans enumerator4 , si bien qu’une instruction marquée fera progresser à la fois l’ enumerator3 et l’ enumerator4 .

Certaines personnes essaient de prétendre que les types de valeur et les types de référence sont les deux types d’ Object , mais ce n’est pas vraiment vrai. Les types de valeurs réelles sont convertibles en Object , mais ne le sont pas. Une instance de List.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 dans un emplacement de type IEnumerator le convertira en un type de référence et se comportera comme un type de référence . Ce dernier est une sorte d’ Object , mais le premier ne l’est pas.

BTW, quelques autres notes: (1) En général, les types de classes mutables doivent avoir leurs méthodes de test d’égalité de référence, mais il n’y a pas de moyen décent pour une structure en boîte 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 de valeur, de même que 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.

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, celle-ci sera placée dans un type de référence, ce qui ira à l’encontre de l’avantage d’utiliser une structure en premier lieu.

La seule raison pour laquelle je vois l’utilisation d’une structure au lieu d’une classe est qu’elle 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 transmettez des interfaces, vous perdez cette nature de type valeur de la structure. Peut-être aussi bien en faire une classe si vous avez besoin d’interfaces.

(Eh bien, rien d’important à append mais n’a pas encore de prouesses d’édition, donc voilà…)
Parfaitement sûr Rien d’illégal avec l’implémentation d’interfaces sur des structures. Cependant, vous devriez vous demander pourquoi vous voulez le faire.

Cependant, l’ obtention d’une référence d’interface à une structure le BOXERA . Donc, la pénalité de performance et ainsi de suite.

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

Je pense que le problème est que cela provoque la boxe parce que 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 …

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

Il n’y a pas de conséquences pour une structure implémentant une interface. Par exemple, les structures système intégrées implémentent des interfaces telles que IComparable et IFormattable .

Il y a très peu de raison pour qu’un type de valeur implémente une interface. Comme vous ne pouvez pas sous-classer un type de valeur, vous pouvez toujours vous y référer en tant que type concret.

À moins bien sûr que vous ayez plusieurs structures qui implémentent toutes la même interface, cela peut être marginalement utile, mais à ce stade, je vous recommande d’utiliser une classe et de le faire correctement.

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

Les structures sont comme les classes qui vivent dans la stack. Je ne vois aucune raison pour laquelle ils devraient être “dangereux”.