105 votes

Est-ce sécuritaire pour les structures d'implémenter des interfaces?

Je semble me souvenir avoir lu quelque chose sur le fait qu'il est mauvais pour les structures d'implémenter des interfaces dans le 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?

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

199voto

ShuggyCoUk Points 24204

Car personne d'autre ne explicitement prévu cette réponse, je vais ajouter ce qui suit:

La mise en œuvre d'une interface sur une structure n'a pas de conséquences négatives que ce soit.

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

  • à l'aide de l'objet résultant de verrouillage (une extrêmement mauvaise idée de toute façon)
  • à l'aide de la référence à l'égalité sémantique et en espérant que cela fonctionne pour les deux en boîte des valeurs de la même structure.

Ces deux serait peu probable, au lieu de cela, vous êtes susceptible de faire une des actions suivantes:

Les génériques

Peut-être que beaucoup des raisons raisonnables pour les structures de mise en œuvre des interfaces est de sorte qu'ils peuvent être utilisés à l'intérieur d'un générique contexte avec des contraintes. Dans ce mode 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. Permettre l'utilisation de la structure en tant que paramètre de type
    • tant qu'aucune autre contrainte comme new() ou class est utilisé.
  2. Permettre à l'évitement de la boxe sur les structures utilisées de cette façon.

Alors ce.une n'est PAS une référence d'interface ainsi, il ne provoque pas une boîte de ce qui est mis en elle. Plus loin, quand le compilateur c# compile les classes génériques et les besoins pour insérer les invocations de l'instance des méthodes définies dans les instances du paramètre de Type T, il peut utiliser la contrainte opcode:

Si thisType est un type de valeur et thisType implémente la méthode, puis ptr est passé non modifiée comme " ce " pointeur à l'appel de la méthode d'instruction, pour la mise en œuvre de la méthode par thisType.

Cela évite la boxe et depuis le type de valeur est la mise en œuvre de l'interface doit implémenter la méthode, donc pas de boxe aura lieu. Dans l'exemple ci-dessus l' Equals() invocation est fait avec pas de case sur cette.un1.

Faible frottement de l'API

La plupart des structures devrait avoir primitive comme sémantique où bit à bit des valeurs identiques sont considérés comme égauxà 2. Le moteur d'exécution de l'approvisionnement de tels comportements dans l'implicite Equals() , mais cela peut être long. Aussi l'implicite de l'égalité est pas exposé comme une œuvre d' IEquatable<T> et empêche ainsi les structs être facilement utilisés comme clés pour les Dictionnaires, à moins qu'ils explicitement l'appliquer eux-mêmes. Par conséquent, il est commun pour beaucoup de public struct types de déclarer qu'ils mettent en IEquatable<T> (où T est eux-même) pour rendre cela plus facile et plus performant ainsi que la cohérence avec le comportement de nombreux types de valeur dans le CLR BCL.

Toutes les primitives de la BCL mettre en œuvre au minimum:

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

Nombreux aussi mettre en oeuvre IFormattable, plus de nombreux définies par le Système de valeur de type DateTime, plage de Temps, et Guid de mettre en œuvre la plupart ou la totalité de ces derniers aussi bien. Si vous êtes à la mise en œuvre d'une de même très utile " type comme un nombre complexe struct ou certains de largeur fixe textuelle des valeurs de mise en œuvre de plusieurs de ces interfaces communes (correctement) feront de votre struct plus utile et utilisable.

Exclusions

Évidemment, si l'interface implique fortement la mutabilité (comme ICollection) puis de la mettre en œuvre est une mauvaise idée car cela signifierait tat vous faites soit la structure mutable (conduisant à des sortes d'erreurs décrites ci-dessus où les modifications se produisent sur la boîte de la valeur plutôt que l'original), ou vous induire les utilisateurs en erreur en ignorant les implications des méthodes comme l' Add() ou de lever des exceptions.

