Dans l'exemple de code suivant, nous avons une classe pour les objets immuables qui représente une pièce. Le nord, le sud, l'est et l'ouest représentent des sorties dans d'autres pièces.
public sealed class Room
{
public Room(string name, Room northExit, Room southExit, Room eastExit, Room westExit)
{
this.Name = name;
this.North = northExit;
this.South = southExit;
this.East = eastExit;
this.West = westExit;
}
public string Name { get; }
public Room North { get; }
public Room South { get; }
public Room East { get; }
public Room West { get; }
}
On voit donc que cette classe est conçue avec une référence circulaire réflexive. Mais parce que la classe est immuable, je suis coincé avec un problème de «poulet ou d'oeuf». Je suis certain que les programmeurs fonctionnels expérimentés savent comment gérer cela. Comment le gérer en C #?
J'essaie de coder un jeu d'aventure basé sur du texte, mais en utilisant des principes de programmation fonctionnels juste pour le plaisir d'apprendre. Je suis coincé sur ce concept et je peux utiliser de l'aide !!! Merci.
MISE À JOUR:
Voici une implémentation fonctionnelle basée sur la réponse de Mike Nakis concernant l'initialisation paresseuse:
using System;
public sealed class Room
{
private readonly Func<Room> north;
private readonly Func<Room> south;
private readonly Func<Room> east;
private readonly Func<Room> west;
public Room(
string name,
Func<Room> northExit = null,
Func<Room> southExit = null,
Func<Room> eastExit = null,
Func<Room> westExit = null)
{
this.Name = name;
var dummyDelegate = new Func<Room>(() => { return null; });
this.north = northExit ?? dummyDelegate;
this.south = southExit ?? dummyDelegate;
this.east = eastExit ?? dummyDelegate;
this.west = westExit ?? dummyDelegate;
}
public string Name { get; }
public override string ToString()
{
return this.Name;
}
public Room North
{
get { return this.north(); }
}
public Room South
{
get { return this.south(); }
}
public Room East
{
get { return this.east(); }
}
public Room West
{
get { return this.west(); }
}
public static void Main(string[] args)
{
Room kitchen = null;
Room library = null;
kitchen = new Room(
name: "Kitchen",
northExit: () => library
);
library = new Room(
name: "Library",
southExit: () => kitchen
);
Console.WriteLine(
$"The {kitchen} has a northen exit that " +
$"leads to the {kitchen.North}.");
Console.WriteLine(
$"The {library} has a southern exit that " +
$"leads to the {library.South}.");
Console.ReadKey();
}
}
la source
Room
exemple.type List a = Nil | Cons of a * List a
. Et un arbre binaire:type Tree a = Leaf a | Cons of Tree a * Tree a
. Comme vous pouvez le voir, ils sont tous deux auto-référentiels (récursifs). Voici comment vous définissez votre chambre:type Room = Nil | Open of {name: string, south: Room, east: Room, north: Room, west: Room}
.Room
classe et celle de aList
sont similaires dans le Haskell que j'ai écrit ci-dessus.Réponses:
De toute évidence, vous ne pouvez pas le faire en utilisant exactement le code que vous avez publié, car à un moment donné, vous devrez construire un objet qui doit être connecté à un autre objet qui n'a pas encore été construit.
Il y a deux façons de penser (que j'ai utilisées auparavant) pour ce faire:
Utilisation de deux phases
Tous les objets sont construits en premier, sans aucune dépendance, et une fois qu'ils ont tous été construits, ils sont connectés. Cela signifie que les objets doivent passer par deux phases dans leur vie: une phase mutable très courte, suivie d'une phase immuable qui dure tout au long de leur vie.
Vous pouvez rencontrer exactement le même type de problème lors de la modélisation de bases de données relationnelles: une table a une clé étrangère qui pointe vers une autre table, et l'autre table peut avoir une clé étrangère qui pointe vers la première table. La manière dont cela est géré dans les bases de données relationnelles est que les contraintes de clé étrangère peuvent (et sont généralement) spécifiées avec une
ALTER TABLE ADD FOREIGN KEY
instruction supplémentaire qui est distincte de l'CREATE TABLE
instruction. Ainsi, vous créez d'abord toutes vos tables, puis vous ajoutez vos contraintes de clé étrangère.La différence entre les bases de données relationnelles et ce que vous voulez faire est que les bases de données relationnelles continuent à autoriser les
ALTER TABLE ADD/DROP FOREIGN KEY
instructions tout au long de la durée de vie des tables, tandis que vous définirez probablement un indicateur `` IamImmutable '' et refuserez toute autre mutation une fois que toutes les dépendances auront été réalisées.Utilisation de l'initialisation paresseuse
Au lieu d'une référence à une dépendance, vous passez un délégué qui retournera la référence à la dépendance en cas de besoin. Une fois la dépendance récupérée, le délégué n'est plus jamais appelé.
Le délégué prend généralement la forme d'une expression lambda, il ne sera donc que légèrement plus verbeux que le fait de passer des dépendances aux constructeurs.
L'inconvénient (minuscule) de cette technique est que vous devez gaspiller l'espace de stockage nécessaire pour stocker les pointeurs vers les délégués qui ne seront utilisés que lors de l'initialisation de votre graphique d'objet.
Vous pouvez même créer une classe générique de "référence paresseuse" qui l'implémente afin que vous n'ayez pas à la réimplémenter pour chacun de vos membres.
Voici une telle classe écrite en Java, vous pouvez facilement la transcrire en C #
(Mon
Function<T>
est comme leFunc<T>
délégué de C #)Cette classe est censée être thread-safe, et le "double contrôle" est lié à une optimisation dans le cas de la concurrence. Si vous ne prévoyez pas d'être multi-thread, vous pouvez supprimer tout cela. Si vous décidez d'utiliser cette classe dans une configuration multi-thread, assurez-vous de lire à propos de "l'idiome de double vérification". (Il s'agit d'une longue discussion au-delà de la portée de cette question.)
la source
Le modèle d'initialisation paresseux dans la réponse de Mike Nakis fonctionne très bien pour une initialisation unique entre deux objets, mais devient difficile à gérer pour plusieurs objets interdépendants avec des mises à jour fréquentes.
Il est beaucoup plus simple et plus facile à gérer de maintenir les liens entre les pièces en dehors des objets de la pièce eux-mêmes, dans quelque chose comme un
ImmutableDictionary<Tuple<int, int>, Room>
. De cette façon, au lieu de créer des références circulaires, vous ajoutez simplement une référence unidirectionnelle simple et facilement actualisable à ce dictionnaire.la source
Room
de paraître avoir ces relations; mais, ils devraient être des getters qui lisent simplement à partir de l'index.La façon de le faire dans un style fonctionnel est de reconnaître ce que vous construisez réellement: un graphique dirigé avec des bords étiquetés.
Un donjon est une structure de données qui permet de suivre un tas de pièces et d'objets, et quelles sont les relations entre eux. Chaque appel "avec" renvoie un nouveau donjon immuable différent . Les chambres ne savent pas ce qui se trouve au nord et au sud; le livre ne sait pas qu'il est dans la poitrine. Le donjon connaît ces faits, et cette chose n'a aucun problème avec les références circulaires car il n'y en a pas.
la source
Le poulet et un œuf ont raison. Cela n'a aucun sens en c #:
Mais cela:
Mais cela signifie que A n'est pas immuable!
Vous pouvez tricher:
Cela cache le problème. Bien sûr, A et B ont un état immuable, mais ils se réfèrent à quelque chose qui n'est pas immuable. Ce qui pourrait facilement vaincre le point de les rendre immuables. J'espère que C est au moins aussi sûr que les threads dont vous avez besoin.
Il existe un schéma appelé gel-dégel:
Maintenant, «a» est immuable. «A» ne l'est pas mais «a» l'est. Pourquoi ça va? Tant que rien d'autre ne connaît «a» avant qu'il ne soit gelé, qui s'en soucie?
Il existe une méthode thaw () mais elle ne change jamais «a». Il fait une copie modifiable de «a» qui peut être mise à jour puis gelée également.
L'inconvénient de cette approche est que la classe n'applique pas l'immuabilité. La procédure suivante est. Vous ne pouvez pas dire si c'est immutabile du type.
Je ne connais pas vraiment un moyen idéal pour résoudre ce problème en c #. Je sais comment cacher les problèmes. Parfois ça suffit.
Quand ce n'est pas le cas, j'utilise une approche différente pour éviter complètement ce problème. Par exemple: regardez comment le modèle d'état est implémenté ici . On pourrait penser qu'ils feraient cela comme référence circulaire, mais ils ne le font pas. Ils lancent de nouveaux objets chaque fois que l'état change. Parfois, il est plus facile d'abuser du ramasse-miettes que de trouver comment retirer les œufs des poulets.
la source
a.freeze()
pourrait retourner leImmutableA
type. ce qui en fait un modèle fondamentalement constructeur.b
reste une référence à l'ancien mutablea
. L'idée est quea
etb
devrait pointer vers des versions immuables les unes des autres avant de les publier sur le reste du système.Certaines personnes intelligentes ont déjà exprimé leur opinion à ce sujet, mais je pense simplement que ce n'est pas la responsabilité de la salle de savoir ce que sont ses voisins.
Je pense que c'est la responsabilité du bâtiment de savoir où se trouvent les pièces. Si la pièce a vraiment besoin de connaître ses voisins, passez-lui INeigbourFinder.
la source