Pergunta

Na sequência dos debates aqui no SO Eu já li várias vezes a observação de que estruturas mutáveis ??são “mal” (como na resposta a esta questão ).

Qual é o problema real com a mutabilidade e estruturas em C #?

Foi útil?

Solução

Estruturas são tipos de valor, o que significa que eles são copiados quando eles são passados ??ao redor.

Então, se você mudar uma cópia que você está mudando apenas essa cópia, e não o original e não quaisquer outras cópias que pode ser ao redor.

Se o seu struct é imutável, em seguida, todas as cópias automáticas resultantes de ser passados ??por valor será o mesmo.

Se você quiser mudá-lo você tem que fazê-lo conscientemente através da criação de uma nova instância da estrutura com os dados modificados. (Não uma cópia)

Outras dicas

Por onde começar ;-p

de Eric Lippert do blog é sempre bom para um orçamento:

Esta é outra razão pela qual mutável tipos de valor são maus. Tente sempre fazer tipos de valores imutáveis.

Primeiro, você tende a alterações perder muito facilmente ... por exemplo, fazer as coisas de uma lista:

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

O que essa mudança? Nada de útil ...

O mesmo com as propriedades:

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

forçá-lo a fazer:

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

menos crítica, há um problema de tamanho; objetos mutáveis ?? tendem para ter várias propriedades; No entanto, se você tem uma estrutura com dois ints, um string, um DateTime e uma bool, você pode rapidamente queimar através de uma grande quantidade de memória. Com uma classe, vários chamadores podem compartilhar uma referência à mesma instância (as referências são pequenos).

Eu não diria que mal , mas mutabilidade é frequentemente um sinal de overeagerness por parte do programador para garantir um máximo de funcionalidade. Na realidade, isso muitas vezes não é necessário e que, por sua vez, faz a interface menor, mais fácil de usar e mais difícil de usar errado (= mais robusta).

Um exemplo disso é leitura / gravação e os conflitos de gravação / gravação em condições de corrida. Estes simplesmente não pode ocorrer em estruturas imutáveis, uma vez que uma gravação não é uma operação válida.

Além disso, eu afirmo que mutabilidade quase nunca é realmente necessário , o programador apenas pensa que pode ser no futuro. Por exemplo, ele simplesmente não faz sentido mudar uma data. Em vez disso, crie uma nova data baseado fora o antigo. Esta é uma operação barata, por isso o desempenho não é uma consideração.

estruturas mutáveis ??não são maus.

Eles são absolutamente necessários em circunstâncias de alto desempenho. Por exemplo, quando as linhas de cache e ou coleta de lixo se tornar um gargalo.

Eu não chamaria o uso de uma estrutura imutável nestes casos de uso perfeitamente válido "mal".

Eu posso ver o ponto de 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, então eu sou tudo para preferindo estruturas imutáveis, que impor imutabilidade , estruturas mais mutáveis.

No entanto, em vez de simplesmente rotular estruturas imutáveis ??como "mal", eu aconselharia a abraçar a linguagem e advogado regra mais útil e construtiva dos polegares.

Por exemplo: "estruturas são tipos de valor, que são copiados por padrão, você precisa de uma referência, se você não quiser copiá-los" ou "tentar trabalhar com estruturas readonly primeiro" .

Structs com campos ou propriedades mutáveis ??públicas não são maus.

métodos Struct (como distinto de setters de propriedade), que sofrem mutação "isto" são um pouco mal, só porque .net não fornece um meio de distinguir os de métodos que não o fazem. métodos struct que não o fazem mutação "este" deve ser invokable mesmo em estruturas só de leitura sem qualquer necessidade de cópia defensiva. Métodos que fazem mutação "isto" não deve ser invokable em tudo em estruturas somente leitura. Desde .net não quer proibir métodos struct que não modificam "isto" de ser invocado em estruturas só de leitura, mas não quer para permitir estruturas de leitura só para ser mutado, ele defensivamente cópias estruturas em leitura única contextos, ficando sem dúvida o pior dos dois mundos.

