Quali sono le regole per l'ordine di valutazione in Java?


86

Sto leggendo del testo Java e ho ricevuto il seguente codice:

int[] a = {4,4};
int b = 1;
a[b] = b = 0;

Nel testo l'autore non ha fornito una spiegazione chiara e l'effetto dell'ultima riga è: a[1] = 0;

Non sono così sicuro di aver capito: come è avvenuta la valutazione?


19
La confusione di seguito sul motivo per cui ciò accade significa, tra l'altro, che non dovresti mai farlo, poiché molte persone dovranno pensare a cosa fa effettivamente, invece di essere ovvio.
Martijn

La risposta corretta a qualsiasi domanda come questa è "non farlo!" L'assegnazione dovrebbe essere trattata come una dichiarazione; l'uso dell'assegnazione come espressione che restituisce un valore dovrebbe sollevare un errore del compilatore, o almeno un avvertimento. Non farlo.
Mason Wheeler

Risposte:


173

Lasciatemelo dire molto chiaramente, perché le persone lo fraintendono tutto il tempo:

L'ordine di valutazione delle sottoespressioni è indipendente sia dall'associatività che dalla precedenza . L'associatività e la precedenza determinano in quale ordine vengono eseguiti gli operatori ma non determinano in quale ordine vengono valutate le sottoespressioni . La tua domanda riguarda l'ordine in cui vengono valutate le sottoespressioni .

Considera A() + B() + C() * D(). La moltiplicazione è una precedenza più alta dell'addizione e l'addizione è associativa a sinistra, quindi equivale a (A() + B()) + (C() * D()) Ma sapere che ti dice solo che la prima addizione avverrà prima della seconda addizione e che la moltiplicazione avverrà prima della seconda addizione. Non ti dice in quale ordine verranno chiamati A (), B (), C () e D ()! (Inoltre non ti dice se la moltiplicazione avviene prima o dopo la prima addizione.) Sarebbe perfettamente possibile obbedire alle regole di precedenza e associatività compilando questo come:

d = D()          // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b      // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last

Qui vengono seguite tutte le regole di precedenza e associatività: la prima addizione avviene prima della seconda addizione e la moltiplicazione avviene prima della seconda addizione. Chiaramente possiamo fare le chiamate ad A (), B (), C () e D () in qualsiasi ordine e comunque obbedire alle regole di precedenza e associatività!

Abbiamo bisogno di una regola non correlata alle regole di precedenza e associatività per spiegare l'ordine in cui vengono valutate le sottoespressioni. La regola rilevante in Java (e C #) è "le sottoespressioni vengono valutate da sinistra a destra". Poiché A () appare a sinistra di C (), A () viene valutato per primo, indipendentemente dal fatto che C () sia coinvolto in una moltiplicazione e A () sia coinvolto solo in un'addizione.

Quindi ora hai abbastanza informazioni per rispondere alla tua domanda. Nelle a[b] = b = 0regole dell'associatività dire che questo è a[b] = (b = 0);ma ciò non significa che la b=0corsa prima! Le regole di precedenza dicono che l'indicizzazione è una precedenza più alta dell'assegnazione, ma ciò non significa che l'indicizzatore venga eseguito prima dell'assegnazione più a destra .

