¿Cómo manejo los setters en campos inmutables?

25

Tengo una clase con dos readonly intcampos. Están expuestos como propiedades:

public class Thing
{
    private readonly int _foo, _bar;

    /// <summary> I AM IMMUTABLE. </summary>
    public Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public int Foo { get { return _foo; } set { } }

    public int Bar { get { return _bar; } set { } }
}

Sin embargo, eso significa que lo siguiente es un código perfectamente legal:

Thing iThoughtThisWasMutable = new Thing(1, 42);

iThoughtThisWasMutable.Foo = 99;  // <-- Poor, mistaken developer.
                                  //     He surely has bugs now. :-(

Los errores que surgen al suponer que funcionarían seguramente serán insidiosos. Claro, el desarrollador equivocado debería haber leído los documentos. Pero eso no cambia el hecho de que ningún error de compilación o tiempo de ejecución le advirtió sobre el problema.

¿Cómo se debe Thingcambiar la clase para que los desarrolladores tengan menos probabilidades de cometer el error anterior?

¿Lanzar una excepción? ¿Usar un método getter en lugar de una propiedad?

kdbanman
fuente
1
Gracias, @gnat. Ese puesto (tanto la pregunta como la respuesta) parece estar hablando acerca de las interfaces, como en Capital I Interfaces. No estoy seguro de que sea lo que estoy haciendo.
kdbanman
66
@gnat, ese es un consejo terrible. Las interfaces ofrecen una excelente manera de ofrecer VO / DTO inmutables públicamente que aún son fáciles de probar.
David Arno
44
@gnat, lo hice y la pregunta no tiene sentido ya que el OP parece pensar que las interfaces destruirán la inmutabilidad, lo cual no tiene sentido.
David Arno
1
@gnat, esa pregunta era sobre declarar interfaces para DTO. La respuesta más votada fue que las interfaces no serían dañinas, pero probablemente innecesarias.
Paul Draper

Respuestas:

107

¿Por qué legalizar ese código?
Saque el set { }si no hace nada.
Así es como define una propiedad pública de solo lectura:

public int Foo { get { return _foo; } }
paparazzo
fuente
3
No pensé que eso fuera legal por alguna razón, ¡pero parece funcionar perfectamente! Perfecto, gracias.
kdbanman
1
@kdbanman Probablemente tenga algo que ver con algo que viste que hizo el IDE en algún momento, probablemente la finalización automática.
Panzercrisis
2
@kdbanman; lo que no es legal (hasta C # 5) es public int Foo { get; }(propiedad automática con solo un captador) Pero lo public int Foo { get; private set; }es.
Laurent LA RIZZA
@LaurentLARIZZA, has corrido mi memoria. Ahí es exactamente de donde vino mi confusión.
kdbanman
45

Con C # 5 y antes, nos enfrentamos con dos opciones para campos inmutables expuestos a través de un captador:

  1. Cree una variable de respaldo de solo lectura y devuélvala a través de un captador manual. Esta opción es segura (se debe eliminar explícitamente readonlypara destruir la inmutabilidad. Sin embargo, creó mucho código de placa de caldera).
  2. Use una propiedad automática, con un setter privado. Esto crea un código más simple, pero es más fácil romper accidentalmente la inmutabilidad.

Sin embargo, con C # 6 (disponible en VS2015, que se lanzó ayer), ahora obtenemos lo mejor de ambos mundos: propiedades automáticas de solo lectura. Esto nos permite simplificar la clase del OP para:

public class Thing
{
    /// <summary> I AM IMMUTABLE. </summary>
    public Thing(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public int Foo { get; }
    public int Bar { get; }
}

Las líneas Foo = fooy Bar = barsolo son válidas en el constructor (que logra el requisito robusto de solo lectura) y el campo de respaldo está implícito, en lugar de tener que definirse explícitamente (lo que logra el código más simple).

David Arno
fuente
2
Y los constructores primarios pueden simplificar esto aún más, al deshacerse de la sobrecarga sintáctica del constructor.
Sebastian Redl
1
@DanLyons Maldición, tenía muchas ganas de usar esto en toda nuestra base de código.
Sebastian Redl
12

Podrías deshacerte de los setters. No hacen nada, confundirán a los usuarios y generarán errores. Sin embargo, podría hacerlos privados y así deshacerse de las variables de respaldo, simplificando su código:

public class Thing
{
    public Thing(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public int Foo { get; private set; }

    public int Bar { get; private set; }
}
David Arno
fuente
1
" public int Foo { get; private set; }" Esa clase ya no es inmutable porque puede cambiar por sí misma. Gracias, sin embargo!
kdbanman
44
La clase descrita es inmutable, ya que no cambia por sí misma.
David Arno
1
Mi clase real es un poco más compleja que el MWE que di. Busco la inmutabilidad real , completa con seguridad de hilo y garantías internas de clase.
kdbanman
44
@kdbanman, y tu punto es? Si su clase solo escribe a los setters privados durante la construcción, entonces ha implementado la "inmutabilidad real". La readonlypalabra clave es solo un poka-yoke, es decir, protege contra la clase que rompe la inmutabilidad en el futuro; Sin embargo, no es necesario para lograr la inmutabilidad.
David Arno
3
Término interesante, tuve que buscarlo en Google: poka-yoke es un término japonés que significa "prueba de errores". ¡Tienes razón! Eso es exactamente lo que estoy haciendo.
kdbanman
6

Dos soluciones

Simple:

No incluya setters como lo señaló David para objetos de solo lectura inmutables.

Alternativamente:

Permitir que los configuradores devuelvan un nuevo objeto inmutable, detallado en comparación con el anterior, pero proporciona un estado en el tiempo para cada objeto inicializado. Este diseño es una herramienta muy útil para la seguridad e inmutabilidad de subprocesos que se extiende sobre todos los lenguajes imperativos de OOP.

Pseudocódigo

public class Thing
{

{readonly vars}

    public Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public Thing withFoo(int foo) {
        return new Thing(foo, getBar());
    }

    public Thing withBar(int bar) {
        return new Thing(getFoo(), bar)
    }

    etc...
}

fábrica pública estática

public class Thing
{

{readonly vars}

    private Thing(int foo, int bar)
    {
        _foo = foo;
        _bar = bar;
    }

    public static with(int foo, int bar) {
        return new Thing(foo, bar)
    }

    public Thing withFoo(int foo) {
        return new Thing(foo, getBar());
    }

    public Thing withBar(int bar) {
        return new Thing(getFoo(), bar)
    }

    etc...
}
Matt Smithies
fuente
2
setXYZ son nombres horribles para los métodos de fábrica, considere withXYZ o similares.
Ben Voigt
Gracias, actualicé mi respuesta para reflejar su útil comentario. :-)
Matt Smithies
La pregunta era específicamente sobre los establecedores de propiedades , no los métodos de establecimiento.
user253751
Es cierto, pero el OP está preocupado por el estado mutable de un posible desarrollador futuro. Las variables que usan palabras clave privadas de solo lectura o palabras clave privadas privadas deben documentarse por sí mismas, eso no es culpa del desarrollador original si eso no se entiende. Comprender los diferentes métodos para cambiar el estado es clave. He agregado otro enfoque que es muy útil. Hay muchas cosas en el mundo del código que causan una paranoia excesiva, los vars de solo lectura privados no deberían ser uno de ellos.
Matt Smithies