Motivo para o operador de incremento (pós / pré) em Java ou C #

8

Recentemente, me deparei com perguntas sobre como prever a saída de código que usa fortemente operadores de pós / pré incremento em números inteiros. Sou programador C experiente, então me senti em casa, mas as pessoas fizeram declarações de que o Java apenas copiou cegamente o de C (ou C ++), e que esse é um recurso inútil no Java.

Dito isto, não consigo encontrar uma boa razão para isso (especialmente tendo em mente a diferença entre as formas pós / pré) em Java (ou C #), porque não é como se você manipulasse matrizes e as usasse como cadeias de caracteres em Java . Além disso, foi há muito tempo que eu analisei o bytecode pela última vez, então não sei se há uma INCoperação, mas não vejo nenhuma razão para isso.

for(int i = 0; i < n; i += 1)

poderia ser menos eficaz do que

for(int i = 0; i < n; i++)

Existe alguma parte específica da linguagem em que isso é realmente útil ou isso é apenas um recurso para trazer os programadores C para a cidade?

Nemanja Boric
fonte
2
"Gosling queria que o código escrito por alguém com experiência em C (não em Java) fosse executado por alguém acostumado a executar o PostScript no NeWS ..."
gnat
7
@ TimothyGroote Eu não acho que seja uma avaliação justa, já que 1) matrizes são mutáveis, Strings não; 2) você não pode atribuir uma String a um char [] ou vice-versa. Eles certamente são do tipo matriz, provavelmente implementados com matrizes, e é fácil o suficiente para converter de e para uma matriz, mas definitivamente não são a mesma coisa.
Doval
4
Muitas respostas boas aqui, para que eu não volte a afirmar o que outros já disseram, exceto para enfatizar que esses operadores são recursos horríveis. Eles são muito confusos; depois de mais de 25 anos, ainda me misturo pré e pós-semântica. Eles incentivam maus hábitos, como combinar a avaliação dos resultados com a produção de efeitos colaterais. Se esses recursos não estivessem em C / C ++ / Java / JavaScript / etc, eles não teriam sido inventados para C #.
precisa
5
Para responder diretamente às reivindicações feitas pelo seu pessoal: O C # não copiou cegamente nada de ninguém. Todos os recursos do C # foram extremamente cuidadosamente projetados. E esses não são recursos inúteis ; Eles têm um uso.
precisa
2
@ EricLippert: Eu acho que (espero) a maioria de nós pode concordar que o incremento pré / pós é péssimo, mas nesse contexto ... por que eles foram incluídos no C #? Pelo bem da familiaridade?
Phoshi

Respostas:

19

isso é apenas um recurso para trazer programadores C na cidade

Certamente é um recurso "trazer os programadores C (ou C ++) para a cidade", mas usar a palavra "just" aqui subestima o valor dessa semelhança.

  • facilita o código (ou pelo menos trechos de código) da porta de C (ou C ++) para Java ou C #

  • i++é menor do que digitar i += 1e x[i++]=0;é muito mais idiomático do quex[i]=0;i+=1;

E sim, o código que usa muito operadores de pós / pré-incremento pode ser difícil de ler e manter, mas para qualquer código em que esses operadores sejam mal utilizados, eu não esperaria um aumento drástico na qualidade do código, mesmo que o idioma não os fornecesse. Isso porque se você tiver desenvolvedores que não se importam com a manutenção, eles sempre encontrarão maneiras de escrever código difícil de entender.

Relacionado: Existe alguma diferença entre os operadores Java e C ++? A partir desta resposta, pode-se deduzir que qualquer comportamento bem definido do operador em C é semelhante em Java, e Java adiciona apenas algumas definições para precedência do operador em que C tem comportamento indefinido. Isso não parece coincidência, parece bem intencional.

