Em C #, por que String é um tipo de referência que se comporta como um tipo de valor?

371

Uma String é um tipo de referência, mesmo que possua a maioria das características de um tipo de valor, como imutável e ter == sobrecarregado para comparar o texto em vez de garantir que eles façam referência ao mesmo objeto.

Por que string não é apenas um tipo de valor?

Davy8
fonte
Como para tipos imutáveis, a distinção é principalmente um detalhe de implementação (deixando os istestes de lado), a resposta é provavelmente "por razões históricas". O desempenho da cópia não pode ser o motivo, pois não há necessidade de copiar fisicamente objetos imutáveis. Agora é impossível mudar sem quebrar o código que realmente usa isverificações (ou restrições semelhantes).
Elazar
BTW, esta é a mesma resposta para C ++ (embora a distinção entre tipos de valor e referência não seja explícita na linguagem), a decisão de se std::stringcomportar como uma coleção é um erro antigo que não pode ser corrigido agora.
Elazar

Respostas:

333

Strings não são tipos de valor, pois podem ser enormes e precisam ser armazenadas na pilha. Os tipos de valor são (em todas as implementações do CLR ainda) armazenados na pilha. Seqüências de alocação de pilha quebrariam todos os tipos de coisas: a pilha é de apenas 1 MB para 32 bits e 4 MB para 64 bits, você teria que encaixotar cada sequência, incorrendo em uma penalidade de cópia, não seria possível estender seqüências e uso de memória balão, etc ...

(Edit: Esclarecimentos adicionados sobre o armazenamento de tipo de valor ser um detalhe de implementação, o que leva a essa situação em que temos um tipo com semântica de valor que não é herdado de System.ValueType. Obrigado, Ben.)

codekaizen
fonte
75
Estou aqui, mas apenas porque me dá a oportunidade de criar um link para uma postagem do blog relevante para a pergunta: os tipos de valor não são necessariamente armazenados na pilha. Geralmente é verdade no ms.net, mas não é especificado pela especificação da CLI. A principal diferença entre os tipos de valor e referência é que os tipos de referência seguem a semântica de copiar por valor. Veja blogs.msdn.com/ericlippert/archive/2009/04/27/… e blogs.msdn.com/ericlippert/archive/2009/05/04/…
Ben Schwehn em
8
@ Qwertie: Stringnão é tamanho variável. Quando você adiciona, na verdade você está criando outro Stringobjeto, alocando nova memória para ele.
codekaizen
5
Dito isto, uma string poderia, em teoria, ter sido um tipo de valor (uma estrutura), mas o "valor" não passaria de uma referência à string. Os designers do .NET decidiram naturalmente cortar o intermediário (o tratamento da estrutura era ineficiente no .NET 1.0, e era natural seguir o Java, no qual as strings já eram definidas como um tipo de referência, e não primitivo. Além disso, se a string fosse um tipo de valor e convertê-lo em objeto exigiria uma caixa, uma ineficiência desnecessária).
Qwertie
7
@codekaizen Qwertie está certa, mas acho que o texto foi confuso. Uma string pode ter um tamanho diferente de outra e, portanto, diferente do tipo de valor verdadeiro, o compilador não sabia de antemão quanto espaço alocar para armazenar o valor da string. Por exemplo, um Int32é sempre 4 bytes, portanto, o compilador aloca 4 bytes sempre que você define uma variável de sequência. Quanta memória o compilador deve alocar quando encontra uma intvariável (se fosse um tipo de valor)? Entenda que o valor ainda não foi atribuído naquele momento.
Kevin Brock
2
Desculpe, um erro de digitação no meu comentário que não posso corrigir agora; isso deveria ter sido .... Por exemplo, an Int32é sempre 4 bytes, portanto, o compilador aloca 4 bytes sempre que você define uma intvariável. Quanta memória o compilador deve alocar quando encontra uma stringvariável (se fosse um tipo de valor)? Entenda que o valor ainda não foi atribuído naquele momento.
Kevin Brock
57

