¿Cuáles son los inconvenientes de los tipos inmutables?

12

Me veo usando más y más tipos inmutables cuando no se espera que las instancias de la clase cambien . Requiere más trabajo (ver el ejemplo a continuación), pero facilita el uso de los tipos en un entorno multiproceso.

Al mismo tiempo, rara vez veo tipos inmutables en otras aplicaciones, incluso cuando la mutabilidad no beneficiaría a nadie.

Pregunta: ¿Por qué los tipos inmutables se usan tan raramente en otras aplicaciones?

  • ¿Es esto porque es más largo escribir código para un tipo inmutable,
  • ¿O me estoy perdiendo algo y hay algunos inconvenientes importantes al usar tipos inmutables?

Ejemplo de la vida real

Digamos que obtienes Weatherde una API RESTful como esa:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

Lo que generalmente veríamos es (nuevas líneas y comentarios eliminados para acortar el código):

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

Por otro lado, lo escribiría de esta manera, dado que obtener un WeatherAPI de la API y luego modificarlo sería extraño: cambiar Temperatureo Skyno cambiaría el clima en el mundo real, y cambiar CorrespondingCitytampoco tiene sentido.

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}
Arseni Mourzenko
fuente
3
"Requiere más trabajo" - cita requerida. En mi experiencia, requiere menos trabajo.
Konrad Rudolph
1
@KonradRudolph: por más trabajo , me refiero a más código para escribir para crear una clase inmutable. El ejemplo de mi pregunta ilustra esto, con 7 líneas para una clase mutable y 19 para una clase inmutable.
Arseni Mourzenko
Puede reducir la escritura de código utilizando la función Fragmentos de código en Visual Studio si lo está utilizando. Puede crear sus fragmentos personalizados y dejar que el IDE le defina el campo y la propiedad al mismo tiempo con unas pocas claves. Los tipos inmutables son esenciales para el subprocesamiento múltiple y se usan ampliamente en idiomas como Scala.
Mert Akcakaya
@Mert: los fragmentos de código son excelentes para cosas simples. Escribir un fragmento de código que generará una clase completa con comentarios de campos y propiedades y un orden correcto no sería una tarea fácil.
Arseni Mourzenko
55
No estoy de acuerdo con el ejemplo dado, la versión inmutable está haciendo más y diferentes cosas. Podrías eliminar las variables de nivel de instancia declarando propiedades con accesores {get; private set;}, e incluso la variable mutable debería tener un constructor, porque todos esos campos siempre deberían establecerse y ¿por qué no aplicarías eso? Hacer esos dos cambios perfectamente razonables los lleva a la paridad de característica y LoC.
Phoshi

Respuestas:

16

Programa en C # y Objective-C. Realmente me gusta la escritura inmutable, pero en la vida real siempre me he visto obligado a limitar su uso, principalmente para tipos de datos, por las siguientes razones:

  1. Esfuerzo de implementación en comparación con los tipos mutables. Con un tipo inmutable, necesitaría un constructor que requiera argumentos para todas las propiedades. Tu ejemplo es bueno. Intenta imaginar que tienes 10 clases, cada una con 5-10 propiedades. Para facilitar las cosas, es posible que deba tener una clase de generador para construir o crear instancias inmutables modificadas de manera similar StringBuildero UriBuilderen C #, o WeatherBuilderen su caso. Esta es la razón principal para mí, ya que muchas clases que diseño no merecen tal esfuerzo.
  2. Usabilidad del consumidor . Un tipo inmutable es más difícil de usar en comparación con el tipo mutable. La instanciación requiere inicializar todos los valores. La inmutabilidad también significa que no podemos pasar la instancia a un método para modificar su valor sin usar un generador, y si necesitamos tener un generador, el inconveniente está en mi (1).
  3. Compatibilidad con el marco del lenguaje. Muchos de los marcos de datos requieren tipos mutables y constructores predeterminados para funcionar. Por ejemplo, no puede hacer consultas LINQ-to-SQL anidadas con tipos inmutables, y no puede vincular propiedades para editarlas en editores como TextBox de Windows Forms.

En resumen, la inmutabilidad es buena para los objetos que se comportan como valores, o que solo tienen algunas propiedades. Antes de hacer algo inmutable, debe considerar el esfuerzo necesario y la usabilidad de la clase en sí después de hacerla inmutable.

