Java'da değerlendirme sırası için kurallar nelerdir?


86

Biraz Java metni okuyorum ve şu kodu aldım:

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

Metinde yazar net bir açıklama yapmadı ve son satırın etkisi: a[1] = 0;

Anladığımdan pek emin değilim: değerlendirme nasıl gerçekleşti?


19
Bunun neden olduğuna dair aşağıdaki kafa karışıklığı, bu arada, bunu asla yapmamanız gerektiği anlamına gelir, çünkü birçok insan, apaçık olmak yerine, gerçekte ne yaptığını düşünmek zorunda kalacak.
Martijn

Böyle bir sorunun doğru cevabı "bunu yapma!" Atama bir ifade olarak ele alınmalıdır; atamanın bir değer döndüren bir ifade olarak kullanılması, bir derleyici hatası veya en azından bir uyarı oluşturmalıdır. Bunu yapma.
Mason Wheeler

Yanıtlar:


173

Bunu çok net bir şekilde söyleyeyim, çünkü insanlar bunu her zaman yanlış anlıyor:

Alt ifadelerin değerlendirme sırası, hem ilişkilendirilebilirlik hem de öncelikten bağımsızdır . Birleşim ve öncelik hangi sırayla belirlemek operatörleri yürütür ama yok hangi sırayla belirlemek alt ifadelere değerlendirilir. Sorunuz, alt ifadelerin değerlendirildiği sırayla ilgilidir .

Düşünün A() + B() + C() * D(). Çarpma, toplamadan daha yüksek önceliğe sahiptir ve toplama, sola ilişkiseldir, bu nedenle bu, (A() + B()) + (C() * D()) Butun size yalnızca ilk toplamanın ikinci toplamadan önce olacağını ve çarpmanın ikinci toplamadan önce olacağını söyleyen Bilmek ile eşdeğerdir . A (), B (), C () ve D () 'nin hangi sırayla çağrılacağını size söylemez! (Ayrıca çarpmanın ilk toplamadan önce mi sonra mı olduğunu da söylemez.) Öncelik ve çağrışım kurallarına uymak şu şekilde derleyerek mükemmel bir şekilde mümkündür :

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

Tüm öncelik ve ilişkisellik kuralları orada takip edilir - ilk toplama ikinci toplamadan önce olur ve çarpma ikinci toplamadan önce gerçekleşir. Açıkçası, A (), B (), C () ve D () çağrılarını herhangi bir sırayla yapabiliriz ve yine de öncelik ve çağrışım kurallarına uyabiliriz!

Alt ifadelerin değerlendirilme sırasını açıklamak için öncelik ve ilişkilendirilebilirlik kurallarıyla ilgisi olmayan bir kurala ihtiyacımız var . Java'da (ve C #) ilgili kural "alt ifadeler soldan sağa değerlendirilir" dir. A (), C () 'nin solunda göründüğünden, C () bir çarpmaya ve A () yalnızca bir toplamaya dahil olmasına bakılmaksızın, önce A () değerlendirilir .

Artık sorunuzu cevaplamak için yeterli bilgiye sahipsiniz. Gelen a[b] = b = 0çağrışımsallık söz sahibi kurallarına bu olduğunu a[b] = (b = 0);ama bu demek değildir b=0ilk çalışır! Öncelik kuralları, indekslemenin atamadan daha yüksek önceliğe sahip olduğunu söyler, ancak bu, indeksleyicinin en sağdaki atamadan önce çalıştığı anlamına gelmez .

