Zwracanie null - alternatywy

0

Nawiązując do Dla takich powinno być osobn...

Jeżeli dobrze zrozumiałem, to zwracanie nulla z funkcji jest antywzorcem, ponieważ potem mogą dziać się różne ciekawe rzeczy takie jak NullPointerException itp. - w takim razie jakie są alternatywy w C#? Załóżmy że mamy metodę, która wyszukuje w liście rekord z zadaną nazwą, więc spodziewamy się sytuacji gdy takiego rekordu nie będzie i trzeba na to jakoś zareagować.

Wyjątki? Wydają mi się dość kosztowne gdybym miał je wszędzie wrzucać, dodatkowo takie wywołania trzeba potem gęsto opakowywać try-catchem - nie chcemy w końcu żeby taka nieistotna metoda wysypywała wszystko :P

To może null-objects? Albo zupełnie inny sposób którego tu nie przytoczyłem?
Nie daje mi to spokoju, ponieważ zawsze wyrzucałem nulle które potem gdzieś wyżej sprawdzałem i odpowiednio reagowałem. A nie chcę iść do piekła, jak to wspomniano we wpisie na mikro :P

3
  1. Null object pattern.
  2. Z metod zwracających kolekcje zwracać pustą kolekcję zamiast null.
  3. Option/maybe type - nie ma w języku, ale na GitHubie/NuGecie się znajdzie.
0

Pozwolę sobie wyjść z komentarzy, bo to co napisała @katelx jest związane z tematem:

mysle ze uzywanie maybe itp w c# to jednak troche przerost formy nad trescia

Gdy użycie NullObject jest w danej sytuacji niepoprawne - to co w takim wypadku - walisz po prostu nullami (lub zgrabnie opakowanymi)?

0

nie wale nullami :) szczerze to ciezko mi znalezc scenariusz gdzie zamiast null object pattern czy jakiegos sensownego porazkowego rezultatu mialoby sens bawic sie w optionale. jak juz operacja ma zdefiniowana porazke to preferuje zrobic jakas logike wokol tego zamiast zwracac maybe ktore nic nie mowi

0

Nie widzę w tym niczego złego. Weźmy taki kod:

Employee FindEmployeeById(int id)
{
  //w jakiś magiczny sposób zwracamy pracownika lub null, jeśli nie ma takiego pracownika
  return getEmployeeIfExists(id);
}

public void LoadEmployee(int id)
{
  Employee emp = FindEmployeeById(id);
  if(emp == null)
  {
    MessageBox.Show("Nie ma takiego pracownika");
    return;
  }

//dalej robimy coś z pracownikiem
}

Co jest złego w takim kodzie?

1
Juhas napisał(a):

Nie widzę w tym niczego złego. Weźmy taki kod:

Employee FindEmployeeById(int id)
{
  //w jakiś magiczny sposób zwracamy pracownika lub null, jeśli nie ma takiego pracownika
  return getEmployeeIfExists(id);
}

public void LoadEmployee(int id)
{
  Employee emp = FindEmployeeById(id);
  if(emp == null)
  {
    MessageBox.Show("Nie ma takiego pracownika");
    return;
  }

//dalej robimy coś z pracownikiem
}

Co jest złego w takim kodzie?

O ile getEmployeeIfExists sugeruje że może nie zwrócić niczego, to FindEmployeeById już nie. A jeżeli nie jesteś pewny czy coś może zwrócić null, to musisz założyć że ten null zostanie zwrócony. Ten kod jest krótki, ale wyobraź sobie że w tym ifie jest jakaś metoda która przyjmuje emp jako argument. Ty jesteś pewny że nie jest nullem, ale ktoś kto będzie pisał tą metodę nie i przez to musi to sprawdzić jeszcze raz. Może też Ci zaufać, ale jak popełnisz błąd to NPE czy tam NRE będzie u niego w kodzie bo zrobił źle. Dlatego o ile samo zwracanie nulla nie jest złe, to w takim wypadku musisz zawsze sprawdzać czy nie jest null, a to tworzy masę kodu który jest w teorii zbędny, przez co jest ucinany i czasem zbyt bardzo. Rozwiązaniem jest właśnie deklarowanie pól które nie zawsze zwracają wynik (Optional, Option, Nullable w kotlinie) i sprawdzać pod groźbą kary chłosty za każdym razem, lub w przeciwnym wypadku zawsze zwracać obiekt i wtedy nie musisz nic sprawdzać.

0
Juhas napisał(a):

Nie widzę w tym niczego złego. Weźmy taki kod:

