Quand dois-je utiliser une structure au lieu d'une classe?

302

MSDN dit que vous devez utiliser des structures lorsque vous avez besoin d'objets légers. Y a-t-il d'autres scénarios où une structure est préférable à une classe?

Certaines personnes ont peut-être oublié que:

  1. les structures peuvent avoir des méthodes.
  2. les structures ne peuvent pas être héritées.

Je comprends les différences techniques entre les structures et les classes, je ne sais tout simplement pas quand utiliser une structure.

Esteban Araya
la source
Juste un rappel - ce que la plupart des gens ont tendance à oublier dans ce contexte, c'est qu'en C #, les structures peuvent également avoir des méthodes.
petr k.

Réponses:

295

MSDN a la réponse: choisir entre les classes et les structures .

Fondamentalement, cette page vous donne une liste de contrôle en 4 points et dit d'utiliser une classe à moins que votre type ne réponde à tous les critères.

Ne définissez une structure que si le type possède toutes les caractéristiques suivantes:

  • Il représente logiquement une valeur unique, similaire aux types primitifs (entier, double, etc.).
  • Il a une taille d'instance inférieure à 16 octets.
  • C'est immuable.
  • Il n'aura pas à être emballé fréquemment.
OwenP
la source
1
Peut-être que je manque quelque chose d'évident, mais je ne comprends pas très bien le raisonnement derrière la partie "immuable". Pourquoi est-ce nécessaire? Quelqu'un pourrait-il l'expliquer?
Tamas Czinege
3
Ils l'ont probablement recommandé parce que si la structure est immuable, alors il importe peu qu'elle ait une sémantique de valeur plutôt qu'une sémantique de référence. La distinction n'a d'importance que si vous mutez l'objet / la structure après avoir fait une copie.
Stephen C. Steel
@DrJokepu: Dans certaines situations, le système crée une copie temporaire d'une structure, puis autorise le passage de cette copie par référence au code qui la modifie; étant donné que la copie temporaire sera abandonnée, la modification sera perdue. Ce problème est particulièrement grave si une structure possède des méthodes qui la mutent. Je suis catégoriquement en désaccord avec la notion que la mutabilité est une raison de faire quelque chose d'une classe, car - malgré certaines lacunes dans c # et vb.net, les structures mutables fournissent une sémantique utile qui ne peut être obtenue autrement; il n'y a aucune raison sémantique de préférer une structure immuable à une classe.
supercat
4
@Chuu: Lors de la conception du compilateur JIT, Microsoft a décidé d'optimiser le code pour copier des structures de 16 octets ou moins; cela signifie que la copie d'une structure de 17 octets peut être considérablement plus lente que la copie d'une structure de 16 octets. Je ne vois aucune raison particulière de s'attendre à ce que Microsoft étende de telles optimisations à des structures plus grandes, mais il est important de noter que même si les structures à 17 octets peuvent être plus lentes à copier que les structures à 16 octets, il existe de nombreux cas où les grandes structures peuvent être plus efficaces que les objets de grande classe et où l'avantage relatif des structures croît avec la taille de la structure.
supercat
3
@Chuu: L'application des mêmes modèles d'utilisation à de grandes structures comme on le ferait aux classes est susceptible d'entraîner un code inefficace, mais la bonne solution est souvent de ne pas remplacer les structures par des classes, mais plutôt d'utiliser les structures plus efficacement; plus particulièrement, il faut éviter de passer ou de renvoyer des structures par valeur. Passez-les comme refparamètres chaque fois qu'il est raisonnable de le faire. Passer une structure avec 4 000 champs comme paramètre ref à une méthode qui en change une sera moins cher que de passer une structure avec 4 champs par valeur à une méthode qui retourne une version modifiée.
supercat
53

Je suis surpris de n'avoir lu aucune des réponses précédentes, que je considère comme l'aspect le plus crucial:

J'utilise des structures lorsque je veux un type sans identité. Par exemple un point 3D:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Si vous avez deux instances de cette structure, vous ne vous souciez pas s'il s'agit d'une seule donnée en mémoire ou de deux. Vous vous souciez juste de la valeur (s) qu'ils détiennent.

