Zapewniaj metodę ToString() w swoich obiektach – część 2

W moich zamierzeniach ten temat miał być omówiony w jednej części, ale duża ilość komentarzy, pytań i uwag spowodowała, że powstała część druga.

Podbij ↑

Istnienie drugiej (nieplanowanej) części posta mogłoby sugerować, że chcę się wycofać z części poglądów, które przedstawiłem w części pierwszej. Częściowo tak, ale nie w tę stronę co myślicie. Rzeczywiście zmieniłem zdanie, od dzisiaj będę pisał ToString() jeszcze częściej, tzn. dla każdej swojej klasy i struktury, nawet jeżeli tworzyłbym bibliotekę dla klienta zewnętrznego.

Ok. Część druga nie musiałaby powstawać, gdybym temat lepiej opisał za pierwszym razem. Moja wina. Tym razem postaram się lepiej wyjaśnić dlaczego warto, a nawet należy pisać swoją wersję ToString() dla własnych obiektów.   W poprzednim wpisie skupiłem się na korzyściach jakie płyną z posiadania własnej wersji ToString() we własnych obiektach podczas debugowania kodu. Tylko “przy okazji” pokazałem, że ToString() czasami przydatna jest w UI. Dzisiaj trochę o tym i o innych zastosowaniach więcej. Jednak najpierw odniosę się do pytań i uwag związanych z ToString() w debugowaniu. Część z nich pochodzi z FB, a część z komentarzy pod poprzednim wpisem. Ciężko było odpowiedzieć na wszystkie w formie odpowiedzi na komentarz dlatego odpowiadam tutaj.

 

“przeciez nie bedziesz zmienial co chwile implementacji ToString() bo teraz inne pole chcesz widziec, a moze uzywasz juz go w comboboxie i wtedy skopiesz wyswietlanie?”

Wiktor

Oczywiście, że nie będę. Nic takiego nie sugerowałem i nie wpadłbym na to. Nie sądziłem również, że ktoś mógłby pomyśleć, że to sugeruję. ToString() napiszę raz i może zmodyfikuję jeszcze raz w przyszłości (chociaż to się naprawdę rzadko zdarza).

Co jak sobie ustawisz w ToString() wyswietlanie hasla (bo debugujesz hashowanie) zostawisz to, a ktos w logach bedzie wysweitlal info(„Nieudanie logowanie uzytkownika: {0}”, user);

Wiktor

Bądźmy poważni 🙂 Jeżeli ktoś chce sobie zrobić krzywdę to sobie zrobi. Jeżeli “potrafi” logować hasła przy pomocy ToString() to będzie “potrafił” także inaczej. Pisząc ToString() trzeba myśleć tak samo jak wtedy kiedy piszemy każdą inną metodę. Dodatkowo skoro ktoś już gdzieś loguje obiekt tak jak to przedstawiłeś tzn., że już nadpisał ToString(), albo ma obowiązek sprawdzić co zostanie zalogowane – zwrócone z ToString(). Bo chyba nie chodzi mu o to, żeby zalogować “Nieudane logowanie użytkownika : MojSuperSystem.User”? Tytuł tego i poprzedniego wpisu to “Zapewniaj metodę ToString()…”, a nie modyfikuj metodę ToString() pod debugowanie (to również komentarz odnośnie pierwszej uwagi). Jeżeli metoda ToString() jest już nadpisana i wykorzystywana to nie robimy niczego specjalnego tylko pod debugowanie.

Poza tym jak w klasie będzie dużo właściwości to podejrzewam że ToString mega wydłuży to okienko przy debugowaniu i ciężko będzie czytać – lepiej sobie rozwinąć i mieć pionowo wypisane wszystkie właściwości (i ewentualnie scrollować pionowo)

Piotrek

