Czy funkcje powinny zwracać wartość null czy pusty obiekt?

209

Jaka jest najlepsza praktyka przy zwracaniu danych z funkcji. Czy lepiej jest zwrócić wartość Null lub pusty obiekt? I dlaczego jeden powinien robić jeden nad drugim?

Rozważ to:

public UserEntity GetUserById(Guid userId)
{
     //Imagine some code here to access database.....

     //Check if data was returned and return a null if none found
     if (!DataExists)
        return null; 
        //Should I be doing this here instead? 
        //return new UserEntity();  
     else
        return existingUserEntity;
}

Udawajmy, że w tym programie byłyby prawidłowe przypadki, że w bazie danych o tym identyfikatorze GUID nie byłoby informacji o użytkowniku. Wyobrażam sobie, że w tym przypadku nie byłoby właściwe rzucenie wyjątku? Mam również wrażenie, że obsługa wyjątków może zaszkodzić wydajności.

7wp
źródło
4
Myślę, że masz na myśli if (!DataExists).
Sarah Vessels
107
To pytanie architektoniczne i jest całkowicie właściwe. Pytanie PO jest ważne bez względu na problem biznesowy, który próbuje rozwiązać.
Joseph Ferris
2
Odpowiedź na to pytanie jest już wystarczająca. Myślę, że to bardzo interesujące pytanie.
John Scipione
„getUser ()” powinno zwrócić null. „getCurrentUserInfo ()” lub „getCurrentPermissions ()”, OTOH, byłyby bardziej ujawniającymi pytaniami - powinny zwrócić obiekt o wartości innej niż null, niezależnie od tego, kto / lub czy ktoś jest zalogowany.
Thomas W
2
Nie @Bergi, ten drugi jest duplikatem. Mój zapytano pierwszy, w październiku, drugi zapytano 3 miesiące później w grudniu. Plus drugi mówi o kolekcji, która jest nieco inna.
7wp

Odpowiedzi:

207

Zwrócenie wartości null jest zwykle najlepszym pomysłem, jeśli zamierzasz wskazać, że żadne dane nie są dostępne.

Pusty obiekt oznacza, że ​​dane zostały zwrócone, a zwrócenie wartości null wyraźnie wskazuje, że nic nie zostało zwrócone.

Ponadto zwrócenie wartości NULL spowoduje wyjątek NULL, jeśli spróbujesz uzyskać dostęp do elementów w obiekcie, co może być przydatne do wyróżnienia błędnego kodu - próba uzyskania dostępu do elementu niczego nie ma sensu. Dostęp do członków pustego obiektu nie zawiedzie, co oznacza, że ​​błędy mogą pozostać nieodkryte.

ljs
źródło
21
Powinieneś zgłaszać wyjątek, nie połykać problemu i zwracać wartość null. Co najmniej powinieneś to zalogować, a następnie kontynuować.
Chris Ballance
130
@Chris: Nie zgadzam się. Jeśli kod wyraźnie udokumentuje, że zwracana wartość jest równa null, jest całkowicie dopuszczalne, aby zwracać wartość null, jeśli nie znaleziono żadnego wyniku spełniającego podane kryteria. Zgłoszenie wyjątku powinno być Twoim OSTATNIM wyborem, a nie PIERWSZYM.
Mike Hofer
12
@Chris: Na jakiej podstawie to decydujesz? Dodanie rejestrowania do równania z pewnością wydaje się nadmierne. Niech konsumujący kod decyduje, co - jeśli cokolwiek - należy zrobić w przypadku braku użytkownika. Podobnie jak w moim poprzednim komentarzu, absolutnie nie ma problemu ze zwróceniem wartości, która jest powszechnie definiowana jako „brak danych”.
Adam Robinson
17
Jestem trochę zaskoczony, że programista Microsoft uważa, że ​​„zwrócenie wartości zerowej” oznacza „przełknięcie problemu”. Jeśli pamięć jest obsługiwana, w systemie Framework istnieją tony metod, w których zwracane są metody null, jeśli nie ma nic, co pasowałoby do żądania dzwoniącego. Czy to „połyka problem?”
Mike Hofer
5
Na bool GetUserById(Guid userId, out UserEntity result)koniec byłyby - które wolałbym od wartości zerowej „null” i które nie są tak ekstremalne, jak zgłaszanie wyjątku. Pozwala na piękny, nullwolny od kodu kod if(GetUserById(x,u)) { ... }.
Marcel Jackwerth
44

To zależy od tego, co ma największy sens dla twojej sprawy.

Czy warto zwracać null, np. „Taki użytkownik nie istnieje”?

Czy ma sens tworzenie domyślnego użytkownika? Jest to najbardziej sensowne, gdy można bezpiecznie założyć, że jeśli użytkownik NIE istnieje, kod wywołujący ma zamiar istnieć, gdy o to poprosi.

Czy ma sens rzucenie wyjątku (a la „FileNotFound”), jeśli kod wywołujący wymaga użytkownika z niepoprawnym identyfikatorem?

Jednak - z punktu widzenia oddzielenia obaw / punktu widzenia SRP, pierwsze dwa są bardziej poprawne. I technicznie pierwszy jest najbardziej poprawny (ale tylko przez włosy) - GetUserById powinien być odpowiedzialny tylko za jedną rzecz - zdobycie użytkownika. Obsługa własnego przypadku „użytkownik nie istnieje” przez zwrócenie czegoś innego może stanowić naruszenie SRP. Rozdzielenie na inną czek - bool DoesUserExist(id)byłoby właściwe, jeśli zdecydujesz się rzucić wyjątek.