Não é um tipo de valor, porque o desempenho (espaço e tempo!) Seria terrível se fosse um tipo de valor e seu valor tivesse que ser copiado toda vez que fosse passado e retornado de métodos, etc.

Tem semântica de valor para manter o mundo são. Você pode imaginar o quão difícil seria codificar se

string s = "hello";
string t = "hello";
bool b = (s == t);

definido bpara ser false? Imagine o quão difícil a codificação seria para qualquer aplicativo.

Jason
fonte
44
Java não é conhecido por ser conciso.
jason
3
@ Matt: exatamente. Quando eu mudei para o C #, isso foi meio confuso, já que eu sempre usava (e ainda o faço algumas vezes) .equals (..) para comparar strings enquanto meus colegas de equipe apenas usavam "==". Eu nunca entendi por que eles não deixaram o "==" para comparar as referências, embora, se você pensar, 90% das vezes provavelmente deseje comparar o conteúdo e não as referências para strings.
Juri
7
@Juri: Na verdade, acho que nunca é desejável verificar as referências, já que algumas vezes new String("foo");outras new String("foo")podem avaliar na mesma referência, o que não é o que você espera que um newoperador faça. (Ou você pode me dizer um caso em que eu iria querer comparar as referências?)
Michael
11
@ Michael Bem, você deve incluir uma comparação de referência em todas as comparações para obter uma comparação com nulo. Outro bom lugar para comparar referências com seqüências de caracteres é quando se compara ao invés de comparar a igualdade. Duas seqüências equivalentes, quando comparadas, devem retornar 0. A verificação desse caso, no entanto, leva tanto tempo quanto a execução de toda a comparação, portanto, não é um atalho útil. A verificação ReferenceEquals(x, y)é um teste rápido e você pode retornar 0 imediatamente e, quando misturado com o teste nulo, nem adiciona mais trabalho.
Jon Hanna
11
... ter strings como um tipo de valor desse estilo, em vez de ser um tipo de classe, significaria que o valor padrão de a stringpoderia se comportar como uma string vazia (como era nos sistemas pré-.net), e não como uma referência nula. Na verdade, minha preferência seria ter um tipo de valor Stringque contivesse um tipo de referência NullableString, com o primeiro tendo um valor padrão equivalente String.Emptye o segundo com o padrão nulle com regras especiais de boxe / unboxing (como valorizado NullableStringdaria uma referência a String.Empty).
Super12
26

A distinção entre tipos de referência e tipos de valor é basicamente uma troca de desempenho no design do idioma. Os tipos de referência têm alguma sobrecarga na construção e destruição e coleta de lixo, porque são criados no heap. Os tipos de valor, por outro lado, têm sobrecarga nas chamadas de método (se o tamanho dos dados for maior que um ponteiro), porque todo o objeto é copiado e não apenas um ponteiro. Como as strings podem ser (e geralmente são) muito maiores que o tamanho de um ponteiro, elas são projetadas como tipos de referência. Além disso, como Servy apontou, o tamanho de um tipo de valor deve ser conhecido em tempo de compilação, o que nem sempre é o caso para seqüências de caracteres.

A questão da mutabilidade é uma questão separada. Os tipos de referência e os tipos de valor podem ser mutáveis ​​ou imutáveis. Os tipos de valor são tipicamente imutáveis, pois a semântica para tipos de valor mutável pode ser confusa.

Os tipos de referência geralmente são mutáveis, mas podem ser projetados como imutáveis, se fizer sentido. As strings são definidas como imutáveis ​​porque possibilitam certas otimizações. Por exemplo, se a mesma string literal ocorrer várias vezes no mesmo programa (o que é bastante comum), o compilador poderá reutilizar o mesmo objeto.

Então, por que "==" está sobrecarregado para comparar seqüências de caracteres por texto? Porque é a semântica mais útil. Se duas seqüências de caracteres forem iguais ao texto, elas podem ou não ser a mesma referência de objeto devido às otimizações. Portanto, comparar referências é bastante inútil, enquanto comparar texto é quase sempre o que você deseja.

