2009-06-29 44 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可以定义哪些字段将被包含在字符串中。