Quando usar ref e quando não é necessário em C #

104

Tenho um objeto que é meu estado na memória do programa e também algumas outras funções de trabalho para as quais passo o objeto para modificar o estado. Tenho passado por ref para as funções de trabalhador. No entanto, me deparei com a seguinte função.

byte[] received_s = new byte[2048];
IPEndPoint tmpIpEndPoint = new IPEndPoint(IPAddress.Any, UdpPort_msg);
EndPoint remoteEP = (tmpIpEndPoint);

int sz = soUdp_msg.ReceiveFrom(received_s, ref remoteEP); 

Isso me confunde porque received_se remoteEPestão retornando coisas da função. Por que remoteEPprecisa de um refe received_snão precisa ?

Eu também sou um programador AC, então estou tendo problemas para tirar dicas da minha cabeça.

Edit: Parece que os objetos em C # são ponteiros para o objeto subjacente. Portanto, quando você passa um objeto para uma função, pode modificar o conteúdo do objeto por meio do ponteiro e a única coisa passada para a função é o ponteiro para o objeto, de forma que o próprio objeto não seja copiado. Use ref ou out se quiser ser capaz de alternar ou criar um novo objeto na função que é como um ponteiro duplo.

Rex Logan
fonte

Respostas:

217

Resposta curta: leia meu artigo sobre passagem de argumento .

Resposta longa: quando um parâmetro de tipo de referência é passado por valor, apenas a referência é passada, não uma cópia do objeto. É como passar um ponteiro (por valor) em C ou C ++. As alterações no valor do próprio parâmetro não serão vistas pelo chamador, mas as alterações no objeto para o qual a referência aponta serão vistas.

Quando um parâmetro (de qualquer tipo) é passado por referência, significa que todas as alterações no parâmetro são vistas pelo chamador - alterações no parâmetro são alterações na variável.

O artigo explica tudo isso com mais detalhes, é claro :)

Resposta útil: você quase nunca precisa usar ref / out . É basicamente uma maneira de obter outro valor de retorno e geralmente deve ser evitado precisamente porque significa que o método provavelmente está tentando fazer muito. Nem sempre é esse o caso ( TryParseetc são os exemplos canônicos de uso razoável de out), mas usar ref / out deve ser uma raridade relativa.

Jon Skeet
fonte
38
Acho que você confundiu sua resposta curta com a resposta longa; esse é um grande artigo!
Programador Outlaw
23
@Outlaw: Sim, mas a resposta curta em si, a diretiva para ler o artigo, tem apenas 6 palavras :)
Jon Skeet
27
Uma referência! :)
gonzobrains
5
@Liam Usar ref como você faz pode parecer mais explícito para você, mas na verdade pode ser confuso para outros programadores (aqueles que sabem o que essa palavra-chave faz de qualquer maneira), porque você está essencialmente dizendo aos chamadores em potencial: "Eu posso modificar a variável que você usado no método de chamada, ou seja, reatribuí-lo a um objeto diferente (ou mesmo a nulo, se possível), portanto, não se apegue a ele, ou certifique-se de validá-lo quando eu terminar ". Isso é muito forte e completamente diferente de "este objeto pode ser modificado", que é sempre o caso sempre que você passa uma referência de objeto como parâmetro.
mbargiel de
1
@ M.Mimpen: C # 7 permitirá (com sorte)if (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet
26

Pense em um parâmetro não ref como sendo um ponteiro e um parâmetro ref como um ponteiro duplo. Isso me ajudou muito.

Você quase nunca deve passar valores por ref. Suspeito que, se não fosse por questões de interoperabilidade, a equipe do .Net nunca o teria incluído na especificação original. A maneira OO de lidar com a maioria dos problemas que os parâmetros ref resolvem é:

Para vários valores de retorno

  • Crie estruturas que representam os vários valores de retorno

Para primitivas que mudam em um método como resultado da chamada do método (o método tem efeitos colaterais nos parâmetros primitivos)

  • Implemente o método em um objeto como um método de instância e manipule o estado do objeto (não os parâmetros) como parte da chamada do método
  • Use a solução de múltiplos valores de retorno e mescle os valores de retorno ao seu estado
  • Crie um objeto que contenha o estado que pode ser manipulado por um método e passe esse objeto como parâmetro, e não os próprios primitivos.