Falando de maneira mais geral, Strings tem o que é chamado de semântica de valores . Esse é um conceito mais geral que os tipos de valor, que é um detalhe de implementação específico do C #. Os tipos de valor têm semântica de valor, mas os tipos de referência também podem ter semântica de valor. Quando um tipo tem semântica de valor, você não pode realmente dizer se a implementação subjacente é um tipo de referência ou tipo de valor; portanto, você pode considerar isso um detalhe da implementação.

JacquesB
fonte
A distinção entre tipos de valor e tipos de referência não é realmente sobre desempenho. É sobre se uma variável contém um objeto real ou uma referência a um objeto. Uma string nunca poderia ser um tipo de valor porque o tamanho de uma string é variável; precisaria ser constante para ser um tipo de valor; o desempenho não tem quase nada a ver com isso. Os tipos de referência também não são caros de criar.
Servy
2
@ Sevy: O tamanho de uma string é constante.
precisa saber é o seguinte
Porque ele apenas contém uma referência a uma matriz de caracteres, que é de tamanho variável. Ter um tipo de valor que é apenas "valor" real era um tipo de referência seria ainda mais confuso, pois ainda teria semântica de referência para todos os fins intensivos.
Servy
11
@ Sevy: O tamanho de uma matriz é constante.
precisa saber é o seguinte
11
Depois de criar uma matriz, seu tamanho é constante, mas todas as matrizes no mundo inteiro não são exatamente do mesmo tamanho. Esse é meu argumento. Para que uma string seja um tipo de valor, todas as strings existentes deverão ter exatamente o mesmo tamanho, porque é assim que os tipos de valor são projetados no .NET. Ele precisa reservar espaço de armazenamento para esses tipos de valor antes de realmente ter um valor ; portanto, o tamanho deve ser conhecido em tempo de compilação . Esse stringtipo precisaria ter um buffer de caracteres de algum tamanho fixo, o que seria restritivo e altamente ineficiente.
Servy 7/11
16

Essa é uma resposta tardia a uma pergunta antiga, mas todas as outras respostas estão erradas, o .NET não tinha genéricos até o .NET 2.0 em 2005.

Stringé um tipo de referência em vez de um tipo de valor, porque era de importância crucial para a Microsoft garantir que as seqüências pudessem ser armazenadas da maneira mais eficiente em coleções não genéricas , como System.Collections.ArrayList.

Armazenar um tipo de valor em uma coleção não genérica requer uma conversão especial para o tipo objectchamado boxe. Quando o CLR caixa um tipo de valor, ele agrupa o valor dentro de System.Objecte armazena-o no heap gerenciado.

A leitura do valor da coleção requer a operação inversa, que é chamada de unboxing.

Tanto o boxe como o unboxing têm um custo não negligenciável: o boxe requer uma alocação adicional, o unboxing requer verificação de tipo.

Algumas respostas afirmam incorretamente que stringnunca poderiam ter sido implementadas como um tipo de valor porque seu tamanho é variável. Na verdade, é fácil implementar cadeias de caracteres como uma estrutura de dados de comprimento fixo usando uma estratégia de Otimização de pequenas cadeias: as cadeias seriam armazenadas na memória diretamente como uma sequência de caracteres Unicode, exceto as cadeias grandes que seriam armazenadas como ponteiro para um buffer externo. Ambas as representações podem ser projetadas para ter o mesmo comprimento fixo, ou seja, o tamanho de um ponteiro.

Se os genéricos existissem desde o primeiro dia, acho que ter uma string como um tipo de valor provavelmente teria sido uma solução melhor, com semântica mais simples, melhor uso de memória e melhor localidade de cache. Uma List<string>contendo apenas pequenas seqüências poderia ter sido um único bloco de memória contíguo.

