Thiếu câu lệnh return trong một phương thức non-void biên dịch


189

Tôi đã gặp một tình huống trong đó một phương thức không trống đang thiếu một câu lệnh return và mã vẫn biên dịch. Tôi biết rằng các câu lệnh sau vòng lặp while không thể truy cập được (mã chết) và sẽ không bao giờ được thực thi. Nhưng tại sao trình biên dịch thậm chí không cảnh báo về việc trả lại một cái gì đó? Hoặc tại sao một ngôn ngữ sẽ cho phép chúng ta có một phương thức không trống có một vòng lặp vô hạn và không trả về bất cứ thứ gì?

public int doNotReturnAnything() {
    while(true) {
        //do something
    }
    //no return statement
}

Nếu tôi thêm một câu lệnh break (thậm chí là một điều kiện) trong vòng lặp while, trình biên dịch sẽ phàn nàn về các lỗi khét tiếng: Method does not return a valuetrong Eclipse và Not all code paths return a valuetrong Visual Studio.

public int doNotReturnAnything() {
    while(true) {
        if(mustReturn) break;
        //do something
    }
    //no return statement
}

Điều này đúng với cả Java và C #.


3
Câu hỏi hay. Tôi sẽ quan tâm đến lý do cho việc này.
Erik Schierboom

18
Một dự đoán: đó là một vòng lặp vô hạn, vì vậy một luồng điều khiển trả về là không liên quan?
Lews Therin

5
"tại sao một ngôn ngữ sẽ cho phép chúng ta có một phương thức không trống có một vòng lặp vô hạn và không trả về bất cứ thứ gì?" <- trong khi có vẻ ngu ngốc, câu hỏi cũng có thể bị đảo ngược: tại sao điều này không được phép? Đây có phải là mã thực tế không?
fge

3
Đối với Java, bạn có thể tìm thấy một lời giải thích hay trong câu trả lời này .
Keppil

4
Giống như những người khác đã giải thích, đó là vì trình biên dịch đủ thông minh để biết vòng lặp là vô hạn. Lưu ý rằng trình biên dịch không chỉ cho phép trả về bị thiếu, nó thực thi nó bởi vì nó biết bất cứ điều gì sau khi vòng lặp không thể truy cập được. Ít nhất là trong Netbeans, nó sẽ thực sự phàn nàn về việc unreachable statementnếu có bất cứ điều gì sau vòng lặp.
Supr

Câu trả lời:


240

Tại sao một ngôn ngữ sẽ cho phép chúng ta có một phương thức không trống có một vòng lặp vô hạn và không trả về bất cứ thứ gì?

Quy tắc cho các phương thức không trống là mọi đường dẫn mã trả về phải trả về một giá trị và quy tắc đó được thỏa mãn trong chương trình của bạn: không có đường dẫn mã nào trả về giá trị trả về. Quy tắc không phải là "mọi phương thức không trống phải có đường dẫn mã trả về".

Điều này cho phép bạn viết các phương thức sơ khai như:

IEnumerator IEnumerable.GetEnumerator() 
{ 
    throw new NotImplementedException(); 
}

Đó là một phương pháp không trống. Nó được một phương pháp phi khoảng trống để đáp ứng giao diện. Nhưng có vẻ ngớ ngẩn khi thực hiện việc này là bất hợp pháp vì nó không trả lại bất cứ điều gì.

Phương pháp của bạn có một điểm kết thúc không thể truy cập được vì một goto(hãy nhớ, while(true)đây chỉ là một cách dễ chịu hơn để viết goto) thay vì một throw(là một hình thức khác goto) không liên quan.

Tại sao trình biên dịch thậm chí không cảnh báo về việc trả lại một cái gì đó?

Bởi vì trình biên dịch không có bằng chứng tốt cho thấy mã sai. Ai đó đã viết while(true)và có vẻ như người đã làm điều đó biết họ đang làm gì.

Tôi có thể đọc thêm về phân tích khả năng tiếp cận trong C # ở đâu?