Apesar dos problemas com a manipulação de métodos automutantes em contextos somente leitura, no entanto, estruturas mutáveis ??muitas vezes oferecem a semântica muito superiores aos tipos de classes mutáveis. Considere as seguintes três assinaturas de método:

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

void Method1(PointyStruct foo);
void Method2(ref PointyStruct foo);
void Method3(PointyClass foo);

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

  1. Assumindo que o método não utiliza qualquer código "inseguro", pode-lo modificar foo?
  2. Se há referências fora de existir 'foo' antes que o método é chamado, poderia um exist referência fora depois?

Respostas:

Pergunta 1:
Method1(): no (clara intenção)
Method2(): sim (clara intenção)
Method3(): sim (incerto intenção)
Pergunta 2:
Method1(): não
Method2(): no (a menos inseguro)
Method3(): sim

Method1 não pode modificar foo, e nunca obtém uma referência. Method2 obtém uma referência de curta duração para foo, que ele pode usar modificar os campos de foo qualquer número de vezes, em qualquer ordem, até que ele retorne, mas não pode persistir essa referência. Antes retornos Method2, a menos que ele usa código inseguro, todas e quaisquer cópias que poderiam ter sido feitos de sua referência 'foo' terá desaparecido. Method3, ao contrário Method2, obtém uma referência promiscuamente-compartilhável para foo, e não há como dizer o que pode fazer com ele. Pode não mudar foo em tudo, ele pode mudar foo e depois voltar, ou pode dar uma referência para foo para outra discussão que pode transformar-lo de alguma forma arbitrária em algum momento futuro arbitrária. A única maneira de limitar o method3 pode fazer para um objeto de classe mutáveis ??passados ??para ele seria encapsular o objeto mutável em um read-only invólucro, que é feio e pesado.

matrizes de estruturas oferecem semântica maravilhosas. Dada RectArray [500] do tipo retângulo, é claro e óbvio como por exemplo elemento 123 copiar para elemento 456 e, em seguida, algum tempo depois definir a largura do elemento 123 para 555, sem elemento perturbador 456. "RectArray [432] = RectArray [321]; ...; RectArray [123] .Width = 555;" . Sabendo que Retângulo é um struct com um campo inteiro chamado Largura dirá uma Tudo o que precisa de saber sobre as afirmações acima.

Agora, suponha RectClass era uma classe com os mesmos campos como o retângulo e um queria fazer as mesmas operações em um RectClassArray [500] do tipo RectClass. Talvez a matriz é suposto para manter 500 referências imutáveis ??pré-inicializado para objetos RectClass mutáveis. nesse caso, o código apropriado seria algo como "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Talvez a matriz é assumido como casos de retenção que não vão mudar, de modo que o código correto seria mais como "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555;" Para saber o que é suposto fazer, seria preciso saber muito mais tanto sobre RectClass (por exemplo, ele suporta um construtor de cópia, uma cópia do método, etc.) eo uso pretendido do array. Longe de ser tãolimpo como usar um struct.

Para ter certeza, infelizmente, não há maneira agradável para qualquer classe de contêiner que não seja uma matriz para oferecer a semântica limpas de uma matriz struct. O melhor que podia fazer, se alguém quisesse uma coleção a ser indexado com por exemplo, uma corda, provavelmente seria a oferecer um método genérico "ActOnItem", que aceitaria uma string para o índice, um parâmetro genérico, e um delegado que ser passados ??por referência tanto o parâmetro genérico eo item de coleção. Isso permitiria quase a mesma semântica como matrizes struct, mas a menos que os VB.NET e C # pessoas podem ser pursuaded para oferecer uma sintaxe agradável, o código vai ser desajeitado-olhando mesmo que seja razoavelmente desempenho (passando um parâmetro genérico seria permitir o uso de um delegado estática e evitaria qualquer necessidade de criar quaisquer instâncias de classe temporários).

