Pourquoi les structures et les classes sont-elles des concepts distincts en C #?

44

Lors de la programmation en C #, je suis tombé sur une décision de conception de langage étrange que je ne comprenais tout simplement pas.

Donc, C # (et le CLR) a deux types de données d'agrégat: struct(type-valeur, stocké sur la pile, pas d'héritage) et class(type-référence, stocké sur le tas, a l'héritage).

Cette configuration sonne bien au début, mais vous tombez ensuite sur une méthode prenant un paramètre de type agrégé. Pour déterminer s’il s’agit bien d’un type de valeur ou d’un type de référence, vous devez trouver la déclaration de ce type. Cela peut parfois être très déroutant.

La solution généralement acceptée au problème semble être de déclarer tous les structs comme "immuables" (en définissant leurs champs sur readonly) pour éviter les erreurs possibles, ce qui limite structleur utilité.

C ++, par exemple, utilise un modèle beaucoup plus utilisable: il vous permet de créer une instance d'objet sur la pile ou sur le tas et de la transmettre par valeur ou par référence (ou par pointeur). J'entends toujours que C # a été inspiré par C ++ et je ne comprends pas pourquoi il n'a pas utilisé cette technique. La combinaison classet structdans une construction avec deux options d'allocation différentes (tas et la pile) et de les transmettre autour de valeurs ou (explicitement) comme références via refet outmots - clés semble comme une chose agréable.

La question qui se pose est la suivante: pourquoi les concepts séparés en C # et le CLR sont class-ils structdevenus des concepts distincts au lieu d’un type agrégé avec deux options d’allocation?

Mints97
la source
34
"La solution généralement admise au problème semble être de déclarer toutes les structures comme" immuables "... limitant leur utilité" De nombreuses personnes diraient que rendre une chose immuable le rend généralement plus utile quand elle n'est pas la cause d'un goulot d'étranglement. De plus, structs ne sont pas toujours stockés sur la pile; considérons un objet avec un structchamp. Cela dit, comme l'a mentionné Mason Wheeler, le problème de découpage en tranches est probablement la principale raison.
Doval
7
Ce n'est pas vrai que C # ait été inspiré par C ++; C # a plutôt été inspiré par toutes les erreurs (bien intentionnées et de bonne sonorité de l'époque) dans la conception de C ++ et de Java.
Pieter Geerkens
18
Remarque: pile et tas sont des détails d'implémentation. Rien n'indique que les instances de struct doivent être allouées sur la pile et que les instances de classe doivent être allouées sur le tas. Et ce n'est même pas vrai. Par exemple, il est très possible qu'un compilateur détermine, à l'aide de Escape Analysis, qu'une variable ne peut pas échapper à la portée locale et l'alloue par conséquent sur la pile, même s'il s'agit d'une instance de classe. Cela ne dit même pas qu'il doit y avoir une pile ou un tas du tout. Vous pouvez allouer des cadres d'environnement sous forme de liste chaînée sur le tas et ne même pas avoir de pile du tout.
Jörg W Mittag
3
"mais alors vous tombez sur une méthode prenant un paramètre de type agrégé, et pour déterminer si elle est en fait d'un type valeur ou d'un type référence, vous devez trouver la déclaration de son type" Umm, pourquoi est-ce important ? La méthode vous demande de passer une valeur. Vous passez une valeur. À quel moment devez-vous vous soucier de savoir s'il s'agit d'un type de référence ou d'un type de valeur?
Luaan

Réponses:

58

La raison pour laquelle C # (et Java et pratiquement tous les autres langages OO développés après C ++) n'a pas copié le modèle C ++ dans cet aspect est parce que la façon dont C ++ le fait est un désordre épouvantable.

Vous avez correctement identifié les points pertinents ci-dessus: structtype de valeur, pas d'héritage. class: type de référence, a héritage. Les types d'héritage et de valeur (ou plus spécifiquement, le polymorphisme et la valeur par valeur) ne font pas bon ménage. Si vous passez un objet de type Derivedà un argument de méthode Baseet que vous appelez ensuite une méthode virtuelle, le seul moyen d'obtenir un comportement correct est de s'assurer que ce qui est passé est une référence.

Entre cela et tous les autres dégâts que vous rencontrez en C ++ en ayant des objets pouvant être hérités en tant que types de valeur (les constructeurs de copie et le découpage d’objets vous viennent à l’esprit!), La meilleure solution est de Just Say No.

Une bonne conception linguistique ne consiste pas seulement à implémenter des fonctionnalités, mais également à identifier les fonctionnalités à ne pas implémenter. L'un des meilleurs moyens de le faire est d'apprendre des erreurs commises par ceux qui vous ont précédé.