Employee FindEmployeeById(int id)
{
  //w jakiś magiczny sposób zwracamy pracownika lub null, jeśli nie ma takiego pracownika
  return getEmployeeIfExists(id);
}

public void LoadEmployee(int id)
{
  Employee emp = FindEmployeeById(id);
  if(emp == null)
  {
    MessageBox.Show("Nie ma takiego pracownika");
    return;
  }

//dalej robimy coś z pracownikiem
}

Co jest złego w takim kodzie?

Co będzie jeśli ktoś zrobi tak:

Employee emp = FindEmployeeById(id);
if(emp.status == EmployeeStatus.New) 
.....

wtedy zawsze musisz martwić się, o to czy nie masz nulla. Naturalnie przygotowany jesteś na obiekt, a musisz pamiętać o nullach. Po co to robić ? Można powiedzieć, że Maluch i Mercedes to tak sam bezpieczne samochodu bo mają strefę zgniotu aż do silnika i bezpiecznie nimi jechać tylko trzeba uważać. Natomiast jak małolat rozwali sie mercem to tylko sie blaca pognie, a w maluchu się skasuje. Nawet senior czasami zapomni sprawdzić czy null, a potem na produkcji się sypie. Jeszcze spoko jak masz wyjątek, gorzej jak masz AV i to bez stosu... weź tego szukaj. Miliony dolarów zostały stracone przez takie błędy więc lepiej zastosować kombajn, niż cepami walić jak głupi ;)

0

Zgadzam się, że nawet senior może zapomnieć sprawdzić nulla. Ale równie dobrze może zrobić inny głupi błąd. Po to są testy, żeby takie sytuacje wyłapać.

OK, można zwracać za każdym razem obiekt Employee (nawet z pustymi danymi, jeśli nie ma takiego pracownika). Ale co, jeśli obiekt Employee ma w sobie pińcet innych obiektów? To te pińcet innych obiektów też musi być stworzone "puste". A jeśli te podobiekty też składają się z innych obiektów? Tu już robimy kombajn, którym będziemy kosić trawnik przed domkiem, bo przecież żadne pole nie może być null :/

Zgadzam się, że takie rozwiązanie nie jest najlepsze. Lepsze już było by rozwiązanie w stylu:

Employee emp;
if(FindEmployeeById(id, out emp))
{

} else
    MessageBox.Show("Nie ma");

A najlepsze chyba faktycznie z tym optionalem (którego jednak nie ma w standardzie). Ale nie zgodzę się, że rozwiązanie z nullem jest aż tak złe, żeby pakować za to do osobnego piekła ;) Rozwiązanie, jak rozwiązanie. Zawsze znajdzie się lepsze.

Nie podoba mi się to (mam ból d**y), że programiści tak często i chętnie opisują pewne rozwiązania jako "nigdy tego nie używaj!", "To antywzorzec ty chuliganie!". Czy to z tym nullem, czy z singletonem. Czytają gdzieś, że singleton jest antywzorcem, a potem ślepo powtarzają za każdym razem, gdy usłyszą to słowo.

A nawet "goto" w pewnych sytuacjach się broni - nawet w językach wysokiego poziomu. Jasne, że jakiś idiota może zrobić pętle z użyciem if i goto (w języku wysokiego poziomu), ale to nie znaczy, że NIGDY nie należy stosować "goto". W przeciwnym razie nie było by go w ogóle w języku, prawda?

Brakuje mi trochę takiego zdroworozsądkowego podejścia.

0

Z piekłem nie chodziło o samo używanie nulli, ale o to że było coś ładnie napisane z optionalem, a ktoś zamiast to kontynuować zamienił na null. Ale spoko, ja akurat się z tym nie zgodziłem w komentarzu ;).

A nawet "goto" w pewnych sytuacjach się broni - nawet w językach wysokiego poziomu.

Uważam że fanatyzm to równe zło co ignorancja, ale tutaj się czepnę. W całym moim życiu nie użyłem goto i nigdy nie miałem takiej potrzeby. Co najwyżej break czy continue. Z samym użyciem spotkałem się raz i to przez juniora. Więc teraz proszę, daj mi 1 sensowny przykład użycia goto.

0

Dzięki za odpowiedzi, ciekawie się czytało :P W takim razie chciałbym rozwiać jeszcze jedną wątpliwość.

Załóżmy że mamy pewną klasę która wczytuje plik i jakoś tam go parsuje i wrzuca do listy. Coś w ten deseń:

public class Record
{
    public String Name { get; set; } = String.Empty;
    public String Value { get; set; } = String.Empty;
}