Przykład z klasą Character (ToString() zwracające wartość wszystkich 3 właściwości), był tylko przykładem. ToString() nie musi zwracać wszystkich danych obiektu. Klasa Character równie dobrze mogła zwracać tylko nazwisko. Chodzi o to, aby obiekt został jakoś wyróżniony. Niekoniecznie przez niepowtarzalny string (nazwiska Nowak, czy Kowalski powtarzałyby się często, ale to nic). Jeżeli debugujemy kod pracujący z kolekcjami to bardzo ułatwia pracę możliwość znalezienia konkretnego obiektu w liście. A wtedy tak jak piszesz. “Rozwijamy” obiekt i szukamy szczegółów jeżeli są nam potrzebne. I wbrew temu co przeczytałem w jednym z komentarzy nie zaoszczędzamy jednego kliknięcia, ale nazbierają się ich setki lub nawet tysiące. Inny przykład (z bardziej rozbudowanym obiektem). Listujesz na stronie 48 książek z danego działu. Książka oprócz tytułu, ISBN, ID bazodanowego, autora (nazwisko lub property typu Author), wydawnictwa (nazwa lub property typu Publisher), piętnastu innych właściwości i w końcu spisu treści nie będzie zwracała tych wszystkich informacji! Przecież nie zwrócimy spisu treści w ToString(). W jednej aplikacji wystarczy sam tytuł, w innej może dodatkowo ISBN, a w jeszcze innej samo nazwisko autora.

Co do ToString() to jednak trzeba być bardzo ostrożnym bo to jest metoda wykorzystywana w automatyce nie tylko debuggera. Czasami domyślna metoda sortowania w jakichś kontrolkach to sortowanie po stringu z ToString() – ale czasem i w mniej oczywistych sytuacjach – wystarczy, że jakiś zewnętrzny komponent woła na dostarczonym elemencie ToString() – i teraz możemy sobie zaburzyć sortowanie lub też sprawić, że metoda ToString będzie wywoływana wielokrotnie zupełnie nie potrzebnie produkując stringi które zajmą dużo pamięci (a jak jeszcze komuś przyjdzie do głowy sklejanie stringów plusem to już brrr)

No i już pomijając fakt, że przy złożonych obiektach to może być ciężkie, generować stado stringów do pamięci,…

Sławek

Akurat nie kojarzę żadnych kontrolek, które sortują wg wyniku ToString(). Jednak jeżeli wyświetlamy w takich nasze obiekty tzn., że już zaimplementowaliśmy ToString()! Czyż nie? W przeciwnym razie otrzymywalibyśmy sortowanie wg. nazw typów. Co może byłoby ok, gdybyśmy implementowali swój Object Explorer 🙂 A jeżeli ToString() już jest nadpisane to wszystko pięknie. Nic nie zmieniamy. Po raz kolejny podkreślę: “zapewniaj ToString w swoich obiektach”, a nie “zmieniaj ToString() pod debugowanie”. Nie rozumiem natomiast komentarza odnośnie możliwości sprawienia, że metoda ToString() może być zawołana wielokrotnie. Override ToString() nie wpływa na to jak często ta metoda będzie wywoływana. Przykład ToString() dla złożonego obiektu (Książka) omówiłem powyżej. A sklejanie stringów wcale nie jest takie złe – The Sad Tragedy of Micro-Optimization Theater.

Jeśli ktoś już upiera się przy nadpisywaniu metody ToString(), może użyć atrybutu [Conditional("DEBUG")]. W takim wypadku nadpisanie metody nastąpi wyłącznie w kodzie kompilowanym w konfiguracjach Debug.

Paweł

To na pewno nie jest dobre rozwiązanie! Patrz przykłady Sławka i przykład z logowaniem Wiktora. Skoro metoda ToString() jest wykorzystywana to “usuwając” ją w RELEASE całkowicie zmieniamy działanie kodu! Atrybutu Conditional mógłby zostać użyty na własnej metodzie DebugOnlyLog(ObjectWithCustomToString o), ale nie na ToString() obiektu ObjectWithCustomToString. Ten atrybut jest użyty na metodach klasy Debug. Np. Write(object) – która to metoda woła ToString() na przekazanym obiekcie.

Pozostałe uwagi dotyczyły tych samych zagadnień.

To tyle, jeżeli chodzi o ToString() i debugowanie. Mam nadzieję, że tym razem lepiej przekazałem o co mi chodziło. Może tamten post mógł zasugerować, że radzę pisać/modyfikować ToString() pod debugowanie (nawet konkretnej funkcji). Tak nie jest. Teraz o czymś innym niż debugowanie.

UI

O UI było poprzednio i wiele dodawał nie będę. Prawdą jest, że zastosowanie ToString() do wyświetlania w GUI obiektów ma swoje ograniczenia. Jeżeli w kilku miejscach wyświetlamy obiekt na kilka sposobów pewnie nie wykorzystałbym ToString() jako jednego z nich.

Logowanie