De nombreuses interfaces n'implique PAS la mutabilité (comme IFormattable) et servir le idiomatiques manière d'exposer un certain nombre de fonctionnalités, de manière uniforme. Souvent l'utilisateur de la structure n'aura pas à s'inquiéter de boxe aériennes pour un tel comportement.

Résumé

Quand fait raisonnablement, sur immuable types de valeur, la mise en œuvre de l'utilité des interfaces est une bonne idée


Notes:

1: Notez que le compilateur peut utiliser ce moment de l'invocation de méthodes virtuelles sur des variables qui sont connus pour être spécifique d'un type struct mais dans lequel il est nécessaire d'invoquer une méthode virtuelle. Par exemple:

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

L'agent recenseur renvoyé par la Liste est une structure, d'une optimisation pour éviter une allocation lors de l'énumération de la liste (Avec d'intéressantes conséquences). Cependant, la sémantique de foreach préciser que si l'agent recenseur implémente IDisposable alors Dispose() sera appelée une fois l'itération est terminée. Bien évidemment cela se fait par le biais d'une boîte d'appel permettrait d'éliminer toutes les prestations de l'agent recenseur être un struct (en fait, il serait pire). Pire, si disposer d'appel modifie l'état de l'agent recenseur, d'une certaine façon alors ce qui allait se passer sur la boîte d'instance et de nombreux bogues subtils pourrait être introduit dans les cas complexes. Donc la IL émise dans ce genre de situation est la suivante:

IL_0001: newobj Système.Les Collections.Génériques.Liste..ctor
IL_0006: stloc.0 
IL_0007: nop 
IL_0008: ldloc.0 
IL_0009: callvirt Système.Les Collections.Génériques.Liste.GetEnumerator
IL_000E: stloc.2 
IL_000F: br.s IL_0019
IL_0011: ldloca.s 02 
IL_0013: Système d'appel.Les Collections.Génériques.Liste.get_Current
IL_0018: stloc.1 
IL_0019: ldloca.s 02 
IL_001B: Système d'appel.Les Collections.Génériques.Liste.MoveNext
IL_0020: stloc.3 
IL_0021: ldloc.3 
IL_0022: brtrue.s IL_0011
IL_0024: quitter.s IL_0035
IL_0026: ldloca.s 02 
IL_0028: contraint. Système.Les Collections.Génériques.Liste.Énumérateur
IL_002E: callvirt Système.IDisposable.Disposer
IL_0033: nop 
IL_0034: endfinally 

Ainsi, la mise en œuvre de IDisposable ne cause pas de problèmes de performances et de la (regrettable) mutable aspect de l'agent recenseur est conservé si la méthode dispose de réellement faire quelque chose!

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

52voto

Scott Dorman Points 25000

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

Il est possible pour une structure à implémenter une interface, mais il ya des préoccupations qui viennent à propos de casting, de la mutabilité et de la performance. 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és pour des objets qui ont une valeur de type sémantique. Par la mise en œuvre d'une interface sur un struct, vous pouvez exécuter en boxe préoccupations que la structure est en fonte et-vient entre la structure et l'interface. Comme résultat de la boxe, les opérations de changement de l'état interne de la structure ne peut pas se comporter correctement.

8voto

supercat Points 25534

Dans certains cas, il peut être bon pour une structure à implémenter une interface (si il n'a jamais été utile, il est peu probable que les créateurs de .net aurait fourni). Si une structure met en œuvre une lecture seule interface comme IEquatable<T>, le stockage de la structure dans un emplacement de stockage (variable, paramètre, l'élément de tableau, etc.) de type IEquatable<T> devront être mis en boîte (chaque type struct définit en fait deux sortes de choses: un emplacement de stockage type qui se comporte comme un type de valeur et un tas de type objet qui se comporte comme un type de classe; la première est implicitement convertible dans le second--"boxe"--et le second peut être converti à la première cast explicite--"unboxing"). Il est possible d'exploiter une structure de mise en œuvre d'une interface sans boxe, cependant, en utilisant ce qu'on appelle des contraintes génériques.