public class Loader : ILoader
{
    List<Record> records { get; set; } = new List<Record>();

    public Loader ()
    {

    }

    //Podejście z nullem
    public GetByName(String name)
    {
        return records.FirstOrDefault(p => p.Name == name);
    }
}

Klasa Record jest bardzo prosta i nie posiada metod, stąd mogę traktować ją jako Null Object w przypadku gdy ją tylko utworzę i od razu zwrócę? Coś takiego np.:

//Podejście z null object (?)
public Record GetByName(String name)
{
    var r = records.FirstOrDefault(p => p.Name == name);
    if(r == null)
        return new Record();
    return r;
}

Ogólnie na wielu stronach widziałem że tworzony jest najperw interfejs typu IRecord i potem implementuje się klasy typu Record i NullRecord. Jednak tam były metody które faktycznie można było jakoś nadpisać, tutaj są tylko właściwości które domyślnie są takie same jakie bym dał w NullRecord.

0
krzysiek050 napisał(a):

Uważam że fanatyzm to równe zło co ignorancja, ale tutaj się czepnę. W całym moim życiu nie użyłem goto i nigdy nie miałem takiej potrzeby. Co najwyżej break czy continue. Z samym użyciem spotkałem się raz i to przez juniora. Więc teraz proszę, daj mi 1 sensowny przykład użycia goto.

W językach wysokiego poziomu też nigdy nie używałem goto (kiedyś zdarzyło się w Delphi, co się okazało pomocne w jednym przypadku - chodziło chyba o walidację czegoś do pierwszego błędu - nie pamiętam teraz dobrze). Microsoft podaje dwa przykłady: https://msdn.microsoft.com/pl-pl/library/13940fs2(v=vs.120).aspx

No, to wszystko da się zrobić inaczej, więc to nie są zbyt sensowne przykłady. Ale z jakiegoś powodu ta instrukcja cały czas znajduje się w językach wysokiego poziomu.

3

zwracanie nulla z funkcji jest antywzorcem, ponieważ potem mogą dziać się różne ciekawe rzeczy takie jak NullPointerException itp

Jak nie czyta się instrukcji, to nic dziwnego że człowiek robi sobie kuku.

Jeśli w dokumentacji funkcji stoi, że w razie niepowodzenia zwraca nulla - to sprawdzamy tego nulla.
Czy to takie trudne?

0
Azarien napisał(a):

zwracanie nulla z funkcji jest antywzorcem, ponieważ potem mogą dziać się różne ciekawe rzeczy takie jak NullPointerException itp

Jak nie czyta się instrukcji, to nic dziwnego że człowiek robi sobie kuku.

Jeśli w dokumentacji funkcji stoi, że w razie niepowodzenia zwraca nulla - to sprawdzamy tego nulla.
Czy to takie trudne?

Nie jest trudne, odnoszę się tylko do tego wpisu na mikro bo byłem ciekawy co na ten temat sądzą bardziej doświadczeni ode mnie. Nie każdy jest od razu takim profesjonalistą jak Ty :P

1
Azarien napisał(a):

zwracanie nulla z funkcji jest antywzorcem, ponieważ potem mogą dziać się różne ciekawe rzeczy takie jak NullPointerException itp

Jak nie czyta się instrukcji, to nic dziwnego że człowiek robi sobie kuku.

Jeśli w dokumentacji funkcji stoi, że w razie niepowodzenia zwraca nulla - to sprawdzamy tego nulla.
Czy to takie trudne?

Tak, dopóki pozostajemy w strefie "tu pytanie, tu odpowiedź". W praktyce wynik tej funkcji może nieść się daleko, daleko, hen przez program. Przepisywany, przepakowywany i podawany. Kilka zakrętów i tracisz już z oczu dokumentację, która tłumaczyła, że pod tajemniczym "nullem" jest zakodowane np. nieobecność pracownika w bazie. Może ten null dalej znaczy to co w punkcie wyjścia, a może po prostu programista zapomniał po drodze o jednej linijce która przepisywała tę wartość z jednego obiektu do drugiego, i teraz null znaczy tyle, że Rysiu zapomniał dodać przypisanie pola? Nie wiemy, póki nie sprawdzimy.

A dokumentacja to oczywiście świetna sprawa, ale powinna służyć jako uzupełnienie i rozszerzenie naszego rozumienia kodu. Nie zwalnia go ona z obowiązku, aby sam w sobie starał się być tak jasny i zrozumiały, jak tylko się da.