Pessoalmente, eu estou irritado com o ódio Eric Lippert et al. vomitar sobre os tipos de valores mutáveis. Eles oferecem a semântica mais limpas muito do que os tipos de referência promíscuas que são usados ??em todo o lugar. Apesar de algumas das limitações com o .net suporte para tipos de valor, há muitos casos em que tipos de valores mutáveis ??são um ajuste melhor do que qualquer outro tipo de entidade.

tipos valor representa basicamente conceitos imutáveis. Fx, não faz sentido para ter um valor matemático, tal como um inteiro, vector, etc e, em seguida, ser capaz de modificar. Isso seria como redefinir o significado de um valor. Em vez de mudar um tipo de valor, faz mais sentido atribuir outro valor único. Pense sobre o fato de que tipos de valores são comparados, comparando todos os valores de suas propriedades. O ponto é que, se as propriedades são as mesmas, então é a mesma representação universal desse valor.

Como Konrad menciona que não faz sentido mudar uma data ou, como o valor representa esse ponto único no tempo e não uma instância de um objeto de tempo que tem qualquer estado ou dependência do contexto.

Espera que isso faz algum sentido para você. É mais sobre o conceito tentar capturar com tipos de valor do que detalhes práticos, para ter certeza.

Há outros casos par de canto que poderiam levar a um comportamento unpredictible de programadores ponto de vista. Aqui par deles.

  1. tipos de valores imutáveis ??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);
    }
}

  1. tipos de valores mutáveis ??e array

Suponha que tem uma série de nossa struct mutável e nós estamos chamando o método IncrementI para o primeiro elemento desse array. O comportamento que você espera desta chamada? 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);

Assim, estruturas mutáveis ??não são maus, desde que você eo resto da equipe de entender claramente o que você está fazendo. Mas há muitos casos de canto quando o comportamento do programa seria diferente do esperado um, que poderia levar a sutil difícil de produzir e difícil de entender erros.

Se você já programou em uma linguagem como C / C ++, estruturas são bons para uso como mutável. Basta passar-los com referência, ao redor e não há nada que pode dar errado. O único problema que encontramos são as restrições do compilador C # e que, em alguns casos, eu sou incapaz de forçar a coisa estúpida de usar uma referência para a estrutura, em vez de uma cópia (como quando uma estrutura é parte de uma classe C # ).

Assim, estruturas mutáveis ??não são maus, C # tem feita -los mal. Eu uso estruturas mutáveis ??em C ++ o tempo todo e eles são muito conveniente e intuitiva. Em contraste, C # me fez abandonar completamente estruturas como membros de classes por causa da forma como lidar com objetos. Sua conveniência tem custo-nos o nosso.

Imagine que você tem um conjunto de 1.000.000 de estruturas. Cada struct que representa um patrimônio com coisas como bid_price, OFFER_PRICE (talvez decimais) e assim por diante, este é criado por C # / VB.

Imagine que matriz é criada em um bloco de memória alocada no heap gerenciado para que algum outro segmento de código nativo é capaz de acessar simultaneamente a matriz (talvez algum código que faz a matemática high-perf).

Imagine a C # código / VB está escutando um feed de variações de preços de mercado, que o código pode ter que acessar algum elemento da matriz (para qualquer segurança) e, em seguida, modificar algum campo preço (s).

Imagine isso está sendo feito ou dezenas ou mesmo centenas de milhares de vezes por segundo.

Bem, vamos encarar os fatos, neste caso, nós realmente queremos essas estruturas para ser mutável, eles precisam ser, porque eles estão sendo compartilhados por algum outro código nativo para que a criação de cópias não vai ajudar; eles precisam ser porque fazer uma cópia de algum struct 120 byte com essas taxas é loucura, especialmente quando uma atualização pode realmente afetar apenas um byte ou dois.

Hugo