Maçon Wheeler
la source
29
Ceci est juste un autre scandale subjectif inutile en C ++. Je ne peux pas voter, mais le ferais si je pouvais.
Bartek Banachewicz
17
@ MasonWheeler: " est un désordre horrible " semble assez subjectif. Cela a déjà été discuté dans un long fil de commentaire avec une autre réponse de votre part ; le fil a été nuk (malheureusement, car il contenait des commentaires utiles bien que dans la sauce de guerre de flamme). Je ne pense pas que cela vaille la peine de répéter le tout ici, mais "C # a bien compris et C ++ ne les a pas" (ce qui semble être le message que vous essayez de transmettre) est en effet une déclaration subjective.
Andy Prowl
15
@MasonWheeler: Je l'ai fait dans le fil de discussion qui a été modifié, ainsi que plusieurs autres personnes - c'est pourquoi je pense qu'il est regrettable qu'il ait été supprimé. Je ne pense pas que ce soit une bonne idée de répliquer ce fil ici, mais la version courte est: en C ++, l' utilisateur du type, et non son concepteur , doit décider avec quelle sémantique un type doit être utilisé (sémantique de référence ou valeur sémantique). Cela a des avantages et des inconvénients: vous rage contre les inconvénients sans considérer (ou savoir?) Les avantages. C'est pourquoi l'analyse est subjective.
Andy Prowl
7
soupir Je crois que la discussion sur la violation du LSP a déjà eu lieu. Et je pense que la majorité des gens ont convenu que la mention du fournisseur de services linguistiques est assez étrange et sans rapport, mais je ne peux pas vérifier car un mod a modifié le fil de commentaires .
Bartek Banachewicz
9
Si vous déplacez votre dernier paragraphe vers le haut et supprimez le premier paragraphe actuel, je pense que vous avez l'argument parfait. Mais le premier paragraphe actuel est simplement subjectif.
Martin York
19

Par analogie, C # est fondamentalement comme un ensemble d'outils de mécanicien dans lequel quelqu'un a lu qu'il fallait généralement éviter les pinces et les clés à molette, de sorte qu'il n'inclut pas de clés à molette, et que les pinces sont verrouillées dans un tiroir spécial portant la mention "non sécurisé". , et ne peut être utilisé qu'avec l'approbation d'un superviseur, après avoir signé un désistement dégageant votre employeur de toute responsabilité vis-à-vis de votre santé.

C ++, en comparaison, comprend non seulement des clés et des pinces ajustables, mais également des outils spéciaux plutôt bizarres dont le but n'est pas immédiatement évident. Si vous ne connaissez pas la bonne façon de les tenir, ils risquent de vous couper facilement la main. thumb (mais une fois que vous avez compris comment les utiliser, vous pouvez faire des choses qui sont essentiellement impossibles avec les outils de base de la boîte à outils C #). En outre, il dispose d'un tour, d'une fraiseuse, d'une rectifieuse, d'une scie à ruban à métaux, etc., pour vous permettre de concevoir et de créer de tout nouveaux outils chaque fois que vous en ressentez le besoin (mais, oui, les outils de ces machinistes peuvent et vont causer blessures graves si vous ne savez pas ce que vous faites avec eux - ou même si vous ne faites pas attention à rien).

Cela reflète la différence fondamentale de philosophie: le C ++ tente de vous fournir tous les outils dont vous pourriez avoir besoin pour la conception de votre choix. Il ne fait pratiquement aucun effort pour contrôler la manière dont vous utilisez ces outils. Il est donc également facile de les utiliser pour créer des dessins qui ne fonctionnent bien que dans de rares situations, ainsi que des dessins qui ne sont probablement qu'une idée moche et que personne ne connaît. ils sont susceptibles de bien fonctionner. En particulier, une grande partie de cela est réalisée en découplant les décisions de conception - même celles qui, dans la pratique, sont presque toujours couplées. En conséquence, il y a une énorme différence entre juste écrire en C ++ et bien écrire en C ++. Pour bien écrire en C ++, vous devez connaître de nombreux idiomes et règles empiriques (y compris les règles empiriques expliquant comment reconsidérer sérieusement la situation avant d'enfreindre d'autres règles empiriques). Par conséquent, C ++ est beaucoup plus orienté vers la facilité d'utilisation (par les experts) que la facilité d'apprentissage. Il y a aussi (beaucoup trop) de cas dans lesquels il n'est pas vraiment facile à utiliser non plus.

C # fait beaucoup plus pour essayer de forcer (ou du moins très fortement suggérer) ce que les concepteurs de langage ont considéré comme de bonnes pratiques de conception. Un certain nombre de choses qui sont découplées en C ++ (mais vont généralement ensemble) sont directement couplées en C #. Cela permet au code "non sécurisé" de repousser légèrement les limites, mais honnêtement, pas beaucoup.

Il en résulte qu’un certain nombre de conceptions pouvant être exprimées assez directement en C ++ sont plus difficiles à exprimer en C #. D'autre part, il est un ensemble beaucoup plus facile à apprendre C #, et les chances de produire un design vraiment horrible qui ne fonctionnera pas pour votre situation (ou probablement tout autre) sont considérablement réduits. Dans de nombreux cas (probablement même dans la plupart des cas), vous pouvez obtenir une conception solide et réalisable simplement en "allant avec le courant", pour ainsi dire. Ou, comme l'un de mes amis (du moins j'aime le penser comme un ami - je ne sais pas s'il est vraiment d'accord) aime le dire, C # facilite la chute dans le gouffre du succès.