Jest taka inżynierska anegdota o gwoździu wystającym ze ścianie. Możesz dać obok kartkę, że "uwaga, gwóźdź" - ale i tak co jakiś czas ktoś się o ten gwóźdź zahaczy. Lepiej jest, gdy gwóźdź nie wystaje ze ściany w ogóle, niż gdy jest to udokumentowane ; )

Jeśli nie wierzysz nam, może przekona cię opinia wynalazcy nulla, który po latach nazwał go "billion-dollar mistake"?

Ja tak na szybko widzę jeszcze kilka kwestii:

  1. Z nullem jak z boolem, mawiał mój dziadek (programista przedwojenny). Czyli mamy do dyspozycji tylko dwie możliwości: albo coś jest nullem, albo nie. Jeśli przyjmiemy już, że null sygnalizuje np. niemożność zwrócenia rekordu pracownika, bo jego ID nie jest obecne w bazie... to nie mamy już kolejnych możliwości, by wyrazić inne ewentualne powody tego, że nie dostajemy rezultatu. Np. co zwrócić, żeby wyrazić, że baza jest akurat zajęta albo niepodpięta, albo że - bo ja wiem - pracownik jest zatrudniony w poufnym dziale ninja, do którego dana sesja nie ma uprawnień dostępu, czy cokolwiek w tym stylu. Czyli i tak trzeba będzie dodać alternatywną sygnalizację błędu, poprzez error object, wyjątki itd. i będziemy utrzymywać równolegle dwie konwencje. Przy czym, co gorsza, ktoś może wtedy zapomnieć o tym, że najstarszy z nieprawidłowych scenariuszy jest wciąż sygnalizowany emitowaniem nulla.

  2. Ponieważ nulle lubią wybuchać w twarz, u programistów rozwija się przez to traumatyczny przykurcz i paranoja. Prowadzi to do inflacji "safety checks", czyli ifów, które badają, czy coś nie jest nullem. Często są to ify piętrowe i skomplikowane. Bardzo często są przy tym zbędne, bo w złożonym programie trudno jest prześledzić każdą możliwą ścieżkę wykonania, w związku z czym dana wartość będzie legitymowana na okoliczność bycia nullem na wszelki wypadek wszędzie, znacznie więcej razy niż trzeba - np. na każdym poziomie stosu. Od czego nasz kod puchnie i brzydnie, aż rzygać się chce.

Miałem w tym momencie napisać trzeci argument, ale przypomniałem sobie te wielokrotne ify i musiałem udać się na przerwę techniczną

4
Juhas napisał(a):

Ale nie zgodzę się, że rozwiązanie z nullem jest aż tak złe, żeby pakować za to do osobnego piekła ;) Rozwiązanie, jak rozwiązanie. Zawsze znajdzie się lepsze.

Czyli nie zgadzasz się z twórcą null, który określił swój wynalazek jako billion dollar mistake?

Gdyby nie było null w ogóle, nie trzeba byłoby tylu ifów, i nie trzeba byłoby tylu godzin spędzać na debugowaniu głupich błędów. Zwracanie skądkolwiek null sprawia, że ktoś będzie musiał dopisać kolejnego głupiego ifa, a wcześniej prawdopodobnie spędzić godziny na poszukiwaniu źródła problemu. Za taką złośliwość wobec kolegów-programistów, oddzielne piekło się po prostu należy.

Bimbała napisał(a):
public class Record
{
    public String Name { get; set; } = String.Empty;
    public String Value { get; set; } = String.Empty;
}

Klasa Record jest bardzo prosta i nie posiada metod, stąd mogę traktować ją jako Null Object w przypadku gdy ją tylko utworzę i od razu zwrócę?

Moim zdaniem nie będzie to zbyt czytelne. Ja bym zrobił raczej tak:

public class Record
{
    public String Name { get; private set; } 
    public String Value { get; private set; } 
    
    public Record (string name, string value)
    {
         this.Name = name;
         this.Value = value;
    }

    public static Record Empty = new Record(string.Empty, string.Empty);
}
0

O kurcze, faktycznie wygląda to teraz dobrze, dzięki @somekind. Widzę że czeka mnie gruntowne przeglądnięcie wszystkich projektów, bo nulle u mnie gęsto zasiane :P

1
somekind napisał(a):
Juhas napisał(a):

Ale nie zgodzę się, że rozwiązanie z nullem jest aż tak złe, żeby pakować za to do osobnego piekła ;) Rozwiązanie, jak rozwiązanie. Zawsze znajdzie się lepsze.