tia
fuente
11
"La creación de instancias necesita todos los valores de antemano": también lo hace un tipo mutable, a menos que acepte el riesgo de tener un objeto con campos no inicializados flotando ...
Giorgio
@Giorgio Para el tipo mutable, el constructor predeterminado debe inicializar la instancia al estado predeterminado y el estado de la instancia se puede modificar más tarde después de la creación de instancias.
tia
8
Para un tipo inmutable, puede tener el mismo constructor predeterminado y hacer una copia más tarde utilizando otro constructor. Si los valores predeterminados son válidos para el tipo mutable, también deberían ser válidos para el tipo inmutable porque en ambos casos está modelando la misma entidad. O cual es la diferencia?
Giorgio
1
Una cosa más a considerar es lo que representa el tipo. Los contratos de datos no son buenos tipos inmutables debido a todos estos puntos, pero los tipos de servicios que se inicializan con dependencias o que solo leen datos y luego realizan operaciones son excelentes para la inmutabilidad porque las operaciones funcionarán de manera consistente y el estado del servicio no puede ser cambiado para arriesgarse
Kevin
1
Actualmente codifico en F #, donde la inmutabilidad es la predeterminada (por lo tanto, más fácil de implementar). Me parece que su punto 3 es el gran obstáculo. Tan pronto como use la mayoría de las bibliotecas .Net estándar, deberá saltar los aros. (Y si usan la reflexión y evitan la inmutabilidad ... ¡argh!)
Guran
5

En general, los tipos inmutables creados en lenguajes que no giran en torno a la inmutabilidad tenderán a costar más tiempo al desarrollador para crearlos y potencialmente a usarlos si requieren algún tipo de objeto "generador" para expresar los cambios deseados (esto no significa que el trabajo será más, pero hay un costo por adelantado en estos casos). Además, independientemente de si el lenguaje hace que sea realmente fácil crear tipos inmutables o no, siempre requerirá algo de procesamiento y sobrecarga de memoria para los tipos de datos no triviales.

Hacer que las funciones carezcan de efectos secundarios

Si está trabajando en idiomas que no giran en torno a la inmutabilidad, entonces creo que el enfoque pragmático no es tratar de hacer que cada tipo de datos sea inmutable. Una mentalidad potencialmente mucho más productiva que le brinda muchos de los mismos beneficios es enfocarse en maximizar la cantidad de funciones en su sistema que causan cero efectos secundarios .

Como ejemplo simple, si tiene una función que causa un efecto secundario como este:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

Entonces no necesitamos un tipo de datos entero inmutable que prohíba a los operadores como la asignación posterior a la inicialización para que esa función evite los efectos secundarios. Simplemente podemos hacer esto:

// Returns the absolute value of 'x'.
int abs(int x);

Ahora la función no se mete xni nada fuera de su alcance, y en este caso trivial, incluso podríamos haber afeitado algunos ciclos evitando cualquier sobrecarga asociada con la indirección / alias. Por lo menos, la segunda versión no debería ser más costosa desde el punto de vista computacional que la primera.

Cosas que son caras de copiar en su totalidad

Por supuesto, la mayoría de los casos no son tan triviales si queremos evitar que una función cause efectos secundarios. Un caso de uso complejo en el mundo real podría ser más parecido a esto:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

En ese momento, la malla puede requerir un par de cientos de megabytes de memoria con más de cien mil polígonos, incluso más vértices y bordes, múltiples mapas de textura, objetivos de transformación, etc. Sería muy costoso copiar toda la malla solo para hacer esto transformFunción libre de efectos secundarios, así:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

Y es en estos casos donde copiar algo en su totalidad normalmente sería una sobrecarga épica en la que he encontrado útil convertirme Meshen una estructura de datos persistente y en un tipo inmutable con el "generador" analógico para crear versiones modificadas de la misma. puede simplemente copiar superficialmente y contar piezas que no son únicas. Todo se centra en poder escribir funciones de malla que no tengan efectos secundarios.

Estructuras de datos persistentes

Y en estos casos donde copiar todo es tan increíblemente costoso, encontré el esfuerzo de diseñar un sistema inmutable Meshque realmente valga la pena a pesar de que tenía un costo inicial ligeramente elevado, ya que no solo simplificaba la seguridad del hilo. También simplificó la edición no destructiva (permitiendo al usuario aplicar capas a las operaciones de malla sin modificar su copia original), deshacer sistemas (ahora el sistema de deshacer puede almacenar una copia inmutable de la malla antes de los cambios realizados por una operación sin destruir memoria use), y excepción-seguridad (ahora si ocurre una excepción en la función anterior, la función no tiene que retroceder y deshacer todos sus efectos secundarios ya que no causó ninguno para empezar).

