Czy niedeterministyczne zarządzanie zasobami jest nieszczelną abstrakcją?

9

Z tego, co widzę, istnieją dwie wszechobecne formy zarządzania zasobami: zniszczenie deterministyczne i wyraźne. Przykładami tego pierwszego byłyby niszczyciele i inteligentne wskaźniki C ++ lub podrzędny DESTROY Perla, a drugim przykładem byłby paradygmat Ruby do zarządzania zasobami lub interfejs IDNET .NET.

Wydaje się, że nowsze języki wybierają to drugie, być może jako efekt uboczny korzystania z systemów wyrzucania elementów bezużytecznych.

Moje pytanie brzmi: biorąc pod uwagę, że destruktory inteligentnych wskaźników lub systemy śmieciowe z liczeniem odniesień - prawie to samo - umożliwiają ukryte i przejrzyste niszczenie zasobów, czy jest to mniej nieszczelna abstrakcja niż niedeterministyczne typy oparte na wyraźnych notacja?

Dam konkretny przykład. Jeśli masz trzy podklasy C ++ jednej nadklasy, jedna może mieć implementację, która nie wymaga żadnego konkretnego zniszczenia. Być może ma swoją magię w inny sposób. Nie wymaga żadnego specjalnego zniszczenia - wszystkie podklasy są nadal używane w ten sam sposób.

Kolejny przykład wykorzystuje bloki Ruby. Dwie podklasy muszą zwolnić zasoby, więc nadklasa wybiera interfejs wykorzystujący blok w konstruktorze, nawet jeśli inne określone podklasy mogą go nie potrzebować, ponieważ nie wymagają specjalnego zniszczenia.

Czy to prawda, że ​​ta ostatnia przecieka szczegóły implementacji zniszczenia zasobów, podczas gdy pierwsza nie.

EDYCJA: Porównując, powiedzmy, Ruby z Perlem może być bardziej sprawiedliwa, ponieważ jedno ma deterministyczne zniszczenie, a drugie nie, ale oba są zbierane w śmieciach.

Louis Jackman
źródło
5
Kusi mnie, aby powiedzieć „tak”, ale lubię słyszeć, co inni mają do powiedzenia na ten temat.
Bart van Ingen Schenau
Przejrzyste niszczenie zasobów? Poza tym, że musisz używać inteligentnych wskaźników zamiast normalnych wskaźników? Nie sądzę, aby było to bardziej przejrzyste niż posiadanie tylko jednego mechanizmu (referencji) w celu uzyskania dostępu do obiektów (w C ++ masz co najmniej cztery lub pięć).
Giorgio
@Giorgio: „Sposoby dostępu do obiektu” są dość niejasne. Masz na myśli czytanie lub pisanie? Stała / zmienna kwalifikacja? Wskaźniki nie są tak naprawdę „sposobem dostępu do obiektu”; praktycznie każde wyrażenie powoduje powstanie obiektu, a dereferencja wskaźnika po prostu nie jest wyjątkowa.
MSalters
1
@Giorgio: W tym sensie OOP nie można wysłać wiadomości do wskaźnika C ++. Musisz wyrenderować wskaźnik, aby wysłać wiadomość (*ptr).Message()lub równoważnie ptr->Message(). Istnieje nieskończony zestaw dozwolonych wyrażeń, co ((*ptr))->Messagejest również równoważne. Ale wszystkie sprowadzają się doexpressionIdentifyingAnObject.Message()
MSalters
1
Podczas ponownego liczenia musisz uważać na unikanie kręgów. Tak więc abstrakcja wycieka również, po prostu w inny sposób.
CodesInChaos

Odpowiedzi:

2

Twój przykład odpowiada na pytanie. Przejrzyste zniszczenie jest wyraźnie mniej nieszczelne niż zniszczenie jawne. Może przeciekać, ale jest mniej nieszczelny.

Jawne zniszczenie jest analogiczne do malloc / free w C ze wszystkimi pułapkami. Może z odrobiną cukru syntaktycznego, aby wyglądał na oparty na zakresie.

Niektóre zalety przezroczystego niszczenia w porównaniu z jawnym: -
taki sam wzorzec użytkowania -
nie można zapomnieć o uwolnieniu zasobu.
- czyszczenie szczegółów nie zaśmieca krajobrazu w miejscu użytkowania.