(AGGIORNAMENTO: Una versione precedente di questa risposta aveva alcune piccole e praticamente irrilevanti omissioni nella sezione che segue che ho corretto. Ho anche scritto un articolo sul blog che descrive perché queste regole sono ragionevoli in Java e C # qui: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

La precedenza e l'associatività ci dicono solo che l'assegnazione di zero a bdeve avvenire prima dell'assegnazione a a[b], perché l'assegnazione di zero calcola il valore assegnato nell'operazione di indicizzazione. Precedenza e associatività da soli non dicono nulla sul fatto che la a[b]viene valutata , prima o dopo il b=0.

Ancora una volta, questo è esattamente lo stesso di: A()[B()] = C()- Tutto ciò che sappiamo è che l'indicizzazione deve avvenire prima dell'assegnazione. Non sappiamo se A (), B () o C () viene eseguito prima in base alla precedenza e all'associatività . Abbiamo bisogno di un'altra regola per dircelo.

La regola è, ancora una volta, "quando hai una scelta su cosa fare prima, vai sempre da sinistra a destra". Tuttavia, c'è una ruga interessante in questo scenario specifico. L'effetto collaterale di un'eccezione generata causata da una raccolta nulla o da un indice fuori intervallo è considerato parte del calcolo del lato sinistro dell'assegnazione o parte del calcolo dell'assegnazione stessa? Java sceglie quest'ultimo. (Ovviamente, questa è una distinzione che importa solo se il codice è già sbagliato , perché il codice corretto non dereferenzia nulla o passa un indice errato in primo luogo.)

Allora cosa succede?

  • Il a[b]è a sinistra del b=0, quindi le a[b]piste prima , con conseguente a[1]. Tuttavia, la verifica della validità di questa operazione di indicizzazione viene ritardata.
  • Poi b=0succede.
  • Quindi aavviene la verifica che è valida ed a[1]è nel range
  • L'assegnazione del valore a a[1]avviene per ultima.

Quindi, sebbene in questo caso specifico ci siano alcune sottigliezze da considerare per quei rari casi di errore che non dovrebbero verificarsi nel codice corretto in primo luogo, in generale puoi ragionare: le cose a sinistra accadono prima delle cose a destra . Questa è la regola che stai cercando. Parlare di precedenza e associatività è al tempo stesso confuso e irrilevante.

Le persone sbagliano sempre queste cose , anche le persone che dovrebbero saperlo meglio. Ho modificato troppi libri di programmazione che affermavano le regole in modo errato, quindi non sorprende che molte persone abbiano convinzioni completamente errate sulla relazione tra precedenza / associatività e ordine di valutazione, ovvero che in realtà non esiste tale relazione ; sono indipendenti.

Se questo argomento ti interessa, consulta i miei articoli sull'argomento per ulteriori letture:

http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/

Riguardano C #, ma la maggior parte di queste cose si applica altrettanto bene a Java.


6
Personalmente preferisco il modello mentale dove nel primo passaggio si costruisce un albero delle espressioni usando precedenza e associatività. E nel secondo passaggio valuta ricorsivamente quell'albero a partire dalla radice. Con la valutazione di un nodo: valutare i nodi figlio immediati da sinistra a destra e quindi la nota stessa. | Un vantaggio di questo modello è che gestisce banalmente il caso in cui gli operatori binari hanno un effetto collaterale. Ma il vantaggio principale è che semplicemente si adatta meglio al mio cervello.
CodesInChaos

Ho ragione che C ++ non garantisce questo? E Python?
Neil G

2
@ Neil: C ++ non garantisce nulla sull'ordine di valutazione e non lo ha mai fatto. (Nemmeno C.) Python lo garantisce rigorosamente per ordine di precedenza; a differenza di tutto il resto, l'assegnazione è R2L.
Donal Fellows

2
@aroth per me sembri solo confuso. E le regole di precedenza implicano solo che i bambini devono essere valutati prima dei genitori. Ma non dicono nulla sull'ordine in cui vengono valutati i bambini. Java e C # hanno scelto da sinistra a destra, C e C ++ hanno scelto un comportamento non definito.
CodesInChaos

6
@noober: OK, considera: M (A () + B (), C () * D (), E () + F ()). Il tuo desiderio è che le sottoespressioni vengano valutate in quale ordine? C () e D () devono essere valutati prima di A (), B (), E () e F () perché la moltiplicazione ha una precedenza maggiore dell'addizione? È facile dire che "ovviamente" l'ordine dovrebbe essere diverso. Trovare una regola effettiva che copra tutti i casi è piuttosto più difficile. I progettisti di C # e Java hanno scelto una regola semplice e facile da spiegare: "vai da sinistra a destra". Qual è la tua proposta di sostituzione e perché credi che la tua regola sia migliore?
Eric Lippert

32

La risposta magistrale di Eric Lippert, tuttavia, non è adeguatamente utile perché si tratta di una lingua diversa. Questo è Java, dove Java Language Specification è la descrizione definitiva della semantica. In particolare, §15.26.1 è rilevante perché descrive l'ordine di valutazione per l' =operatore (sappiamo tutti che è associativo a destra, sì?). Riducendolo un po 'alle parti che ci interessano in questa domanda:

Se l'espressione dell'operando di sinistra è un'espressione di accesso ad array ( §15.13 ), sono necessari molti passaggi:

  • Innanzitutto, viene valutata la sottoespressione del riferimento all'array dell'espressione di accesso all'array dell'operando di sinistra. Se questa valutazione viene completata all'improvviso, l'espressione dell'assegnazione viene completata all'improvviso per lo stesso motivo; la sottoespressione dell'indice (dell'espressione di accesso alla matrice dell'operando di sinistra) e l'operando di destra non vengono valutati e non viene eseguita alcuna assegnazione.
  • In caso contrario, viene valutata la sottoespressione dell'indice dell'espressione di accesso alla matrice dell'operando di sinistra. Se questa valutazione viene completata all'improvviso, l'espressione di assegnazione viene completata all'improvviso per lo stesso motivo e l'operando di destra non viene valutato e non si verifica alcuna assegnazione.
  • In caso contrario, viene valutato l'operando di destra. Se questa valutazione viene completata all'improvviso, l'espressione dell'assegnazione viene completata all'improvviso per lo stesso motivo e non si verifica alcuna assegnazione.

[... poi passa a descrivere il significato effettivo del compito stesso, che possiamo ignorare qui per brevità ...]

In breve, Java ha un ordine di valutazione molto strettamente definito che è praticamente esattamente da sinistra a destra all'interno degli argomenti per qualsiasi chiamata di operatore o metodo. Le assegnazioni di array sono uno dei casi più complessi, ma anche lì è ancora L2R. (Il JLS consiglia di non scrivere codice che necessita di questo tipo di vincoli semantici complessi , e anch'io: puoi metterti nei guai più che sufficienti con un solo compito per istruzione!)

