Dlaczego metoda, która zwraca bool / int i ma rzeczywisty obiekt jako parametr wyjściowy?

12

W bazie danych mojej firmy (aplikacja .NET 3.5) widzę następujący wzór kodu:

bool Foo(int barID, out Baz bazObject) { 
    try { 
            // do stuff
            bazObject = someResponseObject;

            return true;
    }
    catch (Exception ex) { 
        // log error
        return false;
    }
}

// calling code
BazObject baz = new BazObject();
fooObject.Foo(barID, out baz);

if (baz != null) { 
    // do stuff with baz
}

Staram się owijać wokół głowy, dlaczego miałbyś to zrobić zamiast tego, że Foometoda po prostu bierze identyfikator i zwraca Bazobiekt, zamiast zwracać wartość, która nie jest używana, a faktycznym obiektem jest parametr referencyjny lub wyjściowy.

Czy jest jakaś ukryta zaleta tego stylu kodowania, którego mi brakuje?

Wayne Molina
źródło
W przykładzie bazjest nulli zwracany boolistotą falsenie równoważne. new BazObject()jest nigdy null, więc jeśli nie bazObjectzostanie zaktualizowany przed Exceptionwrzuceniem Foo, falseto kiedy zostanie zwrócony baz, nigdy nie będzie null. To pomaga ogromnie jeśli specyfikacja dla Foobyły dostępne. W rzeczywistości jest to prawdopodobnie najpoważniejszy problem związany z tym kodem.
Steve Powell,
Moja pamięć jest niejasna, ponieważ to było dawno temu, ale myślę, że pomyliłem przykład i sprawdzałem, czy „baz” jest fałszywe, a nie, czy było puste. W każdym razie wzorzec wydawał mi się naprawdę archaiczny, tak jak w przypadku VB6, a programista nigdy nie zadał sobie trudu poprawienia swojego kodu (czego nie zrobił)
Wayne Molina

Odpowiedzi:

11

Zwykle używasz tego wzorca, aby móc pisać kod w ten sposób:

if (Foo(barId, out bazObject))
{
  //DoStuff with bazobject
}

jest używany na przykład w CLR dla TryGetValue w klasie słownikowej. Pozwala to uniknąć nadmiarowości, ale parametry out i ref zawsze wydawały mi się nieco niechlujne

Homde
źródło
1
+1, to jest powód, dla którego, choć raczej zwracam obiekt z wartością logiczną i obiektem, zamiast zwracać je oba osobno
pdr
Oznaczę to jako odpowiedź, ponieważ wyjaśnia przyczynę, chociaż wydaje się, że konsensus jest dość przestarzały, z wyjątkiem szczególnych okoliczności.
Wayne Molina
Ta odpowiedź uzasadnia użycie tego wzorca. Ale jeśli jest to WSZYSTKO W bazie kodu, prawdopodobnie jest to zła praktyka, taka jak strona Seana.
Codism
11

Jest to stary kod w stylu C, zanim były wyjątki. Zwracana wartość wskazuje, czy metoda zakończyła się powodzeniem, czy nie, a jeśli tak, parametr zostanie wypełniony wynikiem.

W .net mamy wyjątki w tym celu. Nie powinno być żadnego powodu, aby stosować ten wzór.

[edytuj] Oczywiste są skutki wydajności w obsłudze wyjątków. Może to ma z tym coś wspólnego. Jednak w tym fragmencie kodu jest już zgłaszany wyjątek. Byłoby czystsze pozwolić po prostu przesuwać się w górę stosu, dopóki nie zostanie złapany w bardziej odpowiednim miejscu.