Michael Meadows
fonte
8
Bom Deus. Eu teria que ler isso 20x para entender. Parece-me muito trabalho extra apenas para fazer algo simples.
PositiveGuy
O .NET framework não segue sua regra nº 1. ('Para vários valores de retorno, crie estruturas'). Veja, por exemplo IPAddress.TryParse(string, out IPAddress).
Swen Kooij
@SwenKooij Você está certo. Estou presumindo que na maioria dos lugares onde eles usam parâmetros ou (a) eles estão criando uma API do Win32 ou (b) isso foi no início e os programadores de C ++ estavam tomando decisões.
Michael Meadows
@SwenKooij Sei que esta resposta está muitos anos atrasada, mas atrasou. Estamos acostumados com o TryParse, mas isso não significa que seja bom. Teria sido melhor se, em vez if (int.TryParse("123", out var theInt) { /* use theInt */ }disso, o tivéssemos. var candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }É mais código, mas é muito mais consistente com o design da linguagem C #.
Michael Meadows
9

Você provavelmente poderia escrever um aplicativo C # inteiro e nunca passar nenhum objeto / estrutura por ref.

Tive um professor que me disse isso:

O único lugar onde você usaria refs é onde você:

  1. Deseja passar um objeto grande (ou seja, os objetos / estrutura tem objetos / estruturas dentro dele para vários níveis) e copiá-lo seria caro e
  2. Você está chamando um Framework, API do Windows ou outra API que requer isso.

Não faça isso só porque você pode. Você pode ser mordido na bunda por alguns insetos desagradáveis ​​se começar a alterar os valores em um parâmetro e não estiver prestando atenção.

Concordo com seu conselho e, em meus mais de cinco anos desde a escola, nunca tive necessidade disso fora de chamar o Framework ou a API do Windows.

Chris
fonte
3
Se você planeja implementar "Troca", passar por ref pode ser útil.
Brian
@Chris terá algum problema se eu usar a palavra-chave ref para objetos pequenos?
ManirajSS de
@TechnikEmpire "Porém, as alterações no objeto dentro do escopo da função chamada não são refletidas de volta para o chamador." Isso esta errado. Se eu passar uma pessoa para SetName(person, "John Doe"), a propriedade do nome mudará e essa mudança será refletida para o chamador.
M. Mimpen de
@ M.Mimpen Comentário excluído. Eu mal tinha aprendido C # naquele ponto e estava claramente falando alto. Obrigado por chamar minha atenção para isso.
@Chris - Tenho certeza de que passar um objeto "grande" não é caro. Se você apenas passar por valor, ainda estará passando apenas um único ponteiro, certo?
Ian
3

Visto que received_s é um array, você está passando um ponteiro para aquele array. A função manipula os dados existentes no local, sem alterar o local ou o ponteiro subjacente. A palavra-chave ref significa que você está passando o ponteiro real para o local e atualizando esse ponteiro na função externa, portanto, o valor na função externa mudará.

Por exemplo, a matriz de bytes é um ponteiro para a mesma memória antes e depois, a memória acabou de ser atualizada.

A referência do Endpoint está, na verdade, atualizando o ponteiro para o Endpoint na função externa para uma nova instância gerada dentro da função.

Chris Hynes
fonte
3

Pense em uma referência como significando que você está passando um ponteiro por referência. Não usar um ref significa que você está passando um ponteiro por valor.

Melhor ainda, ignore o que acabei de dizer (provavelmente é enganoso, especialmente com tipos de valor) e leia esta página do MSDN .

Brian
fonte
Na verdade, não é verdade. Pelo menos a segunda parte. Qualquer tipo de referência sempre será passado por referência, quer você use ref ou não.
Erik Funkenbusch
Na verdade, pensando melhor, isso não saiu certo. A referência é realmente passada por valor sem o tipo ref. Ou seja, alterar os valores apontados pela referência altera os dados originais, mas alterar a própria referência não altera a referência original.
Erik Funkenbusch
2
Um tipo de referência não ref não é passado por referência. Uma referência ao tipo de referência é passada por valor. Mas se você quiser pensar em uma referência como um indicador para algo que está sendo referenciado, o que eu disse faz sentido (mas pensar dessa forma pode ser enganoso). Daí minha advertência.
Brian
O link está morto - tente este em vez disso - docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
GIVE-ME-CHICKEN
0

meu entendimento é que todos os objetos derivados da classe Object são passados ​​como ponteiros, enquanto os tipos comuns (int, struct) não são passados ​​como ponteiros e requerem ref. Não tenho certeza sobre string (em última análise, é derivado da classe Object?)

