Czy lepiej jest zwrócić kolekcję zerową lub pustą?

420

To rodzaj ogólnego pytania (ale używam C #), jaki jest najlepszy sposób (najlepsza praktyka), czy zwracasz wartość zerową lub pustą kolekcję dla metody, która ma kolekcję jako typ zwracany?

Omu
źródło
3
@CPerkins - Tak, jest. Jest to wyraźnie określone we własnych wytycznych Microsoft dotyczących projektowania .NET Framework. Szczegółowe informacje można znaleźć w odpowiedzi RichardOD.
Greg Beech
51
WYŁĄCZNIE, jeśli masz znaczenie „Nie mogę obliczyć wyników”. Null nigdy nie powinien mieć semantyki „pusta”, tylko „brakująca” lub „nieznana”. Więcej szczegółów w moim artykule na ten temat: blogs.msdn.com/ericlippert/archive/2009/05/14/…
Eric Lippert,
3
Bozho: „any” to strasznie dużo języków. Co z Common Lisp, gdzie pusta lista jest dokładnie równa wartości zerowej? :-)
Ken
9
W rzeczywistości „duplikat” dotyczy metod zwracających obiekt, a nie kolekcję. To inny scenariusz z różnymi odpowiedziami.
GalacticCowboy

Odpowiedzi:

499

Pusta kolekcja. Zawsze.

To jest do bani:

if(myInstance.CollectionProperty != null)
{
  foreach(var item in myInstance.CollectionProperty)
    /* arrgh */
}

Uznaje się za najlepszą praktykę, aby NIGDY nie zwracać nullprzy zwracaniu kolekcji lub listy. ZAWSZE zwracaj pusty wyliczalny / zbiór. Zapobiega wspomnianym bzdurom i zapobiega popychaniu samochodu przez współpracowników i użytkowników twoich zajęć.

Mówiąc o nieruchomościach, zawsze ustaw swoją właściwość raz i zapomnij o niej

public List<Foo> Foos {public get; private set;}

public Bar() { Foos = new List<Foo>(); }

W .NET 4.6.1 można to bardzo mocno skondensować:

public List<Foo> Foos { get; } = new List<Foo>();

Mówiąc o metodach, które zwracają wyliczenia, możesz łatwo zwrócić pusty wyliczenie zamiast null...

public IEnumerable<Foo> GetMyFoos()
{
  return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}

Używanie Enumerable.Empty<T>()może być postrzegane jako bardziej wydajne niż zwracanie, na przykład nowej pustej kolekcji lub tablicy.

Ryan Lundy
źródło
24
Zgadzam się z Willem, ale myślę, że „zawsze” jest trochę przesadne. Podczas gdy pusta kolekcja może oznaczać „0 elementów”, zwracanie wartości Null może oznaczać „brak kolekcji” - np. jeśli parsujesz HTML, szukasz <ul> o id = "foo", <ul id = "foo"> </ul> może zwrócić pustą kolekcję; jeśli nie ma <ul> z id = "foo", zerowy powrót byłby lepszy (chyba że chcesz obsługiwać tę sprawę z wyjątkiem)
Patonza
30
nie zawsze chodzi o to, czy „możesz łatwo zwrócić pustą tablicę”, ale raczej o to, czy pusta tablica może wprowadzać w błąd w obecnym kontekście. Pusta tablica faktycznie coś znaczy, podobnie jak null. Stwierdzenie, że zawsze powinieneś zwrócić pustą tablicę zamiast wartości null, jest prawie tak samo mylące, jak powiedzenie ci, że metoda boolowska powinna zawsze zwracać wartość true. Obie możliwe wartości przekazują znaczenie.
David Hedlund,
31
W rzeczywistości powinieneś raczej zwrócić System.Linq.Enumerable.Empty <Foo> () zamiast nowego Foo [0]. Jest bardziej wyraźny i oszczędza jeden przydział pamięci (przynajmniej w mojej zainstalowanej implementacji .NET).
Trillian
4
@Will: OP: „czy zwracasz wartość zerową lub pustą kolekcję dla metody, która ma kolekcję jako typ zwracany”. Myślę w tym przypadku, czy to ma tak duże znaczenie, IEnumerableczy ICollectionnie. W każdym razie, jeśli wybierzesz coś w rodzaju, ICollectiononi również zwrócą null... Chciałbym, aby zwrócili pustą kolekcję, ale natknąłem się na ich powrót null, więc pomyślałem, że po prostu wspomnę o tym tutaj. Powiedziałbym, że domyślna kolekcja wyliczalnych jest pusta, a nie zerowa. Nie wiedziałem, że to taki delikatny temat.
Matthijs Wessels
7
Powiązane (artykuł CodeProject): Czy naprawdę lepiej jest „Zwrócić pustą listę zamiast pustej”?
sampathsris,
154