Czyli nie zgadzasz się z twórcą null, który określił swój wynalazek jako billion dollar mistake?

Gdyby nie było null w ogóle, nie trzeba byłoby tylu ifów, i nie trzeba byłoby tylu godzin spędzać na debugowaniu głupich błędów. Zwracanie skądkolwiek null sprawia, że ktoś będzie musiał dopisać kolejnego głupiego ifa, a wcześniej prawdopodobnie spędzić godziny na poszukiwaniu źródła problemu. Za taką złośliwość wobec kolegów-programistów, oddzielne piekło się po prostu należy.

I gdyby nie nulle i zwykłe sprawdzenie (if obj != null), to byłyby inne problemy z jakimiś dziwnymi wynikami. I byłoby to jeszcze gorsze, bo nie wywali programu w przypadku błędu. Sytuacja, w której masz klasę z jednym, czy dwoma polami jest prosta. Ale co, jeśli masz klasę, która składa się z innych klas, które składają się z jeszcze innych klas? Musisz tworzyć kupę niepotrzebnych obiektów i pamiętać, że jak coś zmieniasz, to musisz też zmieniać dla pustych obiektów.

Dla prostej, małej klasy - mogę się zgodzić. Może być fajnie porównać sobie obiekt z jakąś wartością empty, np:

if(obj == Employee.Empty)

ale wymaga to z kolei przeładowania odpowiednich operatorów.

Wg mnie takie tworzenie struktur "pustych", żeby tylko uniknąć nulla nie ma żadnego sensu. I tak w pewnym momencie musisz sprawdzić, czy obiekt jest pusty, czy nie. Bo jeśli tego nie zrobisz, możesz dostać nieprawidłowe dane. I nawet o tym nie wiesz. Jeśli robisz coś na nullu, to wybucha Ci prosto w twarz i wiesz od razu, że coś jest nie tak. A co jest nie tak? No pewnie nie sprawdziłeś, czy obiekt jest nullem.

Więc co za różnica, czy sprawdzasz null, czy empty?

Każda publiczna funkcja w klasie powinna być dokumentowana. To nie problem wpisać w c# slasha i dwie gwiazdki, żeby stworzyć dokumentację i wpisać: "Może zwrócić NULL. W takim przypadku oznacza, że pracownik o danym ID nie istnieje w bazie".

3

Idea przewodnia jest taka, że pusty obiekt możesz podać do dowolnej funkcji, która przyjmuje obiekt danego typu i nic złego się nie stanie. Nie trzeba wewnątrz funkcji sprawdzać, czy coś jest puste, czy nie. To jest dużo lepsze niż NRE w twarz użytkownika (a tak bardzo często się dzieje).

0

No ok, rozumiem ideę. Ale może to prowadzić do złych wyników.

4

Dobra, nulle są złe, exceptiony wolne. Ale w takim wypadku co nam zostaje?
Może dziedziczenie? A może enum definiujący w jakim stanie jest obiekt i rzucający wyjątki w getterach (Ms Plox No :| )

TLDR; Nulle najszybsze

Naskrobałem szybkie porównanie wydajności. (Disclaimer: Nie jestem za/przeciw nullom czy jakiemukolwiek rozwiązaniu, a te testy są troszkę naiwne, ale są :D)

Mamy sobie jakiś tam bazowy rezultat

public abstract class Result
{
    public string SomeData { get; }
}

który czasem się może powieść

 public class SucceededResult : Result
{
    public string Result { get; }
    public SucceededResult(string result)
    {
        this.Result = result;
    }
 }

lub też nie.

public class FailedResult : Result
{
    public string FailureReason { get; }
    public FailedResult(string message)
    {
        this.FailureReason = message;
    }
}

##Ok, no to możemy przejść do sprawdzania

        public static string IsHard()
        {
            var result = Service.SomeServiceCall();
            if (result is SucceededResult)
            {
                return ((SucceededResult)result).Result;
            }
            else if (result is FailedResult)
            {
                return ((FailedResult)result).FailureReason;
            }
            else
            {
                return result.SomeData;
            }
        }

(Elsy są nadmiarowe ofc, ale IMHO kod lepiej wygląda)

##To może tak bardziej miękko :) ?

        public static string IsAs()
        {
            var result = Service.SomeServiceCall();
            if (result is SucceededResult)
            {
                return (result as SucceededResult).Result;
            }
            else if (result is FailedResult)
            {
                return (result as FailedResult).FailureReason;
            }
            else
            {
                return result.SomeData;
            }
        }