Andrei Rînea
la source
4
Bit off topic, mais pourquoi lancez-vous une ArgumentException lorsque obj n'est pas ThreeDimensionalPoint? Ne devriez-vous pas simplement retourner faux dans ce cas?
Svish
4
C'est vrai, j'étais trop impatient. return falseest ce qui aurait dû être là, corrigeant maintenant.
Andrei Rînea
Une raison intéressante d'utiliser une structure. J'ai créé des classes avec GetHashCode et Equals définies similaires à ce que vous montrez ici, mais j'ai toujours dû faire attention à ne pas muter ces instances si je les utilisais comme clés de dictionnaire. Cela aurait probablement été plus sûr si je les avais définis comme des structures. (Parce qu'alors la clé serait une copie des champs au moment où la structure deviendrait une clé de dictionnaire , donc la clé resterait inchangée si je changeais plus tard l'original.)
ToolmakerSteve
Dans votre exemple, c'est ok parce que vous n'avez que 12 octets mais gardez à l'esprit que si vous avez beaucoup de champs dans cette structure qui dépassent 16 octets, vous devez envisager d'utiliser une classe et de remplacer les méthodes GetHashCode et Equals.
Daniel Botero Correa
Un type de valeur dans DDD ne signifie pas que vous devez nécessairement utiliser un type de valeur en C #
Darragh
26

Bill Wagner a un chapitre à ce sujet dans son livre "effective c #" ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Il conclut en utilisant le principe suivant:

  1. Est la responsabilité principale du type de stockage de données?
  2. Son interface publique est-elle entièrement définie par des propriétés qui accèdent ou modifient ses membres de données?
  3. Êtes-vous sûr que votre type n'aura jamais de sous-classes?
  4. Êtes-vous sûr que votre type ne sera jamais traité de manière polymorphe?

Si vous répondez «oui» aux 4 questions: utilisez une structure. Sinon, utilisez une classe.

Bart Gijssens
la source
1
alors ... les objets de transfert de données (DTO) devraient être des structures?
cruizer
1
Je dirais que oui, s'il répond aux 4 critères décrits ci-dessus. Pourquoi un objet de transfert de données devrait-il être traité d'une manière spécifique?
Bart Gijssens
2
@cruizer dépend de votre situation. Sur un projet, nous avions des champs d'audit communs dans nos DTO et avons donc écrit un DTO de base dont d'autres ont hérité.
Andrew Grothe
1
Tous sauf (2) semblent être d'excellents principes. Aurait besoin de voir son raisonnement pour savoir exactement ce qu'il entend par (2), et pourquoi.
ToolmakerSteve
1
@ToolmakerSteve: Vous devrez lire le livre pour cela. Ne pensez pas qu'il est juste de copier / coller de grandes parties d'un livre.
Bart Gijssens
15

Utilisez une structure lorsque vous voulez une sémantique de type valeur au lieu de type référence. Les structures sont copiées par valeur, alors faites attention!

Voir également les questions précédentes, par exemple

Quelle est la différence entre struct et class dans .NET?

Simon Steele
la source
12

J'utiliserais des structures lorsque:

  1. un objet est censé être en lecture seule (chaque fois que vous passez / assignez une structure, il est copié). Les objets en lecture seule sont parfaits en matière de traitement multithread car ils ne nécessitent pas de verrouillage dans la plupart des cas.

  2. un objet est petit et de courte durée. Dans un tel cas, il y a de fortes chances que l'objet soit alloué sur la pile, ce qui est beaucoup plus efficace que de le placer sur le tas managé. De plus la mémoire allouée par l'objet sera libérée dès qu'elle sortira de sa portée. En d'autres termes, c'est moins de travail pour Garbage Collector et la mémoire est utilisée plus efficacement.

Pawel Pabich
la source
10