Z wytycznych projektowania ramowego, wydanie drugie (str. 256):

NIE zwracaj wartości pustych z właściwości kolekcji lub z metod zwracających kolekcje. Zamiast tego zwraca pustą kolekcję lub pustą tablicę.

Oto kolejny interesujący artykuł o korzyściach płynących z nie zwracania wartości zerowych (próbowałem znaleźć coś na blogu Brada Abrama, a on link do tego artykułu).

Edytuj - ponieważ Eric Lippert skomentował już oryginalne pytanie, chciałbym również zamieścić link do jego doskonałego artykułu .

RichardOD
źródło
33
+1. Zawsze najlepszą praktyką jest przestrzeganie wytycznych dotyczących projektowania ram, chyba że masz BARDZO dobry powód, aby tego nie robić.
@Will, absolutnie. Właśnie za tym podążam. Nigdy nie znalazłem potrzeby, by robić inaczej
RichardOD
tak, to jest to, co podążasz. Ale w przypadkach, w których API, na których polegasz, nie podążają za nim, jesteś „uwięziony”;)
Bozho
@ Bozho-yeap. Twoja odpowiedź zawiera kilka fajnych odpowiedzi na te grzywki.
RichardOD
90

Zależy od umowy i konkretnego przypadku . Ogólnie najlepiej jest zwracać puste kolekcje , ale czasami ( rzadko ):

  • null może oznaczać coś bardziej konkretnego;
  • Twój interfejs API (umowa) może zmusić Cię do zwrotu null.

Niektóre konkretne przykłady:

  • komponent interfejsu użytkownika (z biblioteki poza kontrolą), może renderować pustą tabelę, jeśli zostanie przekazana pusta kolekcja lub w ogóle nie będzie tabeli, jeśli zostanie przekazana wartość null.
  • w Object-to-XML (JSON / cokolwiek), gdzie nulloznaczałoby to brak elementu, a pusta kolekcja spowodowałaby nadmiar (i być może niepoprawny)<collection />
  • używasz lub implementujesz interfejs API, który wyraźnie stwierdza, że ​​wartość null powinna zostać zwrócona / przekazana
Bozho
źródło
4
@ Bozho - kilka interesujących przykładów tutaj.
RichardOD
1
Chciałbym zobaczyć, o co ci chodzi, chociaż nie sądzę, że powinieneś budować swój kod wokół innych błędów i z definicji „null” w c # nigdy nie oznacza czegoś konkretnego. Wartość null jest zdefiniowana jako „brak informacji”, a zatem powiedzenie, że przenosi określone informacje, jest oksymoronem. Właśnie dlatego wytyczne .NET stanowią, że powinieneś zwrócić pusty zestaw, jeśli zestaw jest rzeczywiście pusty. zwracanie wartości null mówi: „Nie wiem, gdzie poszedł oczekiwany zestaw”
Rune FS
13
nie, to znaczy „nie ma zestawu” zamiast „zestaw nie ma elementów”
Bozho
Kiedyś w to wierzyłem, potem musiałem pisać dużo TSQL i dowiedziałem się, że nie zawsze tak jest, heheh.
1
Część z Object-To-Xml jest na miejscu
Mihai Caracostea,
36

Jest jeszcze jeden punkt, o którym jeszcze nie wspomniano. Rozważ następujący kod:

    public static IEnumerable<string> GetFavoriteEmoSongs()
    {
        yield break;
    }

Język C # zwróci pusty moduł wyliczający podczas wywoływania tej metody. Dlatego, aby zachować spójność z projektem języka (a tym samym oczekiwaniami programisty), należy zwrócić pustą kolekcję.

Jeffrey L. Whitledge
źródło
1
Nie sądzę technicznie, że to zwraca pustą kolekcję.
FryGuy,
3
@FryGuy - Nie, nie ma. Zwraca obiekt Enumerable, którego metoda GetEnumerator () zwraca Enumerator, który jest (poprzez analizę z kolekcją) pusty. Oznacza to, że metoda MoveNext () modułu wyliczającego zawsze zwraca false. Wywołanie przykładowej metody nie zwraca null, ani nie zwraca obiektu Enumerable, którego metoda GetEnumerator () zwraca null.
Jeffrey L Whitledge
31

Pusty jest o wiele bardziej przyjazny dla konsumenta.

Istnieje jasna metoda tworzenia pustego wyliczenia:

Enumerable.Empty<Element>()
George Polevoy
źródło
2
Dzięki za fajną sztuczkę. Tworzyłem instancję pustej listy <T> () jak idiota, ale wygląda to na znacznie czystsze i prawdopodobnie jest też trochę bardziej wydajne.
Jacobs Data Solutions
18

