2009-06-29 33 views
5

我發現在我的代碼中出現以下錯誤的頻率太高,想知道是否有人知道一些好的策略來避免它。C#:避免由不重寫引起的錯誤ToString

想像這樣的一類:

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

在某些時候,我創建使用利率的字符串,像這樣:

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

現在想象一下,在以後的日子我重構了的InterestRate財產從小數到它自己的類:

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

...但說我忘了以覆蓋InterestRate類中的ToString方法。除非我仔細查看InterestRate屬性的每個用法,否則我可能永遠不會注意到它在某個時刻正在轉換爲字符串。編譯器肯定不會選擇它。我唯一的救星機會就是通過集成測試。

下一次我打電話給我的PrintQuote方法,我會得到一個字符串是這樣的:

「利率是Business.Finance.InterestRate」

Ouch。這怎麼可以避免?

+3

我真的不認爲你想讓編譯器爲你構建任意的ToString()實現。 – 2009-06-29 00:58:13

回答

10

通過在IntrestRate類中創建ToString重寫。

+3

不是我想到它的方式,但這個答案是正確的...有一些有意義的方式表示自己作爲字符串的類應該總是重寫ToString()。 – 2009-06-29 00:53:52

+1

夠公平的,但我認爲這個問題實際上是如何撿起這種錯誤?在創建PrintQuote方法幾個月後,他可能已經更改了InterestRate屬性的定義,但沒有意識到其影響。正如他所說,編譯器在這裏沒有幫助。解決方案是爲每個類型的每個成員設置一個單元測試。 – 2009-06-29 01:43:01

+0

我認爲總是重寫ToString的習慣是一個很好的答案,但我認爲它更容易記住單元測試(至少可以運行測試覆蓋分析,但似乎沒有任何主流工具,會提醒你重寫ToString)。 – cbp 2009-06-29 01:53:10

3

創建ToString的覆蓋只是您爲大多數(即使不是全部)類所做的那些事情之一。當然對於所有「價值」類。


請注意,ReSharper會爲您生成很多樣板代碼。來源:

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

運行產生平等成員,生成格式化成員和生成構造函數的結果是:

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; } 
} 

注意有一個錯誤:它應該主動提出創建一個默認的構造函數。即使ReSharper也不可能是完美的。

-1

坦率地說,您的問題的答案是您的初始設計有缺陷。首先,你將一個屬性公開爲原始類型。 Some believe this is wrong。畢竟,你的代碼允許這...

var double = quote.InterestRate * quote.InterestRate; 

問題是,什麼是結果的單位?利息^ 2?您的設計的第二個問題是您依賴於隱式的ToString()轉換。依賴於隱式轉換的問題在C++(for example)中更爲人熟知,但正如您所指出的那樣,它也可以在C#中咬人。也許如果你的代碼最初有...

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

......你會注意到它在重構。底線是如果你在原始設計中有設計問題,他們可能會陷入重構,而可能不會。最好的辦法就是不要把它們放在第一位。

1

好吧,正如其他人所說,你只需要這樣做。但這裏有幾個想法可以幫助你確保你做到這一點:

1)使用一個基礎對象來覆蓋toString的所有值類,並且拋出一個異常。這將有助於提醒您再次覆蓋它。

2)爲FXCop(免費的Microsoft靜態代碼分析工具)創建一個自定義規則來檢查某些類的toString方法。如何確定哪些類應重寫toString作爲練習留給學生。 :)

3

不是一個混蛋,但每次創建一個類時寫一個測試用例。進入並避免您和其他參與您項目的人的疏忽是一種好習慣。

4

,以防止這類問題的方法是有絕對的所有類成員單元測試,因此這包括你的PrintQuote(Quote quote)方法:

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

在這種情況下,除非你定義之間的隱式轉換你的新InterestRate類和System.Decimal,這個單元測試實際上不再編譯。但那絕對是一個信號!如果您確實在InterestRate類和System.Decimal之間定義了隱式轉換,但忘記覆蓋ToString方法,則此單元測試將進行編譯,但會在Assert.AreEqual()行處(正確)失敗。

對絕對每個班級成員進行單元測試的必要性都不爲過。

0

的情況下的ToString被稱爲上鍵入作爲InterestRate東西靜態,因爲在你的榜樣,或在一個InterestRate被轉換爲Object,然後立即作爲參數傳遞給像某些相關情況string.Format,你可以想象用靜態分析來檢測問題。你可以搜索一個自定義的FxCop規則,它可以近似你想要的,或者寫一個你自己的規則。

請注意,設計一個足夠動態的呼叫模式可能會破壞你的分析,甚至可能不是一個非常複雜的分析;但捕獲最低的掛果應該很容易。

這就是說,我同意其他評論者的一些觀點,即徹底測試可能是解決這個特定問題的最佳方法。

0

對於一個非常不同的觀點,您可以將所有ToString'ing推遲到您的應用程序的單獨關注。 StatePrinter(https://github.com/kbilsted/StatePrinter)是一個這樣的API,您可以使用默認值或根據打印類型進行配置。

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

然後打印

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

,你會得到下面的輸出

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

並用的IValueConverter抽象,您可以定義類型如何打印機,並與FieldHarvester可以定義哪些字段將被包含在字符串中。