(GÜNCELLEME: Bu cevabın önceki bir sürümünde, aşağıdaki bölümde düzelttiğim bazı küçük ve pratik olarak önemsiz eksiklikler vardı. Ayrıca bu kuralların Java ve C # 'da neden mantıklı olduğunu açıklayan bir blog makalesi de burada yazdım: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

O Öncelik ve birleşme sadece bize sıfır atama için bgerçekleşmesi gerekir önce için atama a[b], çünkü sıfır Hesaplamalar indeksleme operasyonda atanan değer atama. Öncelik ve konusunda çağrışımsallık yalnız diyelim hiçbir şey a[b]değerlendirilir önce veya sonrab=0 .

Yine, bu şununla aynıdır: A()[B()] = C()- Tek bildiğimiz, indekslemenin atamadan önce yapılması gerektiğidir. A (), B () veya C () 'nin öncelik ve ilişkiselliğe göre mi çalıştığını bilmiyoruz . Bunu bize söylemek için başka bir kurala ihtiyacımız var.

Kural yine, "ilk olarak ne yapacağına dair bir seçim yaptığında, her zaman soldan sağa git" dir. Ancak, bu özel senaryoda ilginç bir kırışıklık var. Boş bir koleksiyon veya aralık dışı dizinin neden olduğu bir istisnanın yan etkisi, atamanın sol tarafının hesaplanmasının bir parçası mı yoksa atamanın kendisinin hesaplanmasının bir parçası mı? Java ikincisini seçer. (Elbette, bu, yalnızca kod zaten yanlışsa önemli olan bir ayrımdır , çünkü doğru kod ilk etapta boş referansta bulunmaz veya kötü bir dizini geçmez.)

Peki ne olur?

  • a[b]Solunda b=0yüzden, a[b]ishal ilk , neden a[1]. Ancak, bu indeksleme işleminin geçerliliğinin kontrol edilmesi gecikir.
  • Sonra b=0olur.
  • Ardından ageçerli ve a[1]aralık dahilinde olan doğrulama gerçekleşir
  • Değerin atanması en a[1]son gerçekleşir.

Dolayısıyla, bu özel durumda, ilk başta doğru kodda meydana gelmemesi gereken nadir hata durumları için dikkate alınması gereken bazı incelikler olsa da , genel olarak şu sonuca varabilirsiniz: soldaki şeyler sağdaki şeylerden önce gerçekleşir . Aradığınız kural bu. Öncelik ve çağrışımdan bahsetmek hem kafa karıştırıcı hem de alakasızdır.

İnsanlar bu şeyleri her zaman yanlış anlar , daha iyi bilmesi gerekenler bile. Kuralları yanlış ifade eden çok fazla programlama kitabını düzenledim , bu nedenle pek çok insanın öncelik / ilişkilendirilebilirlik ve değerlendirme sıralaması arasındaki ilişki hakkında tamamen yanlış inançlara sahip olması şaşırtıcı değil - yani, gerçekte böyle bir ilişki yok ; bağımsızdırlar.

Bu konu ilginizi çekiyorsa, daha fazla okumak için konuyla ilgili makalelerime bakın:

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

Bunlar C # hakkındadır, ancak bunların çoğu Java için de aynı derecede geçerlidir.


6
Şahsen ben, ilk adımda öncelik ve çağrışımları kullanarak bir ifade ağacı oluşturduğunuz zihinsel modeli tercih ederim. Ve ikinci adımda o ağacı kökten başlayarak özyinelemeli olarak değerlendirin. Bir düğümün değerlendirilmesi şu şekildedir: Soldan sağa hemen alt düğümleri ve ardından notun kendisini değerlendirin. | Bu modelin bir avantajı, ikili operatörlerin bir yan etkiye sahip olduğu durumu önemsiz bir şekilde ele almasıdır. Ama asıl avantajı beynime daha iyi uyması.
CodesInChaos

C ++ 'nın bunu garanti etmediği konusunda haklı mıyım? Python ne olacak?
Neil G

2
@Neil: C ++, değerlendirme sırası hakkında hiçbir şey garanti etmez ve asla vermedi. (C de değildir.) Python bunu kesinlikle öncelik sırasına göre garanti eder; her şeyden farklı olarak, atama R2L'dir.
Donal Fellows

2
@aroth bana kafanız karışık geliyor. Ve öncelik kuralları yalnızca çocukların ebeveynlerden önce değerlendirilmesi gerektiği anlamına gelir. Ancak çocukların değerlendirildiği sıra hakkında hiçbir şey söylemiyorlar. Java ve C # soldan sağa, C ve C ++ tanımsız davranışı seçti.
CodesInChaos

6
@noober: Tamam, düşünün: M (A () + B (), C () * D (), E () + F ()). İsteğiniz alt ifadelerin hangi sırayla değerlendirilmesidir? Çarpma toplamadan daha yüksek önceliğe sahip olduğu için C () ve D (), A (), B (), E () ve F () 'den önce değerlendirilmeli mi? "Açıkçası" sıranın farklı olması gerektiğini söylemek kolay. Tüm vakaları kapsayan gerçek bir kural bulmak oldukça zordur. C # ve Java tasarımcıları basit, açıklaması kolay bir kuralı seçtiler: "soldan sağa git". Bunun yerine geçmeyi önerdiğiniz nedir ve neden kuralınızın daha iyi olduğuna inanıyorsunuz?
Eric Lippert

32

Eric Lippert'in ustaca cevabı, farklı bir dilden bahsettiği için yine de pek yardımcı olmuyor. Bu, Java Dil Belirtiminin anlambilimin kesin açıklaması olduğu Java'dır. Özellikle, §15.26.1 , =operatör için değerlendirme sırasını açıkladığı için önemlidir (hepimiz bunun doğru ilişkisel olduğunu biliyoruz, evet?). Bu soruda önemsediğimiz parçalara biraz kısaltın:

Sol taraftaki işlenen ifadesi bir dizi erişim ifadesiyse ( §15.13 ), birçok adım gereklidir:

  • İlk olarak, sol taraftaki işlenen dizi erişim ifadesinin dizi başvurusu alt ifadesi değerlendirilir. Bu değerlendirme aniden tamamlanırsa, ödev ifadesi aynı nedenle birdenbire tamamlanır; dizin alt ifadesi (sol taraftaki işlenen dizi erişim ifadesinin) ve sağ taraftaki işlenen değerlendirilmez ve atama gerçekleşmez.
  • Aksi takdirde, sol el işlenen dizisi erişim ifadesinin dizin alt ifadesi değerlendirilir. Bu değerlendirme aniden tamamlanırsa, atama ifadesi aynı nedenle aniden tamamlanır ve sağ taraftaki işlenen değerlendirilmez ve herhangi bir atama gerçekleşmez.
  • Aksi takdirde, sağ taraftaki işlenen değerlendirilir. Bu değerlendirme aniden tamamlanırsa, atama ifadesi aynı nedenle aniden tamamlanır ve herhangi bir atama olmaz.

[… Daha sonra, burada kısaca göz ardı edebileceğimiz, ödevin kendisinin gerçek anlamını açıklamaya devam eder…]

Kısacası, Java, herhangi bir operatör veya yöntem çağrısı için argümanlarda hemen hemen tam olarak soldan sağa olan çok yakından tanımlanmış bir değerlendirme sırasına sahiptir. Dizi atamaları daha karmaşık durumlardan biridir, ancak orada bile hala L2R'dir. (JLS, bu tür karmaşık anlamsal kısıtlamalara ihtiyaç duyan bir kod yazmamanızı önerir ve ben de öyle: her ifade başına yalnızca bir atama ile gereğinden fazla sorunla karşılaşabilirsiniz!)

C ve C ++, bu alanda Java'dan kesinlikle farklıdır: dil tanımları, daha fazla optimizasyon sağlamak için kasıtlı olarak değerlendirme sırasını tanımsız bırakır. Görünüşe göre C # Java'ya benziyor, ancak literatürünü biçimsel tanıma işaret edecek kadar iyi bilmiyorum. (Bu gerçekten dile göre değişir, Ruby kesinlikle L2R'dir, tıpkı Tcl'de olduğu gibi - burada ilgili olmayan nedenlerden dolayı kendi başına bir atama operatörü eksiktir - ve Python, L2R'dir, ancak atama açısından R2L'dir , ben tuhaf buluyorum ama işte oraya gidiyorsunuz .)


11
Yani Eric'in cevabının yanlış olduğunu söylüyorsunuz çünkü Java bunu özellikle onun söylediği gibi tanımlıyor?
konfigüratör

8
Java'da (ve C #) ilgili kural "alt ifadeler soldan sağa değerlendirilir" - Bana sanki ikisinden de bahsediyormuş gibi geliyor.
configurator

2
Burada biraz karışık - bu Eric Lippert'in yukarıdaki cevabını daha az doğru kılıyor mu, yoksa sadece bunun neden doğru olduğuna dair belirli bir referansı mı veriyor?
GreenieMeanie

6
@Greenie: Eric'in cevabı doğru ama belirttiğim gibi bu alanda bir dilden bir içgörü alıp başka bir dile uygulayamazsınız. Bu yüzden kesin kaynağı gösterdim.
Donal Fellows

1
Garip olan, sağ el değişkeni çözülmeden önce değerlendirilir; içinde a[-1]=c, cdeğerlendirilir, önce -1geçersiz olarak kabul edilir.
ZhongYu

5
a[b] = b = 0;

1) dizi indeksleme operatörü, atama operatöründen daha yüksek önceliğe sahiptir ( bu cevaba bakınız ):