Wydaje mi się, że powinieneś zwrócić wartość, która jest semantycznie poprawna w kontekście, cokolwiek by to nie było. Reguła, która mówi „zawsze zwracaj pustą kolekcję” wydaje mi się trochę uproszczona.

Załóżmy, że w systemie szpitalnym mamy funkcję, która ma zwrócić listę wszystkich poprzednich hospitalizacji z ostatnich 5 lat. Jeśli klient nie był w szpitalu, warto zwrócić pustą listę. Ale co, jeśli klient pozostawił tę część formularza wstępu pustą? Potrzebujemy innej wartości, aby odróżnić „pustą listę” od „brak odpowiedzi” lub „nie wiem”. Możemy rzucić wyjątek, ale niekoniecznie jest to warunek błędu i niekoniecznie wyprowadza nas z normalnego przepływu programu.

Często byłem sfrustrowany systemami, które nie potrafią rozróżnić od zera do braku odpowiedzi. Wielokrotnie system prosił mnie o podanie pewnej liczby, wpisuję zero i pojawia się komunikat o błędzie z informacją, że muszę wprowadzić wartość w tym polu. Właśnie tak zrobiłem: wpisałem zero! Ale nie przyjmie zera, ponieważ nie może odróżnić go od braku odpowiedzi.


Odpowiedz Saundersowi:

Tak, zakładam, że istnieje różnica między „Osoba nie odpowiedziała na pytanie” a „Odpowiedź była zerowa”. To był punkt ostatniego akapitu mojej odpowiedzi. Wiele programów nie jest w stanie odróżnić „nie wiem” od pustego lub zerowego, co wydaje mi się potencjalnie poważną wadą. Na przykład kupowałem dom jakiś rok temu. Poszedłem na stronę z nieruchomościami i było tam wiele domów z ceną wywoławczą 0 USD. Brzmiało dla mnie całkiem dobrze: oddają te domy za darmo! Ale jestem pewien, że smutną rzeczywistością było to, że po prostu nie podali ceny. W takim przypadku możesz powiedzieć: „Cóż, oczywiście zero oznacza, że ​​nie podali ceny - nikt nie oddaje domu za darmo”. Ale na stronie wymieniono również średnie ceny sprzedaży i sprzedaży domów w różnych miastach. Nie mogę przestać się zastanawiać, czy średnia nie zawierała zer, dając w ten sposób nieprawidłowo niską średnią dla niektórych miejsc. tj. jaka jest średnia 100 000 USD; 120 000 USD; i „nie wiem”? Technicznie odpowiedź brzmi „nie wiem”. To, co prawdopodobnie naprawdę chcemy zobaczyć, to 110 000 USD. Ale prawdopodobnie dostaniemy 73 333 USD, co byłoby całkowicie błędne. A co, gdybyśmy mieli ten problem w witrynie, w której użytkownicy mogą zamawiać online? (Jest to mało prawdopodobne w przypadku nieruchomości, ale jestem pewien, że widziałeś to już w przypadku wielu innych produktów.) Czy naprawdę chcielibyśmy, aby „cena jeszcze nieokreślona” była interpretowana jako „darmowa”? dając w ten sposób nieprawidłowo niską średnią dla niektórych miejsc. tj. jaka jest średnia 100 000 USD; 120 000 USD; i „nie wiem”? Technicznie odpowiedź brzmi „nie wiem”. To, co prawdopodobnie naprawdę chcemy zobaczyć, to 110 000 USD. Ale prawdopodobnie dostaniemy 73 333 USD, co byłoby całkowicie błędne. A co, gdybyśmy mieli ten problem w witrynie, w której użytkownicy mogą zamawiać online? (Jest to mało prawdopodobne w przypadku nieruchomości, ale jestem pewien, że widziałeś to już w przypadku wielu innych produktów.) Czy naprawdę chcielibyśmy, aby „cena jeszcze nieokreślona” była interpretowana jako „darmowa”? dając w ten sposób nieprawidłowo niską średnią dla niektórych miejsc. tj. jaka jest średnia 100 000 USD; 120 000 USD; i „nie wiem”? Technicznie odpowiedź brzmi „nie wiem”. To, co prawdopodobnie naprawdę chcemy zobaczyć, to 110 000 USD. Ale prawdopodobnie dostaniemy 73 333 USD, co byłoby całkowicie błędne. A co, gdybyśmy mieli ten problem w witrynie, w której użytkownicy mogą zamawiać online? (Jest to mało prawdopodobne w przypadku nieruchomości, ale jestem pewien, że widziałeś to już w przypadku wielu innych produktów.) Czy naprawdę chcielibyśmy, aby „cena jeszcze nieokreślona” była interpretowana jako „darmowa”? co byłoby całkowicie błędne. A co, gdybyśmy mieli ten problem w witrynie, w której użytkownicy mogą zamawiać online? (Jest to mało prawdopodobne w przypadku nieruchomości, ale jestem pewien, że widziałeś to już w przypadku wielu innych produktów.) Czy naprawdę chcielibyśmy, aby „cena jeszcze nieokreślona” była interpretowana jako „darmowa”? co byłoby całkowicie błędne. A co, gdybyśmy mieli ten problem w witrynie, w której użytkownicy mogą zamawiać online? (Jest to mało prawdopodobne w przypadku nieruchomości, ale jestem pewien, że widziałeś to już w przypadku wielu innych produktów.) Czy naprawdę chcielibyśmy, aby „cena jeszcze nieokreślona” była interpretowana jako „darmowa”?