Se você ficar com o que estruturas destinam-se (em C #, Visual Basic 6, Pascal / Delphi, C ++ struct tipo (ou classes) quando eles não são usados ??como ponteiros), você vai descobrir que uma estrutura não mais do que é um variável composta . Isso significa que:. Você vai tratá-los como um conjunto embalado de variáveis, sob um nome comum (a variável registro você faz referência membros de)

Eu sei que iria confundir muita gente profundamente usados ??para OOP, mas isso não é razão suficiente para dizer tais coisas são inerentemente mau, se usado corretamente. Algumas estruturas são inmutable como eles pretendem (este é o caso de namedtuple do Python), mas é um outro paradigma a considerar.

Sim: estruturas envolvem uma grande quantidade de memória, mas não vai ser precisamente mais memória fazendo:

point.x = point.x + 1

em comparação com:

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

O consumo de memória será pelo menos o mesmo, ou até mais, no caso inmutable (embora nesse caso, seria temporária, para a pilha atual, dependendo do idioma).

Mas, finalmente, as estruturas são estruturas , e não objetos. Em POO, a principal propriedade de um objeto é a sua identidade , que na maioria das vezes não é mais do que o seu endereço de memória. Struct significa estrutura de dados (não um objeto apropriado, e assim eles não têm identidade de qualquer forma), e os dados podem ser modificados. Em outras línguas, record (em vez de struct , como é o caso de Pascal) é a palavra e mantém o mesmo propósito: apenas uma variável de registro de dados, concebido para ser lido a partir de arquivos, modificados e despejados em arquivos (que é o uso principal e, em muitas línguas, você pode até mesmo definir o alinhamento de dados no registro, enquanto isso não é necessariamente o caso para corretamente chamados de objetos).

Quer um bom exemplo? Estruturas são usados ??para ler arquivos facilmente. Python tem este biblioteca porque, uma vez que é orientada a objeto e não tem suporte para estruturas, teve que implementá-lo de outra maneira, que é um pouco feio. Línguas execução estruturas têm essa característica ... built-in. Tente ler um cabeçalho de bitmap com uma estrutura apropriada em linguagens como Pascal ou C. Será fácil (se a estrutura está devidamente construído e alinhados; em Pascal você não usaria um acesso baseado em disco, mas funções para ler dados binários arbitrário). Então, para arquivos e acesso direto à memória (local), estruturas são melhores do que objetos. Para hoje, estamos acostumados a JSON e XML, e assim podemos esquecer o uso de arquivos binários (e como um efeito colateral, o uso de estruturas). Mas sim: eles existem e têm um propósito

.

Eles não são maus. Apenas usá-los para o propósito certo.

Se você pensar em termos de martelos, você vai querer parafusos tratar como pregos, para encontrar parafusos são mais difíceis de mergulho na parede, e será culpa dos parafusos, e eles serão os maus.

Quando algo pode ser mutado, ganha um sentido 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));

Porque Person é mutável, é mais natural pensar em mudando a posição de Eric de clonagem Eric, movendo-se o clone, e destruindo o original . Ambas as operações conseguiria alterar o conteúdo de eric.position, mas um é mais intuitivo do que o outro. Da mesma forma, é mais intuitiva para passar Eric volta (como referência) para métodos para modificar ele. Dando um método um clone de Eric é quase sempre vai ser surpreendente. Qualquer pessoa que queira Person mutação deve se lembrar de pedir uma referência para Person ou eles vão estar fazendo a coisa errada.

Se você fizer o tipo imutável, o problema desaparece; se eu não posso modificar eric, não faz diferença para mim se eu receber eric ou um clone do eric. Mais geralmente, um tipo é seguro para passar por valor se todos seu estado observável é realizada em membros que sejam:

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

Se essas condições forem satisfeitas, então um mutáveis ??se comporta tipo de valor como um tipo de referência, porque uma cópia superficial ainda permitirá que o receptor para modificar os dados originais.

A intuição de um Person imutável depende do que você está tentando fazer embora. Se Person apenas representa um conjunto de dados sobre uma pessoa, não há nada intuitiva sobre isso; variáveis ??Person realmente representam abstratas valores , e não objetos. (Nesse caso, provavelmente seria mais apropriado para renomeá-lo para PersonData). Se Person está realmente modelando uma pessoa em si, a idéia de constantemente criar e clones em movimento é bobo mesmo se você evitou a armadilha de pensar que você' re modificar o original. Nesse caso provavelmente seria mais natural para simplesmente fazer Person um tipo de referência (ou seja, uma classe).

