Các quy tắc cho thứ tự đánh giá trong Java là gì?


86

Tôi đang đọc một số văn bản Java và nhận được mã sau:

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

Trong văn bản, tác giả không giải thích rõ ràng và tác dụng của dòng cuối cùng là: a[1] = 0;

Tôi không chắc rằng tôi hiểu: đánh giá diễn ra như thế nào?


19
Sự nhầm lẫn dưới đây về lý do tại sao điều này xảy ra có nghĩa là bạn không bao giờ nên làm điều này, vì nhiều người sẽ phải nghĩ về những gì nó thực sự làm, thay vì nó hiển nhiên.
Martijn

Câu trả lời thích hợp cho bất kỳ câu hỏi nào như thế này là "đừng làm vậy!" Bài tập nên được coi như một tuyên bố; việc sử dụng phép gán như một biểu thức trả về một giá trị phải gây ra lỗi trình biên dịch hoặc ít nhất là một cảnh báo. Đừng làm vậy.
Mason Wheeler

Câu trả lời:


173

Hãy để tôi nói điều này rất rõ ràng, bởi vì mọi người luôn hiểu sai điều này:

Thứ tự đánh giá biểu thị phụ độc lập với cả tính liên kết và mức độ ưu tiên . Tính liên kết và mức độ ưu tiên xác định thứ tự các toán tử được thực hiện nhưng không xác định thứ tự các biểu thức con được đánh giá. Câu hỏi của bạn là về thứ tự mà các biểu thức phụ được đánh giá.

Hãy cân nhắc A() + B() + C() * D(). Phép nhân được ưu tiên cao hơn phép cộng và phép cộng có tính chất kết hợp trái, vì vậy điều này tương đương với (A() + B()) + (C() * D()) Nhưng việc biết điều đó chỉ cho bạn biết rằng phép cộng đầu tiên sẽ xảy ra trước phép cộng thứ hai và phép nhân sẽ xảy ra trước phép cộng thứ hai. Nó không cho bạn biết A (), B (), C () và D () sẽ được gọi theo thứ tự nào! (Nó cũng không cho bạn biết liệu phép nhân xảy ra trước hay sau phép cộng đầu tiên.) Bạn hoàn toàn có thể tuân theo các quy tắc ưu tiên và kết hợp bằng cách biên dịch như sau:

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ất cả các quy tắc ưu tiên và liên kết đều được tuân theo ở đó - phép cộng đầu tiên xảy ra trước phép cộng thứ hai và phép nhân xảy ra trước phép cộng thứ hai. Rõ ràng là chúng ta có thể thực hiện các lệnh gọi đến A (), B (), C () và D () theo bất kỳ thứ tự nào mà vẫn tuân theo các quy tắc ưu tiên và kết hợp!