Doc Brown
fonte
10
"Se você tem devs que não se importam com manutenção, eles vão sempre encontrar maneiras de escrever difícil de entender código" - muito bem dito
Lovis
Você concorda com a crença comum entre os programadores C de que você só tem tantas teclas C antes de morrer? Salvar esse pressionamento de tecla parece uma troca ruim para mim.
precisa
@EricLippert: Olá Eric, como um dos criadores de C #, por que você não nos esclarece e nos conta um pouco sobre sua motivação para manter os operadores em C # semelhantes a Java e C?
Doc Brown
@ DocBrown: Sua resposta se aplica muito bem ao C #. Deixei um comentário acima.
precisa
2
@ DocBrown: x[i++]=0é idiomático em C, mas não concordo que seja idiomático em C # e Java. Com que frequência você encontra essas frases, a menos que trabalhe no domínio de engenharia / matemática, o que, presumo, não consome mais de 1% do código Java / C # em estado selvagem?
Mladen Jablanović 27/03
6

Às vezes, uso o incremento do postfix nas instruções de retorno. Por exemplo, considere isso:

//Java
public class ArrayIterator implements Iterator<T> {
   private final T[] array;
   private int index = 0; 
   public ArrayIterator(T ... array) {
      this.array = array;
   }

   public T next {
      if (! hasNext()) throw new NoSuchElementException();
      return array[index++];   
   }
   ...
}

Você não pode fazer isso com index += 1. Portanto, pelo menos a versão do postfix pode salvar você de vez em quando em algumas linhas.

Dito isto, esses operadores não são realmente necessários e podem confundir as pessoas. Eu sei que a Scala decidiu contra esses operadores (você tem apenas += e -=, como sugeriu), e devido à abordagem mais "de alto nível", eu realmente não sinto falta deles por lá. Espero que o Java se desenvolva (mais lentamente) na mesma direção.

No entanto, o Java tem vários outros operadores que você não vê com muita frequência "na natureza" (você já usou ^=ou >>>=?), E ninguém se importa.

Landei
fonte
1
^=é razoavelmente comum. Você pode usá-lo para inverter sinalizadores de campo de bits, trocar dois números inteiros sem uma variável temporária e em alguns algoritmos de hash / cifra. Eu admito que nunca usei >>>=, no entanto. (Então, novamente, eu nunca usei >>>ou <<<em qualquer programa de produção ...)
Brian S
4

Eu construí muitos idiomas ao longo dos anos, principalmente para fins especiais. O que acontece é que você tem um grande público e todos eles têm expectativas. Ao considerar cada recurso, você deve avaliar o benefício do recurso com o custo de possivelmente violar as expectativas.

Uma que eu pessoalmente tive que recuar foi a divisão de números inteiros, que normalmente trunca para baixo, certo? Tipo 3 / 2 = 1.5 = 1né? Bem, nos primórdios de Fortran, alguém decidiu -3 / 2 = -1.5 = -1(não -2). Em outras palavras, se o dividendo é negativo e o divisor é positivo, deve truncar -se . Observe o que isso implica - nesse caso, o restante é negativo . Isso viola a suposição usual de que o restante = módulo. Se você estiver criando gráficos, com coordenadas inteiras, isso pode levar a todos os tipos de problemas. Assim, na língua que eu simplesmente tinha inteiro truncado divisão para baixo todo o tempo, e dizer assim, no doc.

E então, adivinhe o que aconteceu? Todo mundo se soltou. A linguagem teve um erro na divisão de números inteiros!

Não adiantava dizer que o caminho antigo estava quebrado. Era mais fácil mudá-lo do que argumentar.

Portanto, para responder sua pergunta, em linguagens como C # e Java, é mais fácil incluir os operadores ++ (que eu pessoalmente gosto) do que explicar por que não.