##Jakiś inny sposób na obsłużenie wszystkich wypadków? Może nulle :)?

        public static string AsNull()
        {
            var result = Service.SomeServiceCall();
            var succeededResult = result as SucceededResult;
            if (succeededResult != null)
            {
                return succeededResult.Result;
            }
            var failedResult = result as FailedResult;
            if (failedResult != null)
            {
                return failedResult.FailureReason;
            }
            return result.SomeData;
        }

Dobra, to nie wygląda najlepiej ale jest.

Dobrodzieje z .neta uraczyli nas ostatnio PM, więc co nam szkodzi, popaczmy

Patrzaj

        public static string CSharp7IsSwitch()
        {
            var result = Service.SomeServiceCall();
            switch (result)
            {
                case FailedResult fr:
                    return fr.FailureReason;
                case SucceededResult succeeded:
                    return succeeded.Result;
                default:
                    return result.SomeData;
            }
        }

        public static string CSharp7IsIfElse()
        {
            var result = Service.SomeServiceCall();
            if (result is SucceededResult succeeded)
            {
                return succeeded.Result;
            }
            else if (result is FailedResult fr)
            {
                return fr.FailureReason;
            }
            else
            {
                return result.SomeData;
            }
        }

Nie wiem jak Wam, ale mi akurat ten ficzer C#7 się podoba.

Teraz coś dla wielbicieli starych, dobrych nullów

        public static string NullCheck()
        {
            var result = Service.SomeNullServiceCall();
            if (result != null)
            {
                return result.SomeData;
            }
            return "Something goes wrong.";
        }

Tylko troszkę informacji straciliśmy, nie mamy informacji co poszło źle, nie możemy określić dlaczego.
W tym miejscu mogą nam pomóc nasi bardzo dobrzy przyjaciele, wyjątki!


        [Benchmark]
        public static string TryCatchCheck()
        {
            try
            {
                var result = Service.SomeNullWithExceptionLogicDrivenServiceCall();
                if (result != null)
                {
                    return result.SomeData;
                }
            }
            catch (FailedException fe)
            {
                return fe.FailureReason;
            }
            return "Something goes wrong.";
        }

        [Benchmark]
        public static string TryCatchCheckThrows()
        {
            try
            {
                var result = Service.SomeNullWithExceptionLogicDrivenServiceCallThrow();
                if (result != null)
                {
                    return result.SomeData;
                }
            }
            catch (FailedException fe)
            {
                return fe.FailureReason;
            }
            return "Something goes wrong.";
        }

O tak, jeszcze tylko do FCE dodać logowanie do jakiegoś api wystawionego w necie i czekanie na jego odpowiedź i będzie pięknie :)

Ok, to już większość kodziku. Teraz zobaczymy porównanie

Program.CSharp7IsIfElse: DefaultJob
Runtime = Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0; GC = Concurrent Workstation
Mean = 7.9190 ns, StdErr = 0.1111 ns (1.4%); N = 15, StdDev = 0.4304 ns
Min = 7.4792 ns, Q1 = 7.5838 ns, Median = 7.6683 ns, Q3 = 8.2658 ns, Max = 8.6899 ns
IQR = 0.6820 ns, LowerFence = 6.5609 ns, UpperFence = 9.2887 ns
ConfidenceInterval = [7.7012 ns; 8.1368 ns] (CI 95%)
Skewness = 0.66, Kurtosis = 1.75
 
 
Total time: 00:01:20 (80.51 sec)
 
// * Summary *
 
BenchmarkDotNet=v0.10.3.0, OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531249 Hz, Resolution=395.0619 ns, Timer=TSC
  [Host]     : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
  DefaultJob : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
 
 
              Method |           Mean |      StdErr |        StdDev |
-------------------- |--------------- |------------ |-------------- |
              IsHard |      8.0456 ns |   0.0587 ns |     0.2195 ns |
       TryCatchCheck |      6.5391 ns |   0.0993 ns |     0.3848 ns |
 TryCatchCheckThrows | 34,806.6174 ns | 314.8299 ns | 1,219.3311 ns |
           NullCheck |      6.1183 ns |   0.0953 ns |     0.3692 ns |
              AsNull |      8.0413 ns |   0.0988 ns |     0.3825 ns |
                IsAs |      8.0874 ns |   0.0958 ns |     0.3712 ns |
     CSharp7IsSwitch |     10.7629 ns |   0.0982 ns |     0.3804 ns |
     CSharp7IsIfElse |      7.9190 ns |   0.1111 ns |     0.4304 ns |

