Por que as instruções de atribuição retornam um valor?

126

Isso é permitido:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

Para meu entendimento, a atribuição s = ”Hello”;deve apenas “Hello”ser atribuída s, mas a operação não deve retornar nenhum valor. Se isso fosse verdade, ((s = "Hello") != null)produziria um erro, pois nullseria comparado a nada.

Qual é o raciocínio por trás da permissão de declarações de atribuição para retornar um valor?

user437291
fonte
44
Para referência, esse comportamento é definido na especificação do C # : "O resultado de uma expressão de atribuição simples é o valor atribuído ao operando esquerdo. O resultado tem o mesmo tipo que o operando esquerdo e é sempre classificado como um valor".
Michael Petrotta
1
@ Skurmedel Não sou o derrotador, concordo com você. Mas talvez porque se pensasse ser uma pergunta retórica?
precisa saber é
Na atribuição pascal / delphi: = não retorna nada. Eu odeio isso.
Andrey
20
Padrão para idiomas na ramificação C. Atribuições são expressões.
Hans Passant
"atribuição ... só deve fazer com que" Hello "seja atribuído a s, mas a operação não deve retornar nenhum valor" - Por quê? "Qual é o raciocínio por trás da permissão de declarações de atribuição para retornar um valor?" - Porque é útil, como demonstram seus próprios exemplos. (Bem, seu segundo exemplo é bobo, mas while ((s = GetWord()) != null) Process(s);não é).
Jim Balter

Respostas:

160

Para meu entendimento, tarefa s = "Hello"; deve fazer com que "Hello" seja atribuído a s, mas a operação não deve retornar nenhum valor.

Sua compreensão é 100% incorreta. Você pode explicar por que acredita nessa coisa falsa?

Qual é o raciocínio por trás da permissão de declarações de atribuição para retornar um valor?

Primeiro, as instruções de atribuição não produzem um valor. Expressões de atribuição produzem um valor. Uma expressão de atribuição é uma declaração legal; há apenas algumas expressões que são declarações legais em C #: aguarda uma expressão; expressões de construção, incremento, decremento, chamada e atribuição de instância podem ser usadas onde uma declaração é esperada.

Existe apenas um tipo de expressão em C # que não produz algum tipo de valor, a saber, uma invocação de algo digitado como retorno nulo. (Ou, equivalentemente, uma espera de uma tarefa sem valor de resultado associado.) Todos os outros tipos de expressão produzem um valor ou variável ou referência ou acesso a propriedades ou acesso a eventos e assim por diante.

Observe que todas as expressões legais como declarações são úteis para seus efeitos colaterais . Essa é a principal ideia aqui, e acho que talvez seja a causa da sua intuição de que atribuições devem ser declarações e não expressões. Idealmente, teríamos exatamente um efeito colateral por declaração e nenhum efeito colateral em uma expressão. Ele é um pouco estranho que o código-efectuar lado pode ser usado num contexto de expressão de todo.

O raciocínio por trás da permissão desse recurso é porque (1) ele é frequentemente conveniente e (2) é idiomático em idiomas semelhantes a C.

Pode-se notar que a pergunta foi feita: por que isso é idiomático em linguagens do tipo C?

Infelizmente, Dennis Ritchie não está mais disponível para perguntar, mas meu palpite é que uma tarefa quase sempre deixa para trás o valor que acabou de ser atribuído em um registro. C é um tipo de linguagem muito "próximo à máquina". Parece plausível e de acordo com o design de C que haja um recurso de linguagem que basicamente significa "continuar usando o valor que acabei de atribuir". É muito fácil escrever um gerador de código para esse recurso; você apenas continua usando o registro que armazenou o valor que foi atribuído.

Eric Lippert
fonte
1
Não estava ciente de que qualquer coisa que retorna um valor (construção ainda instância é considerada uma expressão)
user437291
9
@ user437291: Se a construção da instância não era uma expressão, não era possível fazer nada com o objeto construído - não era possível atribuí-lo a algo, não era possível passá-lo para um método e não era possível chamar nenhum método nele. Seria bastante inútil, não seria?
Timwi
1
Alterar uma variável é realmente "apenas" um efeito colateral do operador de atribuição, que, criando uma expressão, gera um valor como objetivo principal.
Mk12
2
"Sua compreensão é 100% incorreta." - Embora o uso do inglês pelo OP não seja o melhor, seu "entendimento" é sobre o que deve ser o caso, tornando-o uma opinião, portanto não é o tipo de coisa que pode ser "100% incorreta" . Alguns designers de idiomas concordam com o "deveria" do OP e fazem com que as atribuições não tenham valor ou, de outro modo, as proíbem de serem subexpressões. Eu acho que isso é uma reação exagerada ao =/ ==typo, que é facilmente resolvido com a proibição de usar o valor de, a =menos que esteja entre parênteses. por exemplo, if ((x = y))ou if ((x = y) == true)é permitido, mas if (x = y)não é.
21715 Jim Balter
"Não sabia que algo que retorna um valor ... é considerado uma expressão" - Hmm ... tudo o que retorna um valor é considerado uma expressão - os dois são praticamente sinônimos.
Jim Balter
44

