Por que as estruturas mutáveis ​​são "más"?


485

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 #?


12
Alegando estruturas mutáveis são mal é como afirmar mutáveis ints, bools, 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.
Slipp D. Thompson

41
@slipp inte nãobool são mutáveis ​​..
Blorgbeard sai em

2
.-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 uma a[V][X] = 3.14sintaxe 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 como a.MutateV((v) => { v.X = 3; }) (o exemplo é simplificado demais por causa das limitações que o C # tem em relação à refpalavra - chave, mas com algumas soluções alternativas devem ser possíveis) .
Slipp D. Thompson 5/05

2
@ Slipp Bem, acho exatamente oposto a esse tipo de estrutura. Por que você acha que estruturas que já foram implementadas na biblioteca .NET, como DateTime ou TimeSpan (tão semelhantes) são imutáveis? Talvez possa ser útil alterar apenas um membro do var dessa estrutura, mas é inconveniente demais, leva a muitos problemas. Na verdade, você está errado sobre o que o processador calcula, já que o C # não é compilado no assembler, ele é compilado no IL. Em IL (desde já temos a variável chamada x) esta única operação é de 4 instruções: ldloc.0(cargas a variável 0-índice para ...
Sushi271

1
... tipo. 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. Como ref Tnã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 ...
Sushi271

Respostas:


290

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)


82
"Se sua estrutura é imutável, todas as cópias serão iguais." Não, isso significa que você deve fazer uma cópia conscientemente se quiser um valor diferente. Isso significa que você não será pego modificando uma cópia pensando que está modificando o original.
1555 Lucas

19
@ Lucas: Eu acho que você está falando sobre um tipo diferente de cópia. Eu estou falando sobre as cópias automáticas feitas como resultado de serem passadas por valor. Sua 'cópia conscientemente feita' é diferente de propósito; não é realmente uma cópia, é um instante novo e deliberado que contém dados diferentes.
trampster

3
Sua edição (16 meses depois) torna isso um pouco mais claro. Ainda estou de pé "(estrutura imutável) significa que você não será pego modificando uma cópia pensando que está modificando o original".
Lucas

6
@ Lucas: O perigo de fazer uma cópia de uma estrutura, modificá-la e de alguma forma pensar que alguém está modificando o original (quando o fato de alguém estar escrevendo um campo de estrutura se torna aparente o fato de que alguém está apenas escrevendo sua cópia) parece bastante pequeno comparado ao perigo de alguém que detém um objeto de classe como um meio de manter as informações nele contidas, modificar o objeto para atualizar suas próprias informações e, no processo, corromper as informações mantidas por outro objeto.
Supercat 28/10

2
O terceiro parágrafo parece errado ou pouco claro. Se sua estrutura é imutável, você simplesmente não poderá modificar seus campos ou os campos de quaisquer cópias feitas. "Se você quiser mudá-lo você tem que ..." que é enganosa também, você não pode mudar isso nunca , nem conscientemente nem inconscientemente. Criar uma nova instância na qual os dados que você deseja não tem nada a ver com a cópia original, a não ser ter a mesma estrutura de dados.
Saeb Amini

167

Por onde começar ;-p

O blog de Eric Lippert é sempre bom para uma citação:

Essa é outra razão pela qual tipos de valores mutáveis ​​são maus. Tente sempre tornar imutáveis ​​os tipos de valor.

Primeiro, você tende a perder alterações com bastante facilidade ... por exemplo, tirando as coisas de uma lista:

Foo foo = list[0];
foo.Name = "abc";

o que isso mudou? Nada útil ...

O mesmo com propriedades:

myObj.SomeProperty.Size = 22; // the compiler spots this one

forçando você a fazer:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

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 ints, a string, a DateTimee a bool, 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).


