Dobra czy zła praktyka? Inicjalizacja obiektów w getterze

167

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!

John Willemse
źródło
14
To jest wzorzec leniwego ładowania, nie daje on tutaj wspaniałych korzyści, ale nadal jest dobrą rzeczą imho.
Machinarius
28
Leniwe tworzenie instancji ma sens, jeśli masz wymierny wpływ na wydajność lub jeśli te elementy członkowskie są rzadko używane i zużywają ogromną ilość pamięci, lub jeśli ich utworzenie zajmuje dużo czasu i chcesz to zrobić tylko na żądanie. W każdym razie pamiętaj o uwzględnieniu problemów z bezpieczeństwem wątków (Twój obecny kod nie jest ) i rozważ użycie podanej klasy Lazy <T> .
Chris Sinclair,
10
Myślę, że to pytanie lepiej pasuje do codereview.stackexchange.com
Eduardo Brites
7
@PLB to nie jest pojedynczy wzorzec.
Colin Mackay,
30
Dziwię się, że nikt nie wspomniał o poważnym błędzie w tym kodzie. Masz własność publiczną, którą mogę ustawić z zewnątrz. Jeśli ustawię ją na NULL, zawsze utworzysz nowy obiekt i zignorujesz mój dostęp ustawiający. To może być bardzo poważny błąd. W przypadku nieruchomości prywatnych może to być w porządku. Osobiście nie lubię dokonywać takich przedwczesnych optymalizacji. Dodaje złożoności bez dodatkowych korzyści.
SolutionYogi

Odpowiedzi:

170

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ą:

  1. 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 = null;
    Assert.Null(foo.Bar); // This will fail
    
  2. Wprowadza sporo problemów z foo.Barwątkami : dwóch wywołujących w różnych wątkach może potencjalnie otrzymać dwie różne instancje, Bara jeden z nich będzie bez połączenia z Fooinstancją. Wszelkie zmiany wprowadzone w tej Barinstancji 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:

  1. 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:

    Unikaj zgłaszania wyjątków z metod pobierających właściwości.

    Metody pobierające właściwości powinny być prostymi operacjami bez żadnych warunków wstępnych. Jeśli metoda pobierająca może zgłosić wyjątek, rozważ przeprojektowanie właściwości na metodę.

  2. 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:

  1. Serializacja:
    EricJ stwierdza w jednym komentarzu:

    Obiekt, który może być serializowany, nie będzie miał wywoływanego konstruktora, gdy zostanie zdeserializowany (zależy od serializatora, ale wiele typowych zachowuje się w ten sposób). Umieszczenie kodu inicjującego w konstruktorze oznacza, że ​​musisz zapewnić dodatkową obsługę deserializacji. Ten wzorzec pozwala uniknąć tego specjalnego kodowania.

    Z tym argumentem wiąże się kilka problemów:

    1. Większość obiektów nigdy nie zostanie serializowana. Dodanie jakiegoś wsparcia, gdy nie jest to potrzebne, narusza YAGNI .
    2. Gdy klasa musi obsługiwać serializację, istnieją sposoby na włączenie jej bez obejścia, które na pierwszy rzut oka nie ma nic wspólnego z serializacją.
  2. 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:

    1. W większości przypadków kilka dodatkowych obiektów w pamięci nie ma żadnego wpływu na cokolwiek. Nowoczesne komputery mają wystarczająco dużo pamięci. Bez przypadku rzeczywistych problemów potwierdzonych przez profilera jest to przedwczesna optymalizacja i są ku temu dobre powody.
    2. 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:

      1. Leniwa inicjalizacja potencjalnie obniża wydajność. Może tylko nieznacznie, ale jak pokazała odpowiedź Billa, wpływ jest większy, niż mogłoby się wydawać na pierwszy rzut oka. Więc to podejście zasadniczo polega na zamianie wydajności w porównaniu z pamięcią.
      2. Jeśli masz projekt, w którym typowym przypadkiem użycia jest użycie tylko części klasy, wskazuje to na problem z samym projektem: dana klasa najprawdopodobniej ma więcej niż jedną odpowiedzialność. Rozwiązaniem byłoby podzielenie klasy na kilka bardziej skoncentrowanych klas.
