A imutabilidade vale muito a pena quando não há concorrência?

53

Parece que a segurança do thread é sempre / frequentemente mencionada como o principal benefício do uso de tipos imutáveis ​​e, especialmente, coleções.

Eu tenho uma situação em que gostaria de garantir que um método não modifique um dicionário de seqüências de caracteres (que são imutáveis ​​em C #). Eu gostaria de restringir as coisas o máximo possível.

No entanto, não tenho certeza se vale a pena adicionar uma dependência a um novo pacote (Microsoft Immutable Collections). O desempenho também não é um grande problema.

Portanto, acho que minha pergunta é se coleções imutáveis ​​são altamente recomendadas quando não há requisitos de desempenho rígidos e não há problemas de segurança de threads? Considere que a semântica de valores (como no meu exemplo) também pode ou não ser um requisito.

Den
fonte
11
Modificação simultânea não precisa significar threads. Basta olhar para o nome apropriado, ConcurrentModificationExceptionque geralmente é causado pelo mesmo encadeamento que modifica a coleção no mesmo encadeamento, no corpo de um foreachloop na mesma coleção.
11
Quero dizer que você não está errado, mas isso é diferente do que o OP está pedindo. Essa exceção é lançada porque a modificação durante a enumeração não é permitida. Usar um ConcurrentDictionary, por exemplo, ainda teria esse erro.
Edthethird
13
Talvez você também deva se perguntar: quando vale a mutabilidade?
Giorgio
2
Em Java, se a mutabilidade afetar hashCode()ou equals(Object)alterar o resultado, poderá causar erros ao usar Collections(por exemplo, em um HashSetobjeto que foi armazenado em um "bucket" e, após a mutação, ele deve ir para outro).
SJuan76
2
@ DavorŽdralo No que diz respeito às abstrações das linguagens de alto nível, a imutabilidade generalizada é bastante mansa. É apenas uma extensão natural da abstração muito comum (presente até em C) de criar e descartar silenciosamente "valores temporários". Talvez você queira dizer que é uma maneira ineficiente de usar uma CPU, mas esse argumento também tem falhas: linguagens felizes por mutabilidade, mas dinâmicas geralmente apresentam desempenho pior do que linguagens imutáveis, mas estáticas, em parte porque existem algumas inteligentes (mas bastante simples) truques para programas otimizar malabarismo dados imutáveis: tipos lineares, desmatamento, etc.

Respostas:

101

A imutabilidade simplifica a quantidade de informações que você precisa rastrear mentalmente ao ler o código posteriormente . Para variáveis ​​mutáveis, e especialmente membros da classe mutáveis, é muito difícil saber em que estado eles estarão na linha específica sobre a qual você está lendo, sem executar o código com um depurador. É fácil argumentar sobre dados imutáveis - sempre serão os mesmos. Se você quiser alterá-lo, precisará criar um novo valor.

Eu honestamente preferiria tornar as coisas imutáveis por padrão e depois alterá-las para mutáveis, onde é comprovado que elas precisam estar, se isso significa que você precisa do desempenho ou um algoritmo que você possui não faz sentido para a imutabilidade.

KChaloux
fonte
23
+1 simultaneidade é mutação simultânea, mas mutação que se espalha ao longo do tempo pode ser tão difícil de raciocinar sobre
guillaume31
20
Para expandir isso: uma função que depende de uma variável mutável pode ser considerada como tendo um argumento oculto extra, que é o valor atual da variável mutável. Qualquer função que altere uma variável mutável pode igualmente ser pensada como produzindo um valor de retorno extra, ou seja, o novo valor do estado mutável. Ao olhar para um pedaço de código, você não tem idéia se depende ou muda de estado mutável; portanto, você precisa descobrir e acompanhar as alterações mentalmente. Isso também introduz o acoplamento entre dois pedaços de código que compartilham estado mutável e o acoplamento é ruim.
Doval
29
@Mehrdad As pessoas também conseguiram iniciar grandes programas em montagem por décadas. Em seguida, fizemos algumas décadas de C.
Doval 24/07
11
@Mehrdad Copiar objetos inteiros não é uma boa opção quando os objetos são grandes. Não vejo por que a ordem de magnitude envolvida na melhoria é importante. Você recusaria uma melhoria de 20% (nota: número arbitrário) na produtividade simplesmente porque não era uma melhoria de três dígitos? Imutabilidade é um padrão saudável ; você pode se afastar, mas precisa de um motivo.
Doval
9
@Giorgio Scala me fez perceber o quão pouco frequente você precisa para tornar um valor mutável. Sempre que uso essa linguagem, faço de tudo um val, e apenas em ocasiões muito, muito raras, acho que preciso mudar algo para um var. Muitas das 'variáveis' que eu defino em qualquer idioma específico estão lá apenas para conter um valor que armazena o resultado de alguma computação e não precisa ser atualizado.
KChaloux
22

Seu código deve expressar sua intenção. Se você não quiser que um objeto seja modificado depois de criado, torne impossível a modificação.

A imutabilidade tem vários benefícios:

  • A intenção do autor original é expressa melhor.

    Como você saberia que, no código a seguir, modificar o nome faria com que o aplicativo gerasse uma exceção em algum momento mais tarde?

    public class Product
    {
        public string Name { get; set; }
    
        ...
    }
    
  • É mais fácil garantir que o objeto não apareça em um estado inválido.

    Você tem que controlar isso em um construtor, e somente lá. Por outro lado, se você tiver vários setters e métodos que modificam o objeto, esses controles podem se tornar particularmente difíceis, especialmente quando, por exemplo, dois campos devem mudar ao mesmo tempo para que o objeto seja válido.

    Por exemplo, um objeto é válido se o endereço não estiver null ou as coordenadas GPS não estiverem null, mas será inválido se o endereço e as coordenadas GPS estiverem especificadas. Você pode imaginar o inferno para validar isso se as coordenadas de endereço e GPS tiverem um setter ou ambas forem mutáveis?

  • Concorrência.

A propósito, no seu caso, você não precisa de nenhum pacote de terceiros. O .NET Framework já inclui uma ReadOnlyDictionary<TKey, TValue>classe.

Arseni Mourzenko
fonte
11
+1, especialmente para "Você precisa controlar isso em um construtor e somente lá". IMO esta é uma enorme vantagem.
Giorgio
10
Outro benefício: copiar um objeto é gratuito. Apenas um ponteiro.
Robert Grant
11
@MainMa Obrigado por sua resposta, mas, tanto quanto eu entendo, o ReadOnlyDictionary não oferece garantias de que outra pessoa não altere o dicionário subjacente (mesmo sem simultaneidade, talvez eu queira salvar a referência ao dicionário original no objeto ao qual o método pertence para uso posterior). O ReadOnlyDictionary é declarado em um namespace estranho: System.Collections.ObjectModel.
Den
2
@ Den: Isso se refere a um dos meus ódios de estimação: pessoas que consideram "somente leitura" e "imutáveis" como sinônimos. Se um objeto estiver encapsulado em um wrapper somente leitura e nenhuma outra referência existir ou for mantida em qualquer lugar do universo , o agrupamento do objeto o tornará imutável, e uma referência ao wrapper poderá ser usada como atalho para encapsular o estado do objeto nele contido. No entanto, não existe um mecanismo pelo qual o código possa determinar se é esse o caso. Ao contrário, porque o invólucro esconde o tipo do objeto embrulhado, envolvendo um objeto imutável ...
supercat
2
... tornará impossível para o código saber se o wrapper resultante pode ser considerado com segurança imutável.
22614
13

Existem muitos motivos de thread único para usar a imutabilidade. Por exemplo

O objeto A contém o objeto B.

O código externo consulta seu Objeto B e você o retorna.

Agora você tem três situações possíveis:

  1. B é imutável, não há problema.
  2. B é mutável, você faz uma cópia defensiva e devolve. Desempenho atingido, mas sem risco.
  3. B é mutável, você devolve.

No terceiro caso, o código do usuário pode não perceber o que você fez e pode fazer alterações no objeto, alterando os dados internos do seu objeto, sem que você tenha controle ou visibilidade sobre o que está acontecendo.

Tim B
fonte
9

A imutabilidade também pode simplificar bastante a implementação de coletores de lixo. Do wiki do GHC :

[...] A imutabilidade dos dados nos obriga a produzir muitos dados temporários, mas também ajuda a coletar esse lixo rapidamente. O truque é que dados imutáveis ​​NUNCA apontam para valores mais jovens. De fato, os valores mais jovens ainda não existem no momento em que um valor antigo é criado, portanto, não pode ser apontado do zero. E como os valores nunca são modificados, nem pode ser apontado posteriormente. Essa é a propriedade chave dos dados imutáveis.

Isso simplifica bastante a coleta de lixo (GC). A qualquer momento, podemos verificar os últimos valores criados e liberar aqueles que não estão apontados no mesmo conjunto (é claro, as raízes reais da hierarquia de valores ativos estão ativas na pilha). [...] Portanto, ele tem um comportamento contra-intuitivo: quanto maior o percentual de seus valores são lixo - mais rápido ele funciona. [...]

Petr Pudlák
fonte
5

Expandindo o que o KChaloux resumiu muito bem ...

Idealmente, você tem dois tipos de campos e, portanto, dois tipos de código usando-os. Qualquer um dos campos é imutável e o código não precisa levar em consideração a mutabilidade; ou os campos são mutáveis, e precisamos escrever um código que tire uma captura instantânea ( int x = p.x) ou lide com essas alterações normalmente.

Na minha experiência, a maioria dos códigos se enquadra entre os dois, sendo otimista : ele referencia livremente dados mutáveis, assumindo que a primeira chamada para p.xterá o mesmo resultado que a segunda chamada. E na maioria das vezes, isso é verdade, exceto quando acontece que não é mais. Opa

Então, realmente, vire a questão: quais são minhas razões para tornar isso mutável ?

  • Reduzindo a alocação de memória / livre?
  • Mutável por natureza? (por exemplo, contador)
  • Salva modificadores, ruído horizontal? (const / final)
  • Torna algum código mais curto / fácil? (padrão init, possivelmente substitui depois)

Você escreve código defensivo? A imutabilidade poupará algumas cópias. Você escreve código otimista? A imutabilidade poupará a loucura desse inseto estranho e impossível.

JvR
fonte
3

Outro benefício da imutabilidade é que é o primeiro passo para arredondar esses objetos imutáveis ​​em uma piscina. Em seguida, você pode gerenciá-los para não criar vários objetos que representam conceitualmente e semanticamente a mesma coisa. Um bom exemplo seria String do Java.

É um fenômeno bem conhecido na linguística que algumas palavras aparecem muito, também podem aparecer em outro contexto. Então, em vez de criar vários Stringobjetos, você pode usar um imutável. Mas você precisa manter um gerente de piscina para cuidar desses objetos imutáveis.

Isso economizará muita memória. Este é um artigo interessante para ler também: http://en.wikipedia.org/wiki/Zipf%27s_law

InformedA
fonte
1

Em Java, C # e outras linguagens similares, os campos do tipo classe podem ser usados ​​para identificar objetos ou para encapsular valores ou estados nesses objetos, mas as linguagens não fazem distinção entre esses usos. Suponha que um objeto de classe Georgetem um campo do tipo char[] chars;. Esse campo pode encapsular uma sequência de caracteres em:

  1. Uma matriz que nunca será modificada nem exposta a nenhum código que possa modificá-lo, mas ao qual referências externas possam existir.

  2. Uma matriz para a qual não existem referências externas, mas que George pode modificar livremente.

  3. Uma matriz de propriedade de George, mas na qual podem existir visões externas que poderiam representar o estado atual de George.

Além disso, a variável pode, em vez de encapsular uma sequência de caracteres, encapsular uma exibição ao vivo para uma sequência de caracteres pertencente a algum outro objeto

Se charsatualmente encapsula a sequência de caracteres [vento], e George deseja charsencapsular a sequência de caracteres [varinha], há várias coisas que George poderia fazer:

A. Construa uma nova matriz contendo os caracteres [varinha] e altere charspara identificar essa matriz em vez da antiga.

B. Identifique de alguma forma uma matriz de caracteres preexistente que sempre conterá os caracteres [varinha] e mude charspara identificar essa matriz em vez da antiga.

C. Altere o segundo caractere da matriz identificada por charspara um a.

No caso 1, (A) e (B) são formas seguras de alcançar o resultado desejado. No caso (2), (A) e (C) são seguros, mas (B) não seria [não causaria problemas imediatos, mas, como George presumiria que possui a propriedade da matriz, assumiria que ele pode mudar a matriz à vontade]. No caso (3), as opções (A) e (B) quebrariam quaisquer visões externas e, portanto, apenas a opção (C) está correta. Portanto, saber como modificar a sequência de caracteres encapsulada pelo campo exige saber qual é o tipo de campo semântico.

Se, em vez de usar um campo do tipo char[], que encapsula uma sequência de caracteres potencialmente mutável, o código usou o tipo String, que encapsula uma sequência de caracteres imutável, todos os problemas acima desaparecem. Todos os campos do tipo Stringencapsulam uma sequência de caracteres usando um objeto compartilhável que nunca será alterado. Conseqüentemente, se um campo do tipoStringencapsula "vento", a única maneira de fazê-lo encapsular "varinha" é fazê-lo identificar um objeto diferente - aquele que contém "varinha". Nos casos em que o código contém a única referência ao objeto, a mutação do objeto pode ser mais eficiente do que a criação de um novo, mas sempre que uma classe é mutável, é necessário distinguir entre as diferentes maneiras pelas quais ele pode encapsular o valor. Pessoalmente, acho que o Apps Hungarian deveria ter sido usado para isso (eu consideraria os quatro usos char[]como tipos semanticamente distintos, mesmo que o sistema de tipos os considere idênticos - exatamente o tipo de situação em que o Apps Hungarian brilha), mas como A maneira mais fácil de evitar essas ambiguidades não era a maneira de projetar tipos imutáveis ​​que somente encapsulam valores de uma maneira.

supercat
fonte
Parece uma resposta razoável, mas é um pouco difícil de ler e compreender.
Den
1

Existem alguns bons exemplos aqui, mas eu queria ir com alguns pessoais, onde a imutabilidade ajudou muito. No meu caso, comecei a projetar uma estrutura de dados simultânea imutável, principalmente com a esperança de poder executar código com confiança em paralelo com leituras e gravações sobrepostas e sem ter que me preocupar com as condições da corrida. Houve uma palestra que John Carmack deu que me inspirou a fazê-lo onde ele falou sobre essa ideia. É uma estrutura bastante básica e bastante trivial para implementar assim:

insira a descrição da imagem aqui

É claro que, com mais alguns sinos e assobios, é possível remover elementos em tempo constante e deixar orifícios recuperáveis ​​para trás e fazer com que os blocos sejam eliminados se ficarem vazios e potencialmente liberados para uma determinada instância imutável. Mas, basicamente, para modificar a estrutura, você modifica uma versão "transitória" e submete atomicamente as alterações feitas para obter uma nova cópia imutável que não toque a antiga, com a nova versão criando apenas novas cópias dos blocos que devem ser tornados únicos, enquanto cópias e referências rasas contam os outros.

No entanto, não achei queútil para fins de multithreading. Afinal, ainda existe o problema conceitual em que, digamos, um sistema de física aplica a física simultaneamente, enquanto um jogador está tentando mover elementos em um mundo. Com qual cópia imutável dos dados transformados, o que o jogador transformou ou o que o sistema físico transformou? Portanto, eu realmente não encontrei uma solução simples e agradável para esse problema conceitual básico, exceto por ter estruturas de dados mutáveis ​​que apenas bloqueiam de maneira mais inteligente e desencorajam leituras e gravações sobrepostas nas mesmas seções do buffer para evitar encadeamentos. Isso é algo que John Carmack parece ter descoberto como resolver em seus jogos; pelo menos ele fala sobre isso como se quase pudesse ver uma solução sem abrir um carro de vermes. Eu não cheguei tão longe quanto ele a esse respeito. Tudo o que vejo são infinitas questões de design se eu tentasse paralelizar tudo ao redor de imutáveis. Eu gostaria de poder passar um dia mexendo com seu cérebro, já que a maioria dos meus esforços começou com as idéias que ele jogou fora.

No entanto, achei enorme valor dessa estrutura de dados imutável em outras áreas. Até o uso agora para armazenar imagens realmente estranhas e que fazem com que o acesso aleatório exija mais algumas instruções (mudança à direita e pouco a pouco andjunto com uma camada de indicação indireta), mas abordarei os benefícios abaixo.

Desfazer sistema

Um dos lugares mais imediatos que encontrei para se beneficiar disso foi o sistema de desfazer. Desfazer o código do sistema costumava ser uma das coisas mais suscetíveis a erros na minha área (indústria de efeitos visuais), e não apenas nos produtos em que trabalhei, mas em produtos concorrentes (os sistemas de desfazer também eram esquisitos) porque havia muitas diferenças tipos de dados com os quais se preocupar em desfazer e refazer corretamente (sistema de propriedades, alterações de dados de malha, alterações de shader que não eram baseadas em propriedades, como trocar um pelo outro, alterações na hierarquia de cenas, como alterar o pai de um filho, alterações de imagem / textura, etc. etc. etc.).

Portanto, a quantidade de código de desfazer necessária era enorme, muitas vezes rivalizando com a quantidade de código que implementa o sistema para o qual o sistema de desfazer teve que registrar alterações de estado. Ao me basear nessa estrutura de dados, eu consegui fazer com que o sistema de desfazer fosse exatamente o seguinte:

on user operation:
    copy entire application state to undo entry
    perform operation

on undo/redo:
    swap application state with undo entry

Normalmente, o código acima seria enormemente ineficiente quando os dados da cena abrangem gigabytes para copiá-los por completo. Mas essa estrutura de dados apenas copia superficialmente coisas que não foram alteradas e, na verdade, tornou barato o suficiente armazenar uma cópia imutável de todo o estado do aplicativo. Portanto, agora eu posso implementar sistemas de desfazer tão facilmente quanto o código acima e focar apenas no uso dessa estrutura de dados imutável para tornar a cópia de partes inalteradas do estado do aplicativo cada vez mais baratas. Desde que comecei a usar essa estrutura de dados, todos os meus projetos pessoais desfazem sistemas apenas usando esse padrão simples.

Agora ainda há alguma sobrecarga aqui. Na última vez em que medi, era de cerca de 10 kilobytes para copiar superficialmente todo o estado do aplicativo sem fazer alterações (isso é independente da complexidade da cena, pois a cena é organizada em uma hierarquia, portanto, se nada abaixo da raiz mudar, apenas a raiz é copiado superficialmente sem ter que descer até as crianças). Isso está longe de 0 bytes, pois seria necessário para um sistema desfazer apenas armazenar deltas. Mas com 10 kilobytes de overhead por operação, ainda é apenas um megabyte por 100 operações de usuário. Além disso, eu ainda poderia esmagar isso ainda mais no futuro, se necessário.

Exceção-Segurança

Exceção de segurança com uma aplicação complexa não é uma questão trivial. No entanto, quando o estado do seu aplicativo é imutável e você está apenas usando objetos transitórios para tentar confirmar transações de alteração atômica, é inerentemente seguro para exceções, pois se qualquer parte do código for lançada, o transitório será descartado antes de fornecer uma nova cópia imutável . Isso banaliza uma das coisas mais difíceis que sempre achei acertadas em uma complexa base de código C ++.

Muitas pessoas costumam usar recursos em conformidade com RAII em C ++ e acham que isso é suficiente para ser seguro contra exceções. Geralmente não é, uma vez que uma função geralmente pode causar efeitos colaterais em estados além daqueles locais em seu escopo. Geralmente, você precisa começar a lidar com proteções de escopo e lógica sofisticada de reversão nesses casos. Essa estrutura de dados fez com que eu normalmente não precisasse me preocupar com isso, pois as funções não estão causando efeitos colaterais. Eles estão retornando cópias imutáveis ​​transformadas do estado do aplicativo em vez de transformar o estado do aplicativo.

Edição não destrutiva

insira a descrição da imagem aqui

A edição não destrutiva é basicamente operações de estratificação / empilhamento / conexão, sem tocar nos dados do usuário original (apenas dados de entrada e dados de saída sem tocar na entrada). É normalmente trivial implementar com um aplicativo de imagem simples como o Photoshop e pode não se beneficiar muito dessa estrutura de dados, pois muitas operações podem apenas querer transformar todos os pixels da imagem inteira.

No entanto, com a edição de malha não destrutiva, por exemplo, muitas operações geralmente desejam transformar apenas uma parte da malha. Uma operação pode apenas querer mover alguns vértices aqui. Outro pode apenas querer subdividir alguns polígonos lá. Aqui, a estrutura de dados imutável ajuda bastante a evitar a necessidade de fazer uma cópia inteira de toda a malha apenas para retornar uma nova versão da malha com uma pequena parte alterada.

Minimizando efeitos colaterais

Com essas estruturas em mãos, também facilita a gravação de funções que minimizam os efeitos colaterais sem incorrer em uma enorme penalidade de desempenho. Eu me pego escrevendo mais e mais funções que retornam estruturas de dados imutáveis ​​por valor hoje em dia, sem incorrer em efeitos colaterais, mesmo quando isso parece um pouco inútil.

Por exemplo, normalmente a tentação de transformar várias posições pode ser aceitar uma matriz e uma lista de objetos e transformá-los de maneira mutável. Hoje em dia, acabo retornando uma nova lista de objetos.

Quando você tem mais funções como essa em seu sistema que não causam efeitos colaterais, definitivamente torna mais fácil raciocinar sobre a correção e testar sua correção.

Os benefícios de cópias baratas

Enfim, essas são as áreas em que eu achei mais utilizadas as estruturas de dados imutáveis ​​(ou estruturas de dados persistentes). Também fiquei um pouco zeloso inicialmente e criei uma árvore imutável, uma lista vinculada imutável e uma tabela de hash imutável, mas com o tempo raramente encontrei tanto uso para elas. Eu encontrei principalmente o maior uso do contêiner imutável e robusto no diagrama acima.

Eu também ainda tenho muito código trabalhando com mutáveis ​​(considero uma necessidade prática pelo menos para código de baixo nível), mas o estado principal do aplicativo é uma hierarquia imutável, passando de uma cena imutável para componentes imutáveis ​​dentro dela. Alguns dos componentes mais baratos ainda são copiados na íntegra, mas os mais caros, como malhas e imagens, usam a estrutura imutável para permitir cópias parciais e baratas apenas das peças que precisavam ser transformadas.


fonte
0

Já existem muitas boas respostas. Esta é apenas uma informação extra um pouco específica do .NET. Eu estava vasculhando as antigas postagens do blog .NET e encontrei um bom resumo dos benefícios do ponto de vista dos desenvolvedores do Microsoft Immutable Collections:

  1. Semântica de instantâneo, permitindo que você compartilhe suas coleções de uma maneira que o receptor possa contar sem nunca mudar.

  2. Segurança implícita de threads em aplicativos com vários threads (nenhum bloqueio é necessário para acessar coleções).

  3. Sempre que você tem um membro da classe que aceita ou retorna um tipo de coleção e deseja incluir a semântica somente leitura no contrato.

  4. Programação funcional amigável.

  5. Permitir a modificação de uma coleção durante a enumeração, garantindo que a coleção original não seja alterada.

  6. Eles implementam as mesmas interfaces IReadOnly * que seu código já lida, facilitando a migração.

Se alguém lhe entrega um ReadOnlyCollection, um IReadOnlyList ou um IEnumerable, a única garantia é que você não pode alterar os dados - não há garantia de que a pessoa que entregou a coleção não os alterará. No entanto, muitas vezes você precisa de confiança para que isso não mude. Esses tipos não oferecem eventos para notificá-lo quando o conteúdo é alterado, e se eles mudam, isso pode ocorrer em um encadeamento diferente, possivelmente enquanto você enumera o conteúdo? Esse comportamento levaria a corrupção de dados e / ou exceções aleatórias no seu aplicativo.

Den
fonte