5
Bem, sim, mas o compilador é apenas estúpido assim. Não permitir a atribuição a membros de estrutura de propriedade foi uma decisão estúpida do IMHO, porque é permitido ao ++operador. Nesse caso, o compilador apenas grava a atribuição explícita em vez de apressar o programador.
Konrad Rudolph

12
@Konrad: myObj.SomeProperty.Size = 22 modificaria uma CÓPIA de myObj.SomeProperty. O compilador está salvando você de um bug óbvio. E NÃO é permitido para ++.
1555 Lucas

@ Lucas: Bem, o compilador Mono C # certamente o permite - já que não tenho o Windows, não posso verificar o compilador da Microsoft.
Konrad Rudolph

6
@ Konon - com menos um indireção deve funcionar; é a "mutação de um valor de algo que só existe como valor transitório na pilha e que está prestes a evaporar no nada", que é o caso bloqueado.
Marc Gravell

2
@ Marc Gravell: No trecho de código anterior, você termina com um "Foo" cujo nome é "abc" e cujos outros atributos são os da Lista [0], sem incomodar a Lista [0]. Se Foo fosse uma classe, seria necessário cloná-lo e depois alterar a cópia. Na minha opinião, o grande problema com a distinção entre tipo de valor e classe é o uso do "." operador para dois propósitos. Se eu tivesse meus diretores, as aulas poderiam suportar os dois "." e "->" para métodos e propriedades, mas a semântica normal para "." As propriedades seriam criar uma nova instância com o campo apropriado modificado.
supercat

71

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.


1
Eric Lippert diz que eles são ... veja minha resposta.
Marc Gravell

42
Por mais que eu respeite Eric Lippert, ele não é Deus (ou pelo menos ainda não). A postagem do blog ao qual você vincula e a postagem acima são argumentos razoáveis ​​para tornar estruturas imutáveis, como é óbvio, mas na verdade são muito fracas como argumentos para nunca usar estruturas mutáveis. Esta postagem, no entanto, é um +1.
Stephen Martin

2
Desenvolvendo em C #, você geralmente precisa de mutabilidade de vez em quando - especialmente com o seu Modelo de Negócios, onde você deseja que o streaming etc. funcione sem problemas com as soluções existentes. Eu escrevi um artigo sobre como trabalhar com dados mutáveis e imutáveis, resolver a maioria das questões em torno mutabilidade (espero): rickyhelgesson.wordpress.com/2012/07/17/...
Ricky Helgesson

3
@StephenMartin: estruturas que encapsulam um único valor geralmente devem ser imutáveis, mas estruturas são de longe o melhor meio para encapsular conjuntos fixos de variáveis ​​independentes mas relacionadas (como as coordenadas X e Y de um ponto) que não têm "identidade" como grupo. Estruturas usadas para esse fim geralmente devem expor suas variáveis ​​como campos públicos. Eu consideraria a noção de que é mais apropriado usar uma classe do que uma estrutura para tais propósitos como completamente errada. Classes imutáveis ​​geralmente são menos eficientes e classes mutáveis ​​geralmente têm semântica terrível.
precisa

2
@StephenMartin: considere, por exemplo, um método ou propriedade que deve retornar os seis floatcomponentes 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.
supercat

48

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" .


8
Eu também diria que, se alguém quiser prender um conjunto fixo de variáveis ​​com fita adesiva para que seus valores possam ser processados ​​ou armazenados separadamente ou como uma unidade, faz muito mais sentido pedir ao compilador que prenda um conjunto fixo de variáveis ​​juntas (por exemplo, declarar a structcom 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) #
687

41

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:

struct PointyStruct {public int x, y, z;};
classe PointyClass {public int x, y, z;};

Método void1 (PointyStruct foo);
void Method2 (ref PointyStruct foo);
Método void3 (PointyClass foo);

Para cada método, responda às seguintes perguntas:

  1. Supondo que o método não use nenhum código "inseguro", ele pode ser modificado?
  2. Se nenhuma referência externa a 'foo' existir antes do método ser chamado, uma referência externa poderá existir depois?