RE ma dwie oddzielne funkcje, „czy jest jakaś?” i „jeśli tak, co to jest?” Tak, na pewno możesz to zrobić, ale dlaczego miałbyś chcieć? Teraz program wywołujący musi wykonać dwa połączenia zamiast jednego. Co się stanie, jeśli programista nie wywoła „any”? i przechodzi od razu do „co to jest?” ? Czy program zwróci błędne zero? Rzuć wyjątek? Zwrócić niezdefiniowaną wartość? Tworzy więcej kodu, więcej pracy i więcej potencjalnych błędów.

Jedyną korzyścią, jaką widzę, jest to, że pozwala ci przestrzegać arbitralnej reguły. Czy jest jakaś zaleta tej reguły, która sprawia, że ​​warto kłopotać się jej przestrzeganiem? Jeśli nie, to po co?


Odpowiedz na Jammycakes:

Zastanów się, jak wyglądałby rzeczywisty kod. Wiem, że pytanie brzmiało C #, ale przepraszam, jeśli piszę w Javie. Moje C # nie jest bardzo ostre i zasada jest taka sama.

Ze znakiem zerowym:

HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
   // ... handle missing list ...
}
else
{
  for (HospEntry entry : list)
   //  ... do whatever ...
}

Z osobną funkcją:

if (patient.hasHospitalizationList(patientId))
{
   // ... handle missing list ...
}
else
{
  HospList=patient.getHospitalizationList(patientId))
  for (HospEntry entry : list)
   // ... do whatever ...
}

W rzeczywistości jest to wiersz lub dwa mniej kodu ze znakiem zerowym, więc nie stanowi większego obciążenia dla osoby dzwoniącej, jest mniej.

Nie rozumiem, jak to powoduje problem z SUCHEM. To nie tak, że musimy wykonać połączenie dwukrotnie. Gdybyśmy zawsze chcieli zrobić to samo, gdy lista nie istnieje, być może moglibyśmy przesunąć obsługę do funkcji get-list, zamiast wywoływania wywoływania, a więc umieszczenie kodu w wywołującym byłoby naruszeniem DRY. Ale prawie na pewno nie chcemy zawsze robić tego samego. W funkcjach, w których musimy mieć listę do przetworzenia, brakująca lista jest błędem, który może zatrzymać przetwarzanie. Ale na ekranie edycji z pewnością nie chcemy przerwać przetwarzania, jeśli jeszcze nie wprowadzili danych: chcemy pozwolić im na wprowadzanie danych. Tak więc obsługa „brak listy” musi odbywać się na poziomie osoby dzwoniącej w taki czy inny sposób. A czy robimy to z zerowym znakiem powrotu czy osobną funkcją, nie ma znaczenia dla większej zasady.

Jasne, jeśli program wywołujący nie sprawdzi wartości null, program może zawieść z wyjątkiem wyjątku wskaźnika zerowego. Ale jeśli istnieje osobna funkcja „ma jakąkolwiek”, a osoba dzwoniąca nie wywołuje tej funkcji, ale ślepo wywołuje funkcję „pobierz listę”, to co się dzieje? Jeśli zgłasza wyjątek lub w inny sposób zawiedzie, cóż, to prawie tak samo, jak by się stało, gdyby zwrócił wartość null i nie sprawdził go. Jeśli zwraca pustą listę, to po prostu źle. Nie rozróżniasz między „Mam listę z zerowymi elementami” a „Nie mam listy”. To jak zwracanie zera ceny, gdy użytkownik nie podał żadnej ceny: to po prostu źle.

Nie rozumiem, w jaki sposób dołączenie dodatkowego atrybutu do kolekcji pomaga. Dzwoniący nadal musi to sprawdzić. Dlaczego to jest lepsze niż sprawdzanie wartości null? Ponownie absolutną najgorszą rzeczą, jaka może się zdarzyć, jest zapomnienie przez programistę o sprawdzeniu i podanie niepoprawnych wyników.

