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

Francesco Sblendorio

Francesco Sblendorio

Francesco Sblendorio nasce nel 1977. Nel 1985 fa conoscenza con il mondo dei computer attraverso un Commodore 16: da quel momento la discesa verso il lato oscuro è inesorabile e si trasforma in un geek impenitente. Nell'ambito del retrocomputing ha un sogno: che i vecchi computer non debbano semplicemente sopravvivere (conservando la loro funzionalità), piuttosto devono vivere, attraverso nuovi software sviluppati oggi per i computer di ieri.