我发现在我的代码中经常发生以下错误,并且想知道是否有人知道一些避免它的好策略。
想象一下这样的课程:
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”。
哎哟。如何避免这种情况?
答案 0 :(得分:10)
通过在IntrestRate类中创建ToString的覆盖。
答案 1 :(得分: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()行中(正确)失败
对绝对每个班级成员进行单元测试的需求不容小觑。
答案 2 :(得分:3)
创建ToString的覆盖只是您为大多数(如果不是全部)类所做的事情之一。当然适用于所有“价值”类别。
请注意,ReSharper将为您生成许多样板代码。从:
public class Class1
{
public string Name { get; set; }
public int Id { get; set; }
}
运行Generate Equality Members,生成格式化成员和生成构造函数的结果是:
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都不是完美的。
答案 3 :(得分:3)
不是一个混蛋,而是每次创建一个类时编写一个测试用例。进入并避免对您和参与项目的其他人进行疏忽是一种好习惯。
答案 4 :(得分:1)
嗯,正如其他人所说,你只需要这样做。但是这里有一些想法可以帮助你自己确保这样做:
1)对所有覆盖toString的值类使用基础对象,例如抛出异常。这有助于提醒您再次覆盖它。
2)为FXCop(免费的Microsoft静态代码分析工具)创建自定义规则,以检查某些类型的类的toString方法。如何确定哪些类型的类应覆盖toString,留作学生的练习。 :)
答案 5 :(得分:0)
如果在某个静态上输入ToString作为InterestRate
,就像在您的示例中一样,或者在InterestRate
被强制转换为{的某些相关情况下{1}}然后立即用作string.Format之类的参数,你可以想象用静态分析来检测问题。您可以搜索与您想要的近似的自定义FxCop规则,或者编写自己的规则。
请注意,总是可以设计一个足够动态的调用模式,它会破坏你的分析,可能甚至不是一个非常复杂的分析;),但抓住最低挂的水果应该很容易。
尽管如此,我同意其他一些评论者的观点,即彻底的测试可能是解决这一特定问题的最佳方法。
答案 6 :(得分: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,您可以定义字符串中包含哪些字段。
答案 7 :(得分:-1)
var double = quote.InterestRate * quote.InterestRate;
问题在于,结果的单位是多少?利息^ 2?您的设计的第二个问题是您依赖于隐式ToString()转换。依赖于隐式转换的问题在C ++(for example)中更为人所知,但正如您所指出的,也可以在C#中咬你。也许如果你的代码最初有......
return "The interest rate is " + quote.InterestRate.ToString();
...你会在重构中注意到它。最重要的是,如果您在原始设计中遇到设计问题,它们可能会陷入重构,而可能不会。最好的办法是首先不要这样做。