Chúng ta cần một quy tắc không liên quan đến quy tắc ưu tiên và kết hợp để giải thích thứ tự mà các biểu thức con được đánh giá. Quy tắc liên quan trong Java (và C #) là "biểu thức con được đánh giá từ trái sang phải". Vì A () xuất hiện bên trái C (), A () được đánh giá đầu tiên, bất kể thực tế là C () tham gia vào một phép nhân và A () chỉ tham gia vào một phép cộng.

Vì vậy, bây giờ bạn có đủ thông tin để trả lời câu hỏi của bạn. Trong a[b] = b = 0các quy tắc của thuyết kết hợp nói rằng điều này là a[b] = (b = 0);vậy nhưng điều đó không có nghĩa là b=0chạy trước! Các quy tắc ưu tiên nói rằng lập chỉ mục có mức ưu tiên cao hơn so với gán, nhưng điều đó không có nghĩa là trình lập chỉ mục chạy trước chỉ định ngoài cùng bên phải .

(CẬP NHẬT: Phiên bản trước của câu trả lời này có một số thiếu sót nhỏ và thực tế không quan trọng trong phần mà sau đó tôi đã sửa. Tôi cũng đã viết một bài blog mô tả lý do tại sao các quy tắc này hợp lý trong Java và C # tại đây: https: // ericlippert.com/2019/01/18/indexer-error-case/ )

Tính ưu tiên và tính liên kết chỉ cho chúng ta biết rằng việc gán số 0 cho bphải xảy ra trước khi phép gán cho a[b], bởi vì việc gán số 0 tính giá trị được gán trong thao tác lập chỉ mục. Ưu tiên và associativity mình nói gì về việc liệu a[b]được đánh giá trước khi hoặc sau khi sự b=0.

Một lần nữa, điều này cũng giống như: A()[B()] = C()- Tất cả những gì chúng ta biết là việc lập chỉ mục phải xảy ra trước khi gán. Chúng tôi không biết liệu A (), B () hay C () chạy trước dựa trên mức độ ưu tiên và tính kết hợp . Chúng tôi cần một quy tắc khác để cho chúng tôi biết điều đó.

Một lần nữa, quy tắc là "khi bạn có lựa chọn về việc phải làm đầu tiên, hãy luôn đi từ trái sang phải". Tuy nhiên, có một nếp nhăn thú vị trong kịch bản cụ thể này. Tác dụng phụ của một ngoại lệ được ném gây ra bởi tập hợp rỗng hoặc chỉ mục nằm ngoài phạm vi được coi là một phần của tính toán phía bên trái của bài tập hay một phần của chính việc tính toán bài tập đó? Java chọn cái sau. (Tất nhiên, đây là sự phân biệt chỉ quan trọng nếu mã đã sai , bởi vì mã đúng không bỏ tham chiếu rỗng hoặc chuyển chỉ mục xấu ngay từ đầu.)

Vậy điều gì xảy ra?

  • a[b]bên trái của b=0, do đó, a[b]chạy trước , dẫn đến a[1]. Tuy nhiên, việc kiểm tra tính hợp lệ của hoạt động lập chỉ mục này bị trì hoãn.
  • Sau đó, điều b=0xảy ra.
  • Sau đó, xác minh ahợp lệ và a[1]nằm trong phạm vi sẽ xảy ra
  • Việc gán giá trị a[1]sẽ xảy ra sau cùng.

Vì vậy, mặc dù trong trường hợp cụ thể này, có một số điều tinh tế cần xem xét đối với những trường hợp lỗi hiếm gặp không nên xảy ra trong mã chính xác ngay từ đầu, nhưng nói chung, bạn có thể suy luận: những thứ bên trái xảy ra trước những thứ bên phải . Đó là quy tắc bạn đang tìm kiếm. Nói về mức độ ưu tiên và tính liên kết vừa khó hiểu vừa không liên quan.

Người có được công cụ này sai mọi lúc , ngay cả những người nên biết tốt hơn. Tôi đã chỉnh sửa quá nhiều sách lập trình nêu các quy tắc không chính xác, vì vậy không có gì ngạc nhiên khi rất nhiều người có niềm tin hoàn toàn không đúng về mối quan hệ giữa mức độ ưu tiên / tính liên kết và thứ tự đánh giá - cụ thể là trong thực tế không có mối quan hệ này ; chúng độc lập.

Nếu chủ đề này khiến bạn quan tâm, hãy xem các bài viết của tôi về chủ đề này để đọc thêm:

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

Chúng là về C #, nhưng hầu hết những thứ này đều áp dụng tốt cho Java.


6
Cá nhân tôi thích mô hình tinh thần trong đó ở bước đầu tiên bạn xây dựng một cây biểu thức bằng cách sử dụng mức độ ưu tiên và tính liên kết. Và trong bước thứ hai, đánh giá một cách đệ quy cây đó bắt đầu từ gốc. Với việc đánh giá một nút là: Đánh giá các nút con ngay lập tức từ trái sang phải và sau đó là chính nốt. | Một ưu điểm của mô hình này là nó xử lý rất tốt trường hợp các toán tử nhị phân có tác dụng phụ. Nhưng ưu điểm chính là nó chỉ đơn giản là phù hợp với bộ não của tôi hơn.
CodesInChaos

Tôi có chính xác rằng C ++ không đảm bảo điều này không? Còn Python thì sao?
Neil G

2
@Neil: C ++ không đảm bảo bất cứ điều gì về thứ tự đánh giá và chưa bao giờ làm như vậy. (Cũng không phải C.) Python đảm bảo nó nghiêm ngặt theo thứ tự ưu tiên; không giống như mọi thứ khác, phép gán là R2L.
Donal Fellows

2
@aroth đối với tôi bạn nghe có vẻ bối rối. Và các quy tắc ưu tiên chỉ ngụ ý rằng trẻ em cần được đánh giá trước cha mẹ. Nhưng họ không nói gì về thứ tự đánh giá trẻ em. Java và C # đã chọn từ trái sang phải, C và C ++ đã chọn hành vi không xác định.
CodesInChaos

6
@noober: OK, hãy xem xét: M (A () + B (), C () * D (), E () + F ()). Mong muốn của bạn là các biểu thức con được đánh giá theo thứ tự nào? Có nên đánh giá C () và D () trước A (), B (), E () và F () vì phép nhân được ưu tiên cao hơn phép cộng không? Thật dễ dàng để nói rằng "rõ ràng" thứ tự phải khác. Việc đưa ra một quy tắc thực tế bao gồm tất cả các trường hợp khá khó khăn hơn. Các nhà thiết kế của C # và Java đã chọn một quy tắc đơn giản, dễ giải thích: "đi từ trái sang phải". Sự thay thế được đề xuất của bạn cho nó là gì và tại sao bạn tin rằng quy tắc của mình tốt hơn?
Eric Lippert

32

Câu trả lời tuyệt vời của Eric Lippert vẫn không hữu ích một cách thích hợp vì nó đang nói về một ngôn ngữ khác. Đây là Java, trong đó Đặc tả ngôn ngữ Java là mô tả chính xác về ngữ nghĩa. Đặc biệt, §15.26.1 có liên quan vì điều đó mô tả thứ tự đánh giá cho =toán tử (tất cả chúng ta đều biết rằng nó là liên kết phải, đúng chứ?). Cắt giảm một chút đến những phần mà chúng tôi quan tâm trong câu hỏi này:

Nếu biểu thức toán hạng bên trái là biểu thức truy cập mảng ( §15.13 ), thì cần thực hiện nhiều bước:

  • Đầu tiên, biểu thức con tham chiếu mảng của biểu thức truy cập mảng toán hạng bên trái được đánh giá. Nếu đánh giá này hoàn thành đột ngột, thì biểu thức gán hoàn thành đột ngột vì lý do tương tự; biểu thức con chỉ mục (của biểu thức truy cập mảng toán hạng bên trái) và toán hạng bên phải không được đánh giá và không có phép gán nào xảy ra.
  • Nếu không, biểu thức con chỉ số của biểu thức truy cập mảng toán hạng bên trái được đánh giá. Nếu đánh giá này hoàn thành đột ngột, thì biểu thức gán hoàn thành đột ngột vì lý do tương tự và toán hạng bên phải không được đánh giá và không có phép gán nào xảy ra.
  • Nếu không, toán hạng bên phải được đánh giá. Nếu đánh giá này hoàn thành đột ngột, thì biểu thức gán hoàn thành đột ngột vì lý do tương tự và không có phép gán nào xảy ra.

[… Sau đó nó tiếp tục mô tả ý nghĩa thực tế của chính nhiệm vụ, mà chúng ta có thể bỏ qua ở đây cho ngắn gọn…]

Tóm lại, Java có một thứ tự đánh giá được xác định rất chặt chẽ, chính xác từ trái sang phải trong các đối số cho bất kỳ lệnh gọi toán tử hoặc phương thức nào. Phép gán mảng là một trong những trường hợp phức tạp hơn, nhưng ngay cả khi nó vẫn có L2R. (JLS khuyên bạn không nên viết mã cần các loại ràng buộc ngữ nghĩa phức tạp này và tôi cũng vậy: bạn có thể gặp quá nhiều rắc rối chỉ với một lần gán cho mỗi câu lệnh!)

C và C ++ chắc chắn khác với Java trong lĩnh vực này: các định nghĩa ngôn ngữ của chúng để lại thứ tự đánh giá không được xác định một cách có chủ ý để cho phép tối ưu hóa nhiều hơn. C # có vẻ giống Java, nhưng tôi không biết rõ về tài liệu của nó để có thể chỉ ra định nghĩa chính thức. (Tuy nhiên, điều này thực sự khác nhau tùy theo ngôn ngữ, Ruby hoàn toàn là L2R, cũng như Tcl - mặc dù điều đó thiếu toán tử gán cho mỗi lý do không liên quan ở đây - và Python là L2R nhưng R2L về phép gán , tôi thấy kỳ lạ nhưng bạn cứ làm .)


11
Vì vậy, những gì bạn đang nói là câu trả lời của Eric là sai bởi vì Java định nghĩa nó cụ thể là chính xác những gì anh ấy nói?
cấu hình

8
Quy tắc liên quan trong Java (và C #) là "biểu thức phụ được đánh giá từ trái sang phải" - Tôi nghe như thể anh ấy đang nói về cả hai.
cấu hình

2
Một chút bối rối ở đây - điều này làm cho câu trả lời trên của Eric Lippert ít đúng hơn, hay nó chỉ trích dẫn một tài liệu tham khảo cụ thể về lý do tại sao nó đúng?
GreenieMeanie

5
@Greenie: Câu trả lời của Eric là đúng, nhưng như tôi đã nói, bạn không thể hiểu sâu sắc từ một ngôn ngữ trong lĩnh vực này và áp dụng nó cho ngôn ngữ khác mà không cẩn thận. Vì vậy, tôi đã trích dẫn nguồn cuối cùng.
Donal Fellows

1
điều kỳ lạ là, bên phải được đánh giá trước khi biến bên trái được giải quyết; trong a[-1]=c, cđược đánh giá, trước khi -1được công nhận là không hợp lệ.
ZhongYu

5
a[b] = b = 0;

1) toán tử lập chỉ mục mảng có độ ưu tiên cao hơn toán tử gán (xem câu trả lời này ):

(a[b]) = b = 0;

2) Theo 15,26. Người điều hành nhiệm vụ của JLS

Có 12 toán tử gán; tất cả đều là liên kết phải về mặt cú pháp (chúng nhóm từ phải sang trái). Như vậy, a = b = c có nghĩa là a = (b = c), gán giá trị của c cho b và sau đó gán giá trị của b cho a.

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

3) Theo 15.7. Thứ tự đánh giá của JLS