Com certeza, como programação funcional nos ensinou há benefícios para fazer tudo imutável (ninguém pode secretamente segurar uma referência a eric e transformam-lo), mas desde que isso não é idiomática em OOP é ainda vai ser pouco intuitivo para qualquer outra pessoa que trabalha com o seu código.

Ele não tem nada a ver com estruturas (e não com C #, qualquer um), mas em Java você pode ter problemas com objetos mutáveis ??quando estão por exemplo chaves em um mapa hash. Se você alterá-los depois de adicioná-los a um mapa e muda a sua código hash , mal as coisas podem acontecer.

Pessoalmente quando eu olhar para o código a seguir parece muito desajeitados para mim:

data.value.set (data.value.get () + 1);

em vez de simplesmente

data.value ++; ou data.value = data.value + 1;

Os dados encapsulamento é útil quando a passagem de um torno de classe e você quiser garantir o valor é modificado de forma controlada. No entanto, quando você tem conjunto público e obter funções que fazem pouco mais do que definir o valor ao que já é passado, como isso é uma melhoria em relação simplesmente passando uma estrutura de dados pública ao redor?

Quando eu criar uma estrutura privada dentro de uma classe, eu criei essa estrutura para organizar um conjunto de variáveis ??em um grupo. Eu quero ser capaz de modificar essa estrutura no âmbito classe, não obter cópias de que a estrutura e criar novas instâncias.

Para mim, isso impede um uso válido de estruturas sendo usado para organizar as variáveis ??públicas, se eu queria o controle de acesso que eu usaria uma classe.

Há vários problemas com o exemplo do Sr. Eric Lippert. Ele é planejado para ilustrar o ponto que estruturas são copiados e como isso poderia ser um problema se você não for cuidadoso. Olhando para o exemplo que eu vejo isso como um resultado de um hábito de programação ruim e não é realmente um problema com qualquer estrutura ou a classe.

  1. A estrutura é suposto ter apenas que membros do público e não deve exigir qualquer encapsulamento. Se isso acontecer, então ele realmente deve ser um tipo / classe. Você realmente não precisa de duas construções para dizer a mesma coisa.

  2. Se você tem classe encerrando um struct, você chamaria de um método na classe de mutação do struct membro. Isto é o que eu iria fazer como um bom hábito de programação.

A 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 ele é um problema com o hábito de programação em oposição a um problema com a própria estrutura. Estruturas devem ser mutável, que é a idéia e intenção.

O resultado das mudanças voila se comporta como esperado:

1 2 3 Pressione qualquer tecla para continuar . . .

Há muitas vantagens e desvantagens para dados mutáveis. A desvantagem de milhões de dólares é aliasing. Se o mesmo valor está sendo usado em vários lugares, e um deles muda-lo, então ele vai parecem ter mudado magicamente para os outros lugares que estão usando. Isto está relacionado com, mas não idêntico com, condições de corrida.

A vantagem de milhões de dólares é a modularidade, às vezes. estado mutável pode permitir que você esconder alteração de informações de código que não precisa de saber sobre isso.

A Arte do intérprete vai para esses trade-offs com algum detalhe, e dá alguns exemplos.

Eu não acredito que eles são maus, se usado corretamente. Eu não iria colocá-lo em meu código de produção, mas eu faria para algo como simulações de teste de unidade estruturados, onde a expectativa de vida de um struct é relativamente pequeno.

Usando o exemplo Eric, talvez você queira criar uma segunda instância de que Eric, mas fazer ajustes, como essa é a natureza do seu teste (ou seja, a duplicação, em seguida, modificar). Não importa o que acontece com a primeira instância de Eric se estamos apenas usando Eric2 para o restante do script de teste, a menos que você está pensando em usá-lo como uma comparação teste.

Este seria principalmente útil para testar ou modificar o código legado que superficial define um objeto em particular (o ponto de estruturas), mas por ter uma estrutura imutável, isto impede a sua utilização irritante.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top