W poniższym przykładzie kodu mamy klasę dla niezmiennych obiektów reprezentujących pokój. Północ, południe, wschód i zachód reprezentują wyjścia do innych pomieszczeń.
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; }
}
Widzimy więc, że ta klasa została zaprojektowana z refleksyjną referencją kołową. Ale ponieważ klasa jest niezmienna, utknąłem z problemem „kurczaka lub jajka”. Jestem pewien, że doświadczeni programiści funkcjonalni wiedzą, jak sobie z tym poradzić. Jak można to obsłużyć w C #?
Staram się kodować tekstową grę przygodową, ale wykorzystuję funkcjonalne zasady programowania tylko ze względu na naukę. Utknąłem na tej koncepcji i mogę skorzystać z pomocy !!! Dzięki.
AKTUALIZACJA:
Oto działająca implementacja oparta na odpowiedzi Mike'a Nakisa dotyczącej leniwej inicjalizacji:
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();
}
}
c#
functional-programming
immutability
circular-dependency
Rock Anthony Johnson
źródło
źródło
Room
przykład.type List a = Nil | Cons of a * List a
. I binarne drzewo:type Tree a = Leaf a | Cons of Tree a * Tree a
. Jak widać, oba są samoreferencyjne (rekurencyjne). Oto jak chcesz określić swój pokój:type Room = Nil | Open of {name: string, south: Room, east: Room, north: Room, west: Room}
.Room
klasy i aList
w Haskell, który napisałem powyżej.Odpowiedzi:
Oczywiście nie można tego zrobić przy użyciu dokładnie opublikowanego kodu, ponieważ w pewnym momencie konieczne będzie skonstruowanie obiektu, który należy połączyć z innym obiektem, który nie został jeszcze zbudowany.
Są na to dwa sposoby (z których wcześniej korzystałem):
Korzystanie z dwóch faz
Wszystkie obiekty są konstruowane jako pierwsze, bez żadnych zależności, a po ich zbudowaniu zostają połączone. Oznacza to, że obiekty muszą przejść przez dwie fazy swojego życia: bardzo krótką fazę zmienną, po której następuje niezmienna faza, która trwa przez resztę ich życia.
Dokładnie taki sam problem występuje podczas modelowania relacyjnych baz danych: jedna tabela ma klucz obcy wskazujący na inną tabelę, a druga tabela może mieć klucz obcy wskazujący na pierwszą tabelę. W relacyjnych bazach danych jest to obsługiwane w taki sposób, że ograniczenia klucza obcego można (i zwykle są) określać za pomocą dodatkowej
ALTER TABLE ADD FOREIGN KEY
instrukcji, która jest niezależna odCREATE TABLE
instrukcji. Najpierw utwórz wszystkie tabele, a następnie dodaj ograniczenia klucza obcego.Różnica między relacyjnymi bazami danych a tym, co chcesz zrobić, polega na tym, że relacyjne bazy danych nadal zezwalają na
ALTER TABLE ADD/DROP FOREIGN KEY
instrukcje przez cały okres istnienia tabel, podczas gdy prawdopodobnie ustawisz flagę „IamImmutable” i odrzucisz dalsze mutacje, gdy wszystkie zależności zostaną spełnione.Korzystanie z leniwej inicjalizacji
Zamiast odniesienia do zależności przekazujesz delegata, który w razie potrzeby zwróci odwołanie do zależności. Po pobraniu zależności delegat nigdy nie jest wywoływany ponownie.
Delegat zwykle przyjmuje postać wyrażenia lambda, więc będzie wyglądał tylko nieco bardziej gadatliwie niż przekazywanie zależności konstruktorom.
(Mały) minus tej techniki polega na tym, że musisz zmarnować miejsce do przechowywania potrzebne do przechowywania wskaźników dla delegatów, które zostaną wykorzystane tylko podczas inicjalizacji wykresu obiektowego.
Możesz nawet stworzyć ogólną klasę „leniwego odniesienia”, która implementuje to, abyś nie musiał jej ponownie implementować dla każdego członka.
Oto taka klasa napisana w Javie, możesz łatwo transkrybować ją w C #
(Mój
Function<T>
jest jakFunc<T>
delegat C #)Ta klasa ma być bezpieczna dla wątków, a funkcja „podwójnego sprawdzania” jest związana z optymalizacją w przypadku współbieżności. Jeśli nie planujesz być wielowątkowy, możesz usunąć wszystkie te rzeczy. Jeśli zdecydujesz się użyć tej klasy w konfiguracji wielowątkowej, przeczytaj o „idiomie podwójnego sprawdzania”. (To długa dyskusja wykraczająca poza zakres tego pytania).
źródło
Leniwy wzorzec inicjalizacji w odpowiedzi Mike'a Nakisa działa dobrze w przypadku jednorazowej inicjalizacji między dwoma obiektami, ale staje się niewygodny dla wielu powiązanych ze sobą obiektów z częstymi aktualizacjami.
O wiele prostsze i łatwiejsze do zarządzania jest utrzymywanie połączeń między pokojami na zewnątrz samych obiektów pokojowych, w czymś takim jak
ImmutableDictionary<Tuple<int, int>, Room>
. W ten sposób zamiast tworzyć cykliczne odwołania, dodajesz do tego słownika jedno, łatwe do aktualizacji, jednokierunkowe odniesienie.źródło
Room
od pojawiające mieć te relacje; ale powinny to być metody pobierające, które po prostu czytają z indeksu.Sposobem na zrobienie tego w funkcjonalnym stylu jest rozpoznanie tego, co aktualnie konstruujesz: ukierunkowany wykres z oznaczonymi krawędziami.
Loch to struktura danych, która śledzi wiele pokoi i rzeczy oraz jakie są między nimi relacje. Każde połączenie „z” zwraca nowe, inne niezmienne lochy. Pokoje nie wiedzą, co jest na północ i południe od nich; książka nie wie, że jest w skrzyni. Loch zna te fakty, i że sprawa nie ma problemu z odniesieniami kołowymi ponieważ nie ma.
źródło
Kurczak i jajko mają rację. Nie ma to sensu w języku c #:
Ale to powoduje:
Ale to oznacza, że A nie jest niezmienne!
Możesz oszukiwać:
To ukrywa problem. Pewnie A i B mają niezmienny stan, ale odnoszą się do czegoś, co nie jest niezmienne. Co może z łatwością pokonać punkt, czyniąc je niezmiennymi. Mam nadzieję, że C jest co najmniej tak samo bezpieczne dla wątków, jak potrzebujesz.
Istnieje wzorzec zwany zamrażaniem i rozmrażaniem:
Teraz „a” jest niezmienne. „A” nie jest, ale „a” jest. Dlaczego to jest w porządku? Dopóki nic innego nie wie o „a”, zanim się zamrozi, kogo to obchodzi?
Istnieje metoda thaw (), ale nigdy nie zmienia „a”. Tworzy zmienną kopię „a”, którą można aktualizować, a także zamrażać.
Minusem tego podejścia jest to, że klasa nie wymusza niezmienności. Procedura jest następująca. Nie możesz stwierdzić, czy jest niezmienny od tego typu.
Naprawdę nie znam idealnego sposobu rozwiązania tego problemu w c #. Wiem, jak ukryć problemy. Czasami to wystarczy.
Jeśli tak nie jest, stosuję inne podejście, aby całkowicie uniknąć tego problemu. Na przykład: spójrz na sposób implementacji wzorca stanu tutaj . Można by pomyśleć, że zrobiliby to jako okólnik, ale nie robią tego. Wyrzucają nowe obiekty za każdym razem, gdy zmienia się stan. Czasami łatwiej jest nadużyć pojemnika na śmieci niż dowiedzieć się, jak wydobywać jajka z kurcząt.
źródło
a.freeze()
może zwrócićImmutableA
typ. które sprawiają, że jest to w zasadzie wzorzec budowniczego.b
zostanie zachowane odniesienie do starej zmienneja
. Chodzi o to, żea
ib
powinien wskazywać na niezmiennych wersjami siebie przed zwolnieniem ich z resztą systemu.Niektórzy mądrzy ludzie już wyrazili swoje opinie na ten temat, ale myślę, że to nie jest obowiązkiem pokoju, aby wiedzieć, kim są jego sąsiedzi.
Myślę, że obowiązkiem budynku jest wiedzieć, gdzie są pokoje. Jeśli pokój naprawdę potrzebuje wiedzieć, że sąsiedzi przekazują mu INeigbourFinder.
źródło