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:
-
- Parênteses;
-
- Expoentes;
-
- Multiplicações e divisões; (da esquerda para a direita);
-
- 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:
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:
Nome | Operador(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!