1628 votes

Quand dois-je utiliser un struct plutôt qu'une classe en C# ?

Quand faut-il utiliser struct et non class en C# ? Mon modèle conceptuel est que les structs sont utilisés dans les cas où l'élément est simplement une collection de types de valeurs . Un moyen de les réunir logiquement en un tout cohérent.

Je suis tombé sur ces règles ici :

  • Une structure doit représenter un seul valeur.
  • Un struct doit avoir une mémoire inférieure à 16 octets.
  • Une structure ne doit pas être modifiée après création.

Ces règles fonctionnent-elles ? Que signifie sémantiquement une structure ?

0 votes

Je ne peux qu'être d'accord avec le premier point, les structs sont très utilisés dans la programmation des jeux par exemple.

305 votes

System.Drawing.Rectangle viole ces trois règles.

3 votes

Oui, en partie en tout cas. Je sais qu'il est utilisé pour des parties de jeux, comme NWN2 World Creator. Le C est toujours utilisé pour le noyau (moteur). XNA Game Studio, cherchez sur Google :)

685voto

IAbstract Points 9384

La source mentionnée par l'OP a une certaine crédibilité ... mais qu'en est-il de Microsoft - quelle est sa position sur l'utilisation des structures ? J'ai cherché un peu plus Apprendre de Microsoft et voici ce que j'ai trouvé :

Envisagez de définir une structure au lieu d'une classe si des instances de l'élément type sont petites et généralement de courte durée, ou si elles sont couramment intégrées dans d'autres objets.

Ne définissez pas une structure à moins que le type ne présente toutes les caractéristiques suivantes :

  1. Il représente logiquement une seule valeur, comme les types primitifs (entier, double, etc.).
  2. Il a une taille d'instance inférieure à 16 octets.
  3. Il est immuable.
  4. Elle ne devra pas être mise en boîte fréquemment.

Microsoft viole constamment ces règles

Ok, #2 et #3 en tout cas. Notre cher dictionnaire a 2 structs internes :

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

* Source de référence

La source 'JonnyCantCode.com' a obtenu 3 sur 4 - ce qui est tout à fait pardonnable puisque le numéro 4 ne serait probablement pas un problème. Si vous vous retrouvez à mettre en boîte une structure, repensez votre architecture.

Voyons pourquoi Microsoft utilise ces structs :

  1. Chaque structure, Entry y Enumerator représentent des valeurs uniques.
  2. Vitesse
  3. Entry n'est jamais passé comme paramètre en dehors de la classe Dictionary. Un examen plus approfondi montre que pour satisfaire à l'implémentation de IEnumerable, Dictionary utilise la classe Enumerator qu'il copie à chaque fois qu'un énumérateur est demandé ... c'est logique.
  4. Interne à la classe Dictionnaire. Enumerator est public car le Dictionary est énumérable et doit avoir une accessibilité égale à l'implémentation de l'interface IEnumerator - par exemple IEnumerator getter.

Mise à jour - De plus, il faut savoir que lorsqu'un struct implémente une interface - comme le fait Enumerator - et qu'il est coulé vers ce type implémenté, le struct devient un type de référence et est déplacé vers le tas. Interne à la classe Dictionary, Enumerator est est toujours un type de valeur. Cependant, dès qu'une méthode appelle GetEnumerator() un type de référence IEnumerator est renvoyé.

Ce que nous ne voyons pas ici, c'est une tentative ou une preuve de l'obligation de garder les structures immuables ou de maintenir une taille d'instance de seulement 16 octets ou moins :

  1. Rien dans les structs ci-dessus n'est déclaré readonly - no immuable
  2. La taille de ces structures peut être bien supérieure à 16 octets.
  3. Entry a une durée de vie indéterminée (de Add() à Remove() , Clear() ou la collecte des déchets) ;

Et... 4. Les deux structs stockent TKey et TValue, qui, nous le savons tous, sont tout à fait capables d'être des types de référence (information supplémentaire).

Malgré les clés hachées, les dictionnaires sont rapides en partie parce que l'instanciation d'une structure est plus rapide qu'un type de référence. Ici, j'ai un Dictionary<int, int> qui stocke 300 000 entiers aléatoires avec des clés incrémentées séquentiellement.

Capacité : 312874
MemSize : 2660827 octets
Redimensionnement terminé : 5ms
Temps total de remplissage : 889ms

Capacité : nombre d'éléments disponibles avant que le tableau interne ne doive être redimensionné.

MemSize Le dictionnaire est déterminé en sérialisant le dictionnaire dans un MemoryStream et en obtenant une longueur d'octet (suffisamment précise pour nos besoins).

Redimensionnement terminé le temps qu'il faut pour redimensionner le tableau interne de 150 862 éléments à 31 2874 éléments. Quand on sait que chaque élément est copié séquentiellement via Array.CopyTo() ce n'est pas trop mal.

Temps total de remplissage Il est vrai que les chiffres sont biaisés en raison de l'exploitation forestière et d'une OnResize que j'ai ajouté à la source ; cependant, il est toujours impressionnant de remplir 300k entiers tout en redimensionnant 15 fois pendant l'opération. Juste par curiosité, quel serait le temps total de remplissage si je connaissais déjà la capacité ? 13ms

