Quais são as desvantagens dos tipos imutáveis?

12

Eu me vejo usando tipos cada vez mais imutáveis quando não se espera que as instâncias da classe sejam alteradas . Requer mais trabalho (veja o exemplo abaixo), mas facilita o uso dos tipos em um ambiente multithread.

Ao mesmo tempo, raramente vejo tipos imutáveis ​​em outros aplicativos, mesmo quando a mutabilidade não beneficia ninguém.

Pergunta: Por que tipos imutáveis ​​são tão raramente usados ​​em outras aplicações?

  • Isso ocorre porque é mais demorado escrever código para um tipo imutável,
  • Ou estou faltando alguma coisa e há algumas desvantagens importantes ao usar tipos imutáveis?

Exemplo da vida real

Digamos que você obtenha Weatheruma API RESTful assim:

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

O que geralmente veríamos é (novas linhas e comentários removidos para encurtar o 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 outro lado, eu escreveria dessa maneira, considerando que obter uma WeatherAPI e modificá-la seria estranho: mudar Temperatureou Skynão mudar o clima no mundo real, e mudar CorrespondingCitytambém não faz 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
fonte
3
"Requer mais trabalho" - citação necessária. Na minha experiência, requer menos trabalho.
Konrad Rudolph
1
@ KonradRudolph: com mais trabalho , quero dizer mais código para escrever para criar uma classe imutável. O exemplo da minha pergunta ilustra isso, com 7 linhas para uma classe mutável e 19 para uma classe imutável.
Arseni Mourzenko
Você pode reduzir a digitação de código usando o recurso Snippets de código no Visual Studio, se estiver usando. Você pode criar seus snippets personalizados e deixar o IDE definir o campo e a propriedade ao mesmo tempo com algumas teclas. Tipos imutáveis ​​são essenciais para multithreading e usados ​​extensivamente em idiomas como Scala.
Mert Akcakaya
@Ert: Snippets de código são ótimos para coisas simples. Escrever um trecho de código que criará uma classe completa com comentários de campos e propriedades e a ordem correta não seria uma tarefa fácil.
Arseni Mourzenko
5
Não concordo com o exemplo dado, a versão imutável está fazendo mais e diferentes coisas. Você pode remover as variáveis ​​no nível da instância declarando propriedades com acessadores {get; private set;}, e mesmo a variável mutável deve ter um construtor, porque todos esses campos devem sempre ser definidos e por que você não aplicaria isso? Fazer essas duas mudanças perfeitamente razoáveis ​​os leva à paridade de recursos e LoC.
Phoshi

Respostas:

16

Eu programa em C # e Objective-C. Gosto muito de digitar imutável, mas na vida real sempre fui forçado a limitar seu uso, principalmente para tipos de dados, pelos seguintes motivos:

  1. Esforço de implementação em comparação com tipos mutáveis. Com um tipo imutável, você precisaria ter um construtor exigindo argumentos para todas as propriedades. Seu exemplo é bom. Tente imaginar que você tem 10 classes, cada uma com 5 a 10 propriedades. Para facilitar as coisas, pode ser necessário ter uma classe de construtor para construir ou criar instâncias imutáveis ​​modificadas de maneira semelhante a StringBuilderou UriBuilderem C # ou WeatherBuilderno seu caso. Esse é o principal motivo para mim, pois muitas classes que eu design não valem esse esforço.
  2. Usabilidade do consumidor . Um tipo imutável é mais difícil de usar em comparação com o tipo mutável. A instanciação requer a inicialização de todos os valores. Imutabilidade também significa que não podemos passar a instância para um método para modificar seu valor sem usar um construtor, e se precisarmos de um construtor, a desvantagem é minha (1).
  3. Compatibilidade com a estrutura da linguagem. Muitas estruturas de dados requerem tipos mutáveis ​​e construtores padrão para operar. Por exemplo, você não pode fazer consultas LINQ-SQL aninhadas com tipos imutáveis ​​e não pode vincular propriedades a serem editadas em editores como o TextBox do Windows Forms.

Em resumo, a imutabilidade é boa para objetos que se comportam como valores ou que possuem apenas algumas propriedades. Antes de tornar qualquer coisa imutável, você deve considerar o esforço necessário e a usabilidade da própria classe depois de torná-la imutável.

tia
fonte
11
"A necessidade instanciação todos os valores de antemão.": Também um tipo mutável faz, a menos que você aceitar o risco de ter um objeto com campos não inicializados flutuando ao redor ...
Giorgio
@Giorgio Para o tipo mutável, o construtor padrão deve inicializar a instância para o estado padrão e o estado da instância pode ser alterado posteriormente após a instanciação.
tia
8
Para um tipo imutável, você pode ter o mesmo construtor padrão e fazer uma cópia posteriormente usando outro construtor. Se os valores padrão forem válidos para o tipo mutável, eles também deverão ser válidos para o tipo imutável, porque nos dois casos você está modelando a mesma entidade. Ou qual é a diferença?
Giorgio
1
Mais uma coisa a considerar é o que o tipo representa. Os contratos de dados não são bons tipos imutáveis ​​por causa de todos esses pontos, mas os tipos de serviço que são inicializados com dependências ou leem apenas dados e executam operações são ótimos para imutabilidade, porque as operações terão um desempenho consistente e o estado do serviço não pode ser mudou para arriscar isso.
Kevin Kevin
1
Atualmente, codifico em F #, onde a imutabilidade é o padrão (portanto, mais fácil de implementar). Acho que o seu ponto 3 é o grande obstáculo. Assim que você usar a maioria das bibliotecas .Net padrão, será necessário pular os bastidores. (E se eles usam a reflexão e, portanto, desvio imutabilidade ... argh!)
Guran
5

Geralmente, tipos imutáveis ​​criados em linguagens que não giram em torno da imutabilidade tenderão a custar mais tempo para o desenvolvedor criar e potencialmente usar se exigirem algum tipo de objeto "construtor" para expressar as alterações desejadas (isso não significa que o o trabalho será maior, mas há um custo inicial nesses casos). Além disso, independentemente de o idioma facilitar a criação de tipos imutáveis ​​ou não, ele sempre exigirá algum processamento e sobrecarga de memória para tipos de dados não triviais.

Tornando as funções desprovidas de efeitos colaterais

Se você trabalha em linguagens que não giram em torno da imutabilidade, acho que a abordagem pragmática não é a de tornar imutável todo tipo de dados. Uma mentalidade potencialmente muito mais produtiva que oferece muitos dos mesmos benefícios é se concentrar em maximizar o número de funções em seu sistema que causam zero efeitos colaterais .

Como um exemplo simples, se você tem uma função que causa um efeito colateral como este:

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

Então não precisamos de um tipo de dados inteiro imutável que proíba operadores como atribuição de pós-inicialização para fazer com que essa função evite efeitos colaterais. Podemos simplesmente fazer isso:

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

Agora a função não mexe com xnada fora do seu escopo e, nesse caso trivial, podemos até raspar alguns ciclos, evitando qualquer sobrecarga associada ao indireção / alias. No mínimo, a segunda versão não deve ser mais cara em termos de computação que a primeira.

Coisas caras para copiar na íntegra

É claro que a maioria dos casos não é tão trivial se queremos evitar que uma função cause efeitos colaterais. Um caso de uso complexo do mundo real pode ser mais ou menos assim:

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

Nesse ponto, a malha pode exigir algumas centenas de megabytes de memória com mais de cem mil polígonos, ainda mais vértices e arestas, vários mapas de textura, destinos de metamorfose etc. Seria muito caro copiar essa malha inteira apenas para fazer isso. transformfunção livre de efeitos colaterais, assim:

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

E é nesses casos que copiar algo na sua totalidade normalmente seria uma sobrecarga épica, onde eu achei útil transformar Meshuma estrutura de dados persistente e um tipo imutável com o "construtor" analógico para criar versões modificadas para que pode simplesmente copiar superficialmente e contar referências de peças que não são únicas. É tudo com o foco de poder escrever funções de malha livres de efeitos colaterais.

Estruturas de dados persistentes

E nesses casos em que copiar tudo é tão incrivelmente caro, achei o esforço de projetar um imutável Meshpara realmente render, mesmo que tivesse um custo inicial bastante alto , porque não simplificava apenas a segurança do encadeamento. Ele também simplificou a edição não destrutiva (permitindo que o usuário estratifique operações de malha sem modificar sua cópia original), desfaz sistemas (agora o sistema desfazer pode apenas armazenar uma cópia imutável da malha antes das alterações feitas por uma operação sem explodir a memória use) e exceção-safety (agora, se ocorrer uma exceção na função acima, a função não precisará reverter e desfazer todos os efeitos colaterais, pois não causou nenhum).

Nesses casos, posso dizer com confiança que o tempo necessário para tornar essas pesadas estruturas de dados imutáveis ​​economizou mais tempo do que custa, pois comparei os custos de manutenção desses novos designs com os anteriores, que giravam em torno da mutabilidade e das funções que causavam efeitos colaterais, e os projetos mutáveis ​​anteriores custam muito mais tempo e são muito mais propensos a erros humanos, especialmente em áreas que são realmente tentadoras para os desenvolvedores negligenciarem durante o tempo de crise, como segurança de exceção.

Então, acho que tipos de dados imutáveis ​​realmente valem a pena nesses casos, mas nem tudo precisa ser imutável para tornar a maioria das funções em seu sistema livre de efeitos colaterais. Muitas coisas são baratas o suficiente para copiar apenas na íntegra. Além disso, muitos aplicativos do mundo real precisarão causar alguns efeitos colaterais aqui e ali (no mínimo, como salvar um arquivo), mas normalmente existem muito mais funções que podem ser desprovidas de efeitos colaterais.

O ponto de ter alguns tipos de dados imutáveis ​​para mim é garantir que possamos escrever o número máximo de funções para ficar livre de efeitos colaterais, sem incorrer em sobrecarga épica na forma de copiar em profundidade estruturas maciças de dados, à esquerda e à direita na íntegra, quando apenas pequenas porções deles precisam ser modificados. A existência de estruturas de dados persistentes nesses casos acaba se tornando um detalhe de otimização para permitir que escrevamos nossas funções para estar livre de efeitos colaterais, sem pagar um custo épico para fazê-lo.

Sobrecarga imutável

Agora, conceitualmente, as versões mutáveis ​​sempre terão uma vantagem em eficiência. Sempre há essa sobrecarga computacional associada a estruturas de dados imutáveis. Mas achei uma troca digna nos casos que descrevi acima e você pode se concentrar em tornar a sobrecarga suficientemente mínima por natureza. Eu prefiro esse tipo de abordagem em que a correção se torna fácil e a otimização se torna mais difícil do que a otimização sendo mais fácil, mas a correção se torna mais difícil. Não é tão desmoralizante ter um código que funcione perfeitamente corretamente, necessitando de mais ajustes sobre o código que não funcione corretamente em primeiro lugar, não importa a rapidez com que obtém seus resultados incorretos.


fonte
3

A única desvantagem em que consigo pensar é que, em teoria, o uso de dados imutáveis ​​pode ser mais lento que os mutáveis ​​- é mais lento criar uma nova instância e coletar uma anterior do que modificar uma existente.

O outro "problema" é que você não pode usar apenas tipos imutáveis. No final, você precisa descrever o estado e usar tipos mutáveis ​​para fazer isso - sem alterar o estado, você não pode fazer nenhum trabalho.

Mas a regra geral ainda é usar tipos imutáveis ​​sempre que possível e tornar os tipos mutáveis ​​apenas quando realmente houver uma razão para fazê-lo ...

E para responder à pergunta " Por que tipos imutáveis ​​são tão raramente usados ​​em outros aplicativos? " - Eu realmente não acho que eles estejam ... onde quer que você olhe, todos recomendam tornar suas aulas tão imutáveis ​​quanto possível ... por exemplo: http://www.javapractices.com/topic/TopicAction.do?Id=29

mrpyo
fonte
1
Ambos os seus problemas não estão em Haskell.
Florian Margaine
@FlorianMargaine Você poderia elaborar?
mrpyo
A lentidão não é verdadeira graças a um compilador inteligente. E em Haskell, até a E / S é através de uma API imutável.
Florian Margaine
2
Um problema mais fundamental que a velocidade é que é difícil para objetos imutáveis ​​manter uma identidade enquanto seu estado muda. Se um Carobjeto mutável é atualizado continuamente com a localização de um automóvel físico específico, se eu tiver uma referência a esse objeto, posso descobrir o paradeiro desse automóvel de maneira rápida e fácil. Se Carfosse imutável, encontrar o paradeiro atual provavelmente seria muito mais difícil.
Supercat
Às vezes, é necessário codificar de maneira bastante inteligente para que o compilador descubra que não há referência ao objeto anterior deixado para trás e, portanto, pode modificá-lo no lugar ou fazer transformações de desmatamento, et al. Especialmente em programas maiores. E como @supercat diz, a identidade pode realmente se tornar um problema.
Macke
0

Para modelar qualquer sistema do mundo real onde as coisas possam mudar, o estado mutável precisará ser codificado em algum lugar, de alguma forma. Existem três maneiras principais de um objeto manter um estado mutável:

  • Usando uma referência mutável para um objeto imutável
  • Usando uma referência imutável para um objeto mutável
  • Usando uma referência mutável para um objeto mutável

O uso primeiro facilita para um objeto fazer uma captura instantânea imutável do estado atual. O uso do segundo facilita para um objeto criar uma exibição ao vivo do estado atual. Às vezes, o uso da terceira pode tornar certas ações mais eficientes nos casos em que há pouca necessidade esperada de instantâneos imutáveis ​​nem exibições ao vivo.

Além do fato de que a atualização do estado armazenado usando uma referência mutável para um objeto imutável geralmente é mais lenta do que a atualização do estado armazenado usando um objeto mutável, o uso de uma referência mutável exigirá que você renuncie à possibilidade de construir uma exibição ao vivo barata do estado. Se não for necessário criar uma exibição ao vivo, isso não é um problema; se, no entanto, for necessário criar uma exibição ao vivo, a incapacidade de usar uma referência imutável fará todas as operações com a exibição - lê e grava- muito mais lento do que seria. Se a necessidade de capturas instantâneas imutáveis ​​exceder a necessidade de exibições ao vivo, o desempenho aprimorado de capturas instantâneas imutáveis ​​pode justificar o impacto no desempenho de exibições ao vivo, mas se alguém precisar de exibições ao vivo e não precisar de capturas de tela, é o caminho que faz referências imutáveis ​​a objetos mutáveis. ir.

supercat
fonte
0

No seu caso, a resposta é principalmente porque o C # tem um suporte insuficiente à imutabilidade ...

Seria ótimo se:

  • tudo será imutável por padrão, a menos que indicado de outra forma (por exemplo, com uma palavra-chave 'mutável'), misturar tipos imutáveis ​​e mutáveis ​​é confuso

  • mutação métodos ( With) estará automaticamente disponível - embora isso já pode ser conseguido ver Com

  • haverá uma maneira de dizer que o resultado de uma chamada de método específica (ou seja ImmutableList<T>.Add) não pode ser descartado ou, pelo menos, produzirá um aviso

  • E principalmente se o compilador puder garantir o máximo possível a imutabilidade quando solicitado (consulte https://github.com/dotnet/roslyn/issues/159 )

kofifus
fonte
1
Quanto ao terceiro ponto, o ReSharper possui um MustUseReturnValueAttributeatributo personalizado que faz exatamente isso. PureAttributetem o mesmo efeito e é ainda melhor para isso.
Sebastian Redl
-1

Por que tipos imutáveis ​​são tão raramente usados ​​em outras aplicações?

Ignorância? Inexperiência?

Objetos imutáveis ​​são hoje amplamente considerados superiores, mas é um desenvolvimento relativamente recente. Os engenheiros que não se mantiveram atualizados ou estão simplesmente presos no que sabem não os usarão. E são necessárias algumas alterações no design para usá-las efetivamente. Se os aplicativos são antigos ou os engenheiros são fracos em habilidades de design, usá-los pode ser estranho ou problemático.

Telastyn
fonte
"Engenheiros que não se mantiveram atualizados": Pode-se dizer que um engenheiro também deve aprender sobre tecnologias não convencionais. A idéia de imutabilidade só recentemente se tornou popular, mas é uma idéia bastante antiga e é suportada (se não for aplicada) por idiomas mais antigos, como Scheme, SML, Haskell. Portanto, qualquer pessoa que esteja acostumada a olhar além dos idiomas comuns poderia ter aprendido sobre isso mesmo 30 anos atrás.
Giorgio
@Giorgio: em alguns países, muitos engenheiros ainda escrevem código C # sem LINQ, sem FP, sem iteradores e sem genéricos. Na verdade, eles de alguma forma perderam tudo o que aconteceu com C # desde 2003. Se eles nem conhecem seu idioma de preferência , Eu dificilmente imagino que eles conheçam qualquer idioma que não seja mainstream.
Arseni Mourzenko
@MaMa: Que bom que você escreveu a palavra engenheiros em itálico.
Giorgio
@Giorgio: no meu país, eles também são chamados de arquitetos , consultores e muitos outros termos vaidosos, nunca escritos em itálico. Na empresa em que estou trabalhando, sou chamado de analista de desenvolvimento e espero que gaste meu tempo escrevendo CSS para códigos HTML herdados ruins. Os cargos são perturbadores em muitos níveis.
Arseni Mourzenko
1
@MaMa: eu concordo. Títulos como engenheiro ou consultor geralmente são apenas palavras de ordem sem significado amplamente aceito. Eles são frequentemente usados ​​para tornar alguém ou sua posição mais importante / prestigiosa do que realmente é.
Giorgio