Short-circuit boolean evaluation: mai dare niente per scontato

Leggete questo segmento di codice java:
if (list != null && list.size()>0) { doSomething(); }
Ciò che fa è molto semplice: controlla se la collezione list è non nulla e con almeno un elemento e in tal caso esegue un metodo chiamato doSomething().
La condizione booleana è un AND di due espressioni, e l’ordine in cui vengono calcolate non è affatto indifferente: viene valutata per prima list != null, e se questa risulta già essere FALSE, l’intera condizione viene fatta valere FALSE senza nemmeno valutare list.size()>0, anche perché in tal caso list sarebbe uguale a null, quindi qualunque chiamata a metodo su list finirebbe in una NullPointerException.
Un discorso analogo viene fatto per l’operazione di OR: se il primo operando risulta già essere TRUE, l’espressione viene fatta valere TRUE senza valutare il secondo operando.
Questa caratteristica (la valutazione parziale delle espressioni booleane) si chiama Short-circuit evalutation ed è una differenza fondamentale tra un’operazione di AND in Java e l’operazione di AND nel mondo matematico. Nel mondo della matematica l’ordine di valutazione è totalmente indifferente in quanto l’operatore di AND è commutativo.
Nel mondo “reale” dell’informatica questo trucchetto viene invece utilizzato non tanto per aumentare le performance quanto per scrivere condizioni più compatte: nell’esempio precedente, senza la short-circuit evaluation si solleverebbe una NullPointerException se list fosse null, e bisognerebbe invece riscrivere il codice in questo modo:
if (list != null) { if (list.size()>0) { doSomething(); } }
(evitando cioè “a mano” di eseguire il metodo .size() su un oggetto null).
Questo tipo di controlli torna utile in tutta una serie di occasioni:
- Check dei boundary di un array o di una stringa
- Evitare divisioni per zero
- Evitare di utilizzare valori nulli (come in questo caso)
Tutto ciò oggigiorno viene dato per scontato, ma qui siamo su RetroAcademy, quindi siamo gente a cui piace programmare anche su architetture obsolete e dimenticate da anni in virtù della nostra vena di “archeologi informatici”. I vecchi linguaggi (e relativi compilatori) supportano la short-circuit evaluation?
La risposta è: “dipende”. Alcuni linguaggi, per esempio Modula-2, la supportano by design. Altri linguaggi hanno subìto nel tempo diverse revisioni e standardizzazioni (è il caso del C): in questi casi dipende dal tipo di standard adottato dal compilatore. In altri casi ancora le specifiche sono talmente lasche (è il caso del BASIC) che non vi è dubbio che ciò dipenda dal compilatore/interprete utilizzato.
In questo articolo viene presentato un caso di studio costituito da un semplice test:
– Dati due interi A e B, stampare il valore di A/B se e solo se A/B è maggiore di 1. Altrimenti segnalare errore.
Questo test è stato implementato in 5 linguaggi diversi supponendo che valga la short-circuit evaluation. I test sono stati lanciati e si è visto in questo modo se la short-circuit evaluation sia stata utilizzata o meno.
Il sistema operativo di riferimento utilizzato è stato CP/M-80 (tranne ovviamente nel caso di Java) e i linguaggi/compilatori testati sono stati:
Compilatore | S.O. | Esito | Revisione delle condizione |
Java 7 | JVM | OK: Short-circuit evaluation | – |
Turbo Modula-2 | CP/M-80 | OK: Short-circuit evaluation | – |
Turbo Pascal 3.01A | CP/M-80 | KO: Crash del programma |
IF b<>0 THEN IF a DIV b>1 THEN |
Hi-Tech C 3.09 | CP/M-80 | KO: Crash del sistema operativo |
if (b!=0) if (a/b>1) |
MBASIC-80 5.21 | CP/M-80 | KO: Comportamento imprevisto | IF B<>0 THEN IF A/B>1 THEN |
Di seguito i segmenti di codice nei vari linguaggi:
Java |
public class MainClass { private static void divide(int a, int b) { if (b!=0 && a/b>1) System.out.println("result="+(a/b)); else System.out.println("Wrong condition"); } public static void main(String[] s) { int a,b; a=10; b=0; divide(a,b); } } |
Turbo Modula-2 |
MODULE Main; VAR a,b:INTEGER; PROCEDURE divide(a,b:INTEGER); BEGIN IF (b<>0) AND (a DIV b>1) THEN WRITELN('Result=',a DIV b) ELSE WRITELN('Wrong condition'); END END divide; BEGIN a:=10; b:=0; divide(a,b); END Main. |
Turbo Pascal 3.01A |
PROGRAM Main; VAR a,b:INTEGER; PROCEDURE divide(a,b:INTEGER); BEGIN IF (b<>0) AND (a DIV b>1) THEN WRITELN('Result=',a DIV b) ELSE WRITELN('Wrong condition') END; BEGIN a:=10; b:=0; divide(a,b); END. |
Hi-Tech C 3.09 |
#include <stdio.h> void divide(int a, int b) { if (b!=0 && a/b>1) printf("result=%d\n",a/b); else printf("Wrong condition\n"); } main() { int a,b; a=10; b=0; divide(a,b); } |
MBASIC-80 5.21 |
10 DEFINT A-Z 20 A=10 30 B=0 40 IF B<>0 AND A/B>1 THEN PRINT "Result=";A/B ELSE PRINT "Wrong condition" |
In sintesi: attenzione! Ciò che potrebbe a una prima occhiata sembrare corretto in realtà potrebbe non esserlo, specie se si tratta di un’abitudine ormai data per scontata ma che è solo il risultato dell’evoluzione che i linguaggi hanno subìto negli anni.
Per approfondimenti:
– Pagina di Wikipedia sulla short-circuit evaluation: parla anche dei possibili “side-effect” non desiderati