Donc, maintenant, et si Entry étaient une classe ? Ces époques ou métriques seraient-elles vraiment si différentes ?

Capacité : 312874
MemSize : 2660827 octets
Redimensionnement terminé : 26ms
Temps total de remplissage : 964ms

La grande différence réside évidemment dans le redimensionnement. Une différence si le dictionnaire est initialisé avec la capacité ? Pas suffisamment pour s'en préoccuper... 12ms .

Ce qui se passe, c'est que Entry est une structure, elle ne nécessite pas d'initialisation comme un type de référence. C'est à la fois la beauté et le fléau du type valeur. Afin d'utiliser Entry comme type de référence, j'ai dû insérer le code suivant :

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  

La raison pour laquelle j'ai dû initialiser chaque élément de tableau de Entry comme type de référence peut être trouvé à l'adresse suivante MSDN : Conception de la structure . En bref :

Ne pas fournir un constructeur par défaut pour une structure.

Si une structure définit un constructeur par défaut, lorsque des tableaux du type structure, le moteur d'exécution du langage courant exécute automatiquement exécute automatiquement le constructeur par défaut sur chaque élément du tableau.

Certains compilateurs, tels que le compilateur C#, ne permettent pas aux structures de d'avoir des constructeurs par défaut.

C'est en fait assez simple et nous allons emprunter à Asimov Les trois lois de la robotique :

  1. La structure doit pouvoir être utilisée en toute sécurité
  2. La structure doit remplir sa fonction de manière efficace, à moins que cela ne soit contraire à la règle n° 1.
  3. La structure doit rester intacte pendant son utilisation, à moins que sa destruction ne soit nécessaire pour satisfaire à la règle n° 1.

... qu'est-ce qu'on en retient En bref, soyez responsable de l'utilisation des types de valeurs. Ils sont rapides et efficaces, mais ont la capacité de provoquer de nombreux comportements inattendus s'ils ne sont pas correctement gérés (c'est-à-dire des copies non intentionnelles).

0 votes

Je ne pense pas que le runtime appelle un constructeur de structure lors de la création d'un tableau de structures. La raison pour laquelle la plupart des langages interdisent aux structures d'avoir un constructeur par défaut n'est pas parce que les appeler rendrait la construction de tableaux inefficace, mais plutôt parce qu'il serait déroutant que les constructeurs de structures soient appelés dans certains cas et pas dans d'autres.

11 votes

En ce qui concerne les règles de Microsoft, la règle sur l'immutabilité semble être conçue pour décourager l'utilisation des types de valeur de telle sorte que leur comportement diffère de celui des types de référence, malgré le fait que la sémantique des valeurs modifiables par morceaux peut être utile . Si le fait qu'un type soit mutable par morceaux le rend plus facile à utiliser, et si les emplacements de stockage du type doivent être logiquement détachés les uns des autres, le type doit être une structure "mutable".

26 votes

N'oubliez pas que readonly != immutable.

194voto

dsimcha Points 32831

Quand tu veux :

  1. n'ont pas besoin de polymorphisme,
  2. veulent la sémantique des valeurs, et
  3. veulent éviter l'allocation du tas et la surcharge de la collecte des déchets qui y est associée.

La mise en garde, cependant, est que les structures (arbitrairement grandes) sont plus coûteuses à transmettre que les références de classe (généralement un mot machine), donc les classes pourraient être plus rapides en pratique.

1 votes

Ce n'est qu'un seul "avertissement". Il faut également envisager de "lever" les types de valeurs et les cas tels que (Guid)null (il est possible de transformer un null en un type de référence), entre autres choses.

1 votes

Plus coûteux qu'en C/C++ ? En C++, la méthode recommandée est de passer les objets par valeur.

0 votes

@IonTodirel Ce n'était pas pour des raisons de sécurité de la mémoire, plutôt que de performance ? C'est toujours un compromis, mais passer 32 B par pile sera toujours(TM) plus lent que passer une référence de 4 B par registre. Cependant Lorsque vous passez une référence à un objet, vous passez toujours par valeur, même si vous passez une référence (vous passez la valeur de la référence, pas une référence à la référence, en fait). Ce n'est pas une valeur sémantique mais il s'agit techniquement de "pass-by-value".

177voto

ILoveFortran Points 1670

Je ne suis pas d'accord avec les règles données dans le message original. Voici mes règles :

  1. Les structs sont utilisés pour des raisons de performance lorsqu'ils sont stockés dans des tableaux. (voir aussi Quand les structs sont-ils la solution ? )

  2. Vous en avez besoin dans le code qui transmet des données structurées de/vers C/C++.

  3. N'utilisez les structs que si vous en avez besoin :

    • Ils se comportent différemment des "objets normaux" ( types de référence ) lors de l'affectation et lorsqu'ils sont passés comme arguments, ce qui peut conduire à un comportement inattendu ; Ceci est particulièrement dangereux si la personne qui regarde le code ne sait pas code ne sait pas qu'elle a affaire à un struct.
    • Ils ne peuvent pas être hérités.
    • Passer des structs comme arguments est plus coûteux que les classes.

4 votes

+1 Oui, je suis tout à fait d'accord sur le point 1 (c'est une énorme pour traiter des choses comme les images, etc.) et pour avoir souligné qu'elles sont différents des "objets normaux" et il y a savoir comment le savoir sauf par les connaissances existantes ou en examinant le type lui-même. En outre, vous ne pouvez pas convertir une valeur nulle en un type struct :-) Il s'agit en fait d'un cas où je presque J'aimerais qu'il y ait une sorte de "hongrois" pour les types de valeurs non-Core ou un mot-clé "struct" obligatoire à l'emplacement de la déclaration des variables.