Daniel Hilgarth
źródło
4
@JohnWillemse: To problem twojej architektury. Powinieneś zmienić swoje klasy w taki sposób, aby były mniejsze i bardziej skoncentrowane. Nie twórz jednej klasy dla 5 różnych rzeczy / zadań. Zamiast tego utwórz 5 klas.
Daniel Hilgarth
26
@JohnWillemse Być może rozważ to zatem przypadek przedwczesnej optymalizacji. Jeśli nie masz zmierzonego wąskiego gardła wydajności / pamięci, odradzam to, ponieważ zwiększa złożoność i wprowadza problemy z wątkami.
Chris Sinclair,
2
+1, to nie jest dobry wybór projektowy dla 95% klas. Leniwa inicjalizacja ma swoje zalety, ale nie powinna być generalizowana dla WSZYSTKICH właściwości. Dodaje złożoność, trudności w czytaniu kodu, problemy z bezpieczeństwem wątków ... dla braku dostrzegalnej optymalizacji w 99% przypadków. Ponadto, jak powiedział SolutionYogi w komentarzu, kod OP jest błędny, co dowodzi, że ten wzorzec nie jest trywialny w implementacji i należy go unikać, chyba że faktycznie potrzebna jest leniwa inicjalizacja.
ken2k
2
@DanielHilgarth Dzięki za zapisanie (prawie) wszystkiego złego w bezwarunkowym używaniu tego wzorca. Dobra robota!
Alex
1
@DanielHilgarth Cóż, tak i nie. Naruszenie jest tutaj problemem, więc tak. Ale także „nie”, ponieważ POLS to ściśle zasada, że kod prawdopodobnie Cię nie zaskoczy . Jeśli Foo nie zostało ujawnione poza twoim programem, jest to ryzyko, które możesz podjąć lub nie. W tym przypadku mogę niemal zagwarantować, że w końcu będziesz zaskoczony , ponieważ nie kontrolujesz sposobu uzyskiwania dostępu do właściwości. Ryzyko stało się błędem, a twoja argumentacja w tej nullsprawie stała się znacznie silniejsza. :-)
atlaste
49

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, LookupManagerktó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 nazwie SetBar(Bar value)i nie używałbym inicjalizacji z opóźnieniem

Kolekcje

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.

AMissico
źródło
6
Jeśli możesz wywołać foo.Bar wiele razy w różnych wątkach bez żadnych pośrednich ustawień wartości, ale otrzymujesz różne wartości, to masz kiepską kiepską klasę.
Lie Ryan,
25
Myślę, że to zła zasada bez wielu rozważań. O ile Bar nie jest znawcą zasobów, jest to niepotrzebna mikro optymalizacja. W przypadku, gdy Bar wymaga dużej ilości zasobów, istnieje bezpieczny wątek Lazy <T> wbudowany w .net.
Andrew Hanlon,
10
„łatwiej jest obsługiwać wyjątki inicjowania, które mogą wystąpić we właściwości, niż wyjątki, które występują podczas inicjowania zmiennych poziomu klasy lub konstruktora”. - okej, to jest głupie. Jeśli z jakiegoś powodu obiekt nie może zostać zainicjowany, chcę wiedzieć jak najszybciej. To znaczy natychmiast po zbudowaniu. Istnieją dobre argumenty przemawiające za używaniem leniwej inicjalizacji, ale nie sądzę, aby było dobrym pomysłem używanie jej na szeroką skalę .
millimoose
20
Naprawdę martwię się, że inni programiści zobaczą tę odpowiedź i pomyślą, że to naprawdę dobra praktyka (o rany). Jest to bardzo, bardzo zła praktyka, jeśli używasz jej bezwarunkowo. Poza tym, co już zostało powiedziane, utrudniasz życie wszystkim (programistom klienckim i programistom utrzymania ruchu) przy tak niewielkich korzyściach (jeśli w ogóle w ogóle). Powinieneś usłyszeć od profesjonalistów: Donald Knuth, w serii The Art of Computer Programming, powiedział, że „przedwczesna optymalizacja jest źródłem wszelkiego zła”. To, co robisz, jest nie tylko złe, jest diabelskie!
Alex
4
Jest tak wiele wskaźników, że wybierasz złą odpowiedź (i złą decyzję programistyczną). Na swojej liście masz więcej wad niż zalet. Więcej ludzi ręczy za to niż za to. Bardziej doświadczeni członkowie tej strony (@BillK i @DanielHilgarth), którzy napisali w tym pytaniu, są temu przeciwni. Twój współpracownik już Ci powiedział, że to źle. Poważnie, to źle! Jeśli przyłapię jednego z programistów mojego zespołu (jestem liderem zespołu) na tym, zrobi to 5 minut, a następnie zostanie pouczony, dlaczego nigdy nie powinien tego robić.
Alex
17

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.

Matías Fidemraizer
źródło
Dzięki, teraz to rozumiem i na pewno przyjrzę się Lazy<T>teraz i powstrzymam się od używania tego, co zawsze.
John Willemse,
1
Nie masz magicznego zabezpieczenia nici ... nadal musisz o tym pomyśleć. Z MSDN: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.
Eric J.
@EricJ. Oczywiscie oczywiscie. Bezpieczeństwo wątków uzyskujesz tylko podczas inicjowania obiektu, ale później musisz zająć się synchronizacją jak każdym innym obiektem.
Matías Fidemraizer
9

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.

