2009-06-29 13 views
5

Następnego błędu występuję zbyt często w moim kodzie i zastanawiałem się, czy ktoś zna kilka dobrych strategii, aby tego uniknąć.C#: Unikanie błędów spowodowanych brakiem przesuwania ToString

Wyobraźmy sobie klasę tak:

public class Quote 
{ 
    public decimal InterestRate { get; set; } 
} 

w pewnym momencie utworzyć ciąg, który wykorzystuje stopę procentową, tak:

public string PrintQuote(Quote quote) 
{ 
    return "The interest rate is " + quote.InterestRate; 
} 

Teraz wyobraź sobie w późniejszym terminie I refactored się InterestRate własność od dziesiętnej do własnej klasy:

public class Quote 
{ 
    public InterestRate InterestRate { get; set; } 
} 

... ale powiedz, że zapomniałem zastąpić metodę ToString w klasie InterestRate. Chyba że uważnie przyjrzę się każdemu użyciu właściwości InterestRate, prawdopodobnie nigdy nie zauważę, że w pewnym momencie jest ona konwertowana na ciąg znaków. Kompilator z pewnością tego nie wybierze. Moją jedyną szansą na zbawienie jest test integracyjny.

Następnym razem wzywam moje PrintQuote metodę, chciałbym uzyskać ciąg tak:

„Stopa procentowa jest Business.Finance.InterestRate”.

Ouch. Jak można tego uniknąć?

+3

Naprawdę nie sądzę, że chcesz, aby kompilator tworzył dla ciebie dowolne implementacje ToString(). –

Odpowiedz

10

Tworząc zastąpienie ToString w klasie IntrestRate.

+3

Nie w sposób, w jaki bym to sformułował, ale ta odpowiedź jest prawidłowa ... Klasy, które mają jakiś znaczący sposób reprezentowania się jako ciąg, powinny zawsze przesłonić ToString(). –

+1

Wystarczająco uczciwe, ale myślę, że pytanie brzmi: jak podejść do tego rodzaju błędu? Mógł zmienić definicję właściwości InterestRate miesiąc po stworzeniu metody PrintQuote i nie zdał sobie sprawy z implikacji. Jak powiedział, kompilator nie pomoże tutaj. Rozwiązaniem tego problemu jest posiadanie zestawu testów jednostkowych dla absolutnie każdego członka każdego typu. –

+0

Myślę, że nabycie zwyczaju nadpisywania ToString jest dobrą odpowiedzią, ale wydaje mi się, że łatwiej jest zapamiętać test jednostkowy (można przynajmniej przeprowadzić analizę pokrycia testowego, podczas gdy nie wydaje się, aby istniał jakikolwiek główny nurt narzędzie, które przypomni Ci o przesłonięciu ToString). – cbp

3

Tworzenie nadpisania ToString to tylko jedna z rzeczy, które robisz dla większości, jeśli nie wszystkich, klas. Z całą pewnością dla wszystkich klas "wartości".


Zauważ, że ReSharper wygeneruje dla ciebie dużo kodu standardowego. Od:

public class Class1 
{ 
    public string Name { get; set; } 
    public int Id { get; set; } 
} 

Wynikiem działania Generowanie równości posłów, generowanie Formatowanie Członków i wygenerować konstruktor:

public class Class1 : IEquatable<Class1> 
{ 
    public Class1(string name, int id) 
    { 
     Name = name; 
     Id = id; 
    } 

    public bool Equals(Class1 other) 
    { 
     if (ReferenceEquals(null, other)) 
     { 
      return false; 
     } 
     if (ReferenceEquals(this, other)) 
     { 
      return true; 
     } 
     return Equals(other.Name, Name) && other.Id == Id; 
    } 

    public override string ToString() 
    { 
     return string.Format("Name: {0}, Id: {1}", Name, Id); 
    } 

    public override bool Equals(object obj) 
    { 
     if (ReferenceEquals(null, obj)) 
     { 
      return false; 
     } 
     if (ReferenceEquals(this, obj)) 
     { 
      return true; 
     } 
     if (obj.GetType() != typeof (Class1)) 
     { 
      return false; 
     } 
     return Equals((Class1) obj); 
    } 

    public override int GetHashCode() 
    { 
     unchecked 
     { 
      return ((Name != null ? Name.GetHashCode() : 0)*397)^Id; 
     } 
    } 

    public static bool operator ==(Class1 left, Class1 right) 
    { 
     return Equals(left, right); 
    } 

    public static bool operator !=(Class1 left, Class1 right) 
    { 
     return !Equals(left, right); 
    } 

    public string Name { get; set; } 
    public int Id { get; set; } 
} 

Uwaga jest jeden błąd: powinno być oferowane do tworzenia domyślnego konstruktora. Nawet ReSharper nie może być doskonały.

-1

Szczerze, odpowiedź na twoje pytanie jest taka, że ​​twój początkowy projekt był wadliwy. Najpierw ujawniłeś właściwość jako typ pierwotny. Some believe this is wrong. W końcu twój kod pozwala na ...

var double = quote.InterestRate * quote.InterestRate; 

Problem polega na tym, jaka jest jednostka wyniku? Interes^2? Drugi problem z twoim projektem polega na tym, że polegasz na niejawnej konwersji ToString(). Problemy z poleganiem na niejawnej konwersji są bardziej znane w języku C++ (for example), ale jak zauważysz, mogą cię również ugryźć w C#. Być może, jeśli twój kod miał pierwotnie ...