0 votes

@pst : Il est vrai que l'on doit savoir que quelque chose est un struct pour savoir comment il se comportera, mais si quelque chose est un struct avec des champs exposés, c'est tout ce qu'il faut savoir. Si un objet expose une propriété de type structure à champs exposés, et si du code lit cette structure dans une variable et la modifie, on peut prédire sans risque que cette action n'affectera pas l'objet dont la propriété a été lue, à moins ou jusqu'à ce que la structure soit réécrite. En revanche, si la propriété était un type de classe mutable, sa lecture et sa modification pourraient mettre à jour l'objet sous-jacent comme prévu, mais...

0 votes

...il peut aussi finir par ne rien changer, ou bien changer ou corrompre des objets que l'on n'avait pas l'intention de changer. Avoir un code dont la sémantique dit "changez cette variable autant que vous voulez ; les changements ne feront rien tant que vous ne les aurez pas explicitement stockés quelque part" semble plus clair que d'avoir un code qui dit "Vous obtenez une référence à un objet, qui peut être partagé avec un nombre quelconque d'autres références, ou peut ne pas être partagé du tout ; vous devrez trouver qui d'autre peut avoir des références à cet objet pour savoir ce qui se passera si vous le changez".

98voto

JoshBerke Points 34238

Utilisez une structure lorsque vous voulez une sémantique de valeur par opposition à une sémantique de référence.

Editar

Je ne sais pas pourquoi les gens ont voté contre, mais c'est un point valable, et il a été fait avant l'op a clarifié sa question, et c'est la raison de base la plus fondamentale pour une structure.

Si vous avez besoin d'une sémantique de référence, vous avez besoin d'une classe et non d'une structure.

25 votes

Tout le monde le sait. On dirait qu'il cherche plus qu'une réponse du type "struct est un type de valeur".

27 votes

C'est le cas le plus élémentaire et il faut le dire à tous ceux qui lisent ce post et ne le savent pas.

5 votes

Non pas que cette réponse ne soit pas vraie ; elle l'est évidemment. Ce n'est pas vraiment la question.

67voto

Marc Gravell Points 482669

En plus de la réponse "c'est une valeur", un scénario spécifique pour l'utilisation des structs est lorsque vous connaître Vous avez un ensemble de données qui pose des problèmes de collecte des déchets et vous avez beaucoup d'objets. Par exemple, une grande liste/un grand tableau d'instances de personnes. La métaphore naturelle ici est une classe, mais si vous avez un grand nombre d'instances de personnes à longue durée de vie, elles peuvent finir par encombrer la GEN-2 et provoquer des blocages de GC. Si le scénario le justifie, une approche potentielle consiste à utiliser un tableau (et non une liste) d'instances de Person structures c'est-à-dire Person[] . Maintenant, au lieu d'avoir des millions d'objets dans GEN-2, vous avez un seul morceau sur le LOH (je suppose qu'il n'y a pas de chaînes etc ici - c'est-à-dire une valeur pure sans aucune référence). Cela a un impact GC très faible.

Il est difficile de travailler avec ces données, car elles sont probablement surdimensionnées pour une structure, et vous ne voulez pas copier les grosses valeurs en permanence. Cependant, accéder directement à ces données dans un tableau ne copie pas la structure - c'est en place (contrairement à un indexeur de liste, qui copie). Cela signifie beaucoup de travail avec les index :

int index = ...
int id = peopleArray[index].Id;

Notez que le fait de garder les valeurs elles-mêmes immuables sera utile ici. Pour une logique plus complexe, utilisez une méthode avec un paramètre by-ref :

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

Encore une fois, il s'agit d'une valeur en place - nous n'avons pas copié la valeur.

Dans des scénarios très spécifiques, cette tactique peut s'avérer très efficace ; cependant, il s'agit d'un scénario assez avancé qui ne doit être tenté que si vous savez ce que vous faites et pourquoi. Le défaut ici serait une classe.

0 votes

+1 Réponse intéressante. Seriez-vous prêt à partager des anecdotes réelles sur l'utilisation d'une telle approche ?

0 votes

@Jordao dans sur mobile, mais chercher google pour : +gravell + "agression par GC"

2 votes

Merci beaucoup. Je l'ai trouvé ici .

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