###OLABOGA, NULLE NAJSZYBSZE,
Przecież jak zrobimy 100 000 porównań to różnica będzie blisko 1s!!!One One One </irony>

Żeby zachować obiektywność myślę, że warto byłoby porównać CSharp7IsIfElse do TryCatchCheck ,ale też do TryCatchCheckThrows - te 2 niosą podobną informację jak pierwszy. Sam null nic nie mówi, i prawdopodobnie żeby się dowiedzieć co się stało trzeba byłoby zawołać serwis.OstatniKodBłędu, albo zwracać jakąś kopertę z tymi danymi.

Jeszcze rzut oka na IsSwitch i IsIfElse

Dlaczego IsSwitch jest wolniejszy? Bo go nie zoptymalizowali do końca (IMHO),
Wincyj operacji to wolniejsze, nie ma czego dalej drążyć.

Ogólnie można przyjąć że wszystkie sposoby są akceptowalne imho oprócz robienia logiki na wyjątkach :)

Czekam na hejty i pokazanie mi błędów :D

GH z kodem
Debug anycpu
Release x64 Debugger
Release x64 Standalone
Release x64 Standalone single return
Release x64 Standalone more with inline

0

Fajnie, ale ten temat nie dotyczy wydajności, bo to ma marginalne znaczenie.

A co do ostatniej ciekawostki to porównałem na szybko IL i wykonuje się wszystko w dokładnie takiej samej kolejności, więc to raczej zwykły błąd pomiaru. No chyba że coś przeoczyłem w IL :D

0
Juhas napisał(a):

No ok, rozumiem ideę. Ale może to prowadzić do złych wyników.

Ale czemu? Co nazywasz złym wynikiem?

0

Dajmy taką sytuację, że trzeba znaleźć wszystkie osoby starsze niż 18 lat. I teraz, może dojść do sytuacji, w której pobieramy również "nullowy" obiekt, w którym data urodzenia jest ustawiona domyślnie na 1900-01-01. Wyniki będą błędne. A bez tego, program w najgorszym wypadku się wywali z null pointer exception.

Takich przykładów można wymyślać i wymyślać.

1

Hmmm ale jaki cudem ? Takie zapytanie powinno zwrócić pustą kolekcje generyczną typu Osoba, a nie null, czy jeden obiekt ....

1

Może ja przedstawię jakiś przykład który zobrazuje myśl @Juhas a.

 public Moj.File getFile(string path) {
  // jeżeli znajdzie mi plik to go zwarca 
  // z tym że Moj.File zawiera np Content, Length, Name, CreateDate i jeszcze jakieś tam
}

no i teraz nie lepiej zwrócić od razu nulla, i wiedzieć że tego pliku nie ma, niż zwracać jakieś optionale albo nullObjecty które i tak będziemy musieli sprawdzić czy ten plik jest czy go nie ma. Bo np optional Content="", Length = -1, Name="", CreateDate="0001-01-01"
a nulla możemy sprawdzić i jeżeli jest null to od razu wiemy, że nie znaleźliśmy pliku no i musimy i tak jakoś na to zareagować a nie udawać że otrzymaliśmy plik z pustym Content??

Od razu zaznaczę że nie obstaje za żadną ze stron:p Rozumiem po prostu myśl @Juhas a

0

Jak zrobisz

Moj.File file = getFile(path); 
if(file.content.contains("Jakiś ważny szukany wzorzec"))
{
  //zrób coś ważnego. 
}

To pewnie zadziała to logicznie prawidłowo mimo, ze nie sprawdziłem, że content != "". W przypadku zwrócenia nulla na tym ifie program się wysypie.

2
Juhas napisał(a):

Dajmy taką sytuację, że trzeba znaleźć wszystkie osoby starsze niż 18 lat. I teraz, może dojść do sytuacji, w której pobieramy również "nullowy" obiekt, w którym data urodzenia jest ustawiona domyślnie na 1900-01-01.

To to nie jest nullowy obiekt tylko jakaś patologia.

Takich przykładów można wymyślać i wymyślać.

No wiele przykładów słabej architektury czy antywzorców można podać. Tylko czemu tak bardzo chcesz udowodnić, że utrudnianie sobie życia jest fajne?

vesper0990 napisał(a):

no i teraz nie lepiej zwrócić od razu nulla, i wiedzieć że tego pliku nie ma

Jesteś pewien?
A może plik jest, tylko nie ma do niego dostępu?
A może plik jest na udziale sieciowym, ale sieć padła?
null nie niesie ze sobą żadnej informacji w takim przypadku.