Colin Mackay
źródło
Dziękuję Ci! To ma sens.
John Willemse
9

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ę.

Luis Tellez
źródło
Nie sądzę, żeby to była wada.
Peter Porfy,
Dlaczego to wada? Po prostu sprawdź z any zamiast null. if (! Foo.Bars.Any ())
s.meijer
6
@PeterPorfy: To narusza POLS . Włożyłeś null, ale nie odzyskuj. Zwykle zakładasz, że otrzymujesz z powrotem tę samą wartość, jaką przywiązujesz do właściwości.
Daniel Hilgarth,
@DanielHilgarth Jeszcze raz dziękuję. To bardzo ważny argument, którego wcześniej nie rozważałem.
John Willemse,
6
@AMissico: To nie jest zmyślona koncepcja. W podobny sposób, w jaki oczekuje się, że naciśnięcie przycisku obok drzwi wejściowych zadzwoni do drzwi, rzeczy, które wyglądają jak nieruchomości, powinny zachowywać się jak nieruchomości. Otwarcie zapadni pod stopami to zaskakujące zachowanie, zwłaszcza jeśli przycisk nie jest tak oznaczony.
Bryan Boettcher,
8

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ą).

Tormod
źródło
1
Zgadzam się i trochę zredagowałem moje pytanie. Oczekuję, że pełny łańcuch konstruktorów bazowych zajmie więcej czasu niż tworzenie instancji klas tylko wtedy, gdy są potrzebne.
John Willemse,
8

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:

klasa SafeClass
{
    Nazwa ciągu = "";
    Całkowity wiek = 0;

    public void setName (String newName)
    {
        assert (newName! = null)
        name = newName;
    } // podążaj za tym wzorem dla wieku
    ...
    public String toString () {
        String s = „Bezpieczna klasa ma nazwę:„ + nazwa + ”i wiek:„ + wiek
    }
}

W przypadku Twojego wzorca metoda toString wyglądałaby następująco:

    if (name == null)
        zgłoś nowy IllegalStateException ("SafeClass przeszedł w niedozwolony stan! nazwa ma wartość null")
    if (wiek == null)
        zgłoś nowy wyjątek IllegalStateException ("SafeClass przeszedł w niedozwolony stan! wiek jest pusty")

    public String toString () {
        String s = „Bezpieczna klasa ma nazwę:„ + nazwa + ”i wiek:„ + wiek
    }

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ą?

Bill K.
źródło
Dzięki za wkład. W moim przypadku żadna z klas nie ma żadnych metod, które mogą wymagać sprawdzenia wartości null, więc nie stanowi to problemu. Uwzględnię Twoje inne zastrzeżenia.
John Willemse,
Naprawdę tego nie rozumiem. Oznacza to, że nie używasz swoich członków w klasach, w których są przechowywane - że używasz klas tylko jako struktur danych. Jeśli tak jest, możesz przeczytać javaworld.com/javaworld/jw-01-2004/jw-0102-toolbox.html, który zawiera dobry opis tego, jak można ulepszyć kod, unikając zewnętrznej manipulacji stanem obiektu. Jeśli manipulujesz nimi wewnętrznie, jak to zrobić bez wielokrotnego sprawdzania zerowej wartości?
Bill K,
Części tej odpowiedzi są dobre, ale części wydają się wymyślone. Zwykle podczas korzystania z tego wzorca toString()wywołuje getName(), a nie używa namebezpośrednio.
Izkata
@BillK Tak, klasy to ogromna struktura danych. Cała praca odbywa się w klasach statycznych. Sprawdzę artykuł pod linkiem. Dzięki!
John Willemse,
1
@izkata Właściwie na zajęciach wydaje się, że rzuca się w oczy, czy używasz gettera, czy nie, większość miejsc, w których pracowałem, używało członka bezpośrednio. Poza tym, jeśli zawsze używasz metody pobierającej, metoda if () jest jeszcze bardziej szkodliwa, ponieważ błędy predykcji gałęzi będą występować częściej ORAZ z powodu rozgałęzień środowisko wykonawcze prawdopodobnie będzie miało więcej problemów z wbudowaniem metody pobierającej. Wszystko jest jednak dyskusyjne, w związku z odkryciem Johna, że ​​są to struktury danych i klasy statyczne, to jest rzecz, na której byłbym najbardziej zainteresowany.
Bill K
4

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 Barwcześ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ć.

Branko Dimitrijevic
źródło
2

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.

KaptajnKold
źródło
4
@BenjaminGruenbaum Nie, nie bardzo. I z szacunkiem, nawet gdyby tak było, jaki cel starałeś się nadać?
KaptajnKold