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

Isso é permitido:

int a, b, c; a = b = c = 16; string s = null; while ((s = "Hello") != null) ; 

Para meu entendimento, atribuição s = ”Hello”; só deve fazer com que “Hello” seja atribuído a s , mas a operação não deve retornar nenhum valor. Se isso fosse verdade, então ((s = "Hello") != null) produziria um erro, uma vez que null seria comparado a nada.

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

Para meu entendimento, atribuição s = “Hello”; só 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 porque você acredita nessa coisa falsa?

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

Em primeiro lugar, as instruções de atribuição não produzem um valor. As expressões de atribuição produzem um valor. Uma expressão de atribuição é uma declaração legal; Existem apenas algumas expressões que são declarações legais em C #: espera de uma expressão, construção de instância, incremento, decremento, invocação e atribuição de expressões 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, ou seja, uma invocação de algo que é typescript como retornando vazio. (Ou, equivalentemente, uma espera de uma tarefa sem nenhum valor de resultado associado.) Cada outro tipo de expressão produz um valor ou variável ou referência ou access a propriedade ou access a um evento, e assim por diante.

Observe que todas as expressões que são legais como declarações são úteis para seus efeitos colaterais . Esse é o principal insight aqui, e acho que talvez a causa de sua intuição de que as atribuições devam ser declarações e não expressões. Idealmente, teríamos exatamente um efeito colateral por instrução e nenhum efeito colateral em uma expressão. É um pouco estranho que o código de efeitos colaterais possa ser usado em um contexto de expressão.

O raciocínio por trás de permitir esse recurso é porque (1) é freqüentemente conveniente e (2) é idiomático em linguagens semelhantes a C.

Pode-se notar que a questão foi colocada: por que isso é idiomático em linguagens semelhantes a C?

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

Você não forneceu a resposta? É para permitir 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) // ... 

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

 private string _name; public string Name { get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); } } 

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 possivelmente motivos duvidosos, mas definitivamente há pessoas que gostam dessas construções.

Além das razões já mencionadas (encadeamento de atribuições, set-and-test dentro de loops while, etc), para usar corretamente a instrução using você precisa deste recurso:

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

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

Eu gostaria de elaborar um ponto específico que Eric Lippert fez em sua resposta e colocar os holofotes em uma ocasião particular que não tenha sido tocada por ninguém. Eric disse:

[…] uma atribuição quase sempre deixa para trás o valor que foi atribuído em um registro.

Eu gostaria de dizer que a tarefa sempre deixará para trás o valor que tentamos atribuir ao nosso operando esquerdo. Não apenas “quase sempre”. Mas eu não sei porque não encontrei esse problema comentado na documentação. Pode teoricamente 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 até agora construídos 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 é um wrapper para uma variável privada. Sempre que for chamado, ele retornará verdadeiro, sempre que alguém tentar estabelecer seu valor, ele não fará nada. Assim, sempre que esta propriedade é 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 é impresso? Imprime Unexpected!! . Acontece que o acessador definido é de fato chamado, o que não faz nada. Mas depois disso, o acessador get nunca é chamado. A tarefa simplesmente deixa para trás o valor false que tentamos atribuir à nossa propriedade. E esse valor false é o que a instrução if avalia.

Vou terminar com um exemplo do mundo real que me fez pesquisar esse problema. Eu fiz um indexador que era um wrapper conveniente para uma coleção ( List ) que uma class minha tinha como uma variável privada.

O parâmetro enviado para o indexador era uma string, 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. Assim, o acessador get era outra maneira de usar o método List.Contains .

Se o acessador set do indexador fosse chamado com uma string como um argumento e o operando da direita fosse um bool true , ele adicionaria esse parâmetro à lista. Mas se o mesmo parâmetro foi enviado para o acessador e o operando direito foi um bool false , ele iria, em vez disso, excluir o elemento da lista. Assim, o acessador set foi usado como uma alternativa conveniente para List.Add e List.Remove .

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

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

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

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

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

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

Eu acho que você está entendendo mal como o analisador vai interpretar essa syntax. A atribuição será avaliada primeiro , e o resultado será então comparado a NULL, ou seja, a declaração é equivalente a:

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

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

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

e

 s = "Hello"; s != null; 

não seja equivalente …

Eu acho que o principal motivo é a semelhança (intencional) com C ++ e C. Fazer com que o operador de assedment (e muitas outras construções de linguagem) se comportem como seus equivalentes de C ++ apenas segue o princípio de menor surpresa, e qualquer programador vindo de outro linguagem de colchetes pode usá-los sem gastar muito pensamento. Ser fácil escolher programadores em C ++ foi um dos principais objectives do projeto para C #.

Por dois motivos que você inclui em sua postagem
1) então você pode fazer a = b = c = 16
2) assim você pode testar se uma atribuição foi bem sucedida if ((s = openSomeHandle()) != null)

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, há uma noção geral de que as expressões que podem “retornar” um valor útil também podem fazê-lo. Encadeamento de atribuição pode ser ligeiramente “interessante” em C, e ainda mais interessante em C ++, se todas as variables ​​em questão não tiverem precisamente o mesmo tipo. Tais usos são provavelmente evitados.

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

Agora x = y = b = c = 2 + 3 significa algo diferente na aritmética do que uma linguagem no estilo C; na aritmética, é uma afirmação, afirmamos que x é igual a y etc. e, em uma linguagem de estilo C, é uma instrução que torna x igual a y, etc., após a execução.

Dito isso, ainda há relação suficiente entre a aritmética e o código que não faz sentido proibir o que é natural na aritmética, a menos que haja uma boa razão. (A outra coisa que as linguagens de estilo C tiraram do uso do símbolo equals é o uso de == para comparação de igualdade. Porém, como o valor mais à direita == retorna um valor, esse tipo de encadeamento seria impossível.)

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