Você não forneceu a resposta? É para ativar exatamente os tipos de construções que você mencionou.

Um caso comum em que essa propriedade do operador de atribuição é usada é a leitura de linhas de um arquivo ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Timwi
fonte
1
+1 Esse deve ser um dos usos mais práticos dessa construção #
Mike Burton
11
Eu realmente gosto de inicializadores de propriedades realmente simples, mas você deve ter cuidado e definir sua linha como "simples" com baixa legibilidade: return _myBackingField ?? (_myBackingField = CalculateBackingField());muito menos palha do que verificar se há nulos e atribuições.
Tom Mayfield
34

Meu uso favorito de expressões de atribuição é para propriedades inicializadas preguiçosamente.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Nathan Baulch
fonte
5
É por isso que tantas pessoas sugeriram a ??=sintaxe.
configurator
27

Por um lado, permite encadear suas atribuições, como no seu exemplo:

a = b = c = 16;

Por outro lado, permite atribuir e verificar um resultado em uma única expressão:

while ((s = foo.getSomeString()) != null) { /* ... */ }

Ambos são possíveis motivos duvidosos, mas definitivamente existem pessoas que gostam dessas construções.

Michael Burr
fonte
5
O encadeamento +1 não é um recurso crítico, mas é bom ver que "simplesmente funciona" quando muitas coisas não funcionam.
Mike Burton
+1 para encadeamento também; Eu sempre pensei nisso como um caso especial tratado pela linguagem e não pelo efeito colateral natural de "expressões retornam valores".
Caleb Bell
2
Também permite que você use a atribuição em devoluções:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz 12/13
14

Além dos motivos já mencionados (encadeamento de atribuição, configuração e teste dentro de loops while, etc.), para usar corretamente a usinginstrução, você precisa deste recurso:

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

O MSDN desencoraja a declaração do objeto descartável fora da instrução using, pois ele permanecerá no escopo mesmo depois de descartado (consulte o artigo do MSDN que eu link).

Martin Törnwall
fonte
4
Eu não acho que isso seja realmente um encadeamento de tarefas em si.
Kenny
1
@kenny: Uh ... Não, mas eu também nunca afirmei que era. Como eu disse, além dos motivos já mencionados - que incluem o encadeamento da atribuição - a instrução using seria muito mais difícil de usar se não fosse pelo fato de o operador da atribuição retornar o resultado da operação de atribuição. Isso não está realmente relacionado ao encadeamento de tarefas.
Martin Törnwall 27/09/10
1
Mas, como Eric observou acima, "as declarações de atribuição não retornam um valor". Na verdade, isso é apenas sintaxe de linguagem da instrução using, prova disso é que não é possível usar uma "expressão de atribuição" em uma instrução using.
Kenny
@kenny: Desculpas, minha terminologia estava claramente errada. Percebo que a instrução using pode ser implementada sem que o idioma tenha suporte geral para expressões de atribuição retornando um valor. Isso seria, a meu ver, terrivelmente inconsistente.
Martin Törnwall 28/09/10
2
@kenny: Na verdade, pode-se usar uma declaração de definição e atribuição ou qualquer expressão em using. Todos estes são legais: using (X x = new X()), using (x = new X()), using (x). No entanto, neste exemplo, o conteúdo da instrução using é uma sintaxe especial que não depende de atribuição que retorna um valor - Font font3 = new Font("Arial", 10.0f)não é uma expressão e não é válida em nenhum lugar que espere expressões.
configurator
10

Eu gostaria de elaborar um ponto específico que Eric Lippert fez em sua resposta e colocar os holofotes em uma ocasião específica que nunca foi abordada por mais ninguém. Eric disse:

[...] uma tarefa quase sempre deixa para trás o valor que acabou de ser atribuído em um registro.

