Como as strings são passadas no .NET?

120

Quando passo a stringpara uma função, é passado um ponteiro para o conteúdo da sequência ou toda a sequência é passada para a função na pilha como structseria?

Cole Johnson
fonte

Respostas:

277

Uma referência é passada; no entanto, não é tecnicamente passado por referência. Esta é uma distinção sutil, mas muito importante. Considere o seguinte código:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

Há três coisas que você precisa saber para entender o que acontece aqui:

  1. Strings são tipos de referência em C #.
  2. Eles também são imutáveis; portanto, sempre que você faz algo que parece que está mudando a corda, não está. Uma string completamente nova é criada, a referência é apontada para ela e a antiga é jogada fora.
  3. Mesmo que as strings sejam tipos de referência, elas strMainnão são passadas por referência. É um tipo de referência, mas a própria referência é passada por valor . Sempre que você passa um parâmetro sem a refpalavra - chave (sem contar os outparâmetros), passa algo por valor.

Então isso deve significar que você está ... passando uma referência por valor. Como é um tipo de referência, apenas a referência foi copiada na pilha. Mas o que isso significa?

Passando tipos de referência por valor: você já está fazendo isso

Variáveis ​​C # são tipos de referência ou tipos de valor . Os parâmetros C # são passados ​​por referência ou passados ​​por valor . A terminologia é um problema aqui; estes parecem a mesma coisa, mas não são.

Se você passa um parâmetro de QUALQUER tipo e não usa a refpalavra - chave, passa-o por valor. Se você passou por valor, o que realmente passou foi uma cópia. Mas se o parâmetro era um tipo de referência, o que você copiava era a referência, não o que estava apontando.

Aqui está a primeira linha do Mainmétodo:

string strMain = "main";

Criamos duas coisas nessa linha: uma string com o valor mainarmazenado na memória em algum lugar e uma variável de referência chamada strMainapontando para ela.

DoSomething(strMain);

Agora passamos essa referência para DoSomething. Nós passamos por valor, o que significa que fizemos uma cópia. É um tipo de referência, o que significa que copiamos a referência, não a string em si. Agora, temos duas referências que apontam para o mesmo valor na memória.

Dentro do chamado

Aqui está o topo do DoSomethingmétodo:

void DoSomething(string strLocal)

Nenhuma refpalavra-chave, portanto, strLocale strMainsão duas referências diferentes apontando para o mesmo valor. Se reatribuirmos strLocal...

strLocal = "local";   

... não alteramos o valor armazenado; pegamos a referência chamada strLocale apontamos para uma nova string. O que acontece strMainquando fazemos isso? Nada. Ainda está apontando para a corda antiga.

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

Imutabilidade

Vamos mudar o cenário por um segundo. Imagine que não estamos trabalhando com strings, mas com algum tipo de referência mutável, como uma classe que você criou.

class MutableThing
{
    public int ChangeMe { get; set; }
}

Se você seguir a referência objLocalao objeto para o qual ele aponta, poderá alterar suas propriedades:

void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

Ainda existe apenas uma MutableThingna memória, e a referência copiada e a referência original ainda apontam para ela. As propriedades do MutableThingpróprio foram alteradas :

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

Ah, mas as cordas são imutáveis! Não há ChangeMepropriedade para definir. Você não pode fazer strLocal[3] = 'H'em C # como faria com uma charmatriz de estilo C ; você precisa construir uma sequência totalmente nova. A única maneira de mudar strLocalé apontar a referência para outra string, e isso significa que nada que você faça strLocalpode afetarstrMain . O valor é imutável e a referência é uma cópia.

Passando uma referência por referência

Para provar que há uma diferença, eis o que acontece quando você passa uma referência por referência:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

Desta vez, a string Mainrealmente muda porque você passou a referência sem copiá-la na pilha.

Portanto, mesmo que as strings sejam tipos de referência, transmiti-las por valor significa que tudo o que acontece no receptor não afetará a string no chamador. Mas, como são tipos de referência, você não precisa copiar toda a cadeia de caracteres da memória quando quiser distribuí-la.

Mais recursos:

