C #: предотвращение ошибок, вызванных не переопределением ToString - PullRequest
6 голосов
/ 29 июня 2009

Я нахожу следующую ошибку, которая встречается слишком часто в моем коде, и задаюсь вопросом, знает ли кто-нибудь какие-нибудь хорошие стратегии, чтобы ее избежать.

Представьте себе такой класс:

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

... но скажите, что я забыл переопределить метод ToString в классе InterestRate. Если бы я не внимательно изучил каждое использование свойства InterestRate, я бы, вероятно, никогда не заметил бы, что в какой-то момент оно преобразуется в строку. Компилятор, конечно, не подхватит это. Мой единственный шанс на спасение - через интеграционный тест.

В следующий раз, когда я вызову мой PrintQuote метод, я получу строку, подобную этой:

"Процентная ставка - Business.Finance.InterestRate" .

Уч. Как этого избежать?

Ответы [ 8 ]

10 голосов
/ 29 июня 2009

Путем создания переопределения ToString в классе IntrestRate.

4 голосов
/ 29 июня 2009

Способ предотвращения такого рода проблем состоит в том, чтобы провести модульный тест для абсолютно всех ваших учеников, который поэтому включает в себя ваш метод 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 ().

Невозможно переоценить необходимость проведения модульного теста абсолютно для каждого ученика.

3 голосов
/ 29 июня 2009

Не для того, чтобы быть придурком, а писать тестовый пример каждый раз, когда вы создаете класс. Это хорошая привычка - избегать недосмотра для вас и других участников вашего проекта.

3 голосов
/ 29 июня 2009

Создание переопределения 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 голос
/ 29 июня 2009

Ну, как уже говорили другие, вам просто нужно это сделать. Но вот пара идей, которые помогут вам убедиться, что вы делаете это:

1) использовать базовый объект для всех ваших классов значений, который переопределяет toString и, скажем, выдает исключение. Это поможет напомнить вам, чтобы переопределить его снова.

2) создайте пользовательское правило для FXCop (бесплатный инструмент статического анализа кода Microsoft) для проверки методов toString в определенных типах классов. Как определить, какие типы классов должны переопределять toString, оставлено в качестве упражнения для студента. :)

0 голосов
/ 13 июля 2014

Для совсем другой точки зрения вы можете отложить все ToString до отдельной задачи вашего приложения. 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 вы можете указать, какие поля должны быть включены в строку.

0 голосов
/ 01 июля 2009

В случае, когда ToString вызывается для чего-то статически , напечатанного как InterestRate, как в вашем примере, или в некоторых связанных случаях, когда InterestRate приводится к Object, а затем немедленно используется в качестве параметра для чего-то вроде строки. Формат можно предположить для обнаружения проблемы со статическим анализом. Вы можете найти собственное правило FxCop, которое приблизительно соответствует вашему желанию, или написать свое собственное.

Обратите внимание, что всегда будет возможно придумать достаточно динамичный шаблон вызовов, который нарушит ваш анализ, возможно, даже не очень сложный;), но поймать самый низко висящий фрукт должно быть достаточно легко.

Тем не менее, я согласен с некоторыми другими комментаторами, что тщательное тестирование, вероятно, лучший подход к этой конкретной проблеме.

0 голосов
/ 29 июня 2009

Честно говоря, ответ на ваш вопрос заключается в том, что ваш первоначальный дизайн был ошибочным. Во-первых, вы выставили свойство как примитивный тип. Некоторые считают, что это неправильно . Ведь ваш код позволяет это ...

var double = quote.InterestRate * quote.InterestRate;

Проблема в том, что является единицей результата? Интерес ^ 2? Вторая проблема с вашим дизайном заключается в том, что вы полагаетесь на неявное преобразование ToString (). Проблемы с использованием неявного преобразования более известны в C ++ (, например, ), но, как вы заметили, могут укусить вас и в C #. Возможно, если ваш код изначально имел ...

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

... вы бы заметили это в рефакторе. В итоге, если у вас есть проблемы с дизайном в вашем оригинальном дизайне, они могут быть обнаружены в рефакторинге, а могут - нет. Лучше всего не делать их в первую очередь.

...