W oparciu o obszerne komentarze poniżej : jeśli jest to pytanie projektowe na poziomie API, ta metoda może być analogiczna do „OpenFile” lub „ReadEntireFile”. „Otwieramy” użytkownika z jakiegoś repozytorium i hydratujemy obiekt z powstałych danych. W tym przypadku odpowiedni może być wyjątek . Może nie być, ale może być.

Wszystkie podejścia są akceptowalne - to po prostu zależy od szerszego kontekstu API / aplikacji.

Rex M.
źródło
Ktoś głosował za tobą, a ja głosowałem za tobą, ponieważ nie wydaje mi się to złą odpowiedzią; z wyjątkiem: nigdy nie rzucałbym wyjątku, gdy nie znalazłem użytkownika w metodzie takiej jak ta, którą daje plakat. Jeśli znalezienie żadnego użytkownika nie sugeruje nieprawidłowego identyfikatora lub jakiegoś takiego wartego wyjątku problemu, powinno to nastąpić wyżej - metoda rzucania musi wiedzieć więcej o tym, skąd pochodzi ten identyfikator itp.
Jacob Mattison,
(Zgaduję, że głos był sprzeciwem wobec pomysłu wprowadzenia wyjątku w takich okolicznościach.)
Jacob Mattison,
1
Uzgodnione do ostatniego punktu. Nie ma naruszenia SRP przez zwrócenie wartości, która jest powszechnie zdefiniowana jako „brak danych”. To tak, jakby stwierdzenie, że baza danych SQL powinna zwrócić błąd, jeśli klauzula where nie daje wyników. Chociaż wyjątek jest prawidłowym wyborem projektowym (choć może irytować mnie jako konsumenta), nie jest „bardziej poprawny” niż zwracanie wartości null. I nie, nie jestem DV.
Adam Robinson
@JacobM zgłaszamy wyjątki, gdy wymagamy ścieżki systemu plików, która nie istnieje, nie zwraca wartości null, ale nie z baz danych. Jasne jest, że oba są odpowiednie, o co mi chodzi - to po prostu zależy.
Rex M
2
@Charles: Odpowiadasz na pytanie „w pewnym momencie powinien zostać zgłoszony wyjątek”, ale pytanie brzmi „czy ta funkcja zgłasza wyjątek”. Prawidłowa odpowiedź to „może”, a nie „tak”.
Adam Robinson
30

Osobiście używam NULL. Wyjaśnia, że ​​nie ma danych do zwrócenia. Ale zdarzają się przypadki, gdy Null Object może być przydatny.

Fernando
źródło
Właśnie dodam to jako odpowiedź. Wzór NullObjectPattern lub specjalny przypadek. Następnie możesz wdrożyć po jednym dla każdej sprawy, NoUserEntitiesFound, NullUserEntities itp.
David Swindells
27

Jeśli typem zwracanym jest tablica, zwróć pustą tablicę, w przeciwnym razie zwróć null.

Darin Dimitrov
źródło
Czy w tej chwili 0 pozycji na liście jest tym samym, co lista nieprzypisana?
AnthonyWJones
3
0 pozycji na liście to nie to samo co null. Pozwala na używanie go w foreachinstrukcjach i zapytaniach linq bez obaw NullReferenceException.
Darin Dimitrov
5
Dziwi mnie, że nie zostało to już więcej ocenione. Wydaje mi się to dość rozsądną wskazówką.
shashi
Cóż, pusty kontener to tylko określone wystąpienie wzorca obiektu zerowego. Co może być właściwe, nie możemy powiedzieć.
Deduplicator
Zwracanie pustej tablicy, gdy dane nie są dostępne, jest po prostu błędne . Istnieje różnica między dostępnymi danymi i niezawierającymi elementów a danymi niedostępnymi. Zwrócenie pustej tablicy w obu przypadkach uniemożliwia ustalenie, która jest przypadek. Robienie tego tylko po to, abyś mógł użyć foreach bez sprawdzania, czy dane w ogóle istnieją, jest głupie - osoba dzwoniąca powinna sprawdzić, czy dane istnieją, a wyjątek NullReferenceException, jeśli osoba dzwoniąca nie sprawdza, jest dobra, ponieważ ujawnia błąd ..
Przywróć Monikę
12

Powinieneś zgłosić wyjątek (tylko), jeśli konkretna umowa zostanie zerwana.
W twoim konkretnym przykładzie, prosząc o UserEntity na podstawie znanego identyfikatora, byłoby to zależne od tego, czy brak (usuniętych) użytkowników jest oczekiwanym przypadkiem. Jeśli tak, to wróć, nullale jeśli nie jest to oczekiwany przypadek, wyrzuć wyjątek.
Zauważ, że jeśli funkcja została wywołana UserEntity GetUserByName(string name), prawdopodobnie nie wyrzuciłaby, ale zwróciła null. W obu przypadkach zwrócenie pustej UserEntity byłoby nieprzydatne.

W przypadku ciągów, tablic i kolekcji sytuacja jest zwykle inna. Pamiętam pewne wytyczne z MS, że metody powinny akceptować nulljako „pustą” listę, ale zwracają kolekcje o zerowej długości null. To samo dotyczy ciągów znaków. Pamiętaj, że możesz zadeklarować puste tablice:int[] arr = new int[0];