Eu gostaria de dizer que a atribuição sempre deixará para trás o valor que tentamos atribuir ao nosso operando esquerdo. Não apenas "quase sempre". Mas não sei porque não encontrei esse problema comentado na documentação. Teoricamente, pode ser um procedimento implementado muito eficaz para "deixar para trás" e não reavaliar o operando esquerdo, mas é eficiente?

'Eficiente' sim para todos os exemplos construídos até agora nas respostas deste tópico. Mas eficiente no caso de propriedades e indexadores que usam acessadores get e set? De modo nenhum. Considere este código:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

Aqui temos uma propriedade que nem sequer é um invólucro para uma variável privada. Sempre que for chamado, ele retornará verdadeiro, sempre que alguém tentar definir seu valor, nada fará. Assim, sempre que essa propriedade for avaliada, ele deve ser verdadeiro. Vamos ver o que acontece:

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

Adivinha o que imprime? Imprime Unexpected!!. Como se vê, o acessador definido é realmente chamado, o que não faz nada. Mas, posteriormente, o acessador get nunca é chamado. A atribuição simplesmente deixa para trás o falsevalor que tentamos atribuir à nossa propriedade. E esse falsevalor é o que a instrução if avalia.

Terminarei com um exemplo do mundo real que me levou a pesquisar esse problema. Criei um indexador que era um invólucro conveniente para uma coleção ( List<string>) que uma classe minha tinha como variável privada.

O parâmetro enviado ao indexador era uma sequência que deveria ser tratada como um valor em minha coleção. O acessador get simplesmente retornaria true ou false se esse valor existisse na lista ou não. Portanto, o acessador get era outra maneira de usar oList<T>.Contains método.

Se o acessador de conjunto do indexador fosse chamado com uma string como argumento e o operando certo fosse um bool true, ele adicionaria esse parâmetro à lista. Mas se o mesmo parâmetro fosse enviado ao acessador e o operando certo fosse um bool false, ele excluiria o elemento da lista. Assim, o acessador definido foi usado como uma alternativa conveniente para ambos List<T>.AddeList<T>.Remove .

Eu pensei que tinha uma "API" limpa e compacta, envolvendo a lista com minha própria lógica implementada como gateway. Com a ajuda de um indexador sozinho, eu poderia fazer muitas coisas com algumas combinações de teclas. Por exemplo, como posso tentar adicionar um valor à minha lista e verificar se ela está lá? Eu pensei que esta era a única linha de código necessária:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

Mas, como meu exemplo anterior mostrou, o acessador get, que deveria ver se o valor realmente está na lista, nem foi chamado. O truevalor sempre foi deixado para trás, destruindo efetivamente qualquer lógica que eu havia implementado no meu acessador.

Martin Andersson
fonte
Resposta muito interessante. E isso me fez pensar positivamente sobre o desenvolvimento orientado a testes.
Elliot
1
Embora isso seja interessante, é um comentário sobre a resposta de Eric, não uma resposta à pergunta do OP.
Jim Balter
Eu não esperaria que uma tentativa de expressão de atribuição obtivesse primeiro o valor da propriedade de destino. Se a variável for mutável e os tipos corresponderem, por que importaria qual era o valor antigo? Basta definir o novo valor. Eu não sou fã de tarefas nos testes IF, no entanto. Ele está retornando true porque a atribuição foi bem-sucedida ou é algum valor, por exemplo, um número inteiro positivo que é coagido a true ou porque você está atribuindo um booleano? Independentemente da implementação, a lógica é ambígua.
Davos
6

Se a atribuição não retornasse um valor, a linha a = b = c = 16também não funcionaria.

Também poder escrever coisas como isso while ((s = readLine()) != null)pode ser útil às vezes.

Portanto, a razão por trás de permitir que a atribuição retorne o valor atribuído é permitir que você faça essas coisas.

sepp2k
fonte
Ninguém mencionou as desvantagens - eu vim aqui perguntando por que as atribuições não podiam retornar nulas, para que a atribuição comum versus o erro de comparação 'if (something = 1) {}' falhasse na compilação, em vez de sempre retornar true. Agora vejo que isso criaria ... outros problemas!
11134 Jon
1
@ O erro pode ser facilmente evitado com a proibição ou aviso de =ocorrência de uma expressão que não está entre parênteses (sem contar os parênteses da instrução if / while). O gcc dá esses avisos e, com isso, basicamente elimina essa classe de bugs dos programas C / C ++ que são compilados. É uma pena que outros escritores de compiladores tenham prestado tão pouca atenção a isso e a várias outras boas idéias no gcc.
21715 Jim Balter
4

