2009-06-29 11 views
5

Encuentro con demasiada frecuencia el siguiente error en mi código y me pregunto si alguien conoce algunas buenas estrategias para evitarlo.C#: evitar errores causados ​​por no anular ToString

Imagine una clase como esta:

public class Quote 
{ 
    public decimal InterestRate { get; set; } 
} 

En algún momento se crea una cadena que utiliza el tipo de interés, de esta manera:

public string PrintQuote(Quote quote) 
{ 
    return "The interest rate is " + quote.InterestRate; 
} 

Ahora imagine en una fecha posterior que refactorizado la InterestRate propiedad de un decimal a su propia clase:

public class Quote 
{ 
    public InterestRate InterestRate { get; set; } 
} 

... pero dicen que olvidé para anular el método ToString en la clase InterestRate. A menos que busque cuidadosamente cada uso de la propiedad InterestRate, probablemente nunca notaría que en algún momento se está convirtiendo en una cadena. El compilador ciertamente no captaría esto. Mi única posibilidad de salvador es a través de una prueba de integración.

La próxima vez que llamo mi PrintQuote método, me gustaría tener una cadena como esta:

"La tasa de interés es Business.Finance.InterestRate".

Ouch. ¿Cómo puede esto ser evitado?

+3

Realmente no creo que desee que el compilador invente implementaciones arbitrarias de ToString() para usted. –

Respuesta

10

Al crear una anulación de ToString en la clase IntrestRate.

+3

No del modo en que lo habría redactado, pero esta respuesta es correcta ... Las clases que tienen alguna forma significativa de representarse a sí mismas como una cadena siempre deben anular ToString(). –

+1

Bastante, pero creo que la pregunta es, ¿cómo se puede detectar este tipo de error? Pudo haber cambiado la definición de la propiedad InterestRate meses después de haber creado el método PrintQuote y no haberse dado cuenta de las implicaciones. Como dijo, el compilador no ayudará aquí. La solución a esto es tener una unidad de prueba configurada para absolutamente cada miembro de cada tipo. –

+0

Creo que adquirir el hábito de siempre anular ToString es una buena respuesta, pero creo que tiende a ser más fácil recordar la prueba unitaria (al menos se puede ejecutar un análisis de cobertura de prueba, mientras que no parece haber una corriente principal) herramienta que le recordará que anule ToString). – cbp

3

Crear una anulación de ToString es solo una de esas cosas que hace para la mayoría de las clases, si no para todas. Ciertamente para todas las clases de "valor".


Tenga en cuenta que ReSharper generará una gran cantidad de código repetitivo para usted. De:

public class Class1 
{ 
    public string Name { get; set; } 
    public int Id { get; set; } 
} 

El resultado de ejecutar Generar Igualdad Miembros, Miembros de formato a generar y generar Constructor es:

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

Nota hay un error: que debería haber ofrecido crear un constructor por defecto. Incluso ReSharper no puede ser perfecto.

-1

Francamente, la respuesta a su pregunta es que su diseño inicial fue defectuoso. Primero, expuso una propiedad como un tipo primitivo. Some believe this is wrong. Después de todo, su código permite esto ...

var double = quote.InterestRate * quote.InterestRate; 

El problema es que, ¿cuál es la unidad del resultado? Interés^2? El segundo problema con su diseño es que confía en una conversión implícita de ToString(). Los problemas para confiar en la conversión implícita son más conocidos en C++ (for example), pero como usted señala, también puede morderlo en C#. Quizás si su código originalmente tenía ...

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

... lo habría notado en el refactor. En pocas palabras, si tiene problemas de diseño en su diseño original, podrían quedar atrapados en refactor y podría no ser así. La mejor apuesta es no hacerlos en primer lugar.

+0

En realidad, el problema no se habría detectado incluso si .ToString() se hubiera especificado explícitamente. No creo que valga la pena comerciar con la usabilidad en este caso. También la tasa de interés es solo un ejemplo. Hay muchos casos en los que el tipo primitivo tiene más sentido pero sufre del mismo problema ToString(). Nunca exponer tipos primitivos, aunque puede ser una respuesta bastante decente. – cbp