Mike Dunlavey
fonte
4
Como outro exemplo, a linguagem de programação D (que é, de várias maneiras, a próxima etapa evolutiva do C ++) possui uma instrução switch, e eles mantêm os breaks necessários em cada cláusula, mesmo que a linguagem não permita que ela caia. Enquanto D descartou a compatibilidade retroativa do C ++ com o C, sua política é que qualquer construção C mantida por D surpreenda o mínimo possível o programador C.
Doval
@Doval: Eu até construí uma linguagem chamada D, como C, mas com recursos OO e paralelismo. Não foi a lugar nenhum (misericordiosamente) e não tem relação com o D de que você está falando. Eu dirijo pessoas loucas em C ?? por sempre escrevendo break; case ...;-)
Mike Dunlavey
1
@MikeDunlavey: Minha preferência seria dizer que /produz doubleou float(usando doublenos casos em que ambos funcionariam) e ter operadores separados para a divisão com piso, truncada e "faça o que for", além de módulo, restante e "igualmente divisível por "[o último produzindo um booleano]. Eu diria que qualquer resultado de int x=y/z;tem uma probabilidade significativa de diferir do que foi pretendido; portanto, a expressão não deve produzir nenhum resultado.
Supercat 24/03
Não é incomum em idiomas ter operadores separados para int-split-by-int produzindo int ou ponto flutuante. Além disso, o sinal do restante (afeta o quociente) varia muito. Consulte en.wikipedia.org/wiki/Modulo_operation para obter uma lista de idiomas. Parte da decisão da FORTRAN foi, sem dúvida, baseada no que o hardware eles usaram (IBM pré-360?) E como ele se comportou com dividendos e / ou divisores assinados.
Phil Perry
Sei que Pascal e Python separam os operadores "yielding int" e "yielding ponto flutuante", embora eu não tenha visto nenhum que separa os modos de arredondamento. Acho realmente péssimo o código que deseja, por exemplo, calcular a média de três números e, em Python, poderia ser escrito, como (a+b+c)//3na maioria dos idiomas, como uma bagunça desagradável para lidar com a semântica de divisão truncada (especialmente porque, em muitos processadores) , uma divisão por piso dividido por três poderia ser feita mais rapidamente do que uma truncada!) #
688
2

Sim, não é nada importante por si só, seu único benefício é o açúcar sintático que facilita a digitação.

A razão pela qual está em C em primeiro lugar é apenas porque o PDP-11 que era a base para C, nos velhos tempos, tinha uma instrução especial para aumentar em 1. Naqueles dias, o uso dessa instrução era importante para obtenha mais velocidade do sistema, daí o recurso de idioma.

No entanto, acho que as pessoas gostam tanto do comando que reclamariam se não estivesse lá.

gbjbaanb
fonte
Por uma questão de precisão, não é tanto que o PDP-11 tenha uma instrução para pré ou pós-incremento, mas tenha modos de endereçamento para eles (e decrementar), o que significa que qualquer instrução que acesse a memória através de um registro também pode ser definido como pré ou pós-incremento ou decremento que se registram ao mesmo tempo. A tecnologia do compilador na época realmente não era capaz de fazer isso automaticamente por meio da otimização, então o suporte explícito era a única maneira de fazê-lo funcionar.
Jules
2

Essa é uma sintaxe idiomática em C para muitas situações (como o forloop que você citou); portanto, como outros já disseram, pode ajudar aqueles que fazem a transição para Java como uma nova linguagem e facilita a porta de código C pré-existente. Por esses motivos, inclusive foi uma decisão natural a ser tomada nos primeiros dias de Java para acelerar a adoção da nova linguagem. Faz menos sentido agora, porque o código mais compacto custa às custas dos programadores aprenderem sintaxe que não é estritamente necessária.

Quem gosta, acha conveniente e natural. Ficar sem parece estranho. Usá-lo não ganha eficiência, é uma questão de legibilidade. Se você acha que seus programas são mais fáceis de ler e manter usando esses operadores, use-os.

O sentido original em C era mais do que apenas "adicionar 1" ou "subtrair 1". É muito mais do que apenas açúcar sintático ou pequenos ganhos de desempenho. O operador em C significa "incremento (ou decremento) para o próximo item". Se você tiver um ponteiro para algo maior que um byte e for para o próximo endereço como em *pointer++, o incremento real poderá ser de 4 ou 8 ou o que for necessário. O compilador faz o trabalho para você, acompanhando o tamanho dos itens de dados e pulando o número certo de bytes:

int k;
int *kpointer = &k;
*kpointer++ = 7;

Na minha máquina, isso adiciona 4 ao kpointer, não adiciona 1. Esta é uma ajuda real em C, evitando erros e salvando invocações espúrias de sizeof (). Não há aritmética de ponteiro equivalente em Java, portanto, isso não é uma consideração. Lá, é mais sobre o poder da expressão do que qualquer outra coisa.

K. Eno
fonte