Utilisez une classe si:

  • Son identité est importante. Les structures sont copiées implicitement lorsqu'elles sont passées par valeur dans une méthode.
  • Il aura une grande empreinte mémoire.
  • Ses champs nécessitent des initialiseurs.
  • Vous devez hériter d'une classe de base.
  • Vous avez besoin d'un comportement polymorphe;

Utilisez une structure si:

  • Il agira comme un type primitif (int, long, octet, etc.).
  • Il doit avoir une petite empreinte mémoire.
  • Vous appelez une méthode P / Invoke qui nécessite le passage d'une structure par valeur.
  • Vous devez réduire l'impact de la récupération de place sur les performances des applications.
  • Ses champs doivent être initialisés uniquement à leurs valeurs par défaut. Cette valeur serait zéro pour les types numériques, false pour les types booléens et null pour les types de référence.
    • Notez qu'en C # 6.0, les structures peuvent avoir un constructeur par défaut qui peut être utilisé pour initialiser les champs de la structure à des valeurs non par défaut.
  • Vous n'avez pas besoin d'hériter d'une classe de base (autre que ValueType, dont toutes les structures héritent).
  • Vous n'avez pas besoin d'un comportement polymorphe.
Yashwanth Chowdary Kata
la source
5

J'ai toujours utilisé une structure lorsque je voulais regrouper quelques valeurs pour renvoyer des choses à partir d'un appel de méthode, mais je n'aurai pas besoin de l'utiliser pour quoi que ce soit après avoir lu ces valeurs. Tout comme un moyen de garder les choses propres. J'ai tendance à voir les choses dans une structure comme "jetables" et les choses dans une classe comme plus utiles et "fonctionnelles"

Ryan Skarin
la source
4

Si une entité doit être immuable, la question de l'utilisation d'une structure ou d'une classe sera généralement une question de performance plutôt que de sémantique. Sur un système 32/64 bits, les références de classe nécessitent 4/8 octets à stocker, quelle que soit la quantité d'informations dans la classe; la copie d'une référence de classe nécessite la copie de 4/8 octets. D'autre part, tous distinctsl'instance de classe aura 8/16 octets de surcharge en plus des informations qu'elle contient et du coût de la mémoire des références. Supposons que l'on veuille un tableau de 500 entités, chacune contenant quatre entiers 32 bits. Si l'entité est un type de structure, le tableau nécessite 8 000 octets, que les 500 entités soient toutes identiques, toutes différentes ou quelque part entre les deux. Si l'entité est un type de classe, le tableau de 500 références prendra 4 000 octets. Si ces références pointent toutes vers des objets différents, les objets nécessiteraient 24 octets supplémentaires chacun (12 000 octets pour les 500), soit un total de 16 000 octets, soit le double du coût de stockage d'un type struct. D'un autre côté, du code a créé une instance d'objet, puis copié une référence aux 500 emplacements de tableau, le coût total serait de 24 octets pour cette instance et 4, 000 pour la baie - un total de 4 024 octets. Une économie importante. Peu de situations fonctionneraient aussi bien que la dernière, mais dans certains cas, il peut être possible de copier certaines références dans suffisamment d'emplacements de tableau pour que ce partage en vaille la peine.

Si l'entité est censée être mutable, la question de savoir s'il faut utiliser une classe ou une structure est à certains égards plus facile. Supposons que "Thing" soit une structure ou une classe qui a un champ entier appelé x, et on fait le code suivant:

  Chose t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Veut-on que cette dernière instruction affecte t1.x?

Si Thing est un type de classe, t1 et t2 seront équivalents, ce qui signifie que t1.x et t2.x seront également équivalents. Ainsi, la deuxième instruction affectera t1.x. Si Thing est un type de structure, t1 et t2 seront des instances différentes, ce qui signifie que t1.x et t2.x feront référence à des entiers différents. Ainsi, la deuxième instruction n'affectera pas t1.x.