Justin Morgan
fonte
3
@TheLight - Desculpe, mas você está incorreto aqui quando diz: "Um tipo de referência é passado por referência por padrão". Por padrão, todos os parâmetros são passados ​​por valor, mas com tipos de referência, isso significa que a referência é passada por valor. Você está combinando tipos de referência com parâmetros de referência, o que é compreensível porque é uma distinção muito confusa. Consulte a seção Tipos de referência de passagem por valor aqui. Seu artigo vinculado está bastante correto, mas na verdade apoia meu argumento.
23813 Justin Morgan
1
@JustinMorgan Não para trazer um tópico de comentário morto, mas acho que o comentário de TheLight faz sentido se você pensar em C. Em C, dados são apenas um bloco de memória. Uma referência é um ponteiro para esse bloco de memória. Se você passar todo o bloco de memória para uma função, isso é chamado de "passagem por valor". Se você passa o ponteiro, chama-se "passagem por referência". No C #, não há noção de passar todo o bloco de memória, então eles redefiniram "passagem por valor" para significar passar o ponteiro para dentro. Isso parece errado, mas um ponteiro também é apenas um bloco de memória! Para mim, a terminologia é bastante arbitrária
rliu
@roliu - O problema é que não estamos trabalhando em C, e o C # é extremamente diferente, apesar do nome e sintaxe semelhantes. Por um lado, referências não são as mesmas que indicadores , e pensar nelas dessa maneira pode levar a armadilhas. O maior problema, porém, é que "passagem por referência" tem um significado muito específico em C #, exigindo a refpalavra - chave. Para provar que passar por referência faz a diferença, veja esta demonstração: rextester.com/WKBG5978
Justin Morgan
1
@ JustinMorgan Eu concordo que misturar terminologia C e C # é ruim, mas, embora tenha gostado do post de lippert, não concordo que pensar em referências como ponteiros embaça particularmente qualquer coisa aqui. A postagem do blog descreve como pensar em uma referência como um ponteiro lhe dá muito poder. Estou ciente de que a refpalavra-chave tem utilidade. Estava apenas tentando explicar por que alguém poderia pensar em passar um tipo de referência por valor em C # parece a noção "tradicional" (isto é, C) de passar por referência (e passar um tipo de referência por referência em C # parece mais passar uma referência a uma referência por valor).
rliu
2
Você está correto, mas acho que @roliu foi referência como uma função, como Foo(string bar)poderia ser pensado como Foo(char* bar)passo que Foo(ref string bar)seria Foo(char** bar)(ou Foo(char*& bar)ou Foo(string& bar)em C ++). Claro, não é como você deve pensar nisso todos os dias, mas na verdade me ajudou a finalmente entender o que está acontecendo sob o capô.
Cole Johnson
23

Strings em C # são objetos de referência imutáveis. Isso significa que as referências a eles são passadas (por valor) e, depois que uma string é criada, você não pode modificá-la. Os métodos que produzem versões modificadas da sequência (substrings, versões aparadas etc.) criam cópias modificadas da sequência original.

dasblinkenlight
fonte
10

Strings são casos especiais. Cada instância é imutável. Quando você altera o valor de uma sequência, está alocando uma nova sequência na memória.

Portanto, apenas a referência é passada para sua função, mas quando a string é editada, ela se torna uma nova instância e não modifica a instância antiga.

Enigmatividade
fonte
4
Strings não são um caso especial nesse aspecto. É muito fácil criar objetos imutáveis ​​que possam ter a mesma semântica. (Ou seja, uma instância de um tipo que não expõe um método para transformar isso ...)
Strings são casos especiais - são tipos de referência efetivamente imutáveis ​​que parecem mutáveis, pois se comportam como tipos de valor.
Enigmatividade
1
@ Enigmativity Por essa lógica, então Uri(class) e Guid(struct) também são casos especiais. Não vejo como System.Stringage como um "tipo de valor", mais do que outros tipos imutáveis ​​... de origem de classe ou estrutura.
3
@pst - Strings possuem semântica de criação especial - diferente de Uri& Guid- você pode apenas atribuir um valor literal de string a uma variável de string. A string parece ser mutável, como um intser reatribuído, mas está criando um objeto implicitamente - sem newpalavra-chave.
Enigmatividade
3
String é um caso especial, mas que não tem relevância para esta questão. Tipo de valor, tipo de referência, qualquer que seja o tipo, todos agirão da mesma maneira nesta questão.
Kirk Broadhurst