Ngôn ngữ lập trình Java đảm bảo rằng các toán hạng của các toán tử dường như được đánh giá theo một thứ tự đánh giá cụ thể, cụ thể là từ trái sang phải.

Toán hạng bên trái của toán tử nhị phân dường như được đánh giá đầy đủ trước khi bất kỳ phần nào của toán hạng bên phải được đánh giá.

Vì thế:

a) được (a[b])đánh giá đầu tiên đểa[1]

b) sau đó được (b=0)đánh giá0

c) (a[1] = 0)đánh giá cuối cùng


1

Mã của bạn tương đương với:

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

mà giải thích kết quả.


7
Câu hỏi là tại sao lại như vậy.
Mat

@Mat Câu trả lời là bởi vì đây là những gì xảy ra ẩn khi xem xét mã được cung cấp trong câu hỏi. Đó là cách đánh giá xảy ra.
Jérôme Verstrynge

1
Vâng tôi biết. Vẫn không trả lời câu hỏi thông qua IMO, đó là lý do tại sao đây là cách đánh giá xảy ra.
Mat

1
@Mat 'Tại sao đây là cách đánh giá xảy ra?' không phải là câu hỏi được hỏi. 'đánh giá diễn ra như thế nào?' là câu hỏi được hỏi.
Jérôme Verstrynge