Les structures mutables et les classes mutables ont des comportements fondamentalement différents, bien que .net ait quelques bizarreries dans sa gestion des mutations struct. Si l'on veut un comportement de type valeur (ce qui signifie que "t2 = t1" copiera les données de t1 vers t2 tout en laissant t1 et t2 comme des instances distinctes), et si l'on peut vivre avec les caprices de la gestion des types de valeur par .net, utilisez une structure. Si l'on veut une sémantique de type valeur mais que les caprices de .net entraîneraient une sémantique de type valeur cassée dans son application, utilisez une classe et marmonnez.

supercat
la source
3

De plus, les excellentes réponses ci-dessus:

Les structures sont des types de valeur.

Ils ne peuvent jamais être définis sur Nothing .

Définir une structure = Rien, mettra tous ses types de valeurs à leurs valeurs par défaut.

George Filippakos
la source
2

lorsque vous n'avez pas vraiment besoin de comportement, mais que vous avez besoin de plus de structure qu'un simple tableau ou dictionnaire.

Suivi Voici comment je pense aux structures en général. Je sais qu'ils peuvent avoir des méthodes, mais j'aime garder cette distinction mentale globale.

Jim Deville
la source
Pourquoi dites vous cela? Les structures peuvent avoir des méthodes.
Esteban Araya
2

Comme l'a dit @Simon, les structures fournissent une sémantique de "type valeur", donc si vous avez besoin d'un comportement similaire à un type de données intégré, utilisez une structure. Puisque les structures sont passées par copie, vous voulez vous assurer qu'elles sont de petite taille, environ 16 octets.

Scott Dorman
la source
2

C'est un sujet ancien, mais je voulais fournir un test de référence simple.

J'ai créé deux fichiers .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

et

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Exécuter le benchmark:

  • Créer 1 classe de test
  • Créer 1 TestStruct
  • Créer 100 TestClass
  • Créer 100 TestStruct
  • Créer 10000 TestClass
  • Créer 10000 TestStruct

Résultats:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |
Eduardas Šlutas
la source
1

Hmm ...

Je n'utiliserais pas la récupération de place comme argument pour / contre l'utilisation de structures vs classes. Le tas géré fonctionne un peu comme une pile - la création d'un objet le place simplement en haut du tas, ce qui est presque aussi rapide que l'allocation sur la pile. De plus, si un objet est de courte durée et ne survit pas à un cycle de GC, la désallocation est gratuite car le GC ne fonctionne qu'avec de la mémoire qui est toujours accessible. (Recherchez MSDN, il y a une série d'articles sur la gestion de la mémoire .NET, je suis juste trop paresseux pour aller les chercher).

La plupart du temps, j'utilise une structure, je finis par me donner des coups de pied parce que je découvre plus tard qu'avoir une sémantique de référence aurait rendu les choses un peu plus simples.

Quoi qu'il en soit, ces quatre points dans l'article MSDN publié ci-dessus semblent être une bonne ligne directrice.


la source
1
Si vous avez parfois besoin d'une sémantique de référence avec une structure, déclarez simplement class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, puis a MutableHolder<T>sera un objet avec une sémantique de classe mutable (cela fonctionne aussi bien s'il Ts'agit d'une structure ou d'un type de classe immuable).
supercat
1

Les structures sont sur la pile et non sur le tas, donc elles sont thread-safe et doivent être utilisées lors de l'implémentation du modèle d'objet de transfert, vous ne voulez jamais utiliser d'objets sur le tas, elles sont volatiles, vous voulez dans ce cas utiliser la pile d'appels, ceci est un cas de base pour utiliser une structure, je suis surpris par toutes les réponses ici,

Jack
la source
-3

Je pense que la meilleure réponse est simplement d'utiliser struct quand ce dont vous avez besoin est une collection de propriétés, classe quand c'est une collection de propriétés ET de comportements.

Lucian Gabriel Popescu
la source
les structures peuvent aussi avoir des méthodes
Nithin Chandran
bien sûr, mais si vous avez besoin de méthodes, les chances sont de 99%, vous utilisez incorrectement struct au lieu de classe. Les seules exceptions que j'ai trouvées quand il est correct d'avoir des méthodes dans struct sont les rappels
Lucian Gabriel Popescu