Donc, en regardant plus spécifiquement la question de savoir comment classet structcomment ils sont dans les deux langages: objets créés dans une hiérarchie d'héritage où vous pourriez utiliser un objet d'une classe dérivée sous le couvert de sa classe / interface de base, vous êtes à peu près coincé avec le fait que vous devez normalement le faire via une sorte de pointeur ou de référence - à un niveau concret, ce qui se passe est que l'objet de la classe dérivée contient quelque chose de mémoire qui peut être traitée comme une instance de la classe de base / l'interface, et l'objet dérivé est manipulé via l'adresse de cette partie de la mémoire.

En C ++, il appartient au programmeur de le faire correctement - lorsqu'il utilise l'héritage, il lui appartient de s'assurer que, par exemple, une fonction qui fonctionne avec des classes polymorphes dans une hiérarchie le fait via un pointeur ou une référence à la base. classe.

En C #, ce qui est fondamentalement la même séparation entre les types est beaucoup plus explicite et imposé par le langage lui-même. Le programmeur n'a pas besoin de passer à l'étape suivante pour passer une instance d'une classe par référence, car cela se produira par défaut.

Jerry Coffin
la source
2
En tant que fan de C ++, je pense que c'est un excellent résumé des différences entre C # et la tronçonneuse suisse.
David Thornley
1
@ DavidThornley: J'ai au moins essayé d'écrire ce que je pensais être une comparaison quelque peu équilibrée. Je ne veux pas pointer du doigt, mais une partie de ce que j'ai vu en écrivant ceci m'a semblé ... quelque peu inexacte (pour le dire gentiment).
Jerry Coffin
7

Cela vient de "C #: Pourquoi avons-nous besoin d'une autre langue?" - Gunnerson, Eric:

La simplicité était un objectif de conception important pour C #.

Il est possible d'aller trop loin sur la simplicité et la pureté du langage, mais la pureté pour des raisons de pureté est de peu d'utilité pour le programmeur professionnel. Nous avons donc essayé d’équilibrer notre désir d’avoir un langage simple et concis tout en résolvant les problèmes du monde réel auxquels les programmeurs sont confrontés.

[...]

Les types de valeur , la surcharge des opérateurs et les conversions définies par l'utilisateur ajoutent de la complexité au langage , mais permettent de simplifier énormément un scénario utilisateur important.

La sémantique de référence pour les objets est un moyen d'éviter beaucoup de problèmes (bien sûr et pas seulement le découpage d'objet), mais les problèmes du monde réel peuvent parfois nécessiter des objets avec une sémantique de valeur (par exemple, regardez Sons comme je ne devrais jamais utiliser de sémantique de référence, non? pour un point de vue différent).

Quelle meilleure approche à adopter, donc, que de séparer ces objets sales, laids et mauvais avec une valeur sémantique sous la balise de struct?

manlio
la source
1
Je ne sais pas, peut-être ne pas utiliser ces objets sales, laids et mauvais avec une sémantique de référence?
Bartek Banachewicz
Peut-être ... je suis une cause perdue.
Manlio
2
IMHO, l'un des plus gros défauts de la conception de Java est l'absence de tout moyen de déclarer si une variable est utilisée pour encapsuler une identité ou une propriété , et l'un des plus gros défauts de C # est l'absence de moyen de distinguer les opérations sur une variable d'opérations sur un objet pour lequel une variable contient une référence. Même si le runtime ne se souciait pas de telles distinctions, pouvoir spécifier dans une langue si une variable de type int[]devait être partageable ou modifiable (les tableaux pouvaient être l'un ou l'autre, mais généralement pas les deux) aiderait à donner un faux code à un code erroné.
Supercat
4

Plutôt que de penser aux types de valeur dérivés Object, il serait plus utile de penser aux types d'emplacement de stockage existant dans un univers entièrement distinct des types d'instance de classe, mais que chaque type de valeur ait un type d'objet de segment correspondant. Un emplacement de stockage de type structure contient simplement une concaténation des champs publics et privés du type, et le type de segment de mémoire est généré automatiquement selon un modèle tel que:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

et pour une instruction du type: Console.WriteLine ("La valeur est {0}", somePoint);

à traduire comme: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("La valeur est {0}", box1);

En pratique, étant donné que les types d'emplacement de stockage et les types d'instance de segment de mémoire existent dans des univers distincts, il n'est pas nécessaire d'appeler les types d'instance de segment de mémoire comme par exemple boxed_Int32; puisque le système saurait quels contextes requièrent l'instance d'objet tas et lesquels nécessitent un emplacement de stockage.

Certaines personnes pensent que tout type de valeur qui ne se comporte pas comme un objet devrait être considéré comme "diabolique". Je suis du même avis: puisque les emplacements de stockage des types de valeur ne sont ni des objets ni des références à des objets, l'attente qu'ils se comportent comme des objets ne doit pas être considérée comme une aide. Dans les cas où une structure peut se comporter utilement comme un objet, il n’ya rien de mal à en avoir une, mais chacune structn’est en son cœur qu’une agrégation de champs publics et privés collés avec du ruban adhésif.

supercat
la source