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.
ConcurrentModificationException
que geralmente é causado pelo mesmo encadeamento que modifica a coleção no mesmo encadeamento, no corpo de umforeach
loop na mesma coleção.hashCode()
ouequals(Object)
alterar o resultado, poderá causar erros ao usarCollections
(por exemplo, em umHashSet
objeto que foi armazenado em um "bucket" e, após a mutação, ele deve ir para outro).Respostas:
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.
fonte
val
, e apenas em ocasiões muito, muito raras, acho que preciso mudar algo para umvar
. 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.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?
É 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 estiveremnull
, 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.fonte
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:
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.
fonte
A imutabilidade também pode simplificar bastante a implementação de coletores de lixo. Do wiki do GHC :
fonte
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.x
terá o mesmo resultado que a segunda chamada. E na maioria das vezes, isso é verdade, exceto quando acontece que não é mais. OpaEntão, realmente, vire a questão: quais são minhas razões para tornar isso mutável ?
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.
fonte
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
String
objetos, 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
fonte
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
George
tem um campo do tipochar[] chars;
. Esse campo pode encapsular uma sequência de caracteres em:Uma matriz que nunca será modificada nem exposta a nenhum código que possa modificá-lo, mas ao qual referências externas possam existir.
Uma matriz para a qual não existem referências externas, mas que George pode modificar livremente.
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
chars
atualmente encapsula a sequência de caracteres [vento], e George desejachars
encapsular 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
chars
para 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
chars
para identificar essa matriz em vez da antiga.C. Altere o segundo caractere da matriz identificada por
chars
para uma
.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 tipoString
, que encapsula uma sequência de caracteres imutável, todos os problemas acima desaparecem. Todos os campos do tipoString
encapsulam uma sequência de caracteres usando um objeto compartilhável que nunca será alterado. Conseqüentemente, se um campo do tipoString
encapsula "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 usoschar[]
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.fonte
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:
É 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
and
junto 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:
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
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
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:
fonte