C#:避免因不重写ToString而导致的错误

C#:避免因不重写ToString而导致的错误,c#,tostring,C#,Tostring,我发现下面的错误在我的代码中太频繁了,我想知道是否有人知道一些好的策略来避免它 想象这样一个类: public class Quote { public decimal InterestRate { get; set; } } public string PrintQuote(Quote quote) { return "The interest rate is " + quote.InterestRate; } 在某个时刻,我创建了一个利用利率的字符串,如下所示: publi

我发现下面的错误在我的代码中太频繁了,我想知道是否有人知道一些好的策略来避免它

想象这样一个类:

public class Quote
{
   public decimal InterestRate { get; set; }
}
public string PrintQuote(Quote quote)
{
    return "The interest rate is " + quote.InterestRate;
}
在某个时刻,我创建了一个利用利率的字符串,如下所示:

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方法时,我会得到如下字符串:

“利率是商业、金融、利率”


哎哟。如何避免这种情况?

在IntrestRate类中创建ToString的重写。

创建ToString的重写只是大多数(如果不是全部)类所做的事情之一。当然,对于所有“值”类


请注意,ReSharper将为您生成大量样板代码。发件人:

public class Class1
{
    public string Name { get; set; }
    public int Id { get; set; }
}
运行Generate Equality Members、Generate Formatting Members和Generate Constructor的结果是:

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; }
}
公共类类别1:IEquatable
{
公共类1(字符串名称,int-id)
{
名称=名称;
Id=Id;
}
公共布尔等于(1类其他)
{
if(ReferenceEquals(null,其他))
{
返回false;
}
if(ReferenceEquals(this,other))
{
返回true;
}
返回等于(other.Name,Name)&&other.Id==Id;
}
公共重写字符串ToString()
{
返回string.Format(“名称:{0},Id:{1}”,名称,Id);
}
公共覆盖布尔等于(对象对象对象)
{
if(ReferenceEquals(null,obj))
{
返回false;
}
if(ReferenceEquals(this,obj))
{
返回true;
}
if(obj.GetType()!=typeof(Class1))
{
返回false;
}
回报率等于((1类)obj);
}
公共覆盖int GetHashCode()
{
未经检查
{
返回((Name!=null?Name.GetHashCode():0)*397)^Id;
}
}
公共静态布尔运算符==(左1类,右1类)
{
返回等于(左、右);
}
公共静态布尔运算符!=(左1类,右1类)
{
返回!等于(左,右);
}
公共字符串名称{get;set;}
公共int Id{get;set;}
}

注意有一个bug:它应该提供创建默认构造函数的功能。即使是ReSharper也不可能完美。

坦白地说,你的问题的答案是你最初的设计有缺陷。首先,将属性公开为基元类型。毕竟,您的代码允许这样做

var double = quote.InterestRate * quote.InterestRate;
问题是,结果的单位是什么?利息^2?设计的第二个问题是依赖隐式的ToString()转换。依赖隐式转换的问题在C++()中更为著名,但正如你指出的,也可以在C语言中对你进行攻击。也许如果您的代码最初有

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

。。。你会在重构中注意到它。底线是,如果您的原始设计中存在设计问题,它们可能会在重构中被捕获,而最终可能不会。最好的办法是一开始就不要做这些事。

好吧,正如其他人所说,你只要做就行了。但这里有几个方法可以帮助你自己确保你做到了:

1) 对覆盖toString的所有值类使用一个基对象,比如抛出一个异常。这将有助于提醒您再次覆盖它


2) 为FXCop(免费Microsoft静态代码分析工具)创建自定义规则,以检查某些类型的类上的toString方法。如何确定哪些类型的类应该覆盖toString,留给学生作为练习。:)

不要做傻子,每次创建类时都要编写一个测试用例。这是一个很好的习惯,可以避免你和其他参与你的项目的人的疏忽。

防止这种问题的方法是对你所有的类成员进行单元测试,因此包括你的
PrintQuote(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()行失败


绝对每个类成员都需要一个单元测试,这一点怎么强调都不过分。

在静态地对某个键入为
兴趣值的对象调用ToString的情况下,如您的示例所示,或者在某些相关情况下,
利率
被强制转换为
对象
,然后立即用作string.Format之类的参数,您可以通过静态分析检测问题。您可以搜索一个定制的FxCop规则,该规则与您想要的规则近似,也可以编写自己的规则

请注意,设计一个足够动态的调用模式总是可能的,它会破坏您的分析,甚至可能不是一个非常复杂的分析;),但是,抓住挂在最下面的果实应该很容易


也就是说,我同意其他一些评论者的观点,即彻底的测试可能是解决这个特定问题的最佳方法。

从一个完全不同的角度来看,您可以将所有的尝试都推迟到应用程序的另一个关注点。StatePrinter()是o
new Car() {
    StereoAmplifiers = null
    steeringWheel = new SteeringWheel()
    {
        Size = 3
        Grip = new FoamGrip()
        {
            Material = ""Plastic""
        }
        Weight = 525
    }
    Brand = ""Toyota"" }