1
@JVerstry: Chúng không tương đương với nhau như thế nào? Các tài liệu tham khảo subexpression mảng của các toán hạng tay trái các toán hạng bên trái. Vì vậy, nói "làm ở ngoài cùng bên trái trước" cũng giống như nói "thực hiện tham chiếu mảng trước". Nếu các tác giả đặc tả Java đã chọn cách dài dòng và thừa một cách không cần thiết trong việc giải thích quy tắc cụ thể này, thì tốt cho họ; kiểu này thật khó hiểu và nên ít chữ hơn. Nhưng tôi không thấy đặc điểm ngắn gọn của tôi khác biệt về mặt ngữ nghĩa như thế nào so với đặc điểm chuỗi xoắn của chúng.
Eric Lippert

0

Hãy xem xét một ví dụ khác chuyên sâu hơn bên dưới.

Theo nguyên tắc chung của ngón tay cái:

Tốt nhất bạn nên có sẵn một bảng Quy tắc ưu tiên và sự liên kết để đọc khi giải các câu hỏi này, ví dụ: http://introcs.cs.princeton.edu/java/11precedence/

Đây là một ví dụ tốt:

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

Câu hỏi: Đầu ra của Dòng trên là gì?

Trả lời: Áp dụng Quy tắc ưu tiên và liên kết