Respostas:

Pergunta 1::
Method1()não (intenção clara)
Method2() : sim (intenção clara)
Method3() : sim (intenção incerta)
Pergunta 2
Method1():: não
Method2(): não (a menos que seja inseguro)
Method3() : sim

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.


1
@ Ron Warholic: não é aparente que o SomeRect seja um retângulo. Pode ser algum outro tipo que pode ser implicitamente digitado no Rectangle. Embora, o único tipo definido pelo sistema que pode implicitamente ser convertido a partir do Rectangle seja RectangleF, e o compilador gritaria se alguém tentasse passar os campos de um RectangleF para o construtor do Rectangle (já que o primeiro é Single e o último Inteiro) , pode haver estruturas definidas pelo usuário que permitam tais previsões implícitas. BTW, a primeira instrução funcionaria igualmente bem se SomeRect fosse um Rectangle ou um RectangleF.
Supercat

Tudo o que você mostrou é que, em um exemplo artificial, você acredita que um método é mais claro. Se dermos o seu exemplo, Rectangleeu poderia facilmente encontrar uma localização comum em que você tenha um comportamento altamente obscuro . Considere que o WinForms implementa um Rectangletipo mutável usado na Boundspropriedade 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.
Ron Warholic

3
@Ron Warholic: BTW, eu gostaria de ser capaz de dizer "form.Bounds.X = 10;" e funcione, mas o sistema não fornece uma maneira limpa de fazer isso. Uma convenção para expor propriedades de tipo de valor como métodos que aceitam retornos de chamada pode oferecer um código muito mais limpo, eficiente e confirmavelmente correto do que qualquer abordagem usando classes.
Supercat

4
Esta resposta é muito mais criteriosa do que algumas das respostas mais votadas. É meio absurdo que o argumento contra os tipos de valores mutáveis ​​se baseie na noção de "o que você espera" que aconteça quando você mistura alias e mutação. Isso é uma coisa terrível para fazer de qualquer maneira !
Eamon Nerbonne

2
@supercat: Quem sabe, talvez o recurso ref-return que eles estão falando para o C # 7 possa cobrir essa base (na verdade, eu não o olhei em detalhes, mas soa superficialmente semelhante).
Eamon Nerbonne

24

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.


Bem, eles devem representar conceitos imutáveis, pelo menos ;-p
Marc Gravell

3
Bem, suponho que eles poderiam ter tornado o System.Drawing.Point imutável, mas teria sido um grave erro de design no IMHO. Eu acho que os pontos são realmente um tipo de valor arquetípico e são mutáveis. E eles não causam problemas para ninguém além da programação inicial de 101 iniciantes.
Stephen Martin

3
Em princípio, acho que os pontos também devem ser imutáveis, mas se tornar o tipo mais difícil ou menos elegante de usar, é claro que também deve ser considerado. Não há nenhum ponto em ter construções de código que sustentam as melhores princicples se ninguém quer usá-los;)
Morten Christiansen

3
Os tipos de valor são úteis para representar conceitos imutáveis ​​simples, mas as estruturas de campo exposto são os melhores tipos para manter ou repassar pequenos conjuntos fixos de valores relacionados, mas independentes (como as coordenadas de um ponto). Um local de armazenamento desse tipo de valor encapsula os valores de seus campos e nada mais. Por outro lado, um local de armazenamento de um tipo de referência mutável pode ser usado com a finalidade de manter o estado do objeto mutável, mas também encapsula a identidade de todas as outras referências em todo o universo que existem para o mesmo objeto.
precisa

4
“Tipos de valor representam basicamente conceitos imutáveis”. Não, eles não. Uma das aplicações mais antigas e úteis de uma variável de tipo de valor é um intiterador, 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.
Slipp D. Thompson

19

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

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Tipos de valor mutável e matriz

Suponha que temos uma matriz de nossa Mutableestrutura e estamos chamando o IncrementImé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?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

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.