Puedo decir con confianza en estos casos que el tiempo requerido para hacer que estas estructuras de datos pesadas sean inmutables ahorró más tiempo del que costó, ya que comparé los costos de mantenimiento de estos nuevos diseños con los anteriores que giraban en torno a la mutabilidad y las funciones que causan efectos secundarios, y los diseños mutables anteriores costaban mucho más tiempo y eran mucho más propensos a errores humanos, especialmente en áreas que son realmente tentadores para los desarrolladores que descuidan durante el tiempo de crisis, como la seguridad de excepción.

Por lo tanto, creo que los tipos de datos inmutables realmente dan resultado en estos casos, pero no todo tiene que hacerse inmutable para que la mayoría de las funciones de su sistema estén libres de efectos secundarios. Muchas cosas son lo suficientemente baratas como para copiarlas en su totalidad. Además, muchas aplicaciones del mundo real necesitarán causar algunos efectos secundarios aquí y allá (al menos como guardar un archivo), pero generalmente hay muchas más funciones que podrían estar desprovistas de efectos secundarios.

El punto de tener algunos tipos de datos inmutables para mí es asegurarnos de que podamos escribir el número máximo de funciones para estar libres de efectos secundarios sin incurrir en una sobrecarga épica en forma de copia profunda de estructuras de datos masivas de izquierda a derecha en su totalidad cuando solo pequeñas porciones de ellos necesitan ser modificados. Tener estructuras de datos persistentes en esos casos termina convirtiéndose en un detalle de optimización que nos permite escribir nuestras funciones para estar libres de efectos secundarios sin pagar un costo épico por hacerlo.

Sobrecarga inmutable

Ahora conceptualmente, las versiones mutables siempre tendrán una ventaja en eficiencia. Siempre existe esa sobrecarga computacional asociada con estructuras de datos inmutables. Pero me pareció un intercambio digno en los casos que describí anteriormente, y puede concentrarse en hacer que los gastos generales sean lo suficientemente mínimos por naturaleza. Prefiero ese tipo de enfoque donde la corrección se vuelve fácil y la optimización se vuelve más difícil en lugar de que la optimización sea más fácil, pero la corrección se vuelve más difícil. No es tan desmoralizador tener un código que funcione perfectamente correctamente y necesite más ajustes sobre el código que no funciona correctamente en primer lugar, sin importar cuán rápido logre sus resultados incorrectos.


fuente
3

El único inconveniente que se me ocurre es que, en teoría, el uso de datos inmutables puede ser más lento que los mutables: es más lento crear una nueva instancia y recopilar la anterior que modificar la existente.

El otro "problema" es que no solo puedes usar tipos inmutables. Al final, tiene que describir el estado y debe usar tipos mutables para hacerlo, sin cambiar el estado no puede hacer ningún trabajo.

Pero aún así, la regla general es usar tipos inmutables siempre que sea posible y hacer que los tipos sean mutables solo cuando realmente haya una razón para hacerlo ...

Y para responder la pregunta " ¿Por qué los tipos inmutables se usan tan raramente en otras aplicaciones? " - Realmente no creo que estén ... donde sea que miren, todos recomiendan hacer que sus clases sean tan inmutables como puedan ser ... por ejemplo: http://www.javapractices.com/topic/TopicAction.do?Id=29

mrpyo
fuente
1
Sin embargo, sus dos problemas no están en Haskell.
Florian Margaine
@FlorianMargaine ¿Podrías dar más detalles?
mrpyo
La lentitud no es cierta gracias a un compilador inteligente. Y en Haskell, incluso las E / S se realizan a través de una API inmutable.
Florian Margaine
2
Un problema más fundamental que la velocidad es que es difícil para los objetos inmutables mantener una identidad mientras su estado cambia. Si un Carobjeto mutable se actualiza continuamente con la ubicación de un automóvil físico en particular, entonces si tengo una referencia a ese objeto, puedo averiguar el paradero de ese automóvil de manera rápida y fácil. Si Carfuera inmutable, encontrar el paradero actual probablemente sería mucho más difícil.
supercat
En ocasiones, debe codificar de manera muy inteligente para que el compilador descubra que no hay ninguna referencia al objeto anterior dejado y, por lo tanto, puede modificarlo en su lugar o realizar transformaciones de deforestación, et al. Especialmente en programas más grandes. Y como dice @supercat, la identidad puede convertirse en un problema.
Macke
0

Para modelar cualquier sistema del mundo real en el que las cosas puedan cambiar, el estado mutable deberá codificarse en alguna parte, de alguna manera. Hay tres formas principales en que un objeto puede mantener un estado mutable:

  • Usar una referencia mutable a un objeto inmutable
  • Usar una referencia inmutable a un objeto mutable
  • Usar una referencia mutable a un objeto mutable