Funkcja, która zwraca null, nie jest zaskoczeniem, jeśli programista zna pojęcie wartości null oznaczające „nie ma wartości”, o czym myślę, że każdy kompetentny programista powinien słyszeć, niezależnie od tego, czy uważa, że ​​to dobry pomysł, czy nie. Myślę, że posiadanie oddzielnej funkcji jest raczej problemem „zaskoczenia”. Jeśli programista nie zna interfejsu API, gdy uruchomi test bez danych, szybko odkryje, że czasami odzyskuje wartość zerową. Ale jak odkryłby istnienie innej funkcji, gdyby nie przyszło mu do głowy, że taka funkcja może istnieć, i sprawdza dokumentację, a dokumentacja jest kompletna i zrozumiała? Wolałbym mieć jedną funkcję, która zawsze daje mi znaczącą odpowiedź, niż dwie funkcje, które muszę znać i pamiętać, aby wywołać oba.

Sójka
źródło
4
Dlaczego konieczne jest połączenie odpowiedzi „brak odpowiedzi” i „zero” w tej samej wartości zwracanej? Zamiast tego, czy metoda zwraca „jakiekolwiek wcześniejsze hospitalizacje w ciągu ostatnich pięciu lat” i czy ma osobną metodę, która pyta: „czy poprzednia lista hospitalizacji była kiedykolwiek wypełniona?”. Zakłada się, że istnieje różnica między listą wypełnioną bez wcześniejszych hospitalizacji, a listą niewypełnioną.
John Saunders
2
Ale jeśli zwrócisz zero, i tak już nakładasz dodatkowe obciążenie na dzwoniącego! Dzwoniący musi sprawdzić każdą zwracaną wartość dla wartości zerowej - straszne naruszenie OSUSZANIA. Jeśli chcesz, aby wskazać „rozmówca nie odpowiedział na pytanie” oddzielnie, to jest prostsze, aby utworzyć dodatkowe wywołanie metody, aby wskazać ten fakt. Albo to albo użyj pochodnego typu kolekcji z dodatkową właściwością DeclinedToAnswer. Zasada, by nigdy nie zwracać wartości null, nie jest wcale arbitralna, jest to zasada najmniejszej niespodzianki. Ponadto typ zwracany przez metodę powinien oznaczać, jak mówi jej nazwa. Null prawie na pewno nie.
jammycakes,
2
Zakładasz, że twoja getHospitalizationList jest wywoływana tylko z jednego miejsca i / lub że wszyscy jej wywołujący będą chcieli rozróżniać przypadki „brak odpowiedzi” i „zero”. Nie będzie występować przypadki (prawie na pewno większość) gdzie twój rozmówca nie musi dokonać tego rozróżnienia, a więc zmuszają je dodać zerowe kontrole w miejscach, gdzie nie powinno to być konieczne. Stwarza to znaczne ryzyko dla Twojej bazy kodu, ponieważ ludzie zbyt łatwo mogą o tym zapomnieć - a ponieważ istnieje bardzo niewiele uzasadnionych powodów, aby zwrócić wartość null zamiast kolekcji, będzie to znacznie bardziej prawdopodobne.
jammycakes
3
RE nazwa: Żadna nazwa funkcji nie może całkowicie opisać, co robi funkcja, chyba że jest tak długa jak funkcja, a zatem jest niesamowicie niepraktyczna. Ale w każdym razie, jeśli funkcja zwraca pustą listę, gdy nie udzielono odpowiedzi, to z tego samego powodu nie należy jej nazywać „getHospitalizationListOrEmptyListIfNoAnswer”? Ale tak naprawdę, czy nalegałbyś na zmianę nazwy funkcji Java Reader.read na readCharacterOrReturnMinusOneOnEndOfStream? To ResultSet.getInt powinno być naprawdę „getIntOrZeroIfValueWasNull”? Itd.
Jay
4
RE każde połączenie chce rozróżnić: Cóż, tak, zakładam, że, a przynajmniej autor dzwoniącego powinien podjąć świadomą decyzję, że go to nie obchodzi. Jeśli funkcja zwróci pustą listę „nie wiem”, a osoby dzwoniące na ślepo traktują to „brak”, może to dać bardzo niedokładne wyniki. Wyobraź sobie, że funkcją była „getAllergicReactionToMedicationList”. Program, który na ślepo traktował „listę nie został wpisany”, ponieważ „pacjent nie ma znanych reakcji alergicznych” może dosłownie doprowadzić do zabicia paitent. W wielu innych systemach uzyskałbyś podobne, choć mniej dramatyczne wyniki. ...
Jay
10

