Por que o .NET String é imutável?

Como todos sabemos, String é imutável. Quais são as razões para a String ser imutável e a introdução da class StringBuilder como mutável?

  1. Instâncias de tipos imutáveis ​​são inerentemente seguras para threads, já que nenhum thread pode modificá-lo, o risco de um thread modificá-lo de uma maneira que interfira com outro é removido (a própria referência é um assunto diferente).
  2. Da mesma forma, o fato de o aliasing não poder produzir mudanças (se xey se referirem ao mesmo object, uma mudança para x implica uma mudança para y) permite otimizações consideráveis ​​do compilador.
  3. Otimizações de economia de memory também são possíveis. Internar e atomizar são os exemplos mais óbvios, embora possamos fazer outras versões do mesmo princípio. Certa vez, produzi uma economia de memory de cerca de meio GB, comparando objects imutáveis ​​e substituindo referências a duplicatas, de modo que todos apontassem para a mesma instância (demorada, mas uma boot extra de um minuto para salvar uma grande quantidade de memory era um ganho de desempenho no caso em questão). Com objects mutáveis ​​que não podem ser feitos.
  4. Nenhum efeito colateral pode vir de passar um tipo imutável como um método para um parâmetro, a menos que esteja out ou ref (já que isso altera a referência, não o object). Um programador, portanto, sabe que se string x = "abc" no início de um método, e isso não muda no corpo do método, então x == "abc" no final do método.
  5. Conceitualmente, a semântica é mais como tipos de valor; em particular, a igualdade é baseada no estado e não na identidade. Isso significa que "abc" == "ab" + "c" . Embora isso não exija imutabilidade, o fato de uma referência a essa sequência sempre ser igual a “abc” ao longo de sua existência (que requer imutabilidade) torna os usos como chaves em que manter a igualdade com os valores anteriores é vital, muito mais fácil de garantir de (cordas são de fato comumente usadas como chaves).
  6. Conceitualmente, pode fazer mais sentido ser imutável. Se adicionarmos um mês no Natal, não mudamos o Natal, produzimos uma nova data no final de janeiro. Faz sentido, portanto, que Christmas.AddMonths(1) produza um novo DateTime vez de alterar um mutável. (Outro exemplo, se eu como um object mutável mudar meu nome, o que mudou é qual nome eu estou usando, “Jon” permanece imutável e outros Jons não serão afetados.
  7. Copiar é rápido e simples, para criar um clone basta return this . Como a cópia não pode ser alterada de qualquer forma, fingir que algo é sua cópia é seguro.
  8. [Edit, eu tinha esquecido este aqui]. O estado interno pode ser compartilhado com segurança entre objects. Por exemplo, se você estava implementando uma lista que era apoiada por uma matriz, um índice inicial e uma contagem, a parte mais cara da criação de um subintervalo seria copiar os objects. No entanto, se fosse imutável, o object de subintervalo poderia fazer referência à mesma matriz, com apenas o índice inicial e a contagem precisando ser alterados, com uma alteração muito considerável no tempo de construção.

Em suma, para objects que não estão passando por mudanças como parte de seu propósito, pode haver muitas vantagens em ser imutável. A principal desvantagem é a exigência de construções extras, embora mesmo aqui seja freqüentemente exagerado (lembre-se, você precisa fazer vários anexos antes que o StringBuilder se torne mais eficiente do que a série equivalente de concatenações, com sua construção inerente).

Seria uma desvantagem se a mutabilidade fizesse parte do propósito de um object (quem gostaria de ser modelado por um object Employee cujo salário nunca poderia mudar), embora às vezes, mesmo assim, ele possa ser útil (em muitos sites e outros apátridas aplicações, código fazendo operações de leitura é separado daquele fazendo atualizações, e usar objects diferentes pode ser natural – eu não faria um object imutável e então forçar esse padrão, mas se eu já tivesse esse padrão eu poderia fazer meus objects de “leitura” imutável para o desempenho e correção-garantia de ganho).

Copy-on-write é um meio termo. Aqui, a class “real” contém uma referência a uma class “state”. Classes de estado são compartilhadas em operações de cópia, mas se você alterar o estado, uma nova cópia da class de estado é criada. Isso é mais comumente usado com C ++ que C #, e é por isso que std: string desfruta de algumas, mas não todas, as vantagens de tipos imutáveis, enquanto permanece mutável.

Tornar as cordas imutáveis ​​tem muitas vantagens. Ele fornece segurança automática de thread e faz com que as strings se comportem como um tipo intrínseco de maneira simples e eficaz. Ele também permite eficiências extras em tempo de execução (como permitir a internação efetiva de strings para reduzir o uso de resources) e tem enormes vantagens de segurança, já que é impossível para uma chamada de API de terceiros alterar suas strings.

O StringBuilder foi adicionado para resolver a principal desvantagem das strings imutáveis ​​- a construção em tempo de execução de tipos imutáveis ​​causa muita pressão no GC e é inerentemente lenta. Ao criar uma class mutável explícita para lidar com isso, esse problema é solucionado sem adicionar complicação desnecessária à class de string.

Cordas não são realmente imutáveis. Eles são apenas publicamente imutáveis. Isso significa que você não pode modificá-los a partir de sua interface pública. Mas no interior são realmente mutáveis.

Se você não acredita em mim, veja a definição String.Concat usando o refletor . As últimas linhas são …

 int length = str0.Length; string dest = FastAllocateString(length + str1.Length); FillStringChecked(dest, 0, str0); FillStringChecked(dest, length, str1); return dest; 

Como você pode ver, o FastAllocateString retorna uma string vazia mas alocada e depois é modificada por FillStringChecked

Na verdade, o FastAllocateString é um método externo e o FillStringChecked não é seguro, portanto, ele usa pointers para copiar os bytes.

Talvez existam exemplos melhores, mas este é o que eu encontrei até agora.

O gerenciamento de strings é um processo caro. manter seqüências de caracteres imutáveis ​​permite que seqüências repetidas sejam reutilizadas, em vez de recriadas.

Por que os tipos de string são imutáveis ​​em c #

String é um tipo de referência, portanto, nunca é copiado, mas passado por referência. Compare isso com o object C ++ std :: string (que não é imutável), que é passado por valor. Isto significa que se você quiser usar uma String como uma chave em uma Hashtable, você está bem em C ++, porque C + + irá copiar a string para armazenar a chave na hashtable (na verdade std :: hash_map, mas ainda) para comparação posterior . Então, mesmo que você modifique a instância std :: string, você está bem. Mas no .Net, quando você usa uma String em uma Hashtable, ela armazena uma referência a essa instância. Agora, assuma por um momento que as strings não são imutáveis ​​e veja o que acontece: 1. Alguém insere um valor x com a tecla “hello” em uma Hashtable. 2. A tabela de hash calcula o valor de hash da String e coloca uma referência à string e o valor x no intervalo apropriado. 3. O usuário modifica a instância da cadeia para ser “bye”. 4. Agora alguém quer o valor na hashtable associada a “olá”. Ele acaba procurando no bucket correto, mas ao comparar as strings diz “bye”! = “Hello”, então nenhum valor é retornado. 5. Talvez alguém queira o valor “bye”? “tchau” provavelmente tem um hash diferente, então o hashtable seria exibido em um intervalo diferente. Não há chaves “bye” nesse intervalo, portanto, nossa input ainda não foi encontrada.

Tornar as cordas imutáveis ​​significa que o passo 3 é impossível. Se alguém modifica a string, ele está criando um novo object string, deixando o antigo sozinho. O que significa que a chave na hashtable ainda é “olá” e, portanto, ainda está correta.

Portanto, provavelmente, entre outras coisas, strings imutáveis ​​são uma forma de permitir que strings passadas por referência sejam usadas como chaves em um object de dictionary hashtable ou semelhante.

Você nunca precisa copiar defensivamente dados imutáveis. Apesar do fato de que você precisa copiá-lo para alterá-lo, muitas vezes, a capacidade de usar livremente alias e nunca ter que se preocupar com conseqüências não intencionais desse aliasing pode levar a um melhor desempenho devido à falta de cópias defensivas.

Apenas para lançar isso, uma visão muitas vezes esquecida é de segurança, imagine este cenário se as seqüências de caracteres fossem mutáveis:

 string dir = "C:\SomePlainFolder"; //Kick off another thread GetDirectoryContents(dir); void GetDirectoryContents(string directory) { if(HasAccess(directory) { //Here the other thread changed the string to "C:\AllYourPasswords\" return Contents(directory); } return null; } 

Você vê como poderia ser muito, muito ruim se você tivesse permissão para alterar as strings depois que elas fossem passadas.

Strings e outros objects concretos são normalmente expressos como objects imutáveis ​​para melhorar a legibilidade e a eficiência do tempo de execução. Segurança é outra, um processo não pode mudar sua string e injetar código na string

Strings são passadas como tipos de referência no .NET.

Os tipos de referência colocam um ponteiro na pilha, na instância real que reside no heap gerenciado. Isso é diferente dos tipos de valor, que mantêm toda a instância na pilha.

Quando um tipo de valor é passado como um parâmetro, o tempo de execução cria uma cópia do valor na pilha e passa esse valor para um método. É por isso que os inteiros devem ser passados ​​com uma palavra-chave ‘ref’ para retornar um valor atualizado.

Quando um tipo de referência é passado, o tempo de execução cria uma cópia do ponteiro na pilha. Esse ponteiro copiado ainda aponta para a instância original do tipo de referência.

O tipo de string tem um operador = sobrecarregado que cria uma cópia de si mesmo, em vez de uma cópia do ponteiro – fazendo com que ele se comporte mais como um tipo de valor. No entanto, se apenas o ponteiro fosse copiado, uma segunda operação de string poderia sobrescrever acidentalmente o valor de um membro particular de outra class, causando alguns resultados bem desagradáveis.

Como outros posts mencionaram, a class StringBuilder permite a criação de strings sem a sobrecarga do GC.

Imagine que você passe uma string mutável para uma function, mas não espere que ela seja alterada. Então, e se a function mudar essa string? Em C ++, por exemplo, você poderia simplesmente chamar por valor (diferença entre std::string e std::string& parameter), mas em C # é tudo sobre referências então se você passasse strings mutáveis ​​ao redor de cada function poderia alterá-lo e desencadeie efeitos colaterais inesperados.

Esta é apenas uma das várias razões. O desempenho é outro (strings internadas, por exemplo).

Existem cinco maneiras comuns pelas quais um dado de armazenamento de dados de class que não pode ser modificado fora do controle da class de armazenamento:

  1. Como primitivos do tipo de valor
  2. Mantendo uma referência livremente compartilhável ao object de class cujas propriedades de interesse são todas imutáveis
  3. Mantendo uma referência a um object de class mutável que nunca será exposto a nada que possa alterar quaisquer propriedades de interesse
  4. Como uma estrutura, seja “mutável” ou “imutável”, todos cujos campos são dos tipos # 1- # 4 (não # 5).
  5. Mantendo a única cópia existente de uma referência a um object cujas propriedades só podem ser alteradas por meio dessa referência.

Como as strings são de comprimento variável, elas não podem ser primitivas de tipo de valor, nem seus dados de caractere podem ser armazenados em uma struct. Entre as escolhas restantes, a única que não exigiria que os dados dos caracteres das strings fossem armazenados em algum tipo de object imutável seria # 5. Embora fosse possível projetar um framework em torno da opção 5, essa escolha exigiria que qualquer código que quisesse uma cópia de uma string que não pudesse ser alterada fora de seu controle teria que fazer uma cópia privada para si mesma. Embora dificilmente seja impossível fazer isso, a quantidade de código extra necessária para fazer isso, e a quantidade de processamento extra em tempo de execução necessário para fazer cópias defensivas de tudo, superariam em muito os pequenos benefícios que poderiam advir de ter uma string mutável. , especialmente dado que existe um tipo de string mutável ( System.Text.StringBuilder ) que realiza 99% do que poderia ser realizado com uma string mutável.

Cadeias imutáveis ​​também evitam problemas relacionados à concorrência.

Imagine ser um sistema operacional trabalhando com uma string que algum outro segmento estava modificando nas suas costas. Como você poderia validar qualquer coisa sem fazer uma cópia?