mike30
źródło
2

Niepowodzenie w abstrakcji w rzeczywistości nie polega na tym, że zbieranie śmieci jest niedeterministyczne, ale raczej na tym, że obiekty są „zainteresowane” rzeczami, do których się odwołują, i nie są zainteresowane rzeczami, których nie posiadają Bibliografia. Aby zobaczyć, dlaczego, rozważ scenariusz obiektu, który utrzymuje licznik częstotliwości malowania określonej kontrolki. Podczas tworzenia subskrybuje zdarzenie „malowania” formantu, a po usunięciu rezygnuje z subskrypcji. Zdarzenie click po prostu zwiększa pole, a metoda getTotalClicks()zwraca wartość tego pola.

Kiedy obiekt licznika jest tworzony, musi powodować zapisanie odwołania do siebie w kontrolowanym przez niego kontrolce. Formant naprawdę nie dba o obiekt licznika i byłby równie szczęśliwy, gdyby obiekt licznika i odwołanie do niego przestały istnieć, ale tak długo, jak istnieje odwołanie, będzie wywoływał procedurę obsługi tego obiektu za każdym razem maluje się. Ta akcja jest całkowicie bezużyteczna dla kontroli, ale byłaby przydatna dla każdego, kto kiedykolwiek wezwałby getTotalClicks()obiekt.

Gdyby np. Metoda utworzyła nowy obiekt „licznika farb”, wykonała jakąś akcję na kontrolerze, zaobserwowała, ile razy odmalowano kontrolkę, a następnie porzuciła obiekt licznika farb, obiekt pozostałby objęty zdarzeniem nawet choć nikomu nie zależy, czy przedmiot i wszystkie odniesienia do niego po prostu znikną. Jednak obiekty nie kwalifikują się do kolekcji, dopóki sama kontrola nie będzie. Gdyby metoda ta była wywoływana wiele tysięcy razy w ciągu życia kontrolki [prawdopodobny scenariusz], mogłaby spowodować przepełnienie pamięci, ale z uwagi na fakt, że koszt N wywołań prawdopodobnie wynosiłby O (N ^ 2) lub O (N ^ 3), chyba że przetwarzanie subskrypcji było bardzo wydajne, a większość operacji nie wymagała żadnego malowania.

Ten konkretny scenariusz można rozwiązać, utrzymując słabe odniesienie do obiektu kontrolnego zamiast obiektu silnego. Model słabej subskrypcji jest pomocny, ale nie działa w ogólnym przypadku. Załóżmy, że zamiast chcieć mieć obiekt, który monitoruje jeden rodzaj zdarzenia z jednego elementu sterującego, chcieliśmy mieć obiekt rejestrujący zdarzenia, który monitoruje kilka elementów sterujących, a mechanizm obsługi zdarzeń w systemie był taki, że każdy element sterujący potrzebował odwołania do innego obiektu rejestrującego zdarzenia. W takim przypadku obiekt łączący kontrolkę z rejestratorem zdarzeń powinien pozostać przy życiu tylko tak długo, jak długo obakontrola jest monitorowana, a rejestrator zdarzeń pozostaje użyteczny. Jeśli ani kontrolka, ani rejestrator zdarzeń nie zawierają silnego odniesienia do zdarzenia łączącego, przestanie istnieć, mimo że nadal jest „przydatne”. Jeśli jedno z nich zawiera silne zdarzenie, żywotność łączącego obiektu może zostać bezużytecznie przedłużona, nawet jeśli drugi umrze.

Jeśli w kosmosie nie ma żadnego odniesienia do obiektu, można go bezpiecznie uznać za bezużyteczny i wyeliminować z istnienia. Fakt, że istnieje odwołanie do obiektu, nie oznacza jednak, że obiekt jest „użyteczny”. W wielu przypadkach faktyczna użyteczność obiektów będzie zależeć od istnienia odniesień do innych obiektów, które - z perspektywy GC - są z nimi całkowicie niezwiązane.