ZunTzu
fonte
Meu, obrigado por esta resposta! Estive analisando todas as outras respostas dizendo coisas sobre alocações de heap e pilha, enquanto pilha é um detalhe de implementação . Afinal, stringcontém apenas seu tamanho e um ponteiro para a charmatriz, portanto, não seria um "tipo de valor enorme". Mas esse é um motivo simples e relevante para essa decisão de design. Obrigado!
V0ldek
8

Não apenas as strings são tipos de referência imutáveis. Delegados multi-elenco também. É por isso que é seguro escrever

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

Suponho que as strings sejam imutáveis ​​porque esse é o método mais seguro para trabalhar com elas e alocar memória. Por que eles não são tipos de valor? Os autores anteriores têm razão quanto ao tamanho da pilha, etc. Eu também acrescentaria que tornar as seqüências de caracteres tipos de referência permite economizar no tamanho da montagem quando você usa a mesma sequência constante no programa. Se você definir

string s1 = "my string";
//some code here
string s2 = "my string";

Provavelmente, as duas instâncias da constante "minha string" serão alocadas no seu assembly apenas uma vez.

Se você deseja gerenciar seqüências de caracteres como o tipo de referência usual, coloque a sequência dentro de um novo StringBuilder (string s). Ou use MemoryStreams.

Se você deseja criar uma biblioteca, na qual espera que uma cadeia enorme seja passada em suas funções, defina um parâmetro como um StringBuilder ou como um Stream.

Bogdan_Ch
fonte
11
Existem muitos exemplos de tipos de referência imutáveis. E re o exemplo da corda, que é realmente garantido nas implementações atuais bastante-muito - tecnicamente , é é por módulo (não per-montagem) -, mas que é quase sempre a mesma coisa ...
Marc Gravell
5
Quanto ao último ponto: StringBuilder não ajuda se você estiver tentando passar uma string grande (já que ela é realmente implementada como uma string) - StringBuilder é útil para manipular uma string várias vezes.
Marc Gravell
Você quis dizer delegado manipulador, não hadler? (desculpe a ser exigente .. mas é muito próximo de um (o sobrenome não é comum) eu sei ....)
Pure.Krome
6

Além disso, a maneira como as strings são implementadas (diferentes para cada plataforma) e quando você começa a costurá-las. Como usar um StringBuilder. Ele aloca um buffer para você copiar; assim que chega ao fim, ele aloca ainda mais memória para você, na esperança de que, se você fizer um grande desempenho de concatenação, não seja prejudicado.

Talvez Jon Skeet possa ajudar aqui em cima?

Chris
fonte
5

É principalmente uma questão de desempenho.

O fato de as seqüências se comportarem como o tipo de valor ajuda na escrita do código, mas, se este for um tipo de valor, o desempenho será enorme.

Para uma visão aprofundada, dê uma olhada em um bom artigo sobre strings na estrutura .net.

Denis Troller
fonte
3

Em palavras muito simples, qualquer valor que tenha um tamanho definido pode ser tratado como um tipo de valor.

saurav.net
fonte
Este deve ser um comentário
ρяσѕρєя K
mais fácil de entender para ppl novo em C #
LONGO
2

Como você pode dizer que stringé um tipo de referência? Não tenho certeza de que importa como é implementado. As seqüências de caracteres em C # são imutáveis ​​exatamente para que você não precise se preocupar com esse problema.