C e C ++ sono decisamente diversi da Java in quest'area: le loro definizioni di linguaggio lasciano deliberatamente indefinito l'ordine di valutazione per consentire ulteriori ottimizzazioni. C # è come Java apparentemente, ma non conosco abbastanza bene la sua letteratura per essere in grado di puntare alla definizione formale. (Questo varia davvero in base alla lingua, Ruby è strettamente L2R, così come Tcl - anche se manca di un operatore di assegnazione di per sé per ragioni non rilevanti qui - e Python è L2R ma R2L rispetto all'assegnazione , che trovo strano ma ecco qua .)


11
Quindi quello che stai dicendo è che la risposta di Eric è sbagliata perché Java la definisce specificamente per essere esattamente ciò che ha detto?
configuratore

8
La regola rilevante in Java (e C #) è "le sottoespressioni vengono valutate da sinistra a destra" - Mi sembra che stia parlando di entrambe.
configuratore

2
Un po 'confuso qui: questo rende la risposta di cui sopra di Eric Lippert meno vera, o sta solo citando un riferimento specifico sul motivo per cui è vero?
GreenieMeanie

5
@ Greenie: la risposta di Eric è vera, ma come ho detto non puoi prendere una visione da una lingua in quest'area e applicarla a un'altra senza fare attenzione. Quindi ho citato la fonte definitiva.
Donal Fellows

1
la cosa strana è che la mano destra viene valutata prima che la variabile di sinistra sia risolta; in a[-1]=c, cviene valutato, prima -1viene riconosciuto come non valido.
ZhongYu

5
a[b] = b = 0;

1) l'operatore di indicizzazione dell'array ha una precedenza maggiore dell'operatore di assegnazione (vedi questa risposta ):

(a[b]) = b = 0;

2) Secondo 15.26. Operatori di assegnazione di JLS

Sono disponibili 12 operatori di assegnazione; sono tutti sintatticamente associativi a destra (raggruppano da destra a sinistra). Pertanto, a = b = c significa a = (b = c), che assegna il valore di c a be quindi assegna il valore di b a a.

(a[b]) = (b=0);

3) Secondo 15.7. Ordine di valutazione di JLS

Il linguaggio di programmazione Java garantisce che gli operandi degli operatori sembrano essere valutati in uno specifico ordine di valutazione, cioè da sinistra a destra.

e

