6

我发现在我的代码中经常出现以下错误,并想知道是否有人知道一些好的策略来避免它。

想象这样一个类:

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”

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

4

8 回答 8

10

通过在 IntrestRate 类中创建 ToString 的覆盖。

于 2009-06-29T00:46:50.007 回答
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() 行失败。

绝对不能夸大对每个班级成员进行单元测试的需要。

于 2009-06-29T01:35:50.173 回答
3

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

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

于 2009-06-29T00:54:58.260 回答
3

不要成为一个混蛋,而是在每次创建类时编写一个测试用例。对于您和参与您的项目的其他人来说,进入并避免疏忽是一个好习惯。

于 2009-06-29T01:20:57.443 回答
1

好吧,正如其他人所说,你只需要这样做。但这里有一些想法可以帮助你确保你做到这一点:

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

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

于 2009-06-29T01:14:06.067 回答
0

在像在您的示例中那样静态类型为 an 的内容上调用 ToString 的情况下InterestRate,或者在某些相关情况下,将 anInterestRate强制转换为Object然后立即用作 string.Format 之类的参数,您可以想象用静态分析。您可以搜索与您想要的近似的自定义 FxCop 规则,或编写您自己的规则。

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

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

于 2009-06-30T23:33:07.733 回答
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,您可以定义要包含在字符串中的字段。

于 2014-07-12T20:21:34.323 回答
-1

坦率地说,你的问题的答案是你最初的设计是有缺陷的。首先,您将属性公开为原始类型。有人认为这是错误的。毕竟,您的代码允许这样做...

var double = quote.InterestRate * quote.InterestRate;

问题是,结果的单位是什么?兴趣^2?您的设计的第二个问题是您依赖于隐式 ToString() 转换。依赖隐式转换的问题在 C++ 中更为人所知(例如),但正如您所指出的,在 C# 中也会对您不利。也许如果您的代码最初具有...

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

...您会在重构中注意到它。底线是,如果您在原始设计中存在设计问题,它们可能会在重构中被捕获,也可能不会。最好的选择是一开始就不要这样做。

于 2009-06-29T01:06:59.390 回答