Henk Holterman
źródło
Cieszę się, że wspomniałeś, że ciągi są różne, ponieważ Google pokazało mi to, kiedy decydowałem, czy zwrócić pusty ciąg.
Noumenon,
Ciągi, kolekcje i tablice nie różnią się. Jeśli stwardnienie rozsiane mówi, że stwardnienie rozsiane jest złe. Istnieje różnica między pustym łańcuchem a wartością null oraz między pustą kolekcją a wartością null. W obu przypadkach pierwsza reprezentuje istniejące dane (o rozmiarze 0), a druga reprezentuje brak danych. W niektórych przypadkach rozróżnienie jest bardzo ważne. Na przykład, szukając pozycji w pamięci podręcznej, chcesz poznać różnicę między danymi, które są buforowane, ale są puste, a danymi, które nie są buforowane, więc musisz pobrać je z podstawowego źródła danych, gdzie może nie być pusty.
Przywróć Monikę
1
Wydaje się, że brakuje Ci sensu i kontekstu. .Wher(p => p.Lastname == "qwerty")powinien zwrócić pustą kolekcję, a nie null.
Henk Holterman,
@HenkHolterman Jeśli masz dostęp do pełnej kolekcji i zastosujesz filtr, który nie przyjmuje żadnych elementów w kolekcji, pusta kolekcja jest poprawnym wynikiem. Ale jeśli pełna kolekcja nie istnieje, pusta kolekcja jest bardzo myląca - zerowanie lub wyrzucanie byłoby poprawne w zależności od tego, czy sytuacja jest normalna, czy wyjątkowa. Ponieważ twój post nie zakwalifikował, o której sytuacji mówiłeś (a teraz wyjaśniasz, że mówisz o pierwszej sytuacji), a OP opowiadał o drugiej sytuacji, muszę się z tobą nie zgodzić.
Przywróć Monikę
11

Jest to pytanie biznesowe, zależne od tego, czy istnienie użytkownika o określonym identyfikatorze GUID jest oczekiwanym normalnym przypadkiem użycia tej funkcji, czy też jest to anomalia, która uniemożliwia aplikacji pomyślne wykonanie dowolnej funkcji, którą ta metoda zapewnia użytkownikowi oponować...

Jeśli jest to „wyjątek”, ponieważ brak użytkownika o tym identyfikatorze uniemożliwi aplikacji pomyślne wykonanie dowolnej funkcji (powiedzmy, że tworzymy fakturę dla klienta, do którego wysłaliśmy produkt ... ), wówczas ta sytuacja powinna zgłosić wyjątek ArgumentException (lub inny niestandardowy wyjątek).

Jeśli brakujący użytkownik jest w porządku (jeden z potencjalnych normalnych wyników wywołania tej funkcji), zwróć wartość null ....

EDYCJA: (aby odpowiedzieć na komentarz Adama w innej odpowiedzi)

Jeśli aplikacja zawiera wiele procesów biznesowych, z których co najmniej jeden wymaga pomyślnego ukończenia przez użytkownika, a co najmniej jeden z nich może zakończyć się pomyślnie bez użytkownika, wyjątek powinien zostać zgłoszony dalej w górę stosu wywołań, bliżej miejsca, w którym procesy biznesowe wymagające od użytkownika wywołują ten wątek wykonania. Metody między tą metodą a tym punktem (w którym zgłaszany jest wyjątek) powinny po prostu informować, że nie istnieje żaden użytkownik (null, boolean, cokolwiek - jest to szczegół implementacji).

Ale jeśli wszystkie procesy w aplikacji wymagają użytkownika, nadal rzucałbym wyjątek w tej metodzie ...

Charles Bretana
źródło
-1 do downvocera, +1 do Charlesa - to całkowicie pytanie biznesowe i nie ma na to najlepszej praktyki.
Austin Salonen
Przecina strumienie. To, czy jest to „warunek błędu”, zależy od logiki biznesowej. Jak sobie z tym poradzić, to decyzja dotycząca architektury aplikacji. Logika biznesowa nie będzie narzucać zwracania wartości null, tylko spełnienie wymagań. Jeśli firma decyduje o typach zwracanych metod, są one zbyt zaangażowane w techniczny aspekt wdrożenia.
Joseph Ferris
@Joseph, podstawową zasadą „strukturalnej obsługi wyjątków” jest to, że wyjątki powinny być zgłaszane, gdy metody nie są w stanie wykonać żadnej funkcji, którą zostały zakodowane w celu ich zaimplementowania. Masz rację, jeśli funkcja biznesowa, którą ta metoda została zaimplementowana, może zostać „pomyślnie zakończona” (cokolwiek to oznacza w modelu domeny), to nie musisz zgłaszać wyjątku, możesz zwrócić wartość null , lub logiczną zmienną „FoundUser”, lub cokolwiek ... Sposób, w jaki komunikujesz się z metodą wywołującą, że żaden użytkownik nie został znaleziony, staje się technicznym szczegółem implementacji.
Charles Bretana,
10

Osobiście zwróciłbym wartość null, ponieważ w ten sposób oczekiwałbym działania warstwy DAL / repozytorium.

Jeśli nie istnieje, nie zwracaj niczego, co można by interpretować jako pomyślne pobranie obiektu, nulldziała tutaj pięknie.

Najważniejszą rzeczą jest zachowanie spójności w warstwie DAL / Repos, dzięki czemu nie będziesz się mylić, jak z niej korzystać.

Alex Moore
źródło
7

mam tendencję żeby

  • return nulljeśli identyfikator obiektu nie istnieje, gdy nie wiadomo wcześniej, czy powinien istnieć.
  • throwjeśli identyfikator obiektu nie istnieje, kiedy powinien istnieć.

Rozróżniam te dwa scenariusze za pomocą tych trzech rodzajów metod. Pierwszy:

Boolean TryGetSomeObjectById(Int32 id, out SomeObject o)
{
    if (InternalIdExists(id))
    {
        o = InternalGetSomeObject(id);

        return true;
    }
    else
    {
        return false;
    }
}

Druga:

SomeObject FindSomeObjectById(Int32 id)
{
    SomeObject o;

    return TryGetObjectById(id, out o) ? o : null;
}

Trzeci:

SomeObject GetSomeObjectById(Int32 id)
{
    SomeObject o;

    if (!TryGetObjectById(id, out o))
    {
        throw new SomeAppropriateException();
    }

    return o;
}
Johann Gerell
źródło
Chcesz powiedzieć, że outnieref
Matt Ellen
@Matt: Tak, proszę pana, z pewnością tak! Naprawiony.
Johann Gerell,
2
Naprawdę, jest to jedyna odpowiedź uniwersalna, a zatem całkowicie i wyłącznie prawda! :) Tak, to zależy od założeń, na podstawie których wywoływana jest metoda ... Najpierw wyjaśnij te założenia, a następnie wybierz odpowiednią kombinację powyższych. Musiałem przewinąć za dużo, aby się tu dostać :) +100
yair
Wydaje się, że jest to wzorzec do użycia, ale nie obsługuje on metod asynchronicznych. Odniosłem się do tej odpowiedzi i dodałem asynchroniczne rozwiązanie z Tuple Literals -> tutaj
ttugates
6

Jeszcze inne podejście polega na przekazaniu obiektu wywołania zwrotnego lub delegata, który będzie działał na wartości. Jeśli wartość nie zostanie znaleziona, wywołanie zwrotne nie zostanie wywołane.

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
}

Działa to dobrze, gdy chcesz uniknąć sprawdzania wartości NULL w całym kodzie, a gdy nie można znaleźć wartości, nie jest to błąd. Możesz również zapewnić oddzwonienie, gdy nie zostaną znalezione żadne obiekty, jeśli potrzebujesz specjalnego przetwarzania.

public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
    else
        notFound(); // or notFound.Call();
}

To samo podejście z użyciem pojedynczego obiektu może wyglądać następująco:

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback.Found(userEntity);
    else
        callback.NotFound();
}

Z perspektywy projektowania bardzo podoba mi się to podejście, ale ma tę wadę, że sprawia, że ​​strona wywołująca jest obszerniejsza w językach, które nie obsługują łatwo funkcji pierwszej klasy.

Marc
źródło
Ciekawy. Kiedy zacząłeś mówić o delegatach, od razu zacząłem się zastanawiać, czy można tutaj zastosować wyrażenia Lambda.
7wp
Tak! Jak rozumiem, składnia C # 3.0 i wyższa lambda jest zasadniczo składniowym cukrem dla anonimowych delegatów. Podobnie w Javie, bez ładnej lambda lub anonimowej składni delegatów, możesz po prostu stworzyć anonimową klasę. To trochę brzydsze, ale może być bardzo przydatne. Przypuszczam, że te dni, mój C # przykład mógł użyć Func <UserEntity> lub coś podobnego zamiast nazwanego delegata, ale ostatni projekt C # miałem na wciąż używał wersji 2.
Marc
+1 Podoba mi się to podejście. Problemem jest jednak to, że nie jest konwencjonalny i nieznacznie zwiększa barierę wejścia do bazy kodu.
timoxley,
4

Używamy CSLA.NET i uważa, że ​​nieudane pobranie danych powinno zwrócić „pusty” obiekt. Jest to w rzeczywistości dość denerwujące, ponieważ wymaga konwencji sprawdzania, czy obj.IsNewraczej obj == null.

Jak wspomniano w poprzednim plakacie, zwracane wartości zerowe spowodują natychmiastową awarię kodu, zmniejszając prawdopodobieństwo problemów z ukrywaniem spowodowanych przez puste obiekty.

Osobiście uważam, że nulljest bardziej elegancki.

Jest to bardzo częsty przypadek i jestem zaskoczony, że ludzie tutaj wydają się tym zaskoczeni: w dowolnej aplikacji internetowej dane są często pobierane przy użyciu parametru querystring, który można oczywiście zniekształcić, co wymaga od programisty obsługi przypadków „nie znaleziono” „.

Możesz sobie z tym poradzić:

if (User.Exists (id)) {
  this.User = User.Fetch (id);
} else {
  Response.Redirect ("~ / notfound.aspx");
}

... ale to dodatkowe połączenie z bazą danych za każdym razem, co może być problemem na stronach o dużym ruchu. Natomiast:

this.User = User.Fetch (id);

if (this.User == null) {
  Response.Redirect ("~ / notfound.aspx");
}

... wymaga tylko jednego połączenia.

Keith Williams
źródło
4

Wolę null, ponieważ jest zgodny z operatorem zerowania koalescencji ( ??).

nikt
źródło
4

Powiedziałbym, że return null zamiast pustego obiektu.

Ale w konkretnej instancji, o której tu wspomniałeś, szukasz użytkownika według identyfikatora użytkownika, który jest swego rodzaju kluczem do tego użytkownika, w takim przypadku prawdopodobnie chciałbym zgłosić wyjątek, jeśli nie znaleziono instancji użytkownika .

Zasadę tę ogólnie przestrzegam:

  • Jeśli nie znaleziono wyniku operacji znalezienia przez operację klucza podstawowego, należy zgłosić wyjątek ObjectNotFoundException.
  • Jeśli nie znaleziono wyniku dla znalezienia według innych kryteriów, zwróć null.
  • Jeśli wynik nie zostanie znaleziony według niekluczowych kryteriów, które mogą zwrócić wiele obiektów, zwróci pustą kolekcję.
Partha Choudhury
źródło
Dlaczego miałbyś zgłaszać wyjątek w którymkolwiek z tych przypadków? Czasami użytkownicy nie istnieją w bazie danych i spodziewamy się, że tak się nie stanie. To nie jest wyjątkowe zachowanie.
siride
3

Różni się w zależności od kontekstu, ale ogólnie zwracam null, jeśli szukam jednego konkretnego obiektu (jak w twoim przykładzie) i zwracam pustą kolekcję, jeśli szukam zestawu obiektów, ale nie ma żadnego.

