web-dev-qa-db-fra.com

Pourquoi les structures et les classes sont-elles des concepts séparés en C #?

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

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

Cette configuration semble agréable au début, mais ensuite vous tombez sur une méthode prenant un paramètre d'un type agrégé, et pour savoir s'il s'agit réellement d'un type valeur ou d'un type référence, vous devez trouver la déclaration de son type. Cela peut parfois devenir très déroutant.

La solution généralement acceptée au problème semble déclarer tous les structs comme "immuables" (en définissant leurs champs sur readonly) pour éviter d'éventuelles erreurs, limitant l'utilité de structs.

C++, par exemple, utilise un modèle beaucoup plus utilisable: il vous permet de créer une instance d'objet soit sur la pile soit sur le tas et de la passer par valeur ou par référence (ou par pointeur). Je n'arrête pas d'entendre que C # a été inspiré par C++, et je ne comprends tout simplement pas pourquoi il n'a pas adopté cette seule technique. Combiner class et struct en une seule construction avec deux options d'allocation différentes (tas et pile) et les passer comme valeurs ou (explicitement) comme références via les ref et out les mots clés semblent être une bonne chose.

La question est, pourquoi class et struct sont-ils devenus des concepts séparés en C # et le CLR au lieu d'un type agrégé avec deux options d'allocation?

45
Mints97

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

Vous avez correctement identifié les points pertinents ci-dessus: struct: type de valeur, pas d'héritage. class: type de référence, a l'héritage. Les types d'héritage et de valeur (ou plus précisément, le polymorphisme et le passage par valeur) ne se mélangent pas; si vous passez un objet de type Derived à un argument de méthode de type Base, puis appelez une méthode virtuelle dessus, la seule façon d'obtenir un comportement correct est de vous assurer que ce qui a été transmis était une référence.

Entre cela et tous les autres désordres que vous rencontrez en C++ en ayant des objets héritables comme types de valeur (constructeurs de copie et découpage d'objet viennent à l'esprit!), La meilleure solution est de simplement dire non.

Une bonne conception de langage ne consiste pas seulement à implémenter des fonctionnalités, mais également à savoir quelles fonctionnalités ne pas implémenter, et l'une des meilleures façons de le faire est d'apprendre des erreurs de ceux qui vous ont précédé.

59
Mason Wheeler

Par analogie, C # est essentiellement comme un ensemble d'outils de mécanicien où quelqu'un a lu que vous devriez généralement éviter les pinces et les clés réglables, donc il n'inclut pas du tout les clés réglables, et les pinces sont verrouillées dans un tiroir spécial marqué "dangereux" , et ne peut être utilisé qu'avec l'approbation d'un superviseur, après avoir signé un avertissement exonérant votre employeur de toute responsabilité pour votre santé.

C++, en comparaison, comprend non seulement des clés et des pinces réglables, mais aussi des outils spéciaux plutôt étranges dont le but n'est pas immédiatement apparent, et si vous ne savez pas comment les tenir, ils pourraient facilement couper votre thumb (mais une fois que vous comprenez comment les utiliser, vous pouvez faire des choses qui sont essentiellement impossibles avec les outils de base de la boîte à outils C #). De plus, il possède un tour, une fraiseuse, une meuleuse de surface, une scie à ruban pour couper le métal, etc., pour vous permettre de concevoir et de créer des outils entièrement nouveaux chaque fois que vous en ressentez le besoin (mais oui, ces outils de machiniste peuvent et causeront des blessures graves si vous ne savez pas ce que vous en faites - ou même si vous êtes simplement imprudent).