Lucas
fonte
Isso poderia exigir algum esclarecimento. Qual é a diferença entre modelos de valor e referência. Resp, por que isso é relevante para usar a palavra-chave ref em um parâmetro?
oɔɯǝɹ
Em .net, tudo, exceto ponteiros, parâmetros de tipo e interfaces, é derivado de Object. É importante entender que os tipos "comuns" (corretamente chamados de "tipos de valor") também herdam do objeto. Você está certo a partir disso: os tipos de valor (por padrão) são passados ​​por valor, enquanto os tipos de referência são passados ​​por referência. Se você quiser modificar um tipo de valor, em vez de retornar um novo (tratá-lo como um tipo de referência dentro de um método), você terá que usar a palavra-chave ref nele. Mas isso é um estilo ruim e você simplesmente não deve fazer isso a menos que esteja absolutamente certo de que precisa :)
buddybubble
1
para responder à sua pergunta: string é derivado de objeto. É um tipo de referência que se comporta como um tipo de valor (por questões de desempenho e lógicas)
buddybubble
0

Embora eu concorde com a resposta geral de Jon Skeet e com algumas das outras respostas, há um caso de uso para usar ref, que é para aumentar as otimizações de desempenho. Foi observado durante a criação de perfil de desempenho que definir o valor de retorno de um método tem pequenas implicações de desempenho, ao passo que usar refcomo um argumento pelo qual o valor de retorno é preenchido nesse parâmetro resulta na remoção desse pequeno gargalo.

Isso é realmente útil apenas onde os esforços de otimização são levados a níveis extremos, sacrificando a legibilidade e talvez a testabilidade e a manutenção para economizar milissegundos ou talvez frações de milissegundos.

Jon Davis
fonte
-1

Regra do ponto zero primeiro, primitivos são passados ​​por valor (pilha) e não primitivos por referência (pilha) no contexto de TIPOS envolvidos.

Os parâmetros envolvidos são passados ​​por valor por padrão. Bom post que explica as coisas em detalhes. http://yoda.arachsys.com/csharp/parameters.html

Student myStudent = new Student {Name="A",RollNo=1};

ChangeName(myStudent);

static void ChangeName(Student s1)
{
  s1.Name = "Z"; // myStudent.Name will also change from A to Z
                // {AS s1 and myStudent both refers to same Heap(Memory)
                //Student being the non-Primitive type
}

ChangeNameVersion2(ref myStudent);
static void ChangeNameVersion2(ref Student s1)
{
  s1.Name = "Z"; // Not any difference {same as **ChangeName**}
}

static void ChangeNameVersion3(ref Student s1)
{
    s1 = new Student{Name="Champ"};

    // reference(myStudent) will also point toward this new Object having new memory
    // previous mystudent memory will be released as it is not pointed by any object
}

Podemos dizer (com aviso) Tipos não primitivos nada mais são do que Ponteiros E quando os passarmos por ref podemos dizer que estamos passando Ponteiro Duplo

Surender Singh Malik
fonte
Todos os parâmetros são passados ​​por valor, por padrão, em C #. Qualquer parâmetro pode ser passado por referência. Para tipos de referência, o valor passado (por referência ou por valor) é ele próprio uma referência. Isso é totalmente independente de como é passado.
Servy
Concordo com @Servy! "Quando ouvimos as palavras" referência "ou" valor "usadas, devemos ter muito claro se queremos dizer que um parâmetro é uma referência ou um parâmetro de valor, ou se queremos dizer que o tipo envolvido é uma referência ou tipo de valor 'Pequeno A confusão está do meu lado, QUANDO eu disse a regra do ponto zero primeiro, os primitivos são passados ​​por valor (pilha) e os não primitivos por referência (heap). Eu quis dizer a partir de TIPOS envolvidos, não o parâmetro! Quando falamos sobre parâmetros, você está correto , Todos os parâmetros são passados ​​por valor, por padrão em C #.
Surender Singh Malik
Dizer que um parâmetro é uma referência ou parâmetro de valor não é um termo padrão e é realmente ambíguo quanto ao que você quer dizer. O tipo do parâmetro pode ser um tipo de referência ou um tipo de valor. Qualquer parâmetro pode ser passado por valor ou por referência. Esses são conceitos ortogonais para qualquer parâmetro fornecido. Sua postagem descreve isso incorretamente.
Servy
1
Você passa parâmetros por referência ou por valor. Os termos "por referência" e "por valor" não são usados ​​para descrever se um tipo é um tipo de valor ou um tipo de referência.
Servy
1
Não, é não uma questão de percepção. Quando você usa o termo incorreto para se referir a algo, essa afirmação se torna errada . É importante que as declarações corretas usem a terminologia correta para se referir aos conceitos.
Servy