(a[b]) = b = 0;

2) 15.26. JLS'nin Atama Operatörleri

12 atama operatörü vardır; tümü sözdizimsel olarak sağa ilişkilidir (sağdan sola gruplanırlar). Bu nedenle, a = b = c, c'nin değerini b'ye atayan ve sonra b'nin değerini a'ya atayan a = (b = c) anlamına gelir.

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

3) 15.7'ye göre. JLS'nin Değerlendirme Sırası

Java programlama dili, operatörlerin işlenenlerinin belirli bir değerlendirme sırasına göre, yani soldan sağa, değerlendirildiğini garanti eder.

ve

Bir ikili operatörün sol taraf operandı, sağ taraf operandının herhangi bir kısmı değerlendirilmeden önce tam olarak değerlendirilmiş gibi görünmektedir.

Yani:

a) (a[b])ilk olarak değerlendirilira[1]

b) daha sonra (b=0)değerlendirildi0

c) (a[1] = 0)en son değerlendirildi


1

Kodunuz şuna eşdeğerdir:

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

bu sonucu açıklıyor.


7
Soru neden böyle olduğudur.
Mat

@Mat Cevap, soruda verilen kod göz önüne alındığında kaputun altında olanların olmasıdır. Değerlendirme böyle olur.
Jérôme Verstrynge