Bước 1: Theo quy tắc ưu tiên: các toán tử / và * được ưu tiên hơn các toán tử + -. Do đó, điểm bắt đầu để thực hiện phương trình này sẽ bị thu hẹp thành:

100/10*2

Bước 2: Theo quy tắc và mức độ ưu tiên: / và * được ưu tiên ngang nhau.

Các toán tử as / và * được ưu tiên như nhau, chúng ta cần xem xét sự kết hợp giữa các toán tử đó.

Theo QUY TẮC HỖ TRỢ của hai toán tử cụ thể này, chúng tôi bắt đầu thực hiện phương trình từ LEFT TO RIGHT tức là 100/10 được thực thi trước:

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

Bước 3: Phương trình bây giờ ở trạng thái thực thi sau:

=3+20-13

Theo quy tắc và mức độ ưu tiên: + và - được ưu tiên bằng nhau.

Bây giờ chúng ta cần xem xét sự kết hợp giữa các toán tử + và -. Theo sự kết hợp của hai toán tử cụ thể này, chúng ta bắt đầu thực hiện phương trình từ LEFT đến RIGHT, tức là 3 + 20 được thực thi trước:

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

10 là đầu ra chính xác khi biên dịch

Một lần nữa, điều quan trọng là bạn phải có một bảng Thứ tự quy tắc ưu tiên và sự liên kết với bạn khi giải quyết những câu hỏi này, ví dụ: http://introcs.cs.princeton.edu/java/11precedence/


1
Bạn đang nói rằng "sự kết hợp giữa các toán tử + và -" là "PHẢI TRÁI". Hãy thử sử dụng logic đó và đánh giá 10 - 4 - 3.
Pshemo

1
Tôi nghi ngờ rằng sai lầm này có thể do thực tế là ở đầu intcs.cs.princeton.edu/java/11precedence + là toán tử một ngôi (có tính kết hợp từ phải sang trái), nhưng cộng + và - giống như phép nhân * /% còn lại đến sự kết hợp đúng đắn.
Pshemo

Vâng phát hiện và sửa đổi cho phù hợp, cảm ơn bạn Pshemo
Đánh dấu Burleigh

Câu trả lời này giải thích mức độ ưu tiên và tính liên kết, nhưng, như Eric Lippert đã giải thích , câu hỏi là về thứ tự đánh giá, rất khác. Trên thực tế, điều này không trả lời câu hỏi.
Fabio nói Khôi phục Monica
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.