Jeśli popełniłeś błąd w kodzie i zwracanie wartości null prowadzi do wyjątków wskaźnika null, wówczas im szybciej to zauważysz, tym lepiej. Jeśli zwrócisz pusty obiekt, jego początkowe użycie może działać, ale później mogą pojawić się błędy.

Jacob Mattison
źródło
+1 Kwestionowałem tę samą logikę, o której tu mówisz, dlatego opublikowałem pytanie, aby zobaczyć, jakie będą inne opinie na ten temat
7wp
3

Najlepsze w tym przypadku zwracają „null” w przypadku braku takiego użytkownika. Ustaw także metodę statyczną.

Edytować:

Zazwyczaj takie metody są członkami niektórych klas „User” i nie mają dostępu do członków instancji. W takim przypadku metoda powinna być statyczna, w przeciwnym razie musisz utworzyć instancję „Użytkownik”, a następnie wywołać metodę GetUserById, która zwróci inną instancję „Użytkownik”. Zgadzam się, że jest to mylące. Ale jeśli metoda GetUserById należy do klasy „DatabaseFactory” - nie ma problemu z pozostawieniem jej jako elementu instancji.

Kamarey
źródło
Czy mogę zapytać, dlaczego chciałbym, aby moja metoda była statyczna? Co się stanie, jeśli chcę użyć wstrzykiwania zależności?
7wp
Ok, teraz rozumiem twoją logikę. Ale trzymam się wzorca Repozytorium i lubię używać wstrzykiwania zależności dla moich repozytoriów, dlatego nie mogę używać metod statycznych. Ale +1 za sugerowanie zwrócenia wartości null :)
7wp
3

Osobiście zwracam domyślną instancję obiektu. Powodem jest to, że oczekuję, że metoda zwróci zero do wielu lub zero do jednego (w zależności od celu metody). Jedynym powodem, dla którego byłby to stan błędu dowolnego rodzaju, przy zastosowaniu tego podejścia, jest to, że metoda nie zwróciła żadnych obiektów i zawsze była oczekiwana (w kategoriach zwrotu jeden do wielu lub pojedynczego).

Jeśli chodzi o założenie, że jest to pytanie z dziedziny biznesu - po prostu nie widzę tego z tej strony równania. Normalizacja typów zwracanych danych jest prawidłowym pytaniem dotyczącym architektury aplikacji. Przynajmniej podlega normalizacji w praktykach kodowania. Wątpię, czy istnieje użytkownik biznesowy, który powie „w scenariuszu X, po prostu daj mu wartość zerową”.

Joseph Ferris
źródło
+1 Podoba mi się alternatywny pogląd na problem. Więc w zasadzie mówisz, które podejście, które wybiorę, powinno być w porządku, o ile metoda jest spójna w całej aplikacji?
7wp
1
To jest moje przekonanie. Myślę, że spójność jest niezwykle ważna. Jeśli robisz rzeczy na wiele sposobów w wielu miejscach, zwiększa to ryzyko nowych błędów. My osobiście wybraliśmy domyślne podejście obiektowe, ponieważ działa ono dobrze ze wzorcem Essence, którego używamy w naszym modelu domeny. Mamy jedną ogólną metodę rozszerzenia, którą możemy przetestować na wszystkich obiektach domeny, aby powiedzieć nam, czy jest ona wypełniona, czy nie, abyśmy wiedzieli, że każde DO można przetestować za pomocą wywołania nazwy obiektu.IsDefault () - unikając bezpośrednio jakichkolwiek kontroli równości .
Joseph Ferris
3

W naszych obiektach biznesowych mamy 2 główne metody Get:

Aby zachować prostotę w kontekście lub kwestionujesz, będą to:

// Returns null if user does not exist
public UserEntity GetUserById(Guid userId)
{
}

// Returns a New User if user does not exist
public UserEntity GetNewOrExistingUserById(Guid userId)
{
}

Pierwsza metoda jest używana podczas uzyskiwania określonych encji, druga metoda jest używana szczególnie podczas dodawania lub edytowania encji na stronach internetowych.

To pozwala nam mieć to, co najlepsze z obu światów w kontekście, w którym są używane.

Mark Redman
źródło
3

Jestem francuskim studentem informatyki, więc wybacz mój słaby angielski. W naszych klasach powiedziano nam, że taka metoda nigdy nie powinna zwracać wartości null ani pustego obiektu. Użytkownik tej metody powinien najpierw sprawdzić, czy szukany obiekt istnieje, zanim spróbuje go zdobyć.

Przy użyciu Javy jesteśmy proszeni o dodanie assert exists(object) : "You shouldn't try to access an object that doesn't exist";na początku dowolnej metody, która może zwrócić null, aby wyrazić „warunek wstępny” (nie wiem, co to słowo po angielsku).

IMO to naprawdę nie jest łatwe w użyciu, ale tego właśnie używam, czekając na coś lepszego.

Saend
źródło
1
Dziękuję za odpowiedź. Ale nie podoba mi się pomysł sprawdzenia, czy istnieje. Powodem jest to, że generuje dodatkowe zapytanie do bazy danych. W aplikacji, do której dostęp mają miliony ludzi dziennie, może to spowodować dramatyczną utratę wydajności.
7wp
1
Jedną z korzyści jest to, że sprawdzenie istnienia jest odpowiednio abstrakcyjne: jeśli (userExists) jest nieco bardziej czytelny, bliższy problematycznej dziedzinie i mniej „komputerów” niż: if (user == null)
timoxley
I twierdziłbym, że „if (x == null)” to dziesięciolecia wzorzec, który jeśli nie widziałeś go wcześniej, nie pisałeś kodu zbyt długo (i powinieneś się do niego przyzwyczaić, jak jest w miliony linii kodu). „Komputery”? Mówimy o dostępie do bazy danych ...
Lloyd Sargent
3