Jeśli pusta kolekcja ma sens semantyczny, wolę to zwrócić. Zwracanie pustej kolekcji do GetMessagesInMyInbox()komunikowania „naprawdę nie masz żadnych wiadomości w skrzynce odbiorczej”, podczas gdy zwracanie nullmoże być przydatne, aby poinformować, że dostępne są niewystarczające dane, aby powiedzieć, jak powinna wyglądać zwrócona lista.

David Hedlund
źródło
6
W podanym przykładzie brzmi to tak, jakby metoda prawdopodobnie zgłosiła wyjątek, jeśli nie może spełnić żądania, a nie tylko zwraca wartość null. Wyjątki są znacznie bardziej przydatne do diagnozowania problemów niż wartości zerowe.
Greg Beech
cóż, tak, w przykładzie skrzynki odbiorczej nullwartość na pewno nie wydaje się rozsądna, myślałem o niej bardziej ogólnie. Wyjątki są również świetne do informowania o tym, że coś poszło nie tak, ale jeśli „niewystarczające dane”, o których mowa, są doskonale oczekiwane, to rzucenie wyjątku byłoby złym projektem. Myślę raczej o scenariuszu, w którym jest to całkowicie możliwe i nie ma w ogóle błędu, że metoda czasami nie jest w stanie obliczyć odpowiedzi.
David Hedlund,
6

Zwracanie wartości null może być bardziej wydajne, ponieważ nie jest tworzony nowy obiekt. Często wymagałoby to jednak nullsprawdzenia (lub obsługi wyjątków).

Semantycznie nulli pusta lista nie oznaczają tego samego. Różnice są subtelne i w niektórych przypadkach jeden wybór może być lepszy niż drugi.

Niezależnie od wyboru udokumentuj go, aby uniknąć zamieszania.

Koder Karmiczny
źródło
8
Wydajność prawie nigdy nie powinna być czynnikiem przy rozważaniu poprawności projektu API. W niektórych bardzo szczególnych przypadkach, takich jak prymitywy graficzne, może tak być, ale w przypadku list i większości innych rzeczy na wysokim poziomie bardzo w to wątpię.
Greg Beech
Zgadzam się z Gregiem, zwłaszcza biorąc pod uwagę, że kod, który użytkownik interfejsu API musi napisać, aby skompensować tę „optymalizację”, może być bardziej nieefektywny niż w przypadku zastosowania lepszego projektu.
Craig Stuntz
Zgoda, w większości przypadków po prostu nie jest warta optymalizacji. Puste listy są praktycznie bezpłatne dzięki nowoczesnemu zarządzaniu pamięcią.
Jason Baker
6

Można argumentować, że uzasadnienie Null Object Pattern jest podobne do argumentu przemawiającego za zwróceniem pustej kolekcji.

Dan
źródło
4

Zależy od sytuacji. Jeśli jest to szczególny przypadek, zwróć null. Jeśli funkcja zwraca akurat pustą kolekcję, to oczywiście zwracanie jest w porządku. Jednak zwracanie pustej kolekcji jako specjalnego przypadku z powodu niepoprawnych parametrów lub z innych powodów NIE jest dobrym pomysłem, ponieważ maskuje specjalny przypadek.

Właściwie w tym przypadku zazwyczaj wolę rzucić wyjątek, aby upewnić się, że NAPRAWDĘ nie jest ignorowany :)

Mówienie, że sprawia, że ​​kod jest bardziej niezawodny (przez zwrócenie pustej kolekcji), ponieważ nie muszą one obsługiwać warunku zerowego, jest złe, ponieważ po prostu maskuje problem, który powinien zostać rozwiązany przez kod wywołujący.

Larry Watanabe
źródło
4

Twierdziłbym, że nullto nie to samo, co pusta kolekcja i powinieneś wybrać, która najlepiej reprezentuje to, co oddajesz. W większości przypadków nulljest to nic (oprócz SQL). Pusta kolekcja to coś, choć coś pustego.

Jeśli musisz wybrać jedno lub drugie, powiedziałbym, że powinieneś raczej wybierać pustą kolekcję niż zerową. Ale są chwile, kiedy pusta kolekcja nie jest tym samym, co wartość pusta.

Jason Baker
źródło
4

Zawsze myśl na korzyść swoich klientów (którzy używają interfejsu API):

Zwracanie „null” bardzo często powoduje problemy z klientami niepoprawnie obsługującymi kontrole null, co powoduje wyjątek NullPointerException podczas działania. Widziałem przypadki, w których takie brakujące sprawdzanie wartości zerowej wymuszało priorytetowy problem z produkcją (klient używał foreach (...) dla wartości zerowej). Podczas testowania problem nie wystąpił, ponieważ operowane dane były nieco inne.

Manuel Aldana
źródło
3

Chciałbym tu wyjaśnić, podając odpowiedni przykład.

Rozważ przypadek tutaj ..