Sean Edwards
źródło
Nie. Nie mogę się z tym zgodzić. Nigdy nie używaj wyjątku dla oczekiwanego stanu. Jeśli na przykład analizujesz ciąg znaków do liczby całkowitej, zawsze jest możliwe, że ktoś przekaże ciąg nie do analizy. W tym przypadku nie powinieneś zgłaszać wyjątku
pdr
Nie to powiedziałem. Jeśli czytasz kod opublikowany przez OP, jawnie zdarza się wyjątek, ale metoda łapie go i zwraca false. To pytanie dotyczy obsługi błędów, a nie spodziewanych warunków.
Sean Edwards
Tak, wydaje się, że jest stosowany głównie w metodach, które ładują dane z bazy danych, np. if (baz.Select())Ale najczęściej zwracana wartość jest po prostu wyrzucana i wartość jest sprawdzana pod kątem wartości null lub jakiejś właściwości.
Wayne Molina
@Sean, zobacz, co mówisz, po prostu sprzeciwiam się wyrażeniu „W .NET mamy wyjątki w tym celu”. Wyjątki mają dla mnie zupełnie inny cel
pdr
1
Ok, pozwól mi wyjaśnić. Prawdą jest, że nie należy dodawać wartości logicznej do każdej metody, aby uwzględnić błędy w kodowaniu nieprawidłowych argumentów. Ale tam, gdzie dane wejściowe do metody pochodzą od użytkownika, a nie od programisty , powinieneś być w stanie obsłużyć dane wejściowe bez zgłaszania wyjątku, jeśli się pomylił.
pdr
2

Biorąc pod uwagę fragment kodu, wygląda to zupełnie bezcelowo. Według mnie początkowy wzorzec kodu sugerowałby, że null dla BazObject byłby dopuszczalnym przypadkiem, a powrót bool jest miarą bezpiecznego określenia przypadku niepowodzenia. Jeśli następujący kod to:

// calling code
BazObject baz = new BazObject();
bool result = fooObject.Foo(barID, out baz);

if (result) { 
    // do stuff with baz
    // where baz may be 
    // null without a 
    // thrown exception
}

To miałoby dla mnie większy sens, aby zrobić to w ten sposób. Być może jest to metoda, której ktoś wcześniej używał, aby zagwarantować, że baz był przekazywany przez referencję, nie rozumiejąc, jak parametry obiektu faktycznie działają w języku C #.

Joel Etherton
źródło
Myślę, że został napisany przez obecnego członka zespołu. Być może powinienem się tym martwić o O_O
Wayne Molina
@Wayne M: Mniej zmartwień niż potencjalna okazja do treningu :)
Joel Etherton
1

Czasami ten wzorzec jest przydatny, gdy osoba dzwoniąca nie powinna dbać o to, czy zwracany typ jest typem referencyjnym czy wartościowym. Jeśli wywołujesz taką metodę w celu pobrania typu wartości, ten typ wartości będzie albo potrzebował ustalonej niepoprawnej wartości (np. Double.NaN), albo potrzebujesz innego sposobu ustalenia sukcesu.

Kevin Hsu
źródło
To ma sens. Wydaje się to trochę dziwne, ale to brzmi jak ważny powód.
Wayne Molina
0

Pomysł polega na zwróceniu wartości wskazującej, czy proces się powiódł, umożliwiając następujący kod:

Baz b;
if (fooObject.foo(id, out b)) {
   // do something with b
}
else {
   Error.screamAndRun("object lookup/whatever failed! AAaAAAH!");
}

Jeśli obiekt nie może mieć wartości NULL (co w twoim przykładzie wygląda poprawnie), lepiej zrobić to w następujący sposób:

Baz b = fooObject.foo(id);
if (b != null) {
   // do something with b
}
else {
   Error.screamAndRun("object lookup/whatever failed! AAaAAAH!");
}

Jeśli obiekt może mieć wartość NULL, możliwe są wyjątki:

try {
   Baz b = fooObject.foo(id);
}
catch (BazException e) {
   Error.screamAndRun("object lookup/whatever failed! AAaAAAH!");
}

Jest to powszechny wzorzec używany w C, gdzie nie ma wyjątków. Pozwala to funkcji zwrócić błąd. Wyjątki są ogólnie o wiele czystszym rozwiązaniem.

Michael K.
źródło
Zwłaszcza, że ​​kod już zgłasza wyjątek.
David Thornley