Jeśli sprawa użytkownik nie jest znaleźć pojawia się dość często, a chcesz sobie z tym na różne sposoby, w zależności od okoliczności (czasami rzuca wyjątek, czasami zastępując pusty użytkownika) można też użyć czegoś blisko F # 's Optionlub Haskell w Mayberodzaju , który wyraźnie oddziela przypadek „brak wartości” od „znaleziono coś!”. Kod dostępu do bazy danych może wyglądać następująco:

public Option<UserEntity> GetUserById(Guid userId)
{
 //Imagine some code here to access database.....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return Option<UserEntity>.Nothing; 
 else
    return Option.Just(existingUserEntity);
}

I być używane w ten sposób:

Option<UserEntity> result = GetUserById(...);
if (result.IsNothing()) {
    // deal with it
} else {
    UserEntity value = result.GetValue();
}

Niestety, wszyscy wydają się rzucać takimi typami.

yatima2975
źródło
2

Zwykle zwracam zero. Zapewnia szybki i łatwy mechanizm do wykrycia, czy coś się zepsuło bez rzucania wyjątków i używania ton try / catch w całym miejscu.

Jaka jest nazwa?
źródło
2

W przypadku typów kolekcji zwrócę pustą kolekcję, dla wszystkich innych typów wolę używać wzorców NullObject do zwracania obiektu, który implementuje ten sam interfejs, co interfejs zwracanego typu. Aby uzyskać szczegółowe informacje na temat wzoru, sprawdź tekst linku

Przy użyciu wzorca NullObject byłoby to: -

public UserEntity GetUserById(Guid userId)

{// Wyobraź sobie tutaj kod dostępu do bazy danych .....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return new NullUserEntity(); //Should I be doing this here instead? return new UserEntity();  
 else
    return existingUserEntity;

}

class NullUserEntity: IUserEntity { public string getFirstName(){ return ""; } ...} 
Vikram Nayak
źródło
2

Ujmując to, co inni powiedzieli w bardziej bezpośredni sposób ...

Wyjątki dotyczą wyjątkowych okoliczności

Jeśli ta metoda jest czystą warstwą dostępu do danych, powiedziałbym, że biorąc pod uwagę jakiś parametr, który zostanie uwzględniony w instrukcji select, można oczekiwać, że nie mogę znaleźć żadnych wierszy, z których można zbudować obiekt, a zatem zwrócenie wartości null byłoby dopuszczalne, ponieważ jest logiką dostępu do danych.

Z drugiej strony, jeśli spodziewałem się, że mój parametr będzie odzwierciedlał klucz podstawowy i powinienem odzyskać tylko jeden wiersz, jeśli otrzymam więcej niż jeden, zwróciłbym wyjątek. 0 jest w porządku, aby zwrócić null, 2 nie jest.

Teraz, gdybym miał jakiś kod logowania, który sprawdził się w stosunku do dostawcy LDAP, a następnie sprawdził w stosunku do DB, aby uzyskać więcej szczegółów, i spodziewałem się, że powinny one być zsynchronizowane przez cały czas, mógłbym wtedy podrzucić wyjątek. Jak powiedzieli inni, to reguły biznesowe.

Teraz powiem, że to ogólna zasada. Są chwile, w których możesz chcieć to przerwać. Jednak moje doświadczenia i eksperymenty z C # (partii) i że Java (trochę), które nauczyło mnie, że jest to znacznie droższe wydajność mądry, aby radzić sobie z wyjątkami niż obsłużyć problemy przewidywalne poprzez logiki warunkowej. Mówię o melodii o 2 lub 3 rzędy wielkości droższe w niektórych przypadkach. Więc jeśli to możliwe, twój kod może skończyć się pętlą, radziłbym zwrócić wartość null i przetestować go.

Jim L.
źródło
2

Wybacz mój pseudo-php / kod.

Myślę, że to naprawdę zależy od zamierzonego wykorzystania wyniku.

Jeśli zamierzasz edytować / zmodyfikować wartość zwracaną i zapisać ją, zwróć pusty obiekt. W ten sposób możesz użyć tej samej funkcji do zapełnienia danych nowym lub istniejącym obiektem.

Powiedzmy, że mam funkcję, która pobiera klucz podstawowy i tablicę danych, wypełnia wiersz danymi, a następnie zapisuje wynikowy rekord w db. Ponieważ mam zamiar zapełnić obiekt moimi danymi w jakikolwiek sposób, ogromną zaletą może być odzyskanie pustego obiektu z gettera. W ten sposób mogę wykonać identyczne operacje w obu przypadkach. Korzystasz z wyniku funkcji gettera bez względu na wszystko.

Przykład:

function saveTheRow($prim_key, $data) {
    $row = getRowByPrimKey($prim_key);

    // Populate the data here

    $row->save();
}

Tutaj widzimy, że ta sama seria operacji manipuluje wszystkimi rekordami tego typu.

Jeśli jednak ostatecznym celem wartości zwracanej jest odczytanie i zrobienie czegoś z danymi, zwrócę wartość null. W ten sposób mogę bardzo szybko ustalić, czy dane nie zostały zwrócone i wyświetlić użytkownikowi odpowiedni komunikat.

Zazwyczaj wychwytuję wyjątki w mojej funkcji, która pobiera dane (dzięki czemu mogę rejestrować komunikaty o błędach itp.), A następnie zwracam wartość NULL bezpośrednio od początku. Zasadniczo nie ma znaczenia dla użytkownika końcowego, na czym polega problem, dlatego uważam, że najlepiej jest zawrzeć moje logowanie / przetwarzanie błędów bezpośrednio w funkcji, która pobiera dane. Jeśli utrzymujesz wspólną bazę kodów w dowolnej dużej firmie, jest to szczególnie korzystne, ponieważ możesz wymusić prawidłowe rejestrowanie błędów / obsługę nawet najbardziej leniwego programisty.

