Im folgenden Codebeispiel haben wir eine Klasse für unveränderliche Objekte, die einen Raum darstellt. Nord, Süd, Ost und West repräsentieren Ausgänge in andere Räume.
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; }
}
Wir sehen also, dass diese Klasse mit einem reflexiven Zirkelbezug entworfen wurde. Aber weil die Klasse unveränderlich ist, habe ich ein Problem mit Hühnern oder Eiern. Ich bin mir sicher, dass erfahrene Funktionsprogrammierer damit umgehen können. Wie kann es in C # gehandhabt werden?
Ich bin bestrebt, ein textbasiertes Abenteuerspiel zu programmieren, aber ich verwende funktionale Programmierprinzipien nur zum Zweck des Lernens. Ich bleibe bei diesem Konzept und kann Hilfe gebrauchen !!! Vielen Dank.
AKTUALISIEREN:
Hier ist eine funktionierende Implementierung, die auf der Antwort von Mike Nakis bezüglich der verzögerten Initialisierung basiert:
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();
}
}
quelle
Room
Beispiel.type List a = Nil | Cons of a * List a
. Und ein binärer Baum:type Tree a = Leaf a | Cons of Tree a * Tree a
. Wie Sie sehen, sind beide selbstreferenziell (rekursiv). Hier ist , wie Sie Ihr Zimmer definieren würde:type Room = Nil | Open of {name: string, south: Room, east: Room, north: Room, west: Room}
.Room
Klasse und von aList
in dem oben von mir geschriebenen Haskell ist.Antworten:
Natürlich können Sie nicht genau den Code verwenden, den Sie veröffentlicht haben, da Sie irgendwann ein Objekt erstellen müssen, das mit einem anderen Objekt verbunden werden muss, das noch nicht erstellt wurde.
Es gibt zwei Möglichkeiten, die ich mir vorstellen kann (die ich zuvor verwendet habe), um dies zu tun:
Mit zwei Phasen
Alle Objekte werden zuerst ohne Abhängigkeiten konstruiert und sobald sie alle konstruiert wurden, werden sie verbunden. Dies bedeutet, dass die Objekte in ihrem Leben zwei Phasen durchlaufen müssen: eine sehr kurze veränderbare Phase, gefolgt von einer unveränderlichen Phase, die für den Rest ihres Lebens andauert.
Beim Modellieren relationaler Datenbanken können Sie auf genau dieselbe Art von Problem stoßen: Eine Tabelle verfügt über einen Fremdschlüssel, der auf eine andere Tabelle verweist, und die andere Tabelle verfügt möglicherweise über einen Fremdschlüssel, der auf die erste Tabelle verweist. In relationalen Datenbanken wird dies so gehandhabt, dass die Fremdschlüsseleinschränkungen mit einer zusätzlichen
ALTER TABLE ADD FOREIGN KEY
Anweisung angegeben werden können (und in der Regel sind), die von derCREATE TABLE
Anweisung getrennt ist. Sie erstellen also zuerst alle Ihre Tabellen und fügen dann Ihre Fremdschlüsseleinschränkungen hinzu.Der Unterschied zwischen relationalen Datenbanken und dem, was Sie tun möchten, besteht darin, dass relationale Datenbanken
ALTER TABLE ADD/DROP FOREIGN KEY
während der gesamten Lebensdauer der Tabellen weiterhin Anweisungen zulassen. Wahrscheinlich setzen Sie ein Flag "IamImmutable" und lehnen weitere Mutationen ab, sobald alle Abhängigkeiten erkannt wurden.Lazy Initialization verwenden
Anstelle eines Verweises auf eine Abhängigkeit übergeben Sie einen Delegaten , der bei Bedarf den Verweis auf die Abhängigkeit zurückgibt. Sobald die Abhängigkeit abgerufen wurde, wird der Delegat nie wieder aufgerufen.
Der Delegat hat normalerweise die Form eines Lambda-Ausdrucks, sodass er nur geringfügig ausführlicher aussieht, als wenn Abhängigkeiten tatsächlich an die Konstruktoren übergeben werden.
Der (winzige) Nachteil dieser Technik ist, dass Sie den Speicherplatz verschwenden müssen, der zum Speichern der Zeiger auf die Stellvertreter erforderlich ist, die nur während der Initialisierung Ihres Objektdiagramms verwendet werden.
Sie können sogar eine generische "Lazy Reference" -Klasse erstellen, die dies implementiert, damit Sie es nicht für jedes einzelne Ihrer Mitglieder erneut implementieren müssen.
Hier ist eine solche Klasse in Java geschrieben, Sie können es leicht in C # transkribieren
(Mein
Function<T>
ist wie derFunc<T>
Delegat von C #)Diese Klasse soll thread-sicher sein, und das "Double Check" -Paket bezieht sich auf eine Optimierung im Fall von Parallelität. Wenn Sie nicht vorhaben, mehrere Threads zu verwenden, können Sie all diese Dinge entfernen. Wenn Sie sich für die Verwendung dieser Klasse in einem Multithread-Setup entscheiden, lesen Sie unbedingt das "double check idiom". (Dies ist eine lange Diskussion, die über den Rahmen dieser Frage hinausgeht.)
quelle
Das verzögerte Initialisierungsmuster in der Antwort von Mike Nakis eignet sich gut für eine einmalige Initialisierung zwischen zwei Objekten, wird jedoch für mehrere miteinander verbundene Objekte mit häufigen Aktualisierungen unhandlich.
Es ist viel einfacher und übersichtlicher, die Verknüpfungen zwischen Räumen außerhalb der Raumobjekte selbst in einer Weise wie einer zu halten
ImmutableDictionary<Tuple<int, int>, Room>
. Auf diese Weise erstellen Sie keine Zirkelverweise, sondern fügen nur einen einzelnen, leicht aktualisierbaren Einwegverweis zu diesem Wörterbuch hinzu.quelle
Room
aus erscheint diese Beziehungen zu haben; aber sie sollten Getter sein, die einfach aus dem Index lesen.Die Art und Weise, dies in einem funktionalen Stil zu tun, besteht darin, zu erkennen, was Sie tatsächlich konstruieren: ein gerichtetes Diagramm mit beschrifteten Kanten.
Ein Dungeon ist eine Datenstruktur, die eine Reihe von Räumen und Dingen sowie die Beziehungen zwischen ihnen aufzeichnet. Jeder „mit“ Aufruf gibt einen neuen, verschiedenen unveränderlichen Kerker. Die Räume wissen nicht, was nördlich und südlich von ihnen ist; Das Buch weiß nicht, dass es in der Truhe ist. Der Dungeon kennt diese Tatsachen, und das Ding hat kein Problem mit Zirkelverweisen, weil es keine gibt.
quelle
Huhn und ein Ei ist richtig. Das macht keinen Sinn in c #:
Aber das macht:
Aber das heißt, A ist nicht unveränderlich!
Sie können betrügen:
Das verbirgt das Problem. Sicher, A und B haben einen unveränderlichen Zustand, aber sie beziehen sich auf etwas, das nicht unveränderlich ist. Welches leicht den Punkt besiegen könnte, sie unveränderlich zu machen. Ich hoffe C ist mindestens so threadsicher, wie Sie es brauchen.
Es gibt ein Muster namens Einfrieren-Auftauen:
Jetzt ist 'a' unveränderlich. 'A' ist nicht aber 'a' ist. Warum ist das ok Solange nichts anderes über 'a' weiß, bevor es eingefroren ist, wen interessiert das?
Es gibt eine thaw () -Methode, die jedoch niemals 'a' ändert. Es erstellt eine veränderbare Kopie von 'a', die aktualisiert und dann auch eingefroren werden kann.
Der Nachteil dieses Ansatzes ist, dass die Klasse keine Unveränderlichkeit erzwingt. Folgendes Verfahren ist. Sie können nicht sagen, ob es vom Typ unerschütterlich ist.
Ich kenne keinen idealen Weg, um dieses Problem in c # zu lösen. Ich kenne Möglichkeiten, die Probleme zu verbergen. Manchmal ist das genug.
Wenn dies nicht der Fall ist, benutze ich einen anderen Ansatz, um dieses Problem insgesamt zu vermeiden. Zum Beispiel: schauen, wie die Zustandsmuster implementiert sind hier . Sie würden denken, dass sie das als Zirkelverweis tun würden, aber sie tun es nicht. Sie drehen jedes Mal, wenn sich der Zustand ändert, neue Objekte auf. Manchmal ist es einfacher, den Müllmann zu missbrauchen, als herauszufinden, wie man Eier aus Hühnern holt.
quelle
a.freeze()
könnteImmutableA
Typ zurückgeben. was es im Grunde Builder-Muster machen.b
bleibt ein Verweis auf die alte veränderbarea
. Die Idee ist, dassa
undb
sollte auf unveränderliche Versionen von einander verweisen, bevor Sie sie für den Rest des Systems freigeben.Einige kluge Leute haben bereits ihre Meinung dazu geäußert, aber ich denke nur, dass es nicht die Verantwortung des Raumes ist, zu wissen, was seine Nachbarn sind.
Ich denke, es liegt in der Verantwortung des Gebäudes, zu wissen, wo sich Räume befinden. Wenn der Raum wirklich wissen muss, welche Nachbarn er hat, geben Sie INeigbourFinder an ihn weiter.
quelle