a nulla możemy sprawdzić i jeżeli jest null to od razu wiemy, że nie znaleźliśmy pliku no i musimy i tak jakoś na to zareagować a nie udawać że otrzymaliśmy plik z pustym Content??

W tym przypadku, to w ogóle należałoby zwracać wynik operacji z możliwymi statusami błędów, i wówczas, gdy status jest ok, to dopiero dobierać się do zawartości pliku.

0
somekind napisał(a):
Juhas napisał(a):

Dajmy taką sytuację, że trzeba znaleźć wszystkie osoby starsze niż 18 lat. I teraz, może dojść do sytuacji, w której pobieramy również "nullowy" obiekt, w którym data urodzenia jest ustawiona domyślnie na 1900-01-01.

To to nie jest nullowy obiekt tylko jakaś patologia.

Czemu patologia? Obiekt, jak obiekt. Data ustawiona domyślnie.

somekind napisał(a):

Takich przykładów można wymyślać i wymyślać.

No wiele przykładów słabej architektury czy antywzorców można podać. Tylko czemu tak bardzo chcesz udowodnić, że utrudnianie sobie życia jest fajne?

Utrudniam sobie życie, sprawdzając, czy obiekt jest nullem?
OK, inny przykład. Załóżmy, że jest jakiś kod, który pobiera sobie po jakiś ID klientów z bazy danych (klientów do pobrania jest tysiąc). Ale tak się stało, że tych klientów z jakiegoś powodu już w bazie nie ma. I teraz tworząc puste obiekty, tworzymy zupełnie bez sensu 1000 obiektów. Jest taka sytuacja możliwa? A jeśli nie, to dlaczego. A jeśli tak, to czy taka sytuacja jest lepsza niż lista nulli?

1
Juhas napisał(a):

Czemu patologia? Obiekt, jak obiekt. Data ustawiona domyślnie.

To jest właśnie ta patologia. Jeśli obiekt ma być nullowy, to nie ma czegoś takiego jak domyślna data.
Generalnie domyślne wartości to zło, bo zazwyczaj są źle domyślone, i to nie dotyczy tylko tego wzorca.

A swoją drogą, to jeśli prosty kod zwraca błędne wyniki, to znaczy, że jest błędnie przetestowany.

Utrudniam sobie życie, sprawdzając, czy obiekt jest nullem?

Wymagając tego sprawdzenia poprzez stosowanie nulli w ogóle.
Rozumiem, że trzeba sprawdzać null w przypadku danych przychodzących do systemu z zewnątrz, na to niestety nie ma na naszym świecie rady (billion dollar mistake), ale w kodzie nad którym ma się kontrolę?

OK, inny przykład. Załóżmy, że jest jakiś kod, który pobiera sobie po jakiś ID klientów z bazy danych (klientów do pobrania jest tysiąc). Ale tak się stało, że tych klientów z jakiegoś powodu już w bazie nie ma. I teraz tworząc puste obiekty, tworzymy zupełnie bez sensu 1000 obiektów. Jest taka sytuacja możliwa? A jeśli nie, to dlaczego. A jeśli tak, to czy taka sytuacja jest lepsza niż lista nulli?

W takim wypadku lista powinna być po prostu pusta, tak jak SQL zwróci pusty zbiór. Nie wiem co tu kombinować nad innym rozwiązaniem.

Null obiektów się nie tworzy, jest jeden statyczny danego typu w całej aplikacji, i zwraca się go zawsze, gdy jest potrzebny. Przynajmniej ja nie widzę sensu innej implementacji.

1

@Juhas:

A jeśli tak, to czy taka sytuacja jest lepsza niż lista nulli?

Musisz chyba ten przykład rozpisać, bo dla mnie lista nulli w dosłownym rozumieniu to jakieś kuriozum.
Twój przykład zrozumiałem tak:

List<int> ids = new List<int> {1,2,3,4,5};
var users = repo.GetUsersById(ids);

I teraz zakładając że nie znaleziono użytkowników o id 2, 4, 5, a GetUsersById zwraca listę to dostaniemy:
{user1, null, user3, null, null}
Jaki jest tego sens? Przecież, gdy nie znaleziono użytkownika wystarczy zwrócić, tylko tych co znaleziono:
{user1, user3}
A gdybyśmy chcieli mieć zawsze jakieś info dlaczego użytkownicy nie zostali znalezieni, to właśnie fajnie może się sprawdzić NullObject:
{userIsAlive, userIsDead, userIsAlive, userNotFound, userIsNotInterestedIn}

1 użytkowników online, w tym zalogowanych: 0, gości: 1