return "The interest rate is " + quote.InterestRate.ToString(); 

... zauważyłbyś go w reaktorze. Podsumowując, jeśli masz problemy z projektowaniem w oryginalnym projekcie, mogą one zostać uwięzione w refaktorze, a może nie. Najlepiej jest nie robić ich w pierwszej kolejności.

+0

W rzeczywistości problem nie zostałby wykryty, nawet jeśli .ToString() został określony jawnie. Nie sądzę, że warto handlować z użyteczności w tym przypadku. Również stopa procentowa jest tylko przykładem. Istnieje wiele przypadków, w których typ pierwotny ma więcej sensu, ale cierpi na ten sam problem ToString(). Nigdy nie narażasz podstawowych typów, ale może to być wystarczająco przyzwoita odpowiedź. – cbp

+0

Przepraszam, chciałbym powiedzieć "handlowanie z czytelności" – cbp

1

Cóż, jak powiedzieli inni, po prostu trzeba to zrobić. Ale oto kilka pomysłów, które pomogą Ci się upewnić, że to zrobisz:

1) użyj obiektu podstawowego dla wszystkich klas wartości, które zastępują toString i, powiedzmy, zgłasza wyjątek. Pomoże ci to przypomnieć o tym, aby znowu to zmienić.

2) utwórz niestandardową regułę dla FXCop (bezpłatne narzędzie do analizy statycznej kodu Microsoft), aby sprawdzić metody toString na niektórych typach zajęć. Jak określić, które typy klas mają zastąpić toString, pozostawia się jako ćwiczenie dla ucznia. :)

+0

Jeśli zdecydujesz się użyć wspólnej klasy bazowej, i jeśli możesz zrobić tę klasę 'abstrakcyjną', istnieje możliwość powiedzenia **' public override string ToString(); '**. Następnie klasy pochodne są zmuszone do ponownego wdrożenia 'ToString'. –

3

Nie bądź palantem, ale napisz przypadek testowy za każdym razem, gdy tworzysz klasę. To dobry nawyk, aby dostać się i uniknąć przeoczenia dla ciebie i innych osób uczestniczących w twoim projekcie.

4

Sposobem na zapobieganie tego rodzaju problemu jest posiadanie testów jednostkowych dla absolutnie wszystkich członków klasy, więc także metodę PrintQuote(Quote quote):

[TestMethod] 
public void PrintQuoteTest() 
{ 
    quote = new Quote(); 
    quote.InterestRate = 0.05M; 
    Assert.AreEqual(
     "The interest rate is 0.05", 
     PrintQuote(quote)); 
} 

w tym przypadku, o ile nie zdefiniowano niejawna konwersja między twoja nowa klasa InterestRate i System.Decimal, ten test jednostkowy w rzeczywistości nie byłby już kompilowany. Ale to zdecydowanie byłby sygnał! A jeśli zdefiniowałeś niejawną konwersję między twoją klasą InterestRate a System.Decimal, ale zapomniałeś zastąpić metodę ToString, to ten test jednostkowy by się skompilował, ale (prawidłowo) zawiedzie w linii Assert.AreEqual().

Potrzeba posiadania testu jednostkowego dla absolutnie każdego członka klasy nie może być zawyżona.

0

W przypadku ToString nazywa coś statycznie wpisany jako InterestRate, jak w przykładzie, lub w niektórych powiązanych przypadkach gdy InterestRate jest oddanych do Object a następnie natychmiast wykorzystywanych jako parametr do czegoś jak string.Format, możesz wykryć problem za pomocą analizy statycznej. Możesz wyszukać niestandardową regułę FxCop, która jest zbliżona do tego, co chcesz, lub napisać własną.

Należy pamiętać, że zawsze będzie można opracować wystarczająco dynamiczny wzorzec połączenia, który przerwie analizę, prawdopodobnie nawet bardzo skomplikowaną;), ale chwytanie owoców o najniższym zawieszeniu powinno być łatwe.

To powiedziawszy, zgadzam się z niektórymi innymi komentatorami, że dokładne testowanie jest prawdopodobnie najlepszym podejściem do tego konkretnego problemu.

0

Z bardzo odmiennej perspektywy można odroczyć cały ToStringing od osobnej sprawy związanej z wnioskiem. StatePrinter (https://github.com/kbilsted/StatePrinter) to jeden z takich interfejsów API, w którym można użyć wartości domyślnych lub skonfigurować w zależności od rodzaju drukowanych.

var car = new Car(new SteeringWheel(new FoamGrip("Plastic"))); 
car.Brand = "Toyota"; 

następnie wydrukować go

StatePrinter printer = new StatePrinter(); 
Console.WriteLine(printer.PrintObject(car)); 

i uzyskać następujące dane wyjściowe

new Car() { 
    StereoAmplifiers = null 
    steeringWheel = new SteeringWheel() 
    { 
     Size = 3 
     Grip = new FoamGrip() 
     { 
      Material = ""Plastic"" 
     } 
     Weight = 525 
    } 
    Brand = ""Toyota"" } 

iz abstrakcji IValueConverter można zdefiniować jak typy są drukarki, a także z FieldHarvester można zdefiniować które pola mają być zawarte w łańcuchu.

Powiązane problemy