Usar primero facilita que un objeto realice una instantánea inmutable del estado actual. Usar el segundo facilita que un objeto cree una vista en vivo del estado actual. Usar el tercero a veces puede hacer que ciertas acciones sean más eficientes en casos en los que hay poca necesidad esperada de instantáneas inmutables ni vistas en vivo.

Más allá del hecho de que actualizar el estado almacenado usando una referencia mutable a un objeto inmutable es a menudo más lento que actualizar el estado almacenado usando un objeto mutable, usar una referencia mutable requerirá que uno renuncie a la posibilidad de construir una vista en vivo barata del estado. Si uno no necesita crear una vista en vivo, eso no es un problema; sin embargo, si fuera necesario crear una vista en vivo, la imposibilidad de usar una referencia inmutable hará que todas las operaciones con la vista, tanto lecturas como escriturasMucho más lento de lo que serían de otra manera. Si la necesidad de instantáneas inmutables excede la necesidad de vistas en vivo, el rendimiento mejorado de las instantáneas inmutables puede justificar el impacto de rendimiento para las vistas en vivo, pero si uno necesita vistas en vivo y no necesita instantáneas, utilizar referencias inmutables a objetos mutables es la forma ir.

Super gato
fuente
0

En su caso, la respuesta se debe principalmente a que C # tiene un soporte deficiente para la inmutabilidad ...

Sería genial si:

  • todo será inmutable por defecto a menos que se indique lo contrario (es decir, con una palabra clave 'mutable'), mezclar tipos inmutables y mutables es confuso

  • los métodos de mutación ( With) estarán disponibles automáticamente, aunque esto ya se puede lograr, vea Con

  • habrá una manera de decir que el resultado de una llamada a un método específico (es decir ImmutableList<T>.Add) no se puede descartar o al menos producirá una advertencia

  • Y principalmente si el compilador podría garantizar la inmutabilidad en la medida de lo posible (consulte https://github.com/dotnet/roslyn/issues/159 )

kofifus
fuente
1
En cuanto al tercer punto, ReSharper tiene un MustUseReturnValueAttributeatributo personalizado que hace exactamente eso. PureAttributetiene el mismo efecto y es aún mejor para esto.
Sebastian Redl
-1

¿Por qué los tipos inmutables se usan tan raramente en otras aplicaciones?

¿Ignorancia? ¿Inexperiencia?

Los objetos inmutables son ampliamente considerados como superiores hoy en día, pero es un desarrollo relativamente reciente. Los ingenieros que no se han mantenido actualizados o simplemente están atrapados en "lo que saben" no los usarán. Y se necesitan algunos cambios de diseño para usarlos de manera efectiva. Si las aplicaciones son viejas, o los ingenieros tienen habilidades de diseño débiles, usarlas puede ser incómodo o problemático.

Telastyn
fuente
"Ingenieros que no se han mantenido actualizados": se podría decir que un ingeniero también debe aprender sobre tecnologías no convencionales. La idea de la inmutabilidad se está convirtiendo en una corriente principal recientemente, pero es una idea bastante antigua y es compatible (si no se aplica) con lenguajes más antiguos como Scheme, SML, Haskell. Por lo tanto, cualquiera que esté acostumbrado a mirar más allá de los idiomas convencionales podría haberlo aprendido incluso hace 30 años.
Giorgio
@Giorgio: en algunos países, muchos ingenieros todavía escriben código C # sin LINQ, sin FP, sin iteradores y sin genéricos, por lo que, de hecho, de alguna manera se perdieron todo lo que sucedió con C # desde 2003. Si ni siquiera conocen su idioma de preferencia , Apenas me imagino que sepan un lenguaje no convencional.
Arseni Mourzenko
@MainMa: Qué bueno que escribiste la palabra ingenieros en cursiva.
Giorgio
@Giorgio: en mi país, también se les llama arquitectos , consultores y muchos otros términos vanagloriosos, nunca escritos en cursiva. En la compañía en la que estoy trabajando actualmente, me llaman desarrollador analista , y se espera que pase mi tiempo escribiendo CSS para un código HTML heredado. Los títulos de trabajo son inquietantes en muchos niveles.
Arseni Mourzenko
1
@MainMa: estoy de acuerdo. Títulos como ingeniero o consultor a menudo son solo palabras de moda sin un significado ampliamente aceptado. A menudo se usan para hacer que alguien o su puesto sean más importantes / prestigiosos de lo que realmente es.
Giorgio