Cela reflète la différence fondamentale de philosophie: C++ essaie de vous donner tous les outils dont vous pourriez avoir besoin pour pratiquement n'importe quelle conception que vous souhaitez. Il ne fait pratiquement aucune tentative pour contrôler la façon dont vous utilisez ces outils, il est donc également facile de les utiliser pour produire des conceptions qui ne fonctionnent bien que dans des situations rares, ainsi que des conceptions qui ne sont probablement qu'une mauvaise idée et que personne ne connaît de situation dans laquelle ils sont susceptibles de bien fonctionner du tout. En particulier, cela se fait en grande partie par le découplage des 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 simplement écrire C++ et bien écrire C++. Pour bien écrire en C++, vous devez connaître un grand nombre d'idiomes et de règles empiriques (y compris des règles empiriques sur la façon de reconsidérer sérieusement avant d'enfreindre d'autres règles empiriques). En conséquence, C++ est beaucoup plus orienté vers la facilité d'utilisation (par les experts) que la facilité d'apprentissage. Il y a aussi (trop) de circonstances dans lesquelles ce n'est pas vraiment très facile à utiliser non plus.

C # fait beaucoup plus pour essayer de forcer (ou du moins extrêmement suggère fortement) ce que les concepteurs de langage considéraient comme de bonnes pratiques de conception. Un certain nombre de choses qui sont découplées en C++ (mais qui vont généralement ensemble en pratique) sont directement couplées en C #. Cela permet au code "dangereux" de repousser un peu les limites, mais honnêtement, pas beaucoup.

Le résultat est que d'une part, il existe de nombreux modèles pouvant être exprimés assez directement en C++ qui sont sensiblement plus maladroits à exprimer en C #. D'un autre côté, c'est entier beaucoup plus facile à apprendre le C #, et les chances de produire un design vraiment horrible qui ne fonctionnera pas pour votre situation (ou probablement tout autre) sont drastiquement réduit. Dans de nombreux cas (probablement même dans la plupart des cas), vous pouvez obtenir une conception solide et réalisable en "suivant le courant", pour ainsi dire. Ou, comme un de mes amis (au moins j'aime le considérer comme un ami - je ne sais pas s'il est vraiment d'accord) aime le dire, C # permet de tomber facilement dans le gouffre du succès.

Donc, en regardant plus spécifiquement la question de savoir comment class et struct ont obtenu comment ils sont dans les deux langages: les objets créés dans une hiérarchie d'héritage où vous pourriez utiliser un objet d'une classe dérivée sous forme 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 - au 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/interface de base, et l'objet dérivé est manipulé via l'adresse de cette partie de la mémoire.

En C++, c'est au programmeur de le faire correctement - quand il utilise l'héritage, c'est à lui de s'assurer (par exemple) qu'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 prendre de mesures pour passer une instance d'une classe par référence, car cela se produira par défaut.

20
Jerry Coffin

Cela vient de "C #: Pourquoi avons-nous besoin d'un autre langage?" - 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 la 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 avec la résolution des problèmes du monde réel auxquels les programmeurs sont confrontés.

[...]

Types de valeurs , surcharge d'opérateur et conversions définies par l'utilisateur ajoute de la complexité à la langue, mais autorise un scénario utilisateur important être extrêmement simplifié.

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'objets), mais les problèmes du monde réel peuvent parfois nécessiter des objets avec une valeur sémantique (par exemple, jetez un oeil à il semble que je ne devrais jamais utiliser la référence). sémantique, non? pour un point de vue différent).

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

7
manlio

Plutôt que de penser aux types de valeurs dérivant de Object, il serait plus utile de penser aux types d'emplacement de stockage existant dans un univers entièrement séparé des types d'instance de classe, mais pour chaque type de valeur d'avoir un objet tas correspondant type. Un emplacement de stockage de type structure contient simplement une concaténation des champs public et privé du type, et le type de tas est généré automatiquement selon un modèle comme:

// 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 comme: 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, comme les types d'emplacement de stockage et les types d'instances de tas existent dans des univers distincts, il n'est pas nécessaire d'appeler les types d'instances de tas comme boxed_Int32; car le système saurait quels contextes nécessitent l'instance de 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 doit être considéré comme "mauvais". J'adopte le point de vue opposé: étant donné que 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 doit être considérée comme inutile. Dans les cas où une structure peut se comporter utilement comme un objet, il n'y a rien de mal à en avoir un, mais chaque struct n'est en son cœur rien de plus qu'une agrégation de champs publics et privés collés ensemble avec du ruban adhésif.

4
supercat