Associatividade e precedência de operadores

Associatividade e precedência de operadores é um assunto um pouco marginalizado no estudo de linguagens de programação. Em algum momento todo desenvolvedor vai se deparar com um problema relacionado a isso.

Por exemplo, a expressão 8 * 2 - 1 pode ser vista como (8 * 2) - 1 ou 8 * (2 - 1) e essa diferença muda a forma dela ser calculada e, consequentemente, o seu resultado. Essa ambiguidade foi resolvida lá nos primórdios pelos matemáticos através de regras de precedência e associatividade. Essas mesmas regras são aplicadas nos projetos das linguagens de programação. Quando um projetista de linguagem vai criar a especificação dela, ele precisa definir de antemão todas essas regras.

Precedência: um operador possui maior precedência que outro se ele precisar ser analisado antes em todas as expressões sem parênteses que envolverem apenas os dois operadores. Na matemática, na expressão 8 * 2 - 1, a multiplicação é sempre avaliada antes da subtração, ou seja, ela possui maior precedência. Portanto, a expressão equivalente seria: (8 * 2) - 1.

Já a associatividade especifica se os operadores de igual precedência devem ser avaliados da esquerda para a direita ou da direita para a esquerda. De volta à matemática, o operador “menos” possui associatividade à esquerda, portanto, 10 - 9 - 8 é equivalente a (10 - 9) - 8.

Na matemática

Precedência na matemática, da maior pra menor:

  • 1) Parênteses;
  • 2) Expoentes;
  • 3) Multiplicações e divisões; (da esquerda para a direita);
  • 4) Somas e subtrações. (da esquerda para a direita);

Grande parte das linguagens de programação seguem (sempre que possível) as convenções matemáticas comuns para associatividade e precedência.

Na maioria das linguagens de programação os operadores de atribuição são definidos com associatividade à direita. Ou seja, a = b = c é o equivalente a a = (b = c), que alimenta as variáveis a e b com o valor armazenado em c. As linguagens de programação possuem em suas documentações uma tabela dos operadores e suas precedências, da maior pra menor, bem como a associatividade desses operadores. Portanto, a regra aqui é avaliar essa tabela na linguagem que você utiliza.

Forçando precedência

Assim como na matemática, as linguagens permitem que usemos parênteses para forçar uma maior precedência. Essa expressão aqui:

x = 8 + 9 * 2; // 26

Em qualquer linguagem, vai resultar em 26, pois a multiplicação possui maior precedência que a subtração. No entanto, se utilizarmos parênteses entre a operação de soma, ela passa ter precedência maior que a multiplicação e, consequentemente, o resultado muda:

x = (8 + 9) * 2; // 34

Sem uma definição clara de precedência as expressões com múltiplos operadores se tornariam ambíguas. Um exemplo disso:

x = 4/2*1+3; // 5

Que resultado você espera ter? Pra quem está lendo essa expressão, não é tão óbvio como calculá-la. Por isso forçar precedência é uma boa prática, traz mais legibilidade e coerência para sua operação.

O resultado da expressão acima é diferente do resultado que força precedência:

x = (4/2)*(1+3); // 8

Precedência é sobre agrupamento

Uma observação muito importante a ser feita é que precedência não determina a ordem de avaliação, precedência apenas determina como a operação vai ser agrupada para depois ser avaliada. Ou seja, precedência especifica que a expressão:

f1() + f2() * f3()

Será agrupada como:

f1() + (f2() * f3())

Ou seja, precedência não fala que a função f2() vai ser avaliada primeiro que f1(). A ordem de avaliação padrão da maioria das linguagens de programação é da esquerda para a direita, mas as regras de associatividade podem alterar isso no meio do caminho.

Então, a ordem de avaliação das funções acima seria primeiro a f1(), depois a f2() e por fim a f3(), da esquerda para a direita. A função da precedência foi apenas agrupar as duas expressões (f2() * f3()).

Abaixo exemplos de como diferentes expressões são agrupadas no processo de parsing de uma linguagem de programação (na construção da árvore sintática abstrata):

-a * b                      => ((-a) * b)
!-a                         => (!(-a))
a + b + c                   => ((a + b) + c)
a * b * c                   => ((a * b) * c)
a * b / c                   => ((a * b) / c)
a + b / c                   => (a + (b / c))
a + b * c + d / e - f       => (((a + (b * c)) + (d / e)) - f)
3 + 4 * 5 == 3 * 1 + 4 * 5  => ((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))

Por exemplo, a expressão:

1 + 2 + 3;

No parsing da linguagem, depois de se aplicar as regras de precedência, ela é agrupada dessa forma:

((1 + 2) + 3)

A representação da árvore abstrata dessa expressão seria algo como:

Representação abstrata de precedência

Ou seja, mais à esquerda temos uma expressão que um lado do seu nó é o literal 1 e o outro lado o 2. E essa expressão está à esquerda da expressão que tem o nó do literal 3.

Tabela de precedência e associatividade

Na documentação da sua linguagem de programação, você deverá encontrar uma tabela parecida com essa abaixo, onde os operadores são ordenados pelos de maior precedência e uma coluna especificando a associatividade:

NomeOperador(es)Associatividade
Unário!direita
Aritmética* / %esquerda
Aritmética- +esquerda
Comparação< > <= >=esquerda
Comparação== !=esquerda
Atribuição= += -= *=direita

Nessa especificação temos claro que a operação aritmética de multiplicação * tem precedência maior que a subtração - (por estar numa ordem superior na tabela). Da mesma forma, os operadores ariméticos possuem maior precedência que os operadores de atribuição.

O operador unário ! de negação tem associatividade à direita, ou seja, quer dizer que primeiro a expressão da direita será avaliada antes de fazer a negação. Da mesma forma os operadores de atribuição vão resolver as espressões à direita antes de fazer a atribuição.

Apesar de as linguagens de programação seguirem regras parecidas, não há como generalizar, há sempre algumas diferenças, portanto, não deixe de consultar a documentação da sua linguagem.

Até a próxima!

Deixe seu comentário

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP). @KennedyTedesco

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200