Acho que você está entendendo mal como o analisador interpretará essa sintaxe. A tarefa será avaliada primeiro e o resultado será comparado a NULL, ou seja, a instrução é equivalente a:

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

Como outros já apontaram, o resultado de uma atribuição é o valor atribuído. Acho difícil imaginar a vantagem de ter

((s = "Hello") != null)

e

s = "Hello";
s != null;

não ser equivalente ...

Dan J
fonte
Enquanto você está certo, do ponto de vista prático, de que suas duas linhas são equivalentes, sua afirmação de que a atribuição não está retornando um valor não é tecnicamente verdadeira. Meu entendimento é que o resultado de uma atribuição é um valor (de acordo com a especificação C #) e, nesse sentido, não é diferente de dizer o operador de adição em uma expressão como 1 + 2 + 3
Steve Ellinger
3

Eu acho que o principal motivo é a semelhança (intencional) com C ++ e C. Fazer com que o operador de atribuição (e muitas outras construções de linguagem) se comporte como seus colegas em C ++ apenas segue o princípio da menor surpresa e qualquer programador vindo de outro a linguagem colchete pode usá-los sem gastar muito em pensar. Ser fácil de escolher para programadores de C ++ foi um dos principais objetivos de design do C #.

tdammers
fonte
2

Pelas duas razões que você incluiu em sua postagem
1), para que você possa fazer a = b = c = 16
2) para testar se uma tarefa foi bem-sucedida if ((s = openSomeHandle()) != null)

JamesMLV
fonte
1

O fato de que 'a ++' ou 'printf ("foo")' pode ser útil como uma declaração independente ou como parte de uma expressão maior significa que C deve permitir a possibilidade de que os resultados da expressão possam ou não ser usava. Dado isso, existe uma noção geral de que expressões que podem "retornar" utilmente um valor também podem fazê-lo. O encadeamento de atribuição pode ser um pouco "interessante" em C, e ainda mais interessante em C ++, se todas as variáveis ​​em questão não tiverem exatamente o mesmo tipo. Provavelmente, é melhor evitar esses usos.

supercat
fonte
1

Outro ótimo exemplo de caso de uso, eu uso isso o tempo todo:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once
jazzcat
fonte
0

Uma vantagem extra que não vejo nas respostas aqui é que a sintaxe da atribuição é baseada em aritmética.

Agora x = y = b = c = 2 + 3significa algo diferente em aritmética do que uma linguagem no estilo C; na aritmética sua afirmação, que afirmam que x é igual a y etc. e em uma linguagem de estilo C é uma instrução que as marcas x igual a y etc. depois que ele é executado.

Dito isto, ainda existe relação suficiente entre a aritmética e o código, de modo que não faz sentido desaprovar o que é natural na aritmética, a menos que haja uma boa razão. (A outra coisa que as linguagens no estilo C tiraram do uso do símbolo de igual é o uso de == para comparação de igualdade. Aqui, porém, porque o mais à direita == retorna um valor que esse tipo de encadeamento seria impossível.)

Jon Hanna
fonte
@ JimBalter Gostaria de saber se, em cinco anos, você poderá realmente argumentar contra.
21715 Jon
@ JimBalter não, seu segundo comentário é na verdade um contra-argumento e um bom argumento. Minha resposta foi porque seu primeiro comentário não foi. Ainda assim, quando a aprendizagem aritmética aprendemos a = b = cmeios que ae be csão a mesma coisa. Ao aprender uma linguagem de estilo C aprendemos que depois a = b = c, ae bsão a mesma coisa que c. Certamente há uma diferença na semântica, como minha própria resposta diz, mas ainda quando criança aprendi a programar em uma linguagem usada =para tarefas, mas não permitia que a = b = cisso parecesse irracional para mim, no entanto ...
Jon Hanna
... inevitável, porque essa linguagem também era usada =para comparação de igualdade; portanto, isso a = b = cteria que significar o que a = b == csignifica em linguagens no estilo C. Achei o encadeamento permitido em C muito mais intuitivo, porque eu poderia fazer uma analogia com a aritmética.
21915 Jon Jon Hanna
0

Gosto de usar o valor de retorno da atribuição quando preciso atualizar várias coisas e retornar se houve alguma alteração:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

Tenha cuidado, porém. Você pode pensar que pode reduzi-lo para isso:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

Mas isso realmente irá parar de avaliar as declarações ou depois que encontrar a primeira verdade. Nesse caso, isso significa que ele para de atribuir valores subseqüentes depois de atribuir o primeiro valor diferente.

Veja https://dotnetfiddle.net/e05Rh8 para brincar com isso

Luke Kubat
fonte