Posiadanie ToString() często ułatwia nam logowanie danych obiektów. Weźmy przykład logowania danych użytkownika. Jeżeli ToString() zwraca np. tylko id i login użytkownika to bardzo często będzie to wszystko czego wymagamy w logu – “Użytkownik 123, adamn usunął wszystkie cele ze swojego planu.”, “Użytkownik 123, adamn dodał cele podwładnemu 321 jank”. Nie musimy w każdym miejscu gdzie logujemy wykonywać tego samego string.Format. Może się zdarzy, że gdzieś jednak trzeba będzie zalogować panieńskie nazwisko matki użytkownika, ale na ogół nie.

Testy jednostkowe

Metoda ToString() jest wykorzystywana podczas zwracania komunikatu o błędzie  Assercji w NUnit. Podejrzewam, że podobnie jest w XUnit. Spójrzmy na przykład testu.

unitTest

Uruchamiamy pierwszy raz i oglądamy wynik:

unitTestResultWithoutToString

Zgodzicie się, że komunikat nie jest pomocny. Prawdopodobnie zwróciło nam Janka. Co robimy teraz? Debugujemy? 🙂 W tym przypadku dodanie ToString() właśnie pozwoli nam uniknąć debugowania! Wykorzystując ToString() z poprzedniego postu otrzymujemy:

unitTestResultWithToString 

Wg mnie tak lepiej lepiej. Teraz wiemy, że zwróciło Adama, ale nie zaimplementowaliśmy metody Equals w klasie Character. Jeżeli macie wątpliwości, czy wykorzystanie ToString() tutaj to dobry sposób, to Kent Beck w książce Test Driven Development robi dokładnie to samo – rozdział 10.

Jeżeli to nadal Was nie przekonuje to w książce Effective C# Bill Wagner poświęcił tematowi zapewniania ToString() we wszystkich naszych obiektach nadpisanej wersji ToString() cały piąty rozdział (Item 5). Motywuje to podobnie jak ja łatwiejszym debugowaniem, możliwością wykorzystania w UI oraz tym, że w ten sposób ułatwiamy pracę klientom naszego kodu ponieważ domyślne działanie ToString() jest bezużyteczne!

Jeżeli wszystko to mało, to ostatnie dwa powody powinny chyba przekonać wszystkich. Wiele razy czytałem o tym, że przy okazji pisania bloga dużo można się nauczyć. I przy okazji pisania tego posta rzeczywiście czegoś się nauczyłem. Znam .NET Framework od ok 10 lat i dopiero teraz dowiedziałem się, że Microsoft zaleca pisanie własnej wersji ToString() dla każdej klasy i struktury, którą napiszemy!

When you create a custom class or struct, you should override the ToString method in order to provide information about your type to client code.

Jednocześnie zalecają uwagę i zastanowienie się nad tym, co zwracamy z ToString() (hasło?)

Security Note

When you decide what information to provide through this method, consider whether your class or struct will ever be used by untrusted code. Be careful to ensure that you do not provide any information that could be exploited by malicious code.

Źródło: MSDN

Co ciekawe dla obiektów anonimowych kompilator generuje ToString() wypisującą wartości wszystkich właściwości w postaci { Property = Value, Property2 = Value2 }.

I w tym miejscu zmieniam swoje poglądy. W poprzedniej części pisałem, że ToString() nie nadpisałbym gdybym tworzył bibliotekę dla zewnętrznego klienta, bo ten mógłby oczekiwać domyślnego działania ToString(). Błąd! Teraz zmieniam zdanie!

Również Framework Design Guidelines zaleca definiowanie własnej ToString() – punkt 8.9.3. Tutaj można znaleźć wycinek z tych zaleceń. Co ciekawe, dopuszczają one zwracanie nawet danych wrażliwych! Inna ciekawe porada, która nie jest zamieszczona na SO w linku powyżej pochodzi stąd:

Do override ToString when your exception provides extra properties. The code handling the custom exception may not have caught your specific type of exception, and therefore does not have access to the extra information unless you provide it in ToString.

Co prawda sami twórcy .NET nie wszędzie trzymają się tych zaleceń. Ale sprawdźcie int, czy bool. Nie korzystają z atrybutu DebuggerDisplay. Wyobrażacie sobie konieczność nawigowania do ich prywatnych pól m_value, aby sprawdzić, jaką wartość mają zmienne podczas debugowania? Dla obiektów anonimowych to kompilator generuje ToString() wypisujący wartości wszystkich właściwości obiektu.

Tak więc, zapewniaj metodę ToString() w swoich obiektach.

Podbij ↑

Dodaj komentarz