L'operando di sinistra di un operatore binario sembra essere completamente valutato prima che venga valutata qualsiasi parte dell'operando di destra.

Così:

a) (a[b])valutato prima aa[1]

b) poi (b=0)valutato a0

c) (a[1] = 0)valutato per ultimo


1

Il tuo codice è equivalente a:

int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;

che spiega il risultato.


7
La domanda è perché è così.
Mat

@Mat La risposta è perché questo è ciò che accade sotto il cofano considerando il codice fornito nella domanda. È così che avviene la valutazione.
Jérôme Verstrynge

1
Si, lo so. Non ho ancora risposto alla domanda tramite IMO, motivo per cui è così che avviene la valutazione.
Mat

1
@Mat "Perché è così che avviene la valutazione?" non è la domanda posta. "come è avvenuta la valutazione?" è la domanda posta.
Jérôme Verstrynge

1
@ JVerstry: come sono non equivalenti? La sottoespressione di riferimento di matrice dell'operando di sinistra è l'operando più a sinistra. Quindi dire "fai prima il primo a sinistra" è esattamente lo stesso che dire "prima fai riferimento all'array". Se gli autori delle specifiche Java hanno scelto di essere inutilmente prolissi e ridondanti nello spiegare questa particolare regola, va bene per loro; questo genere di cose crea confusione e dovrebbe essere più piuttosto che meno prolisso. Ma non vedo come la mia caratterizzazione concisa sia semanticamente diversa dalla loro caratterizzazione prolissa.
Eric Lippert

0

Considera un altro esempio più approfondito di seguito.

Come regola generale:

È meglio avere a disposizione una tabella delle regole dell'ordine di precedenza e dell'associatività da leggere quando si risolvono queste domande, ad esempio http://introcs.cs.princeton.edu/java/11precedence/

Ecco un buon esempio:

System.out.println(3+100/10*2-13);

Domanda: qual è l'output della riga sopra?

Risposta: applicare le regole di precedenza e associatività

Passaggio 1: in base alle regole di precedenza: gli operatori / e * hanno la priorità sugli operatori + -. Pertanto il punto di partenza per eseguire questa equazione sarà ristretto a:

100/10*2

Passaggio 2: secondo le regole e la precedenza: / e * sono uguali in precedenza.

Poiché gli operatori / e * sono uguali in precedenza, è necessario esaminare l'associatività tra questi operatori.

Secondo le REGOLE DI ASSOCIAZIONE di questi due particolari operatori, iniziamo ad eseguire l'equazione da SINISTRA A DESTRA, ovvero 100/10 viene eseguito per primo:

100/10*2
=100/10
=10*2
=20

Passaggio 3: l'equazione si trova ora nel seguente stato di esecuzione:

=3+20-13

Secondo le regole e la precedenza: + e - sono uguali in precedenza.

Dobbiamo ora esaminare l'associatività tra gli operatori + e - operatori. In base all'associatività di questi due particolari operatori, iniziamo ad eseguire l'equazione da SINISTRA a DESTRA, ovvero 3 + 20 viene eseguito per primo:

=3+20
=23
=23-13
=10

10 è l'output corretto quando viene compilato

Ancora una volta, è importante avere una tabella delle regole dell'ordine di precedenza e dell'associatività con te quando risolvi queste domande, ad esempio http://introcs.cs.princeton.edu/java/11precedence/


1
Stai dicendo che "l'associatività tra gli operatori + e -" è "DA DESTRA A SINISTRA". Prova a usare quella logica e valuta 10 - 4 - 3.
Pshemo

1
Sospetto che questo errore possa essere causato dal fatto che all'inizio di introcs.cs.princeton.edu/java/11precedence + è un operatore unario (che ha l'associatività da destra a sinistra), ma gli additivi + e - hanno proprio come moltiplicativo * /% a sinistra alla giusta associatività.
Pshemo

Ben individuato e modificato di conseguenza, grazie Pshemo
Mark Burleigh

Questa risposta spiega la precedenza e l'associatività, ma, come ha spiegato Eric Lippert , la domanda riguarda l'ordine di valutazione, che è molto diverso. È un dato di fatto, questo non risponde alla domanda.
Fabio dice Reinstate Monica
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.