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

时间:2009-06-29 00:45:26

标签: 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”

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

8 个答案:

答案 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)

坦率地说,你的问题的答案是你的初始设计存在缺陷。首先,您将属性公开为基本类型。 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();

...你会在重构中注意到它。最重要的是,如果您在原始设计中遇到设计问题,它们可能会陷入重构,而可能不会。最好的办法是首先不要这样做。