1
Evet biliyorum. Yine de IMO'ya rağmen soruyu cevaplamıyorum, bu yüzden değerlendirme böyle oluyor.
Mat

1
@Mat 'Değerlendirme neden böyle oluyor?' sorulan soru değil. "değerlendirme nasıl oldu?" sorulan sorudur.
Jérôme Verstrynge

1
@JVerstry: Nasıl eşdeğer değiller? Soldaki işlenenin dizi başvurusu alt ifadesi , en soldaki işlenendir. Yani "en soldaki önce yap" demek "önce dizi başvurusunu yap" demekle tamamen aynıdır. Java özellik yazarları bu özel kuralı açıklamada gereksiz yere uzun ve gereksiz olmayı seçtiyse, bu onlar için iyidir; bu tür şeyler kafa karıştırıcıdır ve daha az sözlü olmaktan çok daha fazlası olmalıdır. Ancak özlü nitelendirmelerimin, onların prolix nitelendirmelerinden anlamsal olarak ne kadar farklı olduğunu anlamıyorum.
Eric Lippert

0

Aşağıdaki daha derinlemesine başka bir örneği ele alalım.

Genel Bir Kural Olarak:

Bu soruları çözerken okunabilecek Öncelik Sırası Kuralları ve İlişkilendirilebilirlik tablosuna sahip olmak en iyisidir, örneğin http://introcs.cs.princeton.edu/java/11precedence/

İşte güzel bir örnek:

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

Soru: Yukarıdaki Satırın Çıktısı Nedir?

Cevap: Öncelik ve İlişkilendirme Kurallarını Uygulayın

Adım 1: Öncelik kurallarına göre: / ve * operatörleri + - operatörlerine göre önceliklidir. Bu nedenle, bu denklemi yürütmek için başlangıç ​​noktası daraltılacaktır:

100/10*2

Adım 2: Kurallara ve önceliğe göre: / ve *, öncelik bakımından eşittir.

/ Ve * operatörleri öncelik bakımından eşit olduklarından, bu operatörler arasındaki ilişkiye bakmamız gerekir.

Bu iki belirli operatörün DERNEĞİ KURALLARI'na göre, denklemi Soldan sağa doğru çalıştırmaya başlarız, yani önce 100/10 çalıştırılır:

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

Adım 3: Denklem şu anda aşağıdaki yürütme durumundadır:

=3+20-13

Kurallara ve önceliğe göre: + ve - öncelikte eşittir.

Şimdi + ve - operatörleri arasındaki ilişkiye bakmamız gerekiyor. Bu iki belirli operatörün ilişkilendirilebilirliğine göre, denklemi soldan sağa doğru çalıştırmaya başlarız, yani 3 + 20 önce yürütülür:

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

10 derlendiğinde doğru çıktıdır

Yine, bu soruları çözerken yanınızda Öncelik Sırası Kuralları ve İlişkilendirme tablosu bulundurmanız önemlidir, örneğin http://introcs.cs.princeton.edu/java/11precedence/


1
"Operatörler + ve - operatörler arasındaki ilişkinin" "SAĞDAN SOLA" olduğunu söylüyorsunuz. Bu mantığı kullanmaya çalışın ve değerlendirin 10 - 4 - 3.
Pshemo

1
Bu hatanın, introcs.cs.princeton.edu/java/11precedence'ın üst kısmında +tekli operatör (sağdan sola ilişkilendirilebilirlik vardır) olmasından kaynaklandığından şüpheleniyorum , ancak + ve - aynı çarpımsal * /% sol doğru çağrışımsallığa.
Pshemo

İyi tespit edildi ve buna göre düzeltildi, teşekkürler Pshemo
Mark Burleigh

Bu cevap, önceliği ve çağrışımı açıklar, ancak Eric Lippert'in açıkladığı gibi, soru, çok farklı olan değerlendirme sırası ile ilgilidir. Aslında bu soruya cevap vermiyor.
Fabio,
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.