int totalValue = MySession.ListCustomerAccounts()
                          .FindAll(ac => ac.AccountHead.AccountHeadID 
                                         == accountHead.AccountHeadID)
                          .Sum(account => account.AccountValue);

Tutaj rozważ funkcje, których używam ..

1. ListCustomerAccounts() // User Defined
2. FindAll()              // Pre-defined Library Function

Mogę łatwo używać ListCustomerAccounti FindAllzamiast.,

int totalValue = 0; 
List<CustomerAccounts> custAccounts = ListCustomerAccounts();
if(custAccounts !=null ){
  List<CustomerAccounts> custAccountsFiltered = 
        custAccounts.FindAll(ac => ac.AccountHead.AccountHeadID 
                                   == accountHead.AccountHeadID );
   if(custAccountsFiltered != null)
      totalValue = custAccountsFiltered.Sum(account => 
                                            account.AccountValue).ToString();
}

UWAGA: Ponieważ AccountValue nie jest null, funkcja Sum () nie zwróci null., Dlatego mogę z niej korzystać bezpośrednio.

Muthu Ganapathy Nathan
źródło
2

Dyskutowaliśmy w zespole programistów w pracy około tydzień temu i prawie jednogłośnie wybraliśmy pustą kolekcję. Jedna osoba chciała zwrócić wartość zerową z tego samego powodu, który Mike wskazał powyżej.

Henric
źródło
2

Pusta kolekcja. Jeśli używasz C #, założeniem jest, że maksymalizacja zasobów systemowych nie jest niezbędna. Chociaż mniej wydajne, zwracanie Pustej Kolekcji jest znacznie wygodniejsze dla zaangażowanych programistów (z tego powodu zostanie opisany powyżej).

ćmy
źródło
2

Zwracanie pustej kolekcji jest lepsze w większości przypadków.

Powodem tego jest wygoda implementacji dzwoniącego, spójna umowa i łatwiejsza implementacja.

Jeśli metoda zwraca wartość null w celu wskazania pustego wyniku, program wywołujący musi oprócz implementacji wyliczenia zaimplementować adapter sprawdzania wartości null. Ten kod jest następnie duplikowany w różnych obiektach wywołujących, więc dlaczego nie umieścić tego adaptera w metodzie, aby można go było ponownie użyć.

Prawidłowe użycie wartości null dla IEnumerable może wskazywać na brak wyniku lub błąd operacji, ale w takim przypadku należy rozważyć inne techniki, takie jak zgłoszenie wyjątku.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

namespace StackOverflow.EmptyCollectionUsageTests.Tests
{
    /// <summary>
    /// Demonstrates different approaches for empty collection results.
    /// </summary>
    class Container
    {
        /// <summary>
        /// Elements list.
        /// Not initialized to an empty collection here for the purpose of demonstration of usage along with <see cref="Populate"/> method.
        /// </summary>
        private List<Element> elements;

        /// <summary>
        /// Gets elements if any
        /// </summary>
        /// <returns>Returns elements or empty collection.</returns>
        public IEnumerable<Element> GetElements()
        {
            return elements ?? Enumerable.Empty<Element>();
        }

        /// <summary>
        /// Initializes the container with some results, if any.
        /// </summary>
        public void Populate()
        {
            elements = new List<Element>();
        }

        /// <summary>
        /// Gets elements. Throws <see cref="InvalidOperationException"/> if not populated.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>.</returns>
        public IEnumerable<Element> GetElementsStrict()
        {
            if (elements == null)
            {
                throw new InvalidOperationException("You must call Populate before calling this method.");
            }

            return elements;
        }

        /// <summary>
        /// Gets elements, empty collection or nothing.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with zero or more elements, or null in some cases.</returns>
        public IEnumerable<Element> GetElementsInconvenientCareless()
        {
            return elements;
        }

        /// <summary>
        /// Gets elements or nothing.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with elements, or null in case of empty collection.</returns>
        /// <remarks>We are lucky that elements is a List, otherwise enumeration would be needed.</remarks>
        public IEnumerable<Element> GetElementsInconvenientCarefull()
        {
            if (elements == null || elements.Count == 0)
            {
                return null;
            }
            return elements;
        }
    }

    class Element
    {
    }

    /// <summary>
    /// http://stackoverflow.com/questions/1969993/is-it-better-to-return-null-or-empty-collection/
    /// </summary>
    class EmptyCollectionTests
    {
        private Container container;

        [SetUp]
        public void SetUp()
        {
            container = new Container();
        }

        /// <summary>
        /// Forgiving contract - caller does not have to implement null check in addition to enumeration.
        /// </summary>
        [Test]
        public void UseGetElements()
        {
            Assert.AreEqual(0, container.GetElements().Count());
        }