5
O que deveria acontecer, se as linguagens .net tivessem um suporte de tipo de valor um pouco melhor, os métodos struct deveriam ser proibidos de alterar 'this', a menos que sejam explicitamente declarados como fazê-lo, e os métodos declarados assim deveriam ser proibidos em somente leitura contextos. Matrizes de estruturas mutáveis ​​oferecem semântica útil que não pode ser alcançada com eficiência por outros meios.
supercat 28/09

2
estes são bons exemplos de questões muito sutis que surgiriam de estruturas mutáveis. Eu não teria esperado nada desse comportamento. Por que uma matriz forneceria uma referência, mas uma interface forneceria um valor? Eu teria pensado, além dos valores de todos os tempos (o que eu realmente esperaria), que pelo menos seria o contrário: interface fornecendo referências; matrizes dando valores ...
Dave Cousineau

@Sahuagin: Infelizmente, não existe um mecanismo padrão pelo qual uma interface possa expor uma referência. Existem maneiras de o .net permitir que essas coisas sejam feitas com segurança e utilidade (por exemplo, definindo uma estrutura "ArrayRef <T>" especial que contém um T[]índice inteiro e um, e desde que um acesso a uma propriedade do tipo ArrayRef<T>seja interpretado como um acesso ao elemento de matriz apropriado) [se uma classe quisesse expor um ArrayRef<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.
Supercat

2
Oh meu ... isso faz com que estruturas mutáveis ​​danem o mal!
Nawfal # 8/13

1
Eu gosto desta resposta porque contém informações muito valiosas que não são óbvias. Mas, na verdade, esse argumento não é contra estruturas mutáveis, como afirmam alguns. Sim, o que vemos aqui é um "poço de desespero", como Eric teria dito, mas a fonte desse desespero não é mutabilidade. A fonte do desespero são os métodos auto-mutantes das estruturas . (Quanto ao motivo pelo qual matrizes e listas se comportam de maneira diferente, é porque um é basicamente um operador que calcula um endereço de memória e o outro é uma propriedade. Em geral, tudo fica claro quando você entende que uma "referência" é um valor de endereço .)
AnorZaken

18

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.


Ter campos de classe de tipos de estrutura geralmente pode ser um padrão muito útil, embora, reconhecidamente, haja algumas limitações. O desempenho será prejudicado se alguém usar propriedades em vez de campos, ou usar 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 como int[]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.
Supercat ''

13

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:

point.x = point.x + 1

comparado com:

point = Point(point.x + 1, point.y)

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.


12

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


3
É verdade, mas, neste caso, a razão para usar uma estrutura é que isso é imposto ao design do aplicativo por restrições externas (aquelas pelo uso do código nativo). Tudo o mais que você descreve sobre esses objetos sugere que eles devem ser claramente classes em C # ou VB.NET.
Jon Hanna

6
Não sei por que algumas pessoas pensam que as coisas devem ser objetos de classe. Se todos os slots de matriz forem preenchidos com instâncias distintas de referência, o uso de um tipo de classe adicionará doze ou vinte e quatro bytes extras ao requisito de memória, e o acesso seqüencial a uma matriz de referências a objetos de classe provavelmente será muito mais lento que o acesso sequencial. uma matriz de estruturas.
precisa

9

Quando algo pode ser modificado, ele ganha um senso de identidade.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

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 de eric.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 mudar Persondeve se lembrar de pedir uma referência Personou 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 receber ericou receber um clone de eric. De maneira mais geral, um tipo é seguro para passar por valor se todo o seu estado observável for mantido em membros que sejam:

  • imutável
  • tipos de referência
  • seguro passar por valor

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 Persondepende do que você está tentando fazer. Se Personapenas representa um conjunto de dados sobre uma pessoa, não há nada de intuitivo; Personvariáveis ​​representam verdadeiramente valores abstratos , não objetos. (Nesse caso, provavelmente seria mais apropriado renomeá-lo PersonData.) Se Personna 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 criar Personum 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 ericele 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.


3
Seu ponto de vista sobre identidade é bom; pode valer a pena notar que a identidade é relevante apenas quando existem várias referências a algo. Se foodeté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 mutante foo.Xé semanticamente equivalente a fooapontar para um novo objeto, exatamente como o que ele se referia anteriormente, mas com Xsegurando 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.
Supercat3 /

1
Se Thingé um tipo de classe mutável, um Thing[] vai encapsular identidades objeto - se se quer que ele ou não - a menos que se pode garantir que não Thingna 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. Se Thingfor uma estrutura, a Thing[]encapsula apenas valores.
Supercat3

Para objetos, sua identidade vem de sua localização. As instâncias dos tipos de referência têm sua identidade graças à sua localização na memória e você apenas transmite sua identidade (uma referência), não seus dados, enquanto os tipos de valor têm sua identidade no local externo onde estão armazenados. A identidade do seu tipo de valor Eric vem apenas da variável em que ele está armazenado. Se você passar por ele, ele perderá sua identidade.
IllidanS4 quer Monica de volta

6

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.


3
Isso é verdade se você usar uma classe como chave também no mapa.
Marc Gravell

6

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.


1
Direto ao ponto! Estruturas são unidades organizacionais sem restrições de controle de acesso! Infelizmente, o C # os tornou inúteis para esse fim!
ThunderGr

isso está completamente errado, pois os dois exemplos mostram estruturas mutáveis.
vidstige 28/09/15

C # tornava inútil para esta finalidade, pois esse não é o propósito das estruturas
Luiz Felipe

6

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.

  1. 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.

  2. 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.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

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. . .


4
Não há nada errado em projetar pequenas estruturas opacas para se comportarem como objetos de classe imutáveis; as diretrizes do MSDN são razoáveis quando se tenta fazer algo que se comporta como um objeto . Estruturas são apropriadas em alguns casos em que precisamos de coisas leves que se comportam como objetos e em casos em que precisamos de um monte de variáveis ​​coladas com fita adesiva. Por alguma razão, no entanto, muitas pessoas não percebem que as estruturas têm dois usos distintos e que as diretrizes apropriadas para uma são inadequadas para a outra.
Supercat

5

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.


estruturas não podem ser alteradas em c #. Toda atribuição de estrutura é uma cópia.
recursivo

@ recursivo: Em alguns casos, essa é uma grande vantagem de estruturas mutáveis, e uma que me faz questionar a noção de que estruturas não devem ser mutáveis. O fato de que os compiladores às vezes copiam implicitamente estruturas não reduz a utilidade de estruturas mutáveis.
Super10 /

1

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.


A meu ver, uma estrutura é em seu coração um monte de variáveis ​​coladas com fita adesiva. No .NET, é possível que uma estrutura finja ser outra coisa senão um monte de variáveis ​​presas com fita adesiva, e eu sugeriria que, quando prático, um tipo que fingir ser outra coisa que não seja um monte de variáveis ​​presas com fita adesiva deve se comportar como um objeto unificado (o que, para uma estrutura, implicaria imutabilidade), mas às vezes é útil colar várias variáveis ​​com fita adesiva. Mesmo no código de produção, eu consideraria melhor ter um tipo ... #
308

... que claramente não tem semântica além de "cada campo contém a última coisa que foi escrita nele", inserindo toda a semântica no código que usa a estrutura, do que tentar fazer com que uma estrutura faça mais. Dado, por exemplo, um Range<T>tipo com membros Minimume Maximumcampos de tipo Te código Range<double> myRange = foo.getRange();, quaisquer garantias sobre o que Minimume Maximumconter devem vir foo.GetRange();. Tendo Rangeser um struct-campo exposta iria deixar claro que ele não está indo para adicionar qualquer comportamento própria.
Supercat 19/12
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.