Wydaje mi się, że mam dziwny zwyczaj ... przynajmniej według mojego współpracownika. Pracowaliśmy razem nad małym projektem. Sposób, w jaki napisałem zajęcia to (przykład uproszczony):
[Serializable()]
public class Foo
{
public Foo()
{ }
private Bar _bar;
public Bar Bar
{
get
{
if (_bar == null)
_bar = new Bar();
return _bar;
}
set { _bar = value; }
}
}
Zasadniczo więc inicjalizuję dowolne pole tylko wtedy, gdy wywoływana jest metoda pobierająca, a pole jest nadal puste. Pomyślałem, że to zmniejszyłoby przeciążenie, nie inicjując żadnych właściwości, które nie są nigdzie używane.
ETA: Zrobiłem to, ponieważ moja klasa ma kilka właściwości, które zwracają instancję innej klasy, która z kolei ma również właściwości z jeszcze większą liczbą klas i tak dalej. Wywołanie konstruktora dla najwyższej klasy spowodowałoby następnie wywołanie wszystkich konstruktorów dla wszystkich tych klas, gdy nie zawsze są one potrzebne.
Czy są jakieś zastrzeżenia wobec tej praktyki, inne niż osobiste preferencje?
AKTUALIZACJA: Rozważyłem wiele różnych opinii w odniesieniu do tego pytania i podtrzymam moją zaakceptowaną odpowiedź. Jednak teraz znacznie lepiej zrozumiałem koncepcję i jestem w stanie zdecydować, kiedy go użyć, a kiedy nie.
Cons:
- Problemy z bezpieczeństwem wątków
- Nieprzestrzeganie żądania „ustawiającego”, gdy przekazana wartość jest pusta
- Mikrooptymalizacje
- Obsługa wyjątków powinna odbywać się w konstruktorze
- Musisz sprawdzić, czy w kodzie klasy nie ma null
Plusy:
- Mikrooptymalizacje
- Właściwości nigdy nie zwracają wartości null
- Opóźnij lub unikaj ładowania „ciężkich” przedmiotów
Większość wad nie ma zastosowania w mojej obecnej bibliotece, jednak musiałbym sprawdzić, czy „mikro-optymalizacje” w ogóle coś optymalizują.
OSTATNIA AKTUALIZACJA:
Okej, zmieniłem odpowiedź. Moje pierwotne pytanie dotyczyło tego, czy jest to dobry nawyk. Jestem teraz przekonany, że tak nie jest. Może nadal będę go używać w niektórych częściach mojego obecnego kodu, ale nie bezwarunkowo i zdecydowanie nie cały czas. Więc stracę nawyk i pomyślę o tym przed użyciem. Dziękuję wszystkim!
źródło
Odpowiedzi:
To co tu mamy to - naiwna - implementacja "leniwej inicjalizacji".
Krótka odpowiedź:
Bezwarunkowe używanie leniwej inicjalizacji nie jest dobrym pomysłem. Ma swoje miejsce, ale trzeba się liczyć z konsekwencjami tego rozwiązania.
Tło i wyjaśnienie:
Konkretna implementacja:
Najpierw spójrzmy na konkretną próbkę i dlaczego uważam jej implementację za naiwną:
Narusza zasadę najmniejszej niespodzianki (POLS) . Gdy wartość jest przypisana do właściwości, oczekuje się, że ta wartość zostanie zwrócona. W Twojej implementacji nie ma to miejsca w przypadku
null
:foo.Bar
wątkami : dwóch wywołujących w różnych wątkach może potencjalnie otrzymać dwie różne instancje,Bar
a jeden z nich będzie bez połączenia zFoo
instancją. Wszelkie zmiany wprowadzone w tejBar
instancji zostaną po cichu utracone.To kolejny przypadek naruszenia POLS. Gdy uzyskuje się dostęp tylko do przechowywanej wartości właściwości, oczekuje się, że będzie ona bezpieczna wątkowo. Chociaż możesz argumentować, że klasa po prostu nie jest bezpieczna dla wątków - w tym pobierający twoją właściwość - musisz to odpowiednio udokumentować, ponieważ nie jest to normalny przypadek. Co więcej, wprowadzanie tego zagadnienia jest niepotrzebne, co zobaczymy wkrótce.
Ogólnie:
nadszedł czas, aby ogólnie przyjrzeć się leniwej inicjalizacji:
Leniwa inicjalizacja jest zwykle używana do opóźniania konstrukcji obiektów, których budowa zajmuje dużo czasu lub które zajmują dużo pamięci po całkowitym skonstruowaniu.
To jest bardzo ważny powód używania leniwej inicjalizacji.
Jednak takie właściwości zwykle nie mają seterów, co eliminuje pierwszy problem wskazany powyżej.
Ponadto implementacja bezpieczna wątkowo byłaby używana - podobnie jak
Lazy<T>
- w celu uniknięcia drugiego problemu.Nawet biorąc pod uwagę te dwa punkty w implementacji leniwej właściwości, następujące punkty są ogólnymi problemami tego wzorca:
Budowa obiektu może się nie powieść, co spowoduje wyjątek od metody pobierającej właściwość. To kolejne naruszenie POLS i dlatego należy go unikać. Nawet sekcja dotycząca właściwości w „Wytycznych projektowych dotyczących tworzenia bibliotek klas” wyraźnie stwierdza, że metody pobierające właściwości nie powinny zgłaszać wyjątków:
Uszkodzone są automatyczne optymalizacje kompilatora, a mianowicie wstawianie i przewidywanie gałęzi. Zobacz odpowiedź Billa K. po szczegółowe wyjaśnienie.
Wniosek z tych punktów jest następujący:
W przypadku każdej pojedynczej właściwości, która jest wdrażana leniwie, należy rozważyć te punkty.
Oznacza to, że jest to decyzja indywidualna i nie może być traktowana jako ogólna najlepsza praktyka.
Ten wzorzec ma swoje miejsce, ale nie jest to ogólna najlepsza praktyka podczas implementowania klas. Nie należy go używać bezwarunkowo z powodów wymienionych powyżej.
W tej sekcji chcę omówić niektóre z punktów, które inni przedstawili jako argumenty za bezwarunkowym użyciem leniwej inicjalizacji:
Serializacja:
EricJ stwierdza w jednym komentarzu:
Z tym argumentem wiąże się kilka problemów:
Mikrooptymalizacja: Twoim głównym argumentem jest to, że chcesz konstruować obiekty tylko wtedy, gdy ktoś faktycznie uzyskuje do nich dostęp. Więc tak naprawdę mówisz o optymalizacji wykorzystania pamięci.
Nie zgadzam się z tym argumentem z następujących powodów:
Przyznaję, że czasami taka optymalizacja jest uzasadniona. Ale nawet w tych przypadkach leniwa inicjalizacja nie wydaje się być właściwym rozwiązaniem. Istnieją dwa powody, które przemawiają przeciwko temu:
źródło
null
sprawie stała się znacznie silniejsza. :-)To dobry wybór projektowy. Zdecydowanie zalecane dla kodu biblioteki lub klas podstawowych.
Nazywa się to przez pewną „leniwą inicjalizację” lub „opóźnioną inicjalizację” i jest powszechnie uważane za dobry wybór projektowy.
Po pierwsze, jeśli zainicjujesz w deklaracji zmiennych na poziomie klasy lub konstruktora, to podczas konstruowania obiektu masz narzut tworzenia zasobu, którego nigdy nie można użyć.
Po drugie, zasób jest tworzony tylko w razie potrzeby.
Po trzecie, unikasz zbierania śmieci obiektu, który nie był używany.
Wreszcie, łatwiej jest obsługiwać wyjątki inicjalizacji, które mogą wystąpić we właściwości, niż wyjątki, które występują podczas inicjowania zmiennych poziomu klasy lub konstruktora.
Istnieją wyjątki od tej reguły.
Jeśli chodzi o argument wydajności dodatkowego sprawdzenia inicjalizacji we właściwości „get”, jest on nieistotny. Inicjowanie i usuwanie obiektu jest bardziej znaczącym spadkiem wydajności niż proste sprawdzenie wskaźnika zerowego ze skokiem.
Wytyczne projektowe dotyczące tworzenia bibliotek klas pod adresem http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
Jeżeli chodzi o
Lazy<T>
Lazy<T>
Klasa ogólna została utworzona dokładnie dla tego, czego chce plakat, zobacz Lazy Initialization pod adresem http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx . Jeśli masz starsze wersje .NET, musisz użyć wzorca kodu pokazanego w pytaniu. Ten wzorzec kodu stał się tak powszechny, że Microsoft uznał za stosowne dołączyć klasę do najnowszych bibliotek .NET, aby ułatwić implementację wzorca. Ponadto, jeśli Twoja implementacja wymaga zabezpieczenia wątków, musisz je dodać.Prymitywne typy danych i proste klasy
Obvioulsy, nie zamierzasz używać inicjalizacji z opóźnieniem dla pierwotnych typów danych lub prostych zastosowań klas, takich jak
List<string>
.Przed skomentowaniem leniwego
Lazy<T>
został wprowadzony w .NET 4.0, dlatego prosimy o nie dodawanie kolejnego komentarza dotyczącego tej klasy.Przed komentowaniem na temat mikrooptymalizacji
Podczas budowania bibliotek należy wziąć pod uwagę wszystkie optymalizacje. Na przykład w klasach .NET zobaczysz tablice bitowe używane dla zmiennych klas logicznych w całym kodzie w celu zmniejszenia zużycia pamięci i fragmentacji pamięci, żeby nazwać dwie „mikro-optymalizacje”.
Odnośnie interfejsów użytkownika
Nie zamierzasz używać inicjalizacji z opóźnieniem dla klas, które są bezpośrednio używane przez interfejs użytkownika. W zeszłym tygodniu spędziłem większą część dnia na usuwaniu leniwego ładowania ośmiu kolekcji używanych w modelu widoku dla pól kombi. Mam,
LookupManager
który obsługuje leniwe ładowanie i buforowanie kolekcji potrzebnych przez dowolny element interfejsu użytkownika.„Setters”
Nigdy nie używałem set-property („setters”) dla żadnej leniwie ładowanej właściwości. Dlatego nigdy byś nie pozwolił
foo.Bar = null;
. Jeśli chcesz ustawićBar
, utworzyłbym metodę o nazwieSetBar(Bar value)
i nie używałbym inicjalizacji z opóźnieniemKolekcje
Właściwości kolekcji klas są zawsze inicjowane po zadeklarowaniu, ponieważ nigdy nie powinny mieć wartości null.
Klasy złożone
Powtórzę to inaczej, używasz inicjalizacji leniwej dla klas złożonych. Zwykle są to słabo zaprojektowane klasy.
W końcu
Nigdy nie powiedziałem, żebym robił to dla wszystkich klas lub we wszystkich przypadkach. To zły nawyk.
źródło
Czy rozważasz wdrożenie takiego wzorca za pomocą
Lazy<T>
?Oprócz łatwego tworzenia obiektów ładowanych z opóźnieniem, podczas inicjalizacji obiektu można uzyskać bezpieczeństwo wątków:
Jak powiedzieli inni, leniwie ładujesz obiekty, jeśli są naprawdę ciężkie lub ładowanie ich zajmuje trochę czasu podczas budowy obiektu.
źródło
Lazy<T>
teraz i powstrzymam się od używania tego, co zawsze.Making the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
Myślę, że to zależy od tego, co inicjujesz. Prawdopodobnie nie zrobiłbym tego dla listy, ponieważ koszt budowy jest dość niewielki, więc może wejść do konstruktora. Ale gdyby była to wstępnie wypełniona lista, prawdopodobnie nie zrobiłbym tego, dopóki nie byłaby potrzebna po raz pierwszy.
Zasadniczo, jeśli koszt budowy przewyższa koszt wykonania warunkowego sprawdzenia każdego dostępu, to leniwie je twórz. Jeśli nie, zrób to w konstruktorze.
źródło
Wadą, którą widzę, jest to, że jeśli chcesz zapytać, czy Bars jest zerowy, nigdy by tak nie było, i tworzysz tam listę.
źródło
null
, ale nie odzyskuj. Zwykle zakładasz, że otrzymujesz z powrotem tę samą wartość, jaką przywiązujesz do właściwości.Leniwa instancja / inicjalizacja jest wzorcem doskonale wykonalnym. Należy jednak pamiętać, że z reguły konsumenci twojego API nie oczekują, że metody pobierające i ustawiające będą zajmować dostrzegalny czas od punktu widzenia użytkownika końcowego (lub zawiodą).
źródło
Chciałem tylko skomentować odpowiedź Daniela, ale szczerze mówiąc, nie sądzę, żeby to zaszło wystarczająco daleko.
Chociaż jest to bardzo dobry wzorzec do użycia w niektórych sytuacjach (na przykład, gdy obiekt jest inicjowany z bazy danych), jest to STRASZNY nawyk.
Jedną z najlepszych cech obiektu jest to, że oferuje bezpieczne, zaufane środowisko. Najlepszym przypadkiem jest utworzenie jak największej liczby pól „Final”, wypełniając je wszystkie konstruktorem. To sprawia, że twoja klasa jest dość kuloodporna. Zezwolenie na zmianę pól za pomocą seterów jest trochę mniej, ale nie straszne. Na przykład:
W przypadku Twojego wzorca metoda toString wyglądałaby następująco:
Nie tylko to, ale potrzebujesz sprawdzeń null wszędzie tam, gdzie prawdopodobnie możesz użyć tego obiektu w swojej klasie (Poza twoją klasą jest bezpieczna ze względu na zerowe sprawdzenie w getterze, ale powinieneś głównie używać członków swoich klas wewnątrz klasy)
Również twoja klasa jest nieustannie w niepewnym stanie - na przykład, jeśli zdecydowałeś się uczynić tę klasę klasą hibernacyjną, dodając kilka adnotacji, jak byś to zrobił?
Jeśli podejmiesz jakąkolwiek decyzję w oparciu o jakąś mikrooptomizację bez wymagań i testów, prawie na pewno jest to zła decyzja. W rzeczywistości istnieje naprawdę duża szansa, że twój wzorzec faktycznie spowalnia system nawet w najbardziej idealnych okolicznościach, ponieważ instrukcja if może spowodować błąd przewidywania gałęzi na procesorze, co spowolni proces wiele, wiele razy więcej niż po prostu przypisanie wartości w konstruktorze, chyba że tworzony obiekt jest dość złożony lub pochodzi ze zdalnego źródła danych.
Aby zapoznać się z przykładem problemu z przewidywaniem brance (który występuje wielokrotnie, a nie tylko raz), zobacz pierwszą odpowiedź na to niesamowite pytanie: Dlaczego szybciej przetwarzać posortowaną tablicę niż nieposortowaną?
źródło
toString()
wywołujegetName()
, a nie używaname
bezpośrednio.Pozwólcie, że dodam jeszcze jeden punkt do wielu dobrych uwag innych ...
Debugger ( domyślnie ) oceni właściwości podczas przechodzenia przez kod, co może potencjalnie spowodować utworzenie instancji
Bar
wcześniej niż normalnie, po prostu wykonując kod. Innymi słowy, zwykłe debugowanie zmienia sposób wykonywania programu.Może to stanowić problem lub nie (w zależności od skutków ubocznych), ale należy o tym pamiętać.
źródło
Czy na pewno Foo powinno w ogóle tworzyć instancję?
Wydaje mi się śmierdzące (choć niekoniecznie złe ) pozwalanie Foo na tworzenie instancji w ogóle. O ile wyraźnym celem Foo nie jest bycie fabryką, nie powinno tworzyć instancji swoich własnych współpracowników, ale zamiast tego wstawiać ich do swojego konstruktora .
Jeśli jednak celem istnienia Foo jest tworzenie instancji typu Bar, to nie widzę nic złego w robieniu tego leniwie.
źródło