fonte
É um tipo de referência (acredito) porque não deriva de System.ValueType De MSDN Comentários sobre System.ValueType: Tipos de dados são separados em tipos de valor e tipos de referência. Os tipos de valor são alocados à pilha ou alocados em linha em uma estrutura. Os tipos de referência são alocados em heap.
Davy8
Os tipos de referência e valor são derivados do último objeto da classe base. Nos casos em que é necessário que um tipo de valor se comporte como um objeto, um wrapper que faz com que o tipo de valor pareça um objeto de referência é alocado no heap e o valor do tipo de valor é copiado nele.
Davy8
O wrapper é marcado para que o sistema saiba que contém um tipo de valor. Esse processo é conhecido como boxe e o processo inverso é conhecido como unboxing. Boxe e unboxing permitem que qualquer tipo seja tratado como um objeto. (No site traseiro, provavelmente deveria ter apenas um
link
2

Na verdade, as strings têm muito poucas semelhanças com os tipos de valor. Para iniciantes, nem todos os tipos de valor são imutáveis, você pode alterar o valor de um Int32 o quanto quiser e ele ainda seria o mesmo endereço na pilha.

As strings são imutáveis ​​por um motivo muito bom, não tem nada a ver com ser um tipo de referência, mas tem muito a ver com o gerenciamento de memória. É apenas mais eficiente criar um novo objeto quando o tamanho da string mudar do que mudar as coisas na pilha gerenciada. Eu acho que você está misturando tipos de valor / referência e conceitos de objetos imutáveis.

No que diz respeito a "==": Como você disse, "==" é uma sobrecarga de operador e, novamente, foi implementada por uma boa razão para tornar a estrutura mais útil ao trabalhar com strings.

WebMatrix
fonte
Percebo que os tipos de valor não são, por definição, imutáveis, mas a maioria das práticas recomendadas parece sugerir que deveriam ser ao criar a sua. Eu disse características, não propriedades de tipos de valor, o que para mim significa que muitas vezes os tipos de valor apresentam estes, mas não necessariamente por definição
Davy8
5
@ WebMatrix, @ Davy8: Os tipos primitivos (int, double, bool, ...) são imutáveis.
jason
11
@ Jason, eu pensei que o termo imutável se aplica principalmente a objetos (tipos de referência) que não podem ser alterados após a inicialização, como strings quando o valor das strings muda, internamente uma nova instância de uma string é criada e o objeto original permanece inalterado. Como isso se aplica aos tipos de valor?
WebMatrix
8
De alguma forma, em "int n = 4; n = 9;", não é que sua variável int seja "imutável", no sentido de "constante"; é que o valor 4 é imutável, não muda para 9. Sua variável int "n" primeiro tem um valor de 4 e depois um valor diferente, 9; mas os próprios valores são imutáveis. Francamente, para mim, isso é muito próximo do wtf.
Daniel Daranas
11
+1. Estou farto de ouvir essas "strings são como tipos de valor" quando simplesmente não são.
Jon Hanna
1

Não é tão simples quanto Strings são compostas de matrizes de caracteres. Eu vejo as strings como uma matriz de caracteres []. Portanto, eles estão na pilha porque o local da memória de referência é armazenado na pilha e aponta para o início da localização da memória da matriz na pilha. O tamanho da string não é conhecido antes de ser alocado ... perfeito para o heap.

É por isso que uma string é realmente imutável, porque quando você a altera, mesmo que seja do mesmo tamanho, o compilador não sabe disso e precisa alocar uma nova matriz e atribuir caracteres às posições na matriz. Faz sentido se você pensar em strings como uma maneira de as linguagens protegê-lo de ter que alocar memória rapidamente (leia C como programação)

BionicCyborg
fonte
11
"tamanho da string não é conhecido antes de ser alocado" - isso está incorreto no CLR.
precisa saber é o seguinte
-1

Correndo o risco de obter mais um voto negativo misterioso ... o fato de muitos mencionarem a pilha e a memória em relação aos tipos de valor e tipos primitivos é porque eles devem caber em um registro no microprocessador. Você não pode enviar ou empurrar algo da / para a pilha se ela precisar de mais bits do que um registro possui .... as instruções são, por exemplo, "pop eax" - porque eax tem 32 bits de largura em um sistema de 32 bits.

Os tipos primitivos de ponto flutuante são manipulados pela FPU, com 80 bits de largura.

Tudo isso foi decidido muito antes de haver uma linguagem OOP para ofuscar a definição de tipo primitivo e presumo que tipo de valor seja um termo criado especificamente para linguagens OOP.

jinzai
fonte