Pytanie Niezmienne obiekty, które się do siebie odwołują?


Dzisiaj usiłowałem owinąć głowę niezmiennymi obiektami, które się do siebie odwołują. Doszedłem do wniosku, że nie można tego zrobić bez leniwego oceniania, ale w trakcie pisania tego (według mnie) napisałem interesujący kod.

public class A
{
    public string Name { get; private set; }
    public B B { get; private set; }
    public A()
    {
        B = new B(this);
        Name = "test";
    }
}

public class B
{
    public A A { get; private set; }
    public B(A a)
    {
        //a.Name is null
        A = a;
    }
}

Co ciekawe, nie mogę wymyślić innego sposobu obserwowania obiektu typu A w stanie, który nie jest jeszcze w pełni skonstruowany i obejmuje wątki. Dlaczego to jest nawet ważne? Czy istnieją inne sposoby obserwowania stanu obiektu, który nie jest w pełni skonstruowany?


76
2017-10-05 12:49


pochodzenie


Dlaczego uważasz, że jest nieważny? - leppie
Ponieważ rozumiem, że konstruktor ma zagwarantować, że kod, który zawiera, jest wykonywany przed zewnętrznym kodem, który może obserwować stan obiektu. - Stilgar
Kod jest ważny, ale niezbyt wiarygodny. Stilgar ma rację - instancja klasy A przeszedł do B.ctor nie jest w pełni uśpiony. Musisz utworzyć nowe wystąpienie B w A po instancji A jest w pełni zainicjalizowany - powinien to być ostatni wiersz w .ctor. - Karel Frajták


Odpowiedzi:


Dlaczego to jest nawet ważne?

Dlaczego uważasz, że jest nieważny?

Ponieważ konstruktor powinien gwarantować, że kod, który zawiera, jest wykonywany przed zewnętrznym kodem, który może obserwować stan obiektu.

Poprawny. Ale kompilator nie odpowiada za utrzymanie tego niezmiennika. Ty jesteś. Jeśli napiszesz kod, który zrywa ten niezmiennik, a to boli, gdy to zrobisz przestań to robić.

Czy istnieją inne sposoby obserwowania stanu obiektu, który nie jest w pełni skonstruowany?

Pewnie. W przypadku typów referencyjnych wszystkie one w jakiś sposób powodują, że "ten" jest poza konstruktorem, ponieważ jedynym kodem użytkownika przechowującym odwołanie do pamięci jest konstruktor. Niektóre sposoby, które konstruktor może przeciekać "to" to:

  • Umieść "to" w polu statycznym i odwołaj się do niego z innego wątku
  • wykonaj wywołanie metody lub wywołanie konstruktora i przekazuj "to" jako argument
  • nawiązywać połączenia wirtualne - szczególnie nieprzyjemne, jeśli metoda wirtualna jest nadpisywana przez klasę pochodną, ​​ponieważ wtedy działa przed uzyskaniem pochodnego klasy ctor body.

Powiedziałem, że tylko kod użytkownika który ma referencję, to ctor, ale oczywiście śmieciarz również zawiera odniesienie. Dlatego też innym interesującym sposobem, w którym obiekt może być obserwowany w stanie pół-skonstruowanym, jest to, czy obiekt ma destruktor, a konstruktor zgłasza wyjątek (lub otrzymuje asynchroniczny wyjątek, taki jak przerwanie wątku, więcej o tym później. ) W takim przypadku obiekt jest bliski śmierci i dlatego musi zostać sfinalizowany, ale wątek finalizatora może zobaczyć stan inicjalizowany w połowie obiektu. A teraz wracamy do kodu użytkownika, który widzi półbudowany obiekt!

Destruktory muszą być odporne na ten scenariusz. Destruktor nie może zależeć od jakiegokolwiek niezmiennika obiektu ustawionego przez konstruktora, ponieważ zniszczony obiekt mógł nigdy nie zostać w pełni skonstruowany.

Innym wariackim sposobem na to, że obiekt zbudowany na pół może być obserwowany przez kod zewnętrzny, jest oczywiście wtedy, gdy destruktor widzi zainicjowany obiekt w powyższym scenariuszu, a następnie kopiuje odniesienie do tego obiektu do pola statycznego, zapewniając w ten sposób, że pół-skonstruowany, pół-sfinalizowany obiekt zostanie uratowany przed śmiercią. Proszę nie rób tego.Tak jak powiedziałem, jeśli to boli, nie rób tego.

Jeśli jesteś konstruktorem typu wartości, rzeczy są zasadniczo takie same, ale istnieją pewne niewielkie różnice w mechanizmie. Język wymaga, aby wywołanie konstruktora na typie wartości tworzyło tymczasową zmienną, do której dostęp ma tylko ctor, zmutuj tę zmienną, a następnie wykonaj strukturalną kopię zmutowanej wartości do rzeczywistej pamięci. Zapewnia to, że jeśli konstruktor wyrzuci, to ostateczne miejsce przechowywania nie jest w stanie pół-mutacji.

Zauważ, że skoro kopie strukturalne nie mają gwarancji, że są atomowe, to jest możliwe dla innego wątku, aby zobaczyć magazyn w stanie pół-mutacji; używaj zamków poprawnie, jeśli jesteś w takiej sytuacji. Ponadto możliwe jest wyrzucenie asynchronicznego wyjątku, takiego jak przerwanie wątku, w połowie kopii struktury. Te problemy nieatomowe powstają niezależnie od tego, czy kopia pochodzi z tymczasowej, czy też "zwykłej" kopii. Ogólnie rzecz biorąc, bardzo niewiele niezmienników jest zachowanych, jeśli istnieją asynchroniczne wyjątki.

W praktyce kompilator C # będzie optymalizował tymczasową alokację i kopiowanie, jeśli może stwierdzić, że nie ma sposobu na powstanie tego scenariusza. Na przykład, jeśli nowa wartość inicjuje lokalny, który nie jest zamknięty przez lambda, a nie w bloku iteracyjnym, to S s = new S(123); po prostu mutuje s bezpośrednio.

Aby uzyskać więcej informacji na temat działania konstruktorów typów wartości, zobacz:

Obalanie kolejnego mitu o typach wartości

Aby uzyskać więcej informacji o tym, w jaki sposób semantyka języka C # próbuje uratować cię przed sobą, zobacz:

Dlaczego inicjatory działają w przeciwległym porządku jako konstruktorzy? Część pierwsza

Dlaczego inicjatory działają w przeciwległym porządku jako konstruktorzy? Część druga

Wydaje mi się, że zboczyłem z tematu. W strukturze można oczywiście obserwować obiekt, który zostanie w połowie skonstruowany w ten sam sposób - skopiuj skonstruowany obiekt do pola statycznego, wywołaj metodę z "tym" jako argumentem i tak dalej. (Oczywiście wywołanie metody wirtualnej na bardziej wyprowadzonym typie nie stanowi problemu w przypadku struktur.) I, jak już powiedziałem, kopia z tymczasowego do końcowego magazynu nie jest atomowa, a zatem inny wątek może obserwować na wpół skopiowaną strukturę.


Rozważmy teraz podstawową przyczynę twojego pytania: w jaki sposób tworzysz niezmienne obiekty, które się nawzajem odwołują?

Zazwyczaj, jak odkryłeś, nie robisz tego. Jeśli masz dwa niezmienne obiekty odwołujące się do siebie, to logicznie tworzą one ukierunkowany wykres cykliczny. Możesz po prostu zbudować niezmienny, skierowany wykres! Robienie tego jest dość łatwe. Niezmienny, skierowany wykres składa się z:

  • Niezmienna lista niezmiennych węzłów, z których każdy zawiera wartość.
  • Niezmienna lista niezmiennych par węzłów, z których każda ma punkt początkowy i końcowy krawędzi wykresu.

Teraz sposób, w jaki nawiązywane są wzajemne odniesienia "A" i "B" węzłów, brzmi:

A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);

I skończyłeś, masz wykres, w którym A i B "odwołują się" do siebie nawzajem.

Problem polega oczywiście na tym, że nie możesz dostać się do B z A bez posiadania G w ręce. Posiadanie tego dodatkowego poziomu niedozwolonego może być niedopuszczalne.


105
2017-10-05 14:42



Wielkie dzięki. Czytałem artykuł o rodzaju wartości i były one częścią tego, dlaczego uważałem, że język stara się zagwarantować pełną konstrukcję obiektu, zanim będzie można go zaobserwować. W końcu dlatego kopiowana jest wartość. - Stilgar
@Stilgar: Staramy się być językiem "pit jakości", gdzie naprawdę trzeba ciężko pracować, aby napisać program, który robi coś szalonego. Niestety, bardzo trudno zaprojektować użyteczny język, w którym jest gwarantowane że obiekt będzie nigdy być obserwowanym w niespójnym stanie, więc nie próbujemy gwarancja że. Po prostu staramy się mocno popchnąć cię w tym kierunku. (Zasadniczo, dlaczego nie-nullable typy odniesienia nie działają w .NET, jest to bardzo trudne do zagwarantowania w systemie typu że pole typu nie-nullable referencyjnego jest nigdy zauważono, że jest zerowa). - Eric Lippert
Tak, wygląda na to, że wykonałeś tak dobrą robotę, że spodziewałem się, że uniemożliwisz mi napisanie powyższego kodu. - Stilgar
@Stilgar: Problem polega na tym, że jeśli to zrobimy, uniemożliwiamy ci pisanie wielu użytecznych kodów. Czasami bardzo przydatnym jest, aby móc przekazać "to" do metody lub konstruktora innej klasy, szczególnie w tego rodzaju scenariuszach inicjalizacji. Każdego dnia piszę taki kod: w kompilatorze często znajdujemy się w sytuacjach, w których niezmienne "analizatory kodu" konstruują niezmienne "symbole" i muszą być w stanie wzajemnie się ze sobą nawzajem odwoływać. - Eric Lippert
@EricLippert: Jest w rzeczywistości przydatny. Mam przypadki, w których warto było przekazać a this odniesienie do innych obiektów z wnętrza konstruktora. Ale dlaczego kompilator nie zezwala na używanie this słowo kluczowe z inicjalizatorów pól? Oba mogą widzieć częściowo skonstruowane obiekty. To pytanie tak naprawdę nie uzasadniały tego ograniczenia. Jeśli znasz przyczynę, udostępnij. - Allon Guralnek


Tak, jest to jedyny sposób, aby dwa niezmienne obiekty odnosiły się do siebie - przynajmniej jeden z nich musi zobaczyć drugi w nie do końca skonstruowany sposób.

Jego na ogół zły pomysł this uciec od swojego konstruktora ale w przypadkach, gdy jesteś pewien, co robią obaj konstruktorzy, i jest to jedyna alternatywa dla zmienności, nie sądzę, także zły.


47
2017-10-05 12:54



Zaproponowałem przykład wzajemnych odniesień za pomocą this w ta odpowiedź na kolejne pytanie. - Brian


"W pełni skonstruowany" jest definiowany przez twój kod, a nie przez język.

Jest to odmiana wywołania metody wirtualnej od konstruktora,
ogólną wytyczną jest: nie rób tego.

Aby poprawnie wdrożyć pojęcie "w pełni skonstruowane", nie przechodź this z twojego konstruktora.


22
2017-10-05 12:54





Rzeczywiście, wyciek this odniesienie podczas konstruktora pozwoli ci to zrobić; może oczywiście powodować problemy, jeśli metody zostaną wywołane na niekompletnym obiekcie. Jeśli chodzi o "inne sposoby obserwowania stanu obiektu, który nie jest w pełni skonstruowany":

  • wywołać a virtual metoda w konstruktorze; konstruktor podklasy nie został jeszcze wywołany, więc an override może próbować uzyskać dostęp do niekompletnego stanu (pola zadeklarowane lub zainicjalizowane w podklasie itp.)
  • odbicie, być może za pomocą FormatterServices.GetUninitializedObject (który tworzy obiekt bez wywoływania konstruktora w ogóle)

8
2017-10-05 12:54



Refleksja się nie liczy :) - Stilgar
@Stilgar, jeśli pozwala mi obserwować stan obiektu, który nie jest w pełni skonstruowany, a następnie ... meh - Marc Gravell♦


Jeśli weźmiesz pod uwagę kolejność inicjowania

  • Wyprowadzone pola statyczne
  • Pochodny konstruktor statyczny
  • Pochodne pola instancji
  • Podstawowe pola statyczne
  • Podstawowy konstruktor statyczny
  • Podstawowe pola instancji
  • Podstawowy konstruktor instancji
  • Derived instance constructor

wyraźnie przez up-casting możesz uzyskać dostęp do klasy ZANIM wywołasz konstruktor klasy pochodnej (to jest powód, dla którego nie powinieneś używać metod wirtualnych od konstruktorów, mogliby łatwo uzyskać dostęp do pól pochodnych nie zainicjowanych przez konstruktor / konstruktor w klasie pochodnej nie mogło przynieść klasy pochodnej w "spójnym" stanie)


6
2017-10-05 12:53





Możesz uniknąć problemu, wstawiając B ostatnie w swoim constuctor:

 public A() 
    { 
        Name = "test"; 
        B = new B(this); 
    } 

Jeśli to, co sugerujesz, nie było możliwe, wówczas A nie byłby niezmienny.

Edycja: naprawiona, dzięki leppie.


4
2017-10-05 12:54



Bardzo niepodobny do twojego przykładowego kodu ... - leppie
Piszesz, aby utworzyć ostatnie B w konstruktorze, ale w tym przykładzie najpierw je zainicjujesz, tak jak w kodzie z OP. Typo? - Avada Kedavra
Dzięki, ludzie, post jest poprawiony. Naprawdę literówka. - Nick
Uważam, że OP wie o tym i zadaje bardziej fundamentalne pytanie. - Henk Holterman
@Nick: Wery good tillill Masz 3 niezmienne klasy :) `public A () {Name =" test "; B = nowy B (ten); C = nowy C (ten); } ` - VMykyt


Zasada jest taka, że ​​nie pozwól swoim to ucieczka obiektu z korpusu konstruktora.

Innym sposobem zaobserwowania tego problemu jest wywołanie metod wirtualnych wewnątrz konstruktora.


3
2017-10-05 12:53





Jak już wspomniano, kompilator nie ma możliwości sprawdzenia, w którym punkcie obiekt został skonstruowany na tyle dobrze, aby był przydatny; dlatego zakłada, że ​​programista, który mija this od konstruktora będzie wiedział, czy obiekt został skonstruowany na tyle dobrze, aby zaspokoić jego potrzeby.

Dodam jednak, że w przypadku przedmiotów, które mają być naprawdę niezmienne, należy unikać mijania this do dowolnego kodu, który zbada stan pola, zanim zostanie mu przypisana jego ostateczna wartość. To daje do zrozumienia ze thisnie jest przekazywany do arbitralnego kodu zewnętrznego, ale nie oznacza, że ​​jest coś złego w tym, że obiekt w budowie przechodzi na inny obiekt w celu przechowywania referencji zwrotnej, która nie zostanie faktycznie wykorzystana do czasu zakończenia pierwszego konstruktora.

Jeśli ktoś projektował język w celu ułatwienia budowy i używania niezmiennych obiektów, może okazać się przydatny dla stwierdzenia, że ​​metody są użyteczne tylko w trakcie budowy, tylko po zakończeniu budowy; pola można zadeklarować jako nie dereferencyjne podczas konstrukcji i później tylko do odczytu; parametry mogą być również oznaczone, aby wskazać, że nie powinny być dereferencyjne. W takim systemie możliwe byłoby, aby kompilator pozwalał na konstruowanie struktur danych, które się nawzajem odwoływały, ale gdzie żadna właściwość nie mogłaby się zmienić po jej zaobserwowaniu. Jeśli chodzi o to, czy korzyści z takiego sprawdzania statycznego przewyższają koszty, nie jestem pewien, ale może to być interesujące.

Nawiasem mówiąc, przydatną funkcją, która byłaby przydatna, byłaby możliwość zadeklarowania parametrów i zwracanych funkcji jako efemerycznych, zwrotnych lub (domyślnie) trwałych. Jeśli parametr lub funkcja return zostałyby zadeklarowane jako efemeryczne, nie można go skopiować do żadnego pola ani przekazać jako parametr trwałości dla dowolnej metody. Ponadto przekazanie wartości efemerycznej lub zwrotnej jako zwrotnego parametru do metody spowodowałoby, że zwracana wartość funkcji dziedziczy ograniczenia tej wartości (jeśli funkcja ma dwa parametry zwrotne, jej wartość zwracana odziedziczyłaby bardziej restrykcyjne ograniczenie z jej wartości; parametry). Główną słabością w Javie i .net jest to, że wszystkie odwołania do obiektów są rozwiązłe; gdy tylko kod zewnętrzny znajdzie się na jednym z nich, nie wiadomo, kto może z tego skończyć. Jeśli parametry mogłyby być zadeklarowane jako efemeryczne, częściej byłoby możliwe, aby kod, który posiadał jedyne odniesienie do czegoś, aby wiedział, że był jedynym referencją, a tym samym uniknął niepotrzebnych operacji kopiowania obronnego. Dodatkowo, rzeczy takie jak zamknięcia mogą być poddane recyklingowi, jeśli kompilator wie, że nie istnieją żadne odniesienia do nich po ich powrocie.


1
2017-07-25 00:57