Xem các bài viết của tôi về chủ đề này, ở đây:

ATBG: khả năng tiếp cận thực tế và de jure

Và bạn cũng có thể xem xét việc đọc đặc tả C #.


Vậy tại sao mã này đưa ra lỗi thời gian biên dịch: public int doNotReturnAnything() { boolean flag = true; while (flag) { //Do something } //no return } Mã này cũng không có điểm dừng. Vì vậy, bây giờ làm thế nào trình biên dịch biết mã là sai.
Sandeep Poonia

@SandeepPoonia: Bạn có thể muốn đọc các câu trả lời khác ở đây ... Trình biên dịch chỉ có thể phát hiện các điều kiện nhất định.
Daniel Hilgarth

9
@SandeepPoonia: cờ boolean có thể được thay đổi trong thời gian chạy, vì nó không phải là hằng số, do đó vòng lặp không nhất thiết là vô hạn.
Schmuli

9
@SandeepPoonia: Trong C # các đặc điểm kỹ thuật nói rằng if, while, for, switch, vv, nhánh cấu trúc hoạt động trên các hằng số được coi là chi nhánh vô điều kiện bởi trình biên dịch. Định nghĩa chính xác của biểu thức hằng là trong spec. Các câu trả lời cho câu hỏi của bạn là trong đặc điểm kỹ thuật; Lời khuyên của tôi là bạn đọc nó.
Eric Lippert

1
Every code path that returns must return a valueCâu trả lời tốt nhất. Giải quyết rõ ràng cả hai câu hỏi. Cảm ơn
cPu1

38

Trình biên dịch Java đủ thông minh để tìm mã không thể truy cập (mã sau whilevòng lặp)

và vì nó không thể truy cập được , nên không có điểm nào để thêm một returntuyên bố ở đó (sau khi whilekết thúc)

cùng đi với điều kiện if

public int get() {
   if(someBoolean) {   
     return 10;
   }
   else {
     return 5;
   }
   // there is no need of say, return 11 here;
}

do điều kiện boolean someBooleanchỉ có thể đánh giá một trong hai truehoặc false, nên không cần phải cung cấp một return cách rõ ràng sau đó if-else, bởi vì mã đó không thể truy cập được và Java không phàn nàn về điều đó.


4
Tôi không nghĩ rằng điều này thực sự giải quyết câu hỏi. Điều này trả lời tại sao bạn không cần một returncâu lệnh trong mã không thể truy cập được, nhưng không liên quan gì đến lý do tại sao bạn không cần bất kỳ return câu lệnh nào trong mã của OP.
Bobson

Nếu trình biên dịch Java đủ thông minh để tìm mã không thể truy cập (mã sau vòng lặp while) thì tại sao các mã bên dưới này biên dịch, cả hai đều có mã không thể truy cập nhưng phương thức với yêu cầu trả về câu lệnh nhưng phương thức trong một thời gian thì không. public int doNotReturnAnything() { if(true){ System.exit(1); } return 11; } public int doNotReturnAnything() { while(true){ System.exit(1); } return 11;// Compiler error: unreachable code }
Sandeep Poonia

@SandeepPoonia: Bởi vì trình biên dịch không biết rằng System.exit(1)sẽ giết chương trình. Nó chỉ có thể phát hiện một số loại mã không thể truy cập.
Daniel Hilgarth

@Daniel Hilgarth: Trình biên dịch Ok không biết System.exit(1)sẽ giết chương trình, chúng ta có thể sử dụng bất kỳ return statement, bây giờ trình biên dịch nhận ra return statements. Và hành vi là như nhau, trả lại yêu cầu if conditionnhưng không cho while.
Sandeep Poonia

1
Trình diễn tốt phần không thể truy cập mà không sử dụng trường hợp đặc biệt nhưwhile(true)
Matthew

17

Trình biên dịch biết rằng whilevòng lặp sẽ không bao giờ ngừng thực thi, do đó phương thức sẽ không bao giờ kết thúc, do đó returnkhông cần một câu lệnh.


13

Do vòng lặp của bạn đang thực hiện trên một hằng số - trình biên dịch biết rằng đó là một vòng lặp vô hạn - có nghĩa là phương thức không bao giờ có thể quay trở lại.

Nếu bạn sử dụng một biến - trình biên dịch sẽ thực thi quy tắc:

Điều này sẽ không được biên dịch:

// Define other methods and classes here
public int doNotReturnAnything() {
    var x = true;

    while(x == true) {
        //do something
    }
    //no return statement - won't compile
}

Nhưng điều gì xảy ra nếu "làm một cái gì đó" không liên quan đến việc sửa đổi x theo bất kỳ cách nào .. trình biên dịch không thông minh để tìm ra nó? Bum :(
Lát Therin

Không - không giống như nó.
Dave Bish

@Lews nếu bạn có một phương thức được đánh dấu là return int, nhưng thực tế không trả về, thì bạn nên vui mừng rằng trình biên dịch gắn cờ này, vì vậy bạn có thể đánh dấu phương thức là void hoặc sửa nó nếu bạn không có ý định đó hành vi.
MikeFHay

@MikeFHay Yep, không tranh chấp điều đó.
Lát Therin

1
Tôi có thể sai nhưng một số trình sửa lỗi cho phép sửa đổi các biến. Ở đây, trong khi x không được sửa đổi bởi mã và nó sẽ được tối ưu hóa bởi JIT, người ta có thể sửa đổi x thành false và phương thức sẽ trả về một cái gì đó (nếu điều đó được trình gỡ lỗi C # cho phép).
Maciej Piechotka

11

Đặc tả Java định nghĩa một khái niệm gọi là Unreachable statements. Bạn không được phép có một tuyên bố không thể truy cập được trong mã của mình (đó là lỗi thời gian biên dịch). Bạn thậm chí không được phép có một tuyên bố trở lại sau một thời gian (đúng); tuyên bố trong Java. Một while(true);tuyên bố làm cho các tuyên bố sau không thể truy cập theo định nghĩa, do đó bạn không cần một returntuyên bố.

Lưu ý rằng trong khi vấn đề Dừng là không thể giải quyết được trong trường hợp chung, định nghĩa của Tuyên bố không thể truy cập nghiêm ngặt hơn là chỉ dừng lại. Đó là quyết định các trường hợp rất cụ thể trong đó một chương trình chắc chắn không dừng lại. Về mặt lý thuyết, trình biên dịch không thể phát hiện tất cả các vòng lặp vô hạn và các câu lệnh không thể truy cập được nhưng nó phải phát hiện các trường hợp cụ thể được xác định trong đặc tả (ví dụ: while(true)trường hợp)


7

Trình biên dịch đủ thông minh để tìm ra rằng whilevòng lặp của bạn là vô hạn.

Vì vậy, trình biên dịch không thể nghĩ cho bạn. Nó không thể đoán tại sao bạn viết mã đó. Cùng là viết tắt của các giá trị trả về của các phương thức. Java sẽ không phàn nàn nếu bạn không làm gì với các giá trị trả về của phương thức.

Để trả lời câu hỏi của bạn:

Trình biên dịch sẽ phân tích mã của bạn và sau khi phát hiện ra rằng không có đường dẫn thực thi nào dẫn đến việc kết thúc chức năng, nó kết thúc với OK.

Có thể có những lý do chính đáng cho một vòng lặp vô hạn. Ví dụ, rất nhiều ứng dụng sử dụng một vòng lặp chính vô hạn. Một ví dụ khác là một máy chủ web có thể chờ đợi yêu cầu vô thời hạn.


7

Trong lý thuyết loại, có một thứ gọi là loại dưới cùng là một lớp con của mọi loại khác (!) Và được sử dụng để biểu thị sự không kết thúc giữa các thứ khác. (Các ngoại lệ có thể được tính là một loại không chấm dứt - bạn không chấm dứt thông qua đường dẫn thông thường.)

Vì vậy, từ góc độ lý thuyết, những câu lệnh không kết thúc này có thể được coi là trả về một cái gì đó thuộc loại Dưới cùng, là một kiểu con của int, do đó, bạn sẽ nhận được giá trị trả về của mình sau khi từ góc độ loại. Và nó hoàn toàn ổn khi nó không có nghĩa là một loại có thể là một lớp con của mọi thứ khác bao gồm cả int vì bạn không bao giờ thực sự trả về một loại.

Trong mọi trường hợp, thông qua lý thuyết loại rõ ràng hoặc không, trình biên dịch (người viết trình biên dịch) nhận ra rằng việc yêu cầu giá trị trả về sau câu lệnh không kết thúc là ngớ ngẩn: không có trường hợp nào bạn có thể cần giá trị đó. (Thật tuyệt khi trình biên dịch của bạn cảnh báo bạn khi biết điều gì đó sẽ không chấm dứt nhưng có vẻ như bạn muốn nó trả lại một cái gì đó. Nhưng tốt hơn hết là bạn nên kiểm tra kiểu chữ, vì có thể bạn cần chữ ký loại cách cho một số lý do khác (ví dụ: phân lớp) nhưng bạn thực sự muốn không chấm dứt.)


6

Không có tình huống trong đó hàm có thể đi đến cuối mà không trả về một giá trị thích hợp. Do đó, không có gì để trình biên dịch phàn nàn.


5

Visual studio có công cụ thông minh để phát hiện nếu bạn đã gõ một kiểu trả về thì nó sẽ có một câu lệnh return với hàm / phương thức.

Như trong PHP Kiểu trả về của bạn là đúng nếu bạn chưa trả lại bất cứ thứ gì. trình biên dịch nhận 1 nếu không có gì trở lại.

Như thế này

public int doNotReturnAnything() {
    while(true) {
        //do something
    }
    //no return statement
}

Trình biên dịch biết rằng trong khi bản thân câu lệnh có bản chất infinte nên không xem xét nó. và trình biên dịch php sẽ tự động trở thành true nếu bạn viết một điều kiện trong biểu thức while.

Nhưng không phải trong trường hợp của VS, nó sẽ trả về cho bạn một lỗi trong ngăn xếp.


4

Vòng lặp while của bạn sẽ chạy mãi mãi và do đó sẽ không xuất hiện trong khi; nó sẽ tiếp tục thực thi. Do đó, phần bên ngoài của {} là không thể truy cập được và không có điểm nào bằng văn bản trả lại hay không. Trình biên dịch đủ thông minh để tìm ra phần nào có thể truy cập và phần nào không thể truy cập được.

Thí dụ:

public int xyz(){
    boolean x=true;

    while(x==true){
        // do something  
    }

    // no return statement
}

Đoạn mã trên sẽ không được biên dịch, vì có thể có trường hợp giá trị của biến x được sửa đổi bên trong phần thân của vòng lặp while. Vì vậy, điều này làm cho phần bên ngoài của vòng lặp while có thể truy cập! Và do đó trình biên dịch sẽ đưa ra một lỗi 'không tìm thấy câu lệnh return'.

Trình biên dịch không đủ thông minh (hoặc khá lười biếng;)) để tìm hiểu xem giá trị của x sẽ được sửa đổi hay không. Hy vọng điều này sẽ xóa tất cả mọi thứ.


7
Trong trường hợp này thực sự không phải là một câu hỏi về trình biên dịch đủ thông minh. Trong C # 2.0, trình biên dịch đủ thông minh để biết đó int x = 1; while(x * 0 == 0) { ... }là một vòng lặp vô hạn, nhưng đặc tả nói rằng trình biên dịch chỉ nên thực hiện các khấu trừ luồng điều khiển khi biểu thức vòng lặp không đổi và đặc tả xác định biểu thức hằng là không chứa biến . Trình biên dịch do đó quá thông minh . Trong C # 3 tôi đã làm cho trình biên dịch khớp với đặc tả và từ đó nó cố tình kém thông minh hơn về các loại biểu thức này.
Eric Lippert

4

"Tại sao trình biên dịch thậm chí không cảnh báo về việc trả lại một cái gì đó? Hoặc tại sao một ngôn ngữ sẽ cho phép chúng ta có một phương thức không trống có một vòng lặp vô hạn và không trả về bất cứ thứ gì?".

Mã này cũng hợp lệ trong tất cả các ngôn ngữ khác (có lẽ ngoại trừ Haskell!). Bởi vì giả định đầu tiên là chúng tôi "cố ý" viết một số mã.

Và có những tình huống mà mã này có thể hoàn toàn hợp lệ như nếu bạn sẽ sử dụng nó như một luồng; hoặc nếu nó trả về a Task<int>, bạn có thể thực hiện một số kiểm tra lỗi dựa trên giá trị int được trả về - không nên trả về.


3

Tôi có thể sai nhưng một số trình sửa lỗi cho phép sửa đổi các biến. Ở đây, trong khi x không được sửa đổi bởi mã và nó sẽ được tối ưu hóa bởi JIT, người ta có thể sửa đổi x thành false và phương thức sẽ trả về một cái gì đó (nếu điều đó được cho phép bởi trình gỡ lỗi C #).


1

Các chi tiết cụ thể của trường hợp Java cho trường hợp này (có lẽ rất giống với trường hợp C #) là để làm thế nào trình biên dịch Java xác định nếu một phương thức có thể trả về.

Cụ thể, các quy tắc là một phương thức có kiểu trả về phải không thể hoàn thành bình thường và thay vào đó phải luôn luôn hoàn thành đột ngột (đột ngột ở đây chỉ ra thông qua câu lệnh trả về hoặc ngoại lệ) trên JLS 8.4.7 .

Nếu một phương thức được khai báo là có kiểu trả về, thì lỗi thời gian biên dịch xảy ra nếu phần thân của phương thức có thể hoàn thành bình thường. Nói cách khác, một phương thức có kiểu trả về chỉ phải trả về bằng cách sử dụng câu lệnh return cung cấp giá trị trả về; nó không được phép "thả ra khỏi phần cuối của cơ thể" .

Trình biên dịch xem xét liệu có thể chấm dứt bình thường hay không dựa trên các quy tắc được xác định trong JLS 14,21 Báo cáo không thể truy cập vì nó cũng xác định các quy tắc để hoàn thành bình thường.

Đáng chú ý, các quy tắc cho các câu lệnh không thể truy cập tạo ra một trường hợp đặc biệt chỉ dành cho các vòng lặp có truebiểu thức hằng xác định :

Một câu lệnh while có thể hoàn thành bình thường nếu ít nhất một trong những điều sau đây là đúng:

  • Câu lệnh while có thể truy cập và biểu thức điều kiện không phải là biểu thức hằng (§15.28) với giá trị đúng.

  • Có một câu lệnh break có thể truy cập mà thoát khỏi câu lệnh while.

Vì vậy, nếu whilecâu lệnh có thể hoàn thành bình thường , thì câu lệnh trả về bên dưới là cần thiết vì mã được coi là có thể truy cập và bất kỳ whilevòng lặp nào không có câu lệnh ngắt có thể tiếp cận hoặc truebiểu thức hằng được coi là có thể hoàn thành bình thường.

Những quy định này có nghĩa là bạn whiletuyên bố với một biểu thức đúng liên tục và không có một breakkhông bao giờ được coi là hoàn thành bình thường , và vì vậy bất kỳ mã bên dưới nó là không bao giờ được coi là có thể truy cập . Kết thúc của phương thức nằm dưới vòng lặp và vì mọi thứ bên dưới vòng lặp đều không thể truy cập được, nên kết thúc của phương thức, và do đó phương thức không thể hoàn thành bình thường (đó là điều mà trình biên dịch tìm kiếm).

if mặt khác, các tuyên bố không có sự miễn trừ đặc biệt liên quan đến các biểu thức không đổi được dành cho các vòng lặp.

Đối chiếu:

// I have a compiler error!
public boolean testReturn()
{
    final boolean condition = true;

    if (condition) return true;
}

Với:

// I compile just fine!
public boolean testReturn()
{
    final boolean condition = true;

    while (condition)
    {
        return true;
    }
}

Lý do cho sự khác biệt khá thú vị và là do mong muốn cho phép các cờ biên dịch có điều kiện không gây ra lỗi biên dịch (từ JLS):

Người ta có thể mong đợi câu lệnh if được xử lý theo cách sau:

  • Một câu lệnh if-then có thể hoàn thành bình thường nếu ít nhất một trong những điều sau đây là đúng:

    • Câu lệnh if-then có thể truy cập và biểu thức điều kiện không phải là biểu thức hằng có giá trị là đúng.

    • Các tuyên bố sau đó có thể hoàn thành bình thường.

    Câu lệnh then có thể truy cập được nếu câu lệnh if-then có thể truy cập được và biểu thức điều kiện không phải là biểu thức hằng có giá trị là sai.

  • Một câu lệnh if-then-other có thể hoàn thành bình thường nếu câu lệnh then có thể hoàn thành bình thường hoặc câu lệnh khác có thể hoàn thành bình thường.

    • Câu lệnh then có thể truy cập được nếu câu lệnh if-then-other có thể truy cập được và biểu thức điều kiện không phải là biểu thức hằng có giá trị là sai.

    • Câu lệnh other có thể truy cập được nếu câu lệnh if-then-other có thể truy cập được và biểu thức điều kiện không phải là biểu thức hằng có giá trị là đúng.

Cách tiếp cận này sẽ phù hợp với việc xử lý các cấu trúc điều khiển khác. Tuy nhiên, để cho phép câu lệnh if được sử dụng thuận tiện cho mục đích "biên dịch có điều kiện", các quy tắc thực tế khác nhau.

Ví dụ, câu lệnh sau dẫn đến lỗi thời gian biên dịch:

while (false) { x=3; }bởi vì tuyên bố x=3;không thể đạt được; nhưng trường hợp tương tự bề ngoài:

if (false) { x=3; }không dẫn đến lỗi thời gian biên dịch. Trình biên dịch tối ưu hóa có thể nhận ra rằng câu lệnh x=3;sẽ không bao giờ được thực thi và có thể chọn bỏ qua mã cho câu lệnh đó khỏi tệp lớp được tạo, nhưng câu lệnh x=3;không được coi là "không thể truy cập" theo nghĩa kỹ thuật được chỉ định ở đây.

Lý do căn bản cho cách xử lý khác nhau này là cho phép các lập trình viên định nghĩa "các biến cờ" như:

static final boolean DEBUG = false; và sau đó viết mã như:

if (DEBUG) { x=3; } Ý tưởng là có thể thay đổi giá trị DEBUG từ false thành true hoặc từ true thành false và sau đó biên dịch mã chính xác mà không có thay đổi nào khác đối với văn bản chương trình.

Tại sao câu lệnh break có điều kiện dẫn đến lỗi trình biên dịch?

Như được trích dẫn trong các quy tắc khả năng tiếp cận vòng lặp, một vòng lặp while cũng có thể hoàn thành bình thường nếu nó chứa một câu lệnh break có thể tiếp cận. Kể từ khi các quy tắc cho các reachability của một ifcủa tuyên bố sau đó khoản không mất tình trạng của ifxem xét ở tất cả, chẳng hạn một điều kiện iftuyên bố của sau đó khoản luôn được coi là có thể truy cập.

Nếu breakcó thể truy cập, thì mã sau vòng lặp một lần nữa cũng được coi là có thể truy cập. Vì không có mã có thể truy cập dẫn đến kết thúc đột ngột sau vòng lặp, nên phương thức sau đó được coi là có thể hoàn thành bình thường, và do đó trình biên dịch đánh dấu nó là một lỗi.

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.