Após as discussões aqui no SO, eu já li várias vezes a observação de que estruturas mutáveis são "más" (como na resposta a esta pergunta ).
Qual é o problema real com mutabilidade e estruturas em C #?
c#
struct
immutability
mutable
Dirk Vollmar
fonte
fonte
int
s,bool
s, e todos os outros tipos de valor são maus. Existem casos de mutabilidade e imutabilidade. Esses casos dependem do papel que os dados desempenham, não do tipo de alocação / compartilhamento de memória.int
e nãobool
são mutáveis ...
-Sintaxe, tornando as operações com dados digitados ref e dados digitados com valor iguais, mesmo que sejam distintamente diferentes. Isso é uma falha das propriedades do C #, não das estruturas - alguns idiomas oferecem umaa[V][X] = 3.14
sintaxe alternativa para a mutação no local. Em C #, seria melhor oferecer métodos mutadores de membros de estrutura como 'MutateV (Action <ref Vector2> mutator) `e usá-lo comoa.MutateV((v) => { v.X = 3; })
(o exemplo é simplificado demais por causa das limitações que o C # tem em relação àref
palavra - chave, mas com algumas soluções alternativas devem ser possíveis) .x
) esta única operação é de 4 instruções:ldloc.0
(cargas a variável 0-índice para ...T
é do tipo Ref é apenas uma palavra-chave que faz com que a variável seja passada para um método em si, e não uma cópia dele. Também faz sentido para os tipos de referência, pois podemos alterar a variável , ou seja, a referência fora do método apontará para outro objeto após ser alterada dentro do método. Comoref T
não é um tipo, mas a maneira de passar um parâmetro de método, você não pode colocá-lo<>
, porque apenas tipos podem ser colocados lá. Portanto, está incorreto. Talvez fosse conveniente fazê-lo, talvez o C # equipa poderia fazer isso por algum nova versão, mas agora eles estão trabalhando em algum ...Respostas:
Estruturas são tipos de valor, o que significa que são copiadas quando transmitidas.
Portanto, se você alterar uma cópia, estará alterando apenas essa cópia, não o original e nenhuma outra cópia que possa estar presente.
Se sua estrutura for imutável, todas as cópias automáticas resultantes da transmissão de valor serão iguais.
Se você deseja alterá-lo, é necessário fazê-lo conscientemente, criando uma nova instância da estrutura com os dados modificados. (não uma cópia)
fonte
Por onde começar ;-p
O blog de Eric Lippert é sempre bom para uma citação:
Primeiro, você tende a perder alterações com bastante facilidade ... por exemplo, tirando as coisas de uma lista:
o que isso mudou? Nada útil ...
O mesmo com propriedades:
forçando você a fazer:
menos criticamente, há um problema de tamanho; objetos mutáveis tendem a ter várias propriedades; No entanto, se você tiver uma estrutura com dois
int
s, astring
, aDateTime
e abool
, poderá rapidamente queimar muita memória. Com uma classe, vários chamadores podem compartilhar uma referência para a mesma instância (as referências são pequenas).fonte
++
operador. Nesse caso, o compilador apenas grava a atribuição explícita em vez de apressar o programador.Eu não diria mal, mas a mutabilidade geralmente é um sinal de excesso de interesse por parte do programador para fornecer o máximo de funcionalidade. Na realidade, isso geralmente não é necessário e, por sua vez, torna a interface menor, mais fácil de usar e mais difícil de usar incorretamente (= mais robusta).
Um exemplo disso são os conflitos de leitura / gravação e gravação / gravação em condições de corrida. Isso simplesmente não pode ocorrer em estruturas imutáveis, pois uma gravação não é uma operação válida.
Além disso, eu afirmo que a mutabilidade quase nunca é realmente necessária , o programador apenas pensa que pode ser no futuro. Por exemplo, simplesmente não faz sentido alterar uma data. Em vez disso, crie uma nova data com base na antiga. Como é uma operação barata, o desempenho não é considerado.
fonte
float
componentes de uma transformação de gráficos. Se esse método retornar uma estrutura de campo exposto com seis componentes, é óbvio que a modificação dos campos da estrutura não modificará o objeto gráfico do qual foi recebido. Se esse método retornar um objeto de classe mutável, talvez alterar suas propriedades altere o objeto gráfico subjacente e talvez não - ninguém realmente sabe.Estruturas mutáveis não são más.
Eles são absolutamente necessários em circunstâncias de alto desempenho. Por exemplo, quando linhas de cache e / ou coleta de lixo se tornam um gargalo.
Eu não chamaria o uso de uma estrutura imutável nesses casos de uso perfeitamente válidos "mau".
Percebo que a sintaxe do C # não ajuda a distinguir o acesso de um membro de um tipo de valor ou de um tipo de referência; portanto, sou a favor preferir estruturas imutáveis, que reforçam a imutabilidade, em vez de estruturas mutáveis.
No entanto, em vez de simplesmente rotular estruturas imutáveis como "más", aconselho a adotar a linguagem e advogar regras mais úteis e construtivas.
Por exemplo: "estruturas são tipos de valor, que são copiados por padrão. Você precisa de uma referência se não quiser copiá-las" ou "tente trabalhar com estruturas somente leitura primeiro" .
fonte
struct
com campos públicos) do que definir uma classe que pode ser usada desajeitadamente para atingir os mesmos fins ou adicionar um monte de lixo a uma estrutura para fazê-la emular tal classe (em vez de tê-la se comporta como um conjunto de variáveis grudadas na fita adesiva, que é o que realmente se quer em primeiro lugar) #Estruturas com campos ou propriedades públicas mutáveis não são más.
Os métodos Struct (que diferem dos configuradores de propriedades) que modificam "isso" são um tanto ruins, apenas porque o .net não fornece um meio de distingui-los dos métodos que não o fazem. Os métodos Struct que não modificam "this" devem ser invocáveis, mesmo em estruturas somente leitura, sem a necessidade de cópia defensiva. Os métodos que modificam "this" não devem ser invocáveis em estruturas somente leitura. Como o .net não deseja proibir que métodos struct que não modificam "this" sejam invocados em estruturas somente leitura, mas não deseja permitir que estruturas somente leitura sejam alteradas, copia defensivamente as estruturas em read- apenas contextos, sem dúvida o pior dos dois mundos.
Apesar dos problemas com o manuseio de métodos auto-mutantes em contextos somente leitura, as estruturas mutáveis geralmente oferecem semântica muito superior aos tipos de classe mutáveis. Considere as três assinaturas de método a seguir:
Para cada método, responda às seguintes perguntas:
Respostas:
O Method1 não pode modificar foo e nunca obtém uma referência. O Method2 obtém uma referência de curta duração a foo, que pode ser usada para modificar os campos de foo inúmeras vezes, em qualquer ordem, até que ele retorne, mas não pode persistir nessa referência. Antes que o Method2 retorne, a menos que use código não seguro, toda e qualquer cópia que possa ter sido feita com sua referência 'foo' desaparecerá. O Method3, ao contrário do Method2, obtém uma referência promiscuamente compartilhável ao foo, e não há como dizer o que ele pode fazer com ele. Pode não mudar nada, pode mudar novamente e, em seguida, retornar ou pode dar uma referência a outro tópico que pode ser modificado de alguma maneira arbitrária em algum momento futuro arbitrário.
Matrizes de estruturas oferecem semântica maravilhosa. Dado RectArray [500] do tipo Rectangle, é claro e óbvio como, por exemplo, copiar o elemento 123 para o elemento 456 e, algum tempo depois, definir a largura do elemento 123 para 555, sem perturbar o elemento 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; ". Saber que Rectangle é uma estrutura com um campo inteiro chamado Width informará tudo o que você precisa saber sobre as instruções acima.
Agora, suponha que RectClass fosse uma classe com os mesmos campos que Rectangle e que se desejasse realizar as mesmas operações em um RectClassArray [500] do tipo RectClass. Talvez o array deva conter 500 referências imutáveis pré-inicializadas para objetos RectClass mutáveis. nesse caso, o código apropriado seria algo como "RectClassArray [321]. SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Talvez se presuma que o array contém instâncias que não serão alteradas, portanto o código apropriado seria mais parecido com "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = Novo RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; " Para saber o que se deve fazer, é preciso saber muito mais sobre o RectClass (por exemplo, ele suporta um construtor de cópias, um método de copiar de etc.) ) e o uso pretendido da matriz. Nem de longe tão limpo quanto usar uma estrutura.
Certamente, infelizmente, não há uma maneira agradável de qualquer classe de contêiner além de uma matriz oferecer a semântica limpa de uma matriz struct. O melhor que se poderia fazer, se alguém quisesse que uma coleção fosse indexada com, por exemplo, uma string, provavelmente seria oferecer um método "ActOnItem" genérico que aceitaria uma string para o índice, um parâmetro genérico e um delegado que seria passado por referência ao parâmetro genérico e ao item de coleção. Isso permitiria quase a mesma semântica que as matrizes struct, mas, a menos que as pessoas vb.net e C # possam ser perseguidas para oferecer uma boa sintaxe, o código terá uma aparência desajeitada, mesmo que seja razoavelmente desempenho (passar um parâmetro genérico permitir o uso de um representante estático e evitaria a necessidade de criar instâncias de classe temporárias).
Pessoalmente, estou irritado com o ódio de Eric Lippert et al. vomitar sobre tipos de valores mutáveis. Eles oferecem semântica muito mais limpa do que os tipos de referência promíscuos que são usados em todo o lugar. Apesar de algumas das limitações do suporte do .net para tipos de valor, há muitos casos em que os tipos de valor mutável são mais adequados do que qualquer outro tipo de entidade.
fonte
Rectangle
eu poderia facilmente encontrar uma localização comum em que você tenha um comportamento altamente obscuro . Considere que o WinForms implementa umRectangle
tipo mutável usado naBounds
propriedade do formulário . Se eu quiser alterar os limites, gostaria de usar sua sintaxe agradável:form.Bounds.X = 10;
No entanto, isso não altera nada no formulário (e gera um erro adorável informando você disso). Inconsistência é o problema da programação e é por isso que a imutabilidade é desejada.Tipos de valor representam basicamente conceitos imutáveis. Por exemplo, não faz sentido ter um valor matemático como um número inteiro, vetor etc. e, em seguida, poder modificá-lo. Seria como redefinir o significado de um valor. Em vez de alterar um tipo de valor, faz mais sentido atribuir outro valor exclusivo. Pense no fato de que os tipos de valor são comparados comparando todos os valores de suas propriedades. O ponto é que, se as propriedades são as mesmas, é a mesma representação universal desse valor.
Como Konrad menciona, também não faz sentido alterar uma data, pois o valor representa esse momento único no tempo e não uma instância de um objeto de tempo com qualquer estado ou dependência de contexto.
Espera que isso faça algum sentido para você. É mais sobre o conceito que você tenta capturar com tipos de valor do que detalhes práticos, para ter certeza.
fonte
int
iterador, que seria completamente inútil se fosse imutável. Eu acho que você está confundindo “implementações de compilador / tempo de execução de tipos de valor” com “variáveis digitadas em um tipo de valor” - o último certamente é mutável para qualquer um dos valores possíveis.Existem alguns outros casos que podem levar a um comportamento imprevisível do ponto de vista do programador.
Tipos de valor imutável e campos somente leitura
Tipos de valor mutável e matriz
Suponha que temos uma matriz de nossa
Mutable
estrutura e estamos chamando oIncrementI
método para o primeiro elemento dessa matriz. Que comportamento você espera dessa ligação? Ele deve alterar o valor da matriz ou apenas uma cópia?Portanto, estruturas mutáveis não são más, desde que você e o resto da equipe entendam claramente o que estão fazendo. Porém, existem muitos casos em que o comportamento do programa seria diferente do esperado, que poderia levar a erros sutis, difíceis de produzir e difíceis de entender.
fonte
T[]
índice inteiro e um, e desde que um acesso a uma propriedade do tipoArrayRef<T>
seja interpretado como um acesso ao elemento de matriz apropriado) [se uma classe quisesse expor umArrayRef<T>
para qualquer outra finalidade, poderia fornecer um método - em oposição a uma propriedade - para recuperá-lo]. Infelizmente, essas disposições não existem.Se você já programou em uma linguagem como C / C ++, é ótimo usar estruturas como mutáveis. Basta passá-los com ref, por aí e não há nada que possa dar errado. O único problema que encontro são as restrições do compilador C # e que, em alguns casos, sou incapaz de forçar o estúpido a usar uma referência à estrutura, em vez de uma cópia (como quando uma estrutura faz parte de uma classe C # )
Portanto, estruturas mutáveis não são más, o C # as tornou más. Eu uso estruturas mutáveis em C ++ o tempo todo e são muito convenientes e intuitivas. Por outro lado, o C # me fez abandonar completamente as estruturas como membros de classes devido à maneira como elas lidam com objetos. A conveniência deles nos custou a nossa.
fonte
readonly
, mas se alguém evitar fazer essas coisas, os campos de classe dos tipos de estrutura são bons. A única limitação realmente fundamental das estruturas é que um campo struct de um tipo de classe mutável comoint[]
pode encapsular identidade ou um conjunto imutável de valores, mas não pode ser usado para encapsular valores mutáveis sem também encapsular uma identidade indesejada.Se você se ater ao que as estruturas são destinadas (em C #, Visual Basic 6, Pascal / Delphi, tipo de estrutura C ++ (ou classes) quando elas não são usadas como ponteiros), você descobrirá que uma estrutura não é mais que uma variável composta . Isso significa: você as tratará como um conjunto compactado de variáveis, com um nome comum (uma variável de registro da qual você faz referência a membros).
Eu sei que isso confundiria muitas pessoas profundamente acostumadas a OOP, mas isso não é motivo suficiente para dizer que essas coisas são inerentemente más, se usadas corretamente. Algumas estruturas são imutáveis como pretendem (é o caso do Python
namedtuple
), mas é outro paradigma a considerar.Sim: as estruturas envolvem muita memória, mas não será exatamente mais memória fazendo:
comparado com:
O consumo de memória será pelo menos o mesmo, ou até mais, no caso imutável (embora esse caso seja temporário, para a pilha atual, dependendo do idioma).
Mas, finalmente, estruturas são estruturas , não objetos. No POO, a propriedade principal de um objeto é sua identidade , que na maioria das vezes não passa de seu endereço de memória. Struct significa estrutura de dados (não é um objeto adequado e, portanto, eles não têm identidade) e os dados podem ser modificados. Em outras línguas, record (em vez de struct , como é o caso de Pascal) é a palavra e tem o mesmo objetivo: apenas uma variável de registro de dados, destinada a ser lida de arquivos, modificada e despejada em arquivos (que é a principal use e, em muitos idiomas, você pode até definir o alinhamento de dados no registro, embora esse não seja necessariamente o caso dos objetos chamados corretamente).
Quer um bom exemplo? As estruturas são usadas para ler arquivos facilmente. O Python possui essa biblioteca porque, como é orientada a objetos e não tem suporte para estruturas, teve que implementá-la de outra maneira, o que é um pouco feio. As estruturas de implementação de idiomas têm esse recurso ... incorporado. Tente ler um cabeçalho de bitmap com uma estrutura apropriada em linguagens como Pascal ou C. Será fácil (se a estrutura for construída e alinhada adequadamente; em Pascal você não usaria um acesso baseado em registro, mas funciona para ler dados binários arbitrários). Portanto, para arquivos e acesso direto à memória (local), as estruturas são melhores que os objetos. Hoje, estamos acostumados a JSON e XML, e esquecemos o uso de arquivos binários (e, como efeito colateral, o uso de estruturas). Mas sim: eles existem e têm um propósito.
Eles não são maus. Basta usá-los para o propósito certo.
Se você pensa em martelos, vai querer tratar os parafusos como pregos, achar os parafusos mais difíceis de mergulhar na parede, e isso será culpa dos parafusos, e eles serão os maus.
fonte
Imagine que você tem uma matriz de 1.000.000 de estruturas. Cada estrutura representando um patrimônio com itens como bid_price, offer_price (talvez decimais) e assim por diante, isso é criado por C # / VB.
Imagine que a matriz seja criada em um bloco de memória alocado no heap não gerenciado, de modo que algum outro encadeamento de código nativo possa acessar simultaneamente a matriz (talvez algum código de alto desempenho fazendo matemática).
Imagine que o código C # / VB está ouvindo um feed do mercado de alterações de preço; esse código pode ter que acessar algum elemento da matriz (para qualquer segurança) e modificar alguns campos de preço.
Imagine que isso esteja sendo feito dezenas ou mesmo centenas de milhares de vezes por segundo.
Bem, vamos encarar os fatos, neste caso, realmente queremos que essas estruturas sejam mutáveis, elas precisam ser porque estão sendo compartilhadas por algum outro código nativo, para que criar cópias não ajude; eles precisam ser porque fazer uma cópia de uma estrutura de 120 bytes nessas taxas é uma loucura, especialmente quando uma atualização pode realmente afetar apenas um ou dois bytes.
Hugo
fonte
Quando algo pode ser modificado, ele ganha um senso de identidade.
Como
Person
é mutável, é mais natural pensar em mudar a posição de Eric do que em clonar Eric, mover o clone e destruir o original . Ambas as operações conseguiriam alterar o conteúdo deeric.position
, mas uma é mais intuitiva que a outra. Da mesma forma, é mais intuitivo informar Eric (como referência) sobre métodos para modificá-lo. Dar a um método um clone de Eric quase sempre será surpreendente. Qualquer pessoa que queira mudarPerson
deve se lembrar de pedir uma referênciaPerson
ou estará fazendo a coisa errada.Se você tornar o tipo imutável, o problema desaparecerá; se não consigo modificar
eric
, não faz diferença para mim recebereric
ou receber um clone deeric
. De maneira mais geral, um tipo é seguro para passar por valor se todo o seu estado observável for mantido em membros que sejam:Se essas condições forem atendidas, um tipo de valor mutável se comportará como um tipo de referência, porque uma cópia superficial ainda permitirá que o receptor modifique os dados originais.
A intuitividade de um imutável
Person
depende do que você está tentando fazer. SePerson
apenas representa um conjunto de dados sobre uma pessoa, não há nada de intuitivo;Person
variáveis representam verdadeiramente valores abstratos , não objetos. (Nesse caso, provavelmente seria mais apropriado renomeá-loPersonData
.) SePerson
na verdade estiver modelando uma pessoa, a ideia de criar e mover constantemente clones é tola, mesmo que você evite a armadilha de pensar que está modificando o original. Nesse caso, provavelmente seria mais natural simplesmente criarPerson
um tipo de referência (ou seja, uma classe).É verdade que, como a programação funcional nos ensinou, há benefícios em tornar tudo imutável (ninguém pode se apegar a uma referência a
eric
ele e transformá-lo em segredo ), mas como isso não é idiomático no OOP, ainda não será adequado para quem trabalha com o seu código.fonte
foo
detém a única referência ao seu destino em qualquer lugar do universo, e nada capturou o valor do hash de identidade desse objeto, o campo mutantefoo.X
é semanticamente equivalente afoo
apontar para um novo objeto, exatamente como o que ele se referia anteriormente, mas comX
segurando o valor desejado. Com tipos de classe, geralmente é difícil saber se existem várias referências a alguma coisa, mas com estruturas é fácil: elas não existem.Thing
é um tipo de classe mutável, umThing[]
vai encapsular identidades objeto - se se quer que ele ou não - a menos que se pode garantir que nãoThing
na matriz à qual existem quaisquer referências externas nunca vai ser mutado. Se não se deseja que os elementos da matriz encapsulem a identidade, geralmente é necessário garantir que nenhum item ao qual ele contém referências seja mutado ou que nunca existam referências externas a nenhum item que possua [abordagens híbridas também podem funcionar ] Nenhuma das abordagens é terrivelmente conveniente. SeThing
for uma estrutura, aThing[]
encapsula apenas valores.Ele não tem nada a ver com estruturas (e não com C #), mas em Java você pode ter problemas com objetos mutáveis quando eles são, por exemplo, chaves em um mapa de hash. Se você alterá-los após adicioná-los a um mapa e ele alterar seu código de hash , podem ocorrer coisas ruins.
fonte
Pessoalmente, quando olho para o código, o seguinte parece bastante desajeitado para mim:
data.value.set (data.value.get () + 1);
ao invés de simplesmente
data.value ++; ou data.value = data.value + 1;
O encapsulamento de dados é útil ao passar uma classe e você deseja garantir que o valor seja modificado de forma controlada. No entanto, quando você define um conjunto público e obtém funções que fazem pouco mais do que definir o valor para o que já foi passado, como isso é uma melhoria em relação à simples transmissão de uma estrutura de dados públicos?
Quando crio uma estrutura privada dentro de uma classe, criei essa estrutura para organizar um conjunto de variáveis em um grupo. Quero poder modificar essa estrutura no escopo da classe, não obter cópias dessa estrutura e criar novas instâncias.
Para mim, isso impede que um uso válido de estruturas seja usado para organizar variáveis públicas, se eu quisesse o controle de acesso, usaria uma classe.
fonte
Existem vários problemas com o exemplo do Sr. Eric Lippert. É um exemplo para ilustrar o ponto em que as estruturas são copiadas e como isso pode ser um problema se você não tomar cuidado. Olhando para o exemplo, eu o vejo como resultado de um mau hábito de programação e não realmente um problema com struct ou com a classe.
Uma estrutura deve ter apenas membros públicos e não deve exigir nenhum encapsulamento. Se isso acontecer, realmente deve ser um tipo / classe. Você realmente não precisa de duas construções para dizer a mesma coisa.
Se você tiver uma classe encerrando uma estrutura, chamaria um método na classe para alterar a estrutura do membro. Isto é o que eu faria como um bom hábito de programação.
Uma implementação adequada seria a seguinte.
Parece que é um problema com o hábito de programação, em vez de um problema com o próprio struct. Estruturas devem ser mutáveis, essa é a idéia e a intenção.
O resultado das alterações voila se comporta conforme o esperado:
1 2 3 Pressione qualquer tecla para continuar. . .
fonte
Existem muitas vantagens e desvantagens nos dados mutáveis. A desvantagem de um milhão de dólares é alias. Se o mesmo valor estiver sendo usado em vários locais e um deles o alterar, ele parecerá ter sido magicamente alterado para os outros locais que o estão usando. Isso está relacionado, mas não é idêntico, às condições da corrida.
A vantagem de um milhão de dólares é modularidade, às vezes. O estado mutável pode permitir que você oculte as informações alteradas do código que não precisa saber sobre isso.
A arte do intérprete aborda essas compensações com alguns detalhes e fornece alguns exemplos.
fonte
Eu não acredito que eles são maus, se usados corretamente. Eu não o colocaria no meu código de produção, mas sim para algo como simulação de testes de unidade estruturada, onde a vida útil de uma estrutura é relativamente pequena.
Usando o exemplo do Eric, talvez você queira criar uma segunda instância desse Eric, mas faça ajustes, pois essa é a natureza do seu teste (ou seja, duplicação e modificação). Não importa o que acontece com a primeira instância do Eric, se estamos apenas usando o Eric2 para o restante do script de teste, a menos que você esteja planejando usá-lo como uma comparação de teste.
Isso seria útil principalmente para testar ou modificar o código legado que define superficialmente um objeto específico (o objetivo das estruturas), mas por ter uma estrutura imutável, isso impede seu uso de maneira irritante.
fonte
Range<T>
tipo com membrosMinimum
eMaximum
campos de tipoT
e códigoRange<double> myRange = foo.getRange();
, quaisquer garantias sobre o queMinimum
eMaximum
conter devem virfoo.GetRange();
. TendoRange
ser um struct-campo exposta iria deixar claro que ele não está indo para adicionar qualquer comportamento própria.