Przykład:

function displayData($row_id) {
    // Logging of the error would happen in this function
    $row = getRow($row_id);
    if($row === null) {
        // Handle the error here
    }

    // Do stuff here with data
}

function getRow($row_id) {
 $row = null;
 try{
     if(!$db->connected()) {
   throw excpetion("Couldn't Connect");
  }

  $result = $db->query($some_query_using_row_id);

  if(count($result) == 0 ) {
   throw new exception("Couldn't find a record!");
  }

  $row = $db->nextRow();

 } catch (db_exception) {
  //Log db conn error, alert admin, etc...
  return null; // This way I know that null means an error occurred
 }
 return $row;
}

To moja ogólna zasada. Do tej pory działało dobrze.


źródło
2

Interesujące pytanie i myślę, że nie ma „właściwej” odpowiedzi, ponieważ zawsze zależy od odpowiedzialności twojego kodu. Czy Twoja metoda wie, czy żadne znalezione dane nie stanowią problemu? W większości przypadków odpowiedź brzmi „nie” i dlatego zwracanie wartości null i umożliwienie obsłużeniu dzwoniącego sytuacji jest idealne.

Być może dobrym podejściem do odróżnienia metod rzucania od metod zwracających wartość zerową jest znalezienie konwencji w swoim zespole: Metody, które mówią, że coś „dostają”, powinny rzucić wyjątek, jeśli nie ma nic do zdobycia. Metody, które mogą zwracać null, można nazwać inaczej, na przykład „Znajdź ...”.

Marc Wittke
źródło
+1 Podoba mi się pomysł użycia jednolitej konwencji nazewnictwa w celu zasygnalizowania programiście, w jaki sposób ma zostać wykorzystana ta funkcja.
7wp
1
Nagle uświadamiam sobie, że właśnie to robi LINQ: rozważmy First (...) vs. FirstOrDefault (...)
Marc Wittke,
2

Jeśli zwrócony obiekt jest czymś, co można powtórzyć, zwrócę pusty obiekt, aby nie musiałem najpierw testować na wartość NULL.

Przykład:

bool IsAdministrator(User user)
{
    var groupsOfUser = GetGroupsOfUser(user);

    // This foreach would cause a run time exception if groupsOfUser is null.
    foreach (var groupOfUser in groupsOfUser) 
    {
        if (groupOfUser.Name == "Administrators")
        {
            return true;
        }
    }

    return false;
}
Jan Aagaard
źródło
2

Lubię nie zwracać wartości null z żadnej metody, ale zamiast tego używać typu funkcjonalnego Option. Metody, które nie mogą zwrócić wyniku, zwracają pustą opcję zamiast wartości zerowej.

Ponadto metody, które nie mogą zwrócić żadnego wyniku, powinny wskazywać to poprzez ich nazwę. Zwykle umieszczam Try lub TryGet lub TryFind na początku nazwy metody, aby wskazać, że może zwrócić pusty wynik (np. TryFindCustomer, TryLoadFile itp.).

To pozwala dzwoniącemu zastosować różne techniki, takie jak potokowanie zbierania (patrz Rurociąg zbierania Martina Fowlera ) w wyniku.

Oto kolejny przykład, w którym zwracanie Opcji zamiast wartości zerowej jest stosowane w celu zmniejszenia złożoności kodu: Jak zmniejszyć złożoność cykliczną: Typ Funkcjonalny Opcji

Zoran Horvat
źródło
1
Napisałem odpowiedź, widzę, że jest ona podobna do twojej, gdy przewijałem w górę, i zgadzam się, że możesz zaimplementować typ opcji z ogólną kolekcją z 0 lub 1 elementami. Dzięki za dodatkowe linki.
Gabriel P.
1

Więcej mięsa do zmielenia: powiedzmy, że mój DAL zwraca NULL dla GetPersonByID, jak niektórzy zalecają. Co powinien zrobić mój (raczej cienki) BLL, jeśli otrzyma NULL? Przekaż NULL i pozwól konsumentowi się tym martwić (w tym przypadku jest to strona ASP.Net)? A może BLL zgłosi wyjątek?

BLL może być używany przez ASP.Net i Win App lub inną bibliotekę klas - myślę, że niesprawiedliwe jest oczekiwanie, że konsument końcowy z natury „dowie się”, że metoda GetPersonByID zwraca wartość null (chyba że użyte są typy null, chyba ).

Uważam (na ile warto), że mój DAL zwraca NULL, jeśli nic nie zostanie znalezione. W PRZYPADKU NIEKTÓRYCH OBIEKTÓW, to jest w porządku - może to być lista 0: wiele rzeczy, więc brak rzeczy jest w porządku (np. Lista ulubionych książek). W takim przypadku moja BLL zwraca pustą listę. W przypadku większości pojedynczych elementów (np. Użytkownik, konto, faktura), jeśli nie mam, to z pewnością jest to problem i rzuca kosztowny wyjątek. Jednak ponieważ pobieranie użytkownika za pomocą unikalnego identyfikatora, który został wcześniej podany przez aplikację, powinno zawsze zwracać użytkownika, wyjątek jest „prawidłowym” wyjątkiem, ponieważ jest wyjątkowy. Końcowy konsument BLL (ASP.Net, f'rinstance) oczekuje tylko, że rzeczy będą hunky-dory, więc zamiast obsługi każdego pojedynczego wywołania GetPersonByID w bloku try-catch zostanie użyty nieobsługiwany moduł obsługi wyjątków.

Jeśli w moim podejściu występuje rażący problem, daj mi znać, ponieważ zawsze chętnie się uczę. Jak powiedzieli inni plakaty, wyjątki są kosztowne, a podejście „najpierw sprawdzanie” jest dobre, ale wyjątki powinny być właśnie takie - wyjątkowe.

Podoba mi się ten post, wiele dobrych sugestii dotyczących scenariuszy „to zależy” :-)