        /// <summary>
        /// Forget to <see cref="Container.Populate"/> and use strict method.
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void WrongUseOfStrictContract()
        {
            container.GetElementsStrict().Count();
        }

        /// <summary>
        /// Call <see cref="Container.Populate"/> and use strict method.
        /// </summary>
        [Test]
        public void CorrectUsaOfStrictContract()
        {
            container.Populate();
            Assert.AreEqual(0, container.GetElementsStrict().Count());
        }

        /// <summary>
        /// Inconvenient contract - needs a local variable.
        /// </summary>
        [Test]
        public void CarefulUseOfCarelessMethod()
        {
            var elements = container.GetElementsInconvenientCareless();
            Assert.AreEqual(0, elements == null ? 0 : elements.Count());
        }

        /// <summary>
        /// Inconvenient contract - duplicate call in order to use in context of an single expression.
        /// </summary>
        [Test]
        public void LameCarefulUseOfCarelessMethod()
        {
            Assert.AreEqual(0, container.GetElementsInconvenientCareless() == null ? 0 : container.GetElementsInconvenientCareless().Count());
        }

        [Test]
        public void LuckyCarelessUseOfCarelessMethod()
        {
            // INIT
            var praySomeoneCalledPopulateBefore = (Action)(()=>container.Populate());
            praySomeoneCalledPopulateBefore();

            // ACT //ASSERT
            Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
        }

        /// <summary>
        /// Excercise <see cref="ArgumentNullException"/> because of null passed to <see cref="Enumerable.Count{TSource}(System.Collections.Generic.IEnumerable{TSource})"/>
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void UnfortunateCarelessUseOfCarelessMethod()
        {
            Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
        }

        /// <summary>
        /// Demonstrates the client code flow relying on returning null for empty collection.
        /// Exception is due to <see cref="Enumerable.First{TSource}(System.Collections.Generic.IEnumerable{TSource})"/> on an empty collection.
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void UnfortunateEducatedUseOfCarelessMethod()
        {
            container.Populate();
            var elements = container.GetElementsInconvenientCareless();
            if (elements == null)
            {
                Assert.Inconclusive();
            }
            Assert.IsNotNull(elements.First());
        }

        /// <summary>
        /// Demonstrates the client code is bloated a bit, to compensate for implementation 'cleverness'.
        /// We can throw away the nullness result, because we don't know if the operation succeeded or not anyway.
        /// We are unfortunate to create a new instance of an empty collection.
        /// We might have already had one inside the implementation,
        /// but it have been discarded then in an effort to return null for empty collection.
        /// </summary>
        [Test]
        public void EducatedUseOfCarefullMethod()
        {
            Assert.AreEqual(0, (container.GetElementsInconvenientCarefull() ?? Enumerable.Empty<Element>()).Count());
        }
    }
}
George Polevoy
źródło
2

Nazywam to moim błędem za miliard dolarów… W tym czasie projektowałem pierwszy kompleksowy system typów dla odniesień w języku zorientowanym obiektowo. Moim celem było upewnienie się, że każde użycie referencji powinno być całkowicie bezpieczne, a sprawdzanie wykonywane automatycznie przez kompilator. Ale nie mogłem oprzeć się pokusie wprowadzenia zerowego odniesienia, po prostu dlatego, że było to tak łatwe do wdrożenia. Doprowadziło to do niezliczonych błędów, słabych punktów i awarii systemu, które prawdopodobnie spowodowały miliard dolarów bólu i szkód w ciągu ostatnich czterdziestu lat. - Tony Hoare, wynalazca ALGOL W.

Zobacz tutajnull ogólnie misterną burzę gówna . Nie zgadzam się ze stwierdzeniem, które undefinedjest inne null, ale nadal warto je przeczytać. I wyjaśnia, dlaczego nullw ogóle powinieneś unikać, a nie tylko w przypadku, o który prosiłeś. Istotą jest to, że nullw każdym języku jest to szczególny przypadek. Musisz myśleć o tym nulljako o wyjątku. undefinedróżni się tym, że kod zajmujący się niezdefiniowanym zachowaniem jest w większości przypadków tylko błędem. C i większość innych języków ma również niezdefiniowane zachowanie, ale większość z nich nie ma identyfikatora tego w języku.

Ceving
źródło
1

Z punktu widzenia zarządzania złożonością, głównym celem inżynierii oprogramowania, chcemy uniknąć propagowania niepotrzebnej cykliczności złożoności wśród klientów interfejsu API. Zwracanie wartości zerowej do klienta jest jak zwracanie cyklicznego kosztu złożoności innej gałęzi kodu.

(Odpowiada to obciążeniu związanemu z testowaniem jednostkowym. Konieczne byłoby napisanie testu dla zerowej skrzynki zwrotnej, oprócz pustej skrzynki zwrotnej kolekcji).

dthal
źródło