+0

Disculpa, significa decir 'intercambiando la legibilidad' – cbp

1

Bueno, como han dicho otros, solo tienes que hacerlo. Pero aquí hay un par de ideas para ayudarse a sí mismo a asegurarse de hacerlo:

1) use un objeto base para todas sus clases de valor que anula aString y, por ejemplo, arroja una excepción. Esto ayudará a recordarle que lo anule de nuevo.

2) cree una regla personalizada para FXCop (herramienta gratuita de análisis de código estático de Microsoft) para verificar los métodos toString en ciertos tipos de clases. Cómo determinar qué tipos de clases deben prevalecer sobre String se deja como ejercicio para el alumno. :)

+0

Si eliges usar una clase base común, y si puedes hacer que esa clase sea 'abstracta', existe la posibilidad de decir ** 'public abstract override string ToString(); '**. A continuación, las clases derivadas se ven obligadas a volver a implementar 'ToString'. –

3

No sea un idiota, pero escriba un caso de prueba cada vez que cree una clase. Es un buen hábito entrar y evitar descuidos para usted y otros participantes en su proyecto.

4

La manera de evitar este tipo de problema es tener una prueba de unidad para absolutamente todos los miembros de su clase, que por lo tanto incluye su PrintQuote(Quote quote) método:

[TestMethod] 
public void PrintQuoteTest() 
{ 
    quote = new Quote(); 
    quote.InterestRate = 0.05M; 
    Assert.AreEqual(
     "The interest rate is 0.05", 
     PrintQuote(quote)); 
} 

En este caso, a menos que haya definido una conversión implícita entre su nueva clase InterestRate y System.Decimal, esta prueba unitaria ya no se compilará. ¡Pero eso definitivamente sería una señal! Y si definió una conversión implícita entre su clase InterestRate y System.Decimal, pero olvidó sobrescribir el método ToString, entonces esta prueba unitaria se compilaría, pero fallaría (correctamente) en la línea Assert.AreEqual().

No se puede exagerar la necesidad de tener una unidad de prueba para absolutamente todos los miembros de la clase.

0

En el caso en que ToString se llama en algo estáticamente escrito como un InterestRate, como en el ejemplo, o en ciertos casos relacionados donde un InterestRate se echa a Object y luego inmediatamente utiliza como parámetro a algo así como string.Format, posiblemente puedas detectar el problema con el análisis estático. Puede buscar una regla de FxCop personalizada que se aproxime a lo que desea o escribir una propia.

Tenga en cuenta que siempre será posible idear un patrón de llamada suficientemente dinámico que rompa su análisis, probablemente ni siquiera uno muy complicado;), pero capturar la fruta más baja debería ser lo suficientemente fácil.

Dicho esto, estoy de acuerdo con algunos de los otros comentaristas que las pruebas exhaustivas son probablemente el mejor enfoque para este problema específico.

0

Para una perspectiva muy diferente, puede diferir todo ToString'ing a una preocupación separada de su aplicación. StatePrinter (https://github.com/kbilsted/StatePrinter) es una de esas API en la que puede usar los valores predeterminados o configurar según los tipos para imprimir.

var car = new Car(new SteeringWheel(new FoamGrip("Plastic"))); 
car.Brand = "Toyota"; 

luego imprimirlo

StatePrinter printer = new StatePrinter(); 
Console.WriteLine(printer.PrintObject(car)); 

y se obtiene la siguiente salida

new Car() { 
    StereoAmplifiers = null 
    steeringWheel = new SteeringWheel() 
    { 
     Size = 3 
     Grip = new FoamGrip() 
     { 
      Material = ""Plastic"" 
     } 
     Weight = 525 
    } 
    Brand = ""Toyota"" } 

y con la abstracción IValueConverter puede definir el tipo son impresora, y con la FieldHarvester puede definir qué campos se incluirán en la cadena.

Cuestiones relacionadas