Par exemple, si 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 à cocher thing1 ou thing2. Si thing1 se trouve être, par exemple, un Int32, l'exécution de savoir que lorsqu'il génère le code pour CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Car elle permettra de connaître le type exact de fois la chose d'hébergement de la méthode et de la chose qui est passée en tant que paramètre, il n'aura pas de zone à l'autre.

Le plus gros problème avec les structures qui mettent en œuvre des interfaces est une structure qui est stocké dans un emplacement de type d'interface, Objectou ValueType (plutôt qu'à l'emplacement de son propre type) va se comporter comme un objet de classe. Pour lire uniquement les interfaces ce n'est généralement pas un problème, mais pour une mutation de l'interface comme IEnumerator<T> il peut donner de drôles de sémantique.

Considérons, 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

Marquée de la déclaration n ° 1 sera le premier enumerator1 de lire le premier élément. L'état de l'agent recenseur sera copié enumerator2. Marquée de la déclaration n ° 2, avance que la copie de lire le deuxième élément, mais ne va pas affecter enumerator1. L'état de cette deuxième agent recenseur sera alors copié dans enumerator3, qui sera avancé par marquée de la déclaration n ° 3. Ensuite, parce qu' enumerator3 et enumerator4 sont les deux types de référence, une RÉFÉRENCE à l' enumerator3 sera alors copié dans enumerator4, ainsi marquée déclaration d'avancer efficacement à la fois enumerator3 et enumerator4.

Certaines personnes essaient de prétendre que les types valeur et les types référence sont les deux types d' Object, mais c'est pas vraiment vrai. Valeur réelle types sont convertibles Object, mais ne sont pas des instances de celui-ci. Une instance de List<String>.Enumerator qui est stocké dans un emplacement de ce type est une valeur de type et se comporte comme un type de valeur; de le copier à un emplacement de type IEnumerator<String> va le convertir en un type de référence, et il va se comporter comme un type de référence. Ce dernier est une sorte d' Object, mais la première ne l'est pas.

BTW, un couple plus de notes: (1) En général, mutable types de classe devraient avoir leur Equals méthodes de test de référence, de l'égalité, mais il n'y a pas de manière décente pour un coffret struct à le faire; (2) en dépit de 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, comme le sont tous les types qui en découlent ValueType , à l'exception des System.Enum, mais les deux ValueType et System.Enum sont des types de classe.

3voto

Gishu Points 59012

(Bien eu rien de majeur à ajouter, mais n'ont pas d'édition des prouesses encore donc, ici, va..)
Parfaitement Sûr. Rien d'illégal à la mise en œuvre des interfaces des structures. Cependant, vous devriez vous demander pourquoi vous voulez le faire.

Cependant, l'obtention d'une référence d'interface pour une struct va BOÎTE . Donc, les performances et ainsi de suite.

La seule valide scénario qui, je pense dès maintenant est illustré dans mon post ici. Lorsque vous souhaitez modifier une structure de l'etat stockées dans une collection, vous auriez à le faire via une interface supplémentaire exposés sur les struct.

3voto

dotnetengineer Points 584

Les structures sont mises en œuvre comme la valeur des types et des classes sont des types référence. Si vous avez une variable de type Foo, et vous le stockage d'une instance de Fubar, il "Boîte" dans un type de référence, empêchant donc l'avantage de l'utilisation d'un struct dans la première place.

La seule raison que je vois à utiliser une structure plutôt qu'une classe est parce que ce sera un type de valeur et pas un type de référence, mais la structure ne peut pas hériter d'une classe. Si vous avez la struct hériter d'une interface, et que vous passez autour des interfaces, vous perdez un type de valeur de la nature de la structure. Pourrait tout aussi bien en faire une classe, si vous avez besoin d'interfaces.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X