Mike Kingscott
źródło
I oczywiście dzisiaj spotkałem się ze scenariuszem, w którym mam zamiar zwrócić NULL z mojej BLL ;-) To powiedziawszy, nadal mogę rzucić wyjątek i użyć try / catch w mojej klasie konsumenckiej, ALE wciąż mam problem : skąd moja klasa konsumująca wie, jak używać try / catch, podobnie skąd wiedzą, czy sprawdzać NULL?
Mike Kingscott,
Możesz udokumentować, że metoda zgłasza wyjątek za pomocą doctag @throws, a dokumentować fakt, że może zwrócić wartość null w doctag @return.
timoxley,
1

Sądzę, że funkcje nie powinny zwracać wartości null, dla zdrowia twojej bazy kodu. Mogę wymyślić kilka powodów:

Będzie duża liczba klauzul ochronnych dotyczących zerowego odniesienia if (f() != null).

Co to nulljest zaakceptowana odpowiedź lub problem? Czy wartość null jest poprawnym stanem dla określonego obiektu? (wyobraź sobie, że jesteś klientem kodu). Mam na myśli, że wszystkie typy referencji mogą być zerowe, ale czy powinny?

Mając nullkręci prawie zawsze dać kilka nieoczekiwanych wyjątków NullRef od czasu do czasu jako kod baza rośnie.

Istnieje kilka rozwiązań tester-doer patternlub implementacja option typeprogramowania funkcjonalnego.

Gabriel P.
źródło
0

Jestem zakłopotany liczbą odpowiedzi (w całej sieci), które mówią, że potrzebujesz dwóch metod: metody „IsItThere ()” i metody „GetItForMe ()”, co prowadzi do warunków wyścigu. Co jest złego w funkcji, która zwraca null, przypisując ją do zmiennej i sprawdzając zmienną pod kątem Null wszystko w jednym teście? Mój poprzedni kod C był pełen

if (NULL! = (zmienna = funkcja (argumenty ...))) {

Otrzymujesz więc wartość (lub null) w zmiennej i wynik naraz. Czy ten idiom został zapomniany? Czemu?

bez zrozumienia
źródło
0

Zgadzam się z większością postów tutaj, które mają tendencję do null.

Moje rozumowanie jest takie, że generowanie pustego obiektu o właściwościach nie dopuszczających wartości zerowej może powodować błędy. Na przykład jednostka z int IDwłaściwością miałaby wartość początkową ID = 0, która jest wartością całkowicie prawidłową. Gdyby ten obiekt, w pewnych okolicznościach, został zapisany w bazie danych, byłoby źle.

Do wszystkiego z iteratorem zawsze używałbym pustej kolekcji. Coś jak

foreach (var eachValue in collection ?? new List<Type>(0))

według mnie to zapach kodu. Właściwości kolekcji nigdy nie powinny mieć wartości zerowej.

Obudowa krawędzi jest String. Wiele osób twierdzi, że String.IsNullOrEmptytak naprawdę nie jest to konieczne, ale nie zawsze można odróżnić pusty ciąg znaków od wartości null. Co więcej, niektóre systemy baz danych (Oracle) w ogóle ich nie rozróżniają ( ''są przechowywane jako DBNULL), więc jesteś zmuszony traktować je jednakowo. Powodem tego jest to, że większość wartości ciągu albo pochodzi z danych wejściowych użytkownika, albo z systemów zewnętrznych, podczas gdy ani pola tekstowe, ani większość formatów wymiany nie ma różnych reprezentacji dla ''i null. Więc nawet jeśli użytkownik chce usunąć wartość, nie może zrobić nic więcej niż wyczyścić kontrolę wejścia. Rozróżnienie nvarcharpól zerowych i zerowych w bazie danych jest więcej niż wątpliwe, jeśli DBMS nie jest wyrocznią - pole obowiązkowe, które pozwala''jest dziwne, twój interfejs użytkownika nigdy na to nie pozwala, więc twoje ograniczenia nie są mapowane. Moim zdaniem odpowiedź tutaj polega na tym, aby zawsze traktować je jednakowo.

Jeśli chodzi o twoje pytanie dotyczące wyjątków i wydajności: Jeśli zgłaszasz wyjątek, którego nie możesz całkowicie obsłużyć w logice programu, musisz w pewnym momencie przerwać cokolwiek twój program i poprosić użytkownika o ponowne wykonanie tego, co właśnie zrobił. W takim przypadku kara za wydajność a catchjest naprawdę najmniejszym z twoich zmartwień - musisz zapytać użytkownika, czy jest słoniem w pokoju (co oznacza ponowne renderowanie całego interfejsu użytkownika lub wysyłanie kodu HTML przez Internet). Jeśli więc nie postępujesz zgodnie ze schematem „ Przebieg programu z wyjątkami ”, nie przejmuj się, po prostu rzuć jeden, jeśli ma to sens. Nawet w skrajnych przypadkach, takich jak „wyjątek walidacji”, wydajność nie jest tak naprawdę problemem, ponieważ w każdym razie musisz ponownie zapytać użytkownika.

Oliver Schimmer
źródło
0

Asynchronous TryGet Wzór:

Dla metod synchronicznych, wierzę @Johann Gerell za odpowiedź jest wzór do wykorzystania we wszystkich przypadkach.

Jednak wzorzec TryGet z outparametrem nie działa z metodami Async.

Dzięki literałom Tuple C # 7 możesz teraz to zrobić:

async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}
ttugates
źródło