Jeśli obiekty zostaną powiadomione deterministycznie, gdy nikt nie będzie nimi zainteresowany, będą mogli wykorzystać te informacje, aby upewnić się, że każdy, kto skorzysta z tej wiedzy, zostanie o tym poinformowany. W przypadku braku takiego powiadomienia nie ma jednak ogólnego sposobu ustalenia, które obiekty są uważane za „przydatne”, jeśli znany jest tylko zbiór istniejących odniesień, a nie znaczenie semantyczne przypisywane tym odniesieniom. Zatem każdy model, który zakłada, że ​​istnienie lub nieistnienie referencji jest wystarczające do zautomatyzowanego zarządzania zasobami, byłoby skazane na zagładę, nawet gdyby GC mógł natychmiast wykryć porzucenie obiektu.

supercat
źródło
0

Brak „destruktora lub innego interfejsu, który mówi„ ta klasa musi zostać zniszczona ”nie jest umową tego interfejsu. Jeśli stworzysz podtyp, który nie wymaga specjalnego zniszczenia, skłaniam się do uznania, że ​​narusza zasadę podstawienia Liskowa .

Jeśli chodzi o C ++ w porównaniu do innych, nie ma dużej różnicy. C ++ wymusza ten interfejs na wszystkich swoich obiektach. Abstrakcje nie mogą przeciekać, jeśli są wymagane przez język.

Telastyn
źródło
4
„Jeśli zrobisz podtyp, który nie wymaga specjalnego zniszczenia”, to nie jest naruszenie LSP, ponieważ brak operacji jest ważnym specjalnym przypadkiem zniszczenia. Problem polega na dodaniu wymogu niszczenia do klasy pochodnej.
CodesInChaos
Tu się mylę. Jeśli trzeba dodać specjalny kod zniszczenia do podklasy C ++, to wcale nie zmienia wzorców użycia, ponieważ jest to automatyczne. Oznacza to, że nadklasa i podklasa mogą być nadal używane zamiennie. Ale przy wyraźnym zapisie dotyczącym zarządzania zasobami podklasa wymagająca jawnego zniszczenia sprawiłaby, że jej użycie byłoby niezgodne z nadklasą, prawda? (Zakładając, że nadklasa nie wymagała wyraźnego zniszczenia.)
Louis Jackman,
@CodesInChaos - ah tak, przypuszczam, że to prawda.
Telastyn
@ljackman: Klasa, która wymaga specjalnego zniszczenia, obciąża każdego, kto wywoła swojego konstruktora, aby upewnić się, że zostanie przeprowadzony. Nie powoduje to naruszenia LSP, ponieważ DerivedFooThatRequiresSpecialDestructionmożna go utworzyć tylko za pomocą kodu, który wywołuje new DerivedFooThatRequiresSpecialDestruction(). Z drugiej strony, metoda fabryczna, która zwróciła DerivedFooThatRequiresSpecialDestructionkod do kodu, który nie oczekiwał czegoś wymagającego zniszczenia, byłaby naruszeniem LSP.
supercat
0

Moje pytanie brzmi: biorąc pod uwagę, że destruktory inteligentnych wskaźników lub systemy śmieciowe z liczeniem odniesień - prawie to samo - umożliwiają ukryte i przejrzyste niszczenie zasobów, czy jest to mniej nieszczelna abstrakcja niż niedeterministyczne typy oparte na wyraźnych notacja?

Konieczność ręcznego obserwowania cykli nie jest ani domniemana, ani przejrzysta. Jedynym wyjątkiem jest referencyjny system zliczania z językiem, który zabrania cykli z założenia. Erlang może być przykładem takiego systemu.

Oba podejścia przeciekają. Główną różnicą jest to, że destruktory przeciekają wszędzie w C ++, ale IDisposejest bardzo rzadkie w .NET.

Jon Harrop
źródło
1
Tyle, że cykle są niezwykle rzadkie i praktycznie nigdy nie występują, z wyjątkiem struktur danych, które są wyraźnie cykliczne. Główną różnicą jest to, że destruktory w C ++ są poprawnie obsługiwane wszędzie, ale IDispose rzadko obsługuje problem w .NET.
DeadMG
„Z wyjątkiem cykli niezwykle rzadkich”. W nowoczesnych językach? Podważyłbym tę hipotezę.
Jon Harrop,