Tại sao câu lệnh 'continue' không thể nằm trong khối 'cuối cùng'?


107

Tôi không có một vấn đề; Tôi chỉ tò mò. Hãy tưởng tượng tình huống sau:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

Điều này sẽ không biên dịch, vì nó làm tăng lỗi trình biên dịch CS0157 :

Điều khiển không thể rời khỏi nội dung của mệnh đề cuối cùng

Tại sao?


7
Vì thế. Chỉ tò mò. Nếu nó hoàn toàn có ý nghĩa với bạn tại sao nó không biên dịch, tại sao bạn muốn ai đó giải thích những gì đã có ý nghĩa? =)
J. Steen

14
tại sao bạn cần một khối continue;trong finally? nó không giống như continue;sau khi try -- catchkhối?
bansi

6
@ J.Steen Không, tôi biết rằng đó finally/continuelà một hạn chế của trình biên dịch C # :-) Tôi cũng tò mò về lý do của hạn chế này.
xanatos

5
@xanatos - Về mặt kỹ thuật, đó là một hạn chế của CIL cơ bản. Từ đặc tả ngôn ngữ : "Việc chuyển quyền kiểm soát không bao giờ được phép nhập một mệnh đề xử lý bắt hoặc cuối cùng ngoại trừ thông qua cơ chế xử lý ngoại lệ." và "Việc chuyển quyền kiểm soát ra khỏi vùng được bảo vệ chỉ được phép thông qua một lệnh ngoại lệ (leave, end.filter, end.catch hoặc end.finally)." Dòng brlệnh rẽ nhánh không thể thực hiện được điều này.
Chưa ký

1
@Unsigned: nó là một giới hạn tốt quá, cho phép nó sẽ là kỳ lạ :)
user7116

Câu trả lời:


150

finallykhối chạy cho dù một ngoại lệ được ném ra hay không. Nếu một ngoại lệ được ném ra, thì cái quái continuegì sẽ làm? Bạn không thể tiếp tục thực hiện vòng lặp, bởi vì một ngoại lệ không có sẵn sẽ chuyển quyền điều khiển sang một chức năng khác.

Ngay cả khi không có ngoại lệ nào được ném ra, finallysẽ chạy khi các câu lệnh chuyển điều khiển khác bên trong khối try / catch chạy, chẳng hạn như một return, chẳng hạn, điều này gây ra cùng một vấn đề.

Nói tóm lại, với ngữ nghĩa của finallynó, việc cho phép chuyển quyền điều khiển từ bên trong một finallykhối ra bên ngoài nó là không hợp lý.

Hỗ trợ điều này bằng một số ngữ nghĩa thay thế sẽ khó hiểu hơn là hữu ích, vì có những cách giải quyết đơn giản giúp hành vi dự định trở nên rõ ràng hơn. Vì vậy, bạn gặp lỗi và buộc phải suy nghĩ đúng đắn về vấn đề của mình. Đó là ý tưởng "ném bạn vào hố thành công" chung chung trong C #.

C #, bạn và ra nếu thành công

Nếu bạn muốn bỏ qua các ngoại lệ (thường xuyên hơn không phải là một ý tưởng tồi) và tiếp tục thực hiện vòng lặp, hãy sử dụng khối catch all:

foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

Nếu bạn chỉ muốn thực hiện continuekhi không có trường hợp ngoại lệ nào chưa được giải quyết, chỉ cần đặt continuebên ngoài khối thử.


12
Tôi sẽ chấp nhận đây là câu trả lời, bởi vì bạn hiểu lý do tại sao Microsoft có thể quyết định không chấp nhận tiếp tục cuối cùng. Có lẽ rằng hình ảnh đã thuyết phục tôi quá :)
lpaloub

20
bạn có thể nói rõ hơn về ý tưởng "ném bạn vào hố thành công" không? Tôi không hiểu :-D
Ant


13
Hình ảnh được thực hiện bởi Jon Skeet, btw. Tôi nhận được từ đây: msmvps.com/blogs/jon_skeet/archive/2010/09/02/…
R. Martinho Fernandes

1
Tất nhiên, một continuetrong một finallykhối sẽ ổn nếu nó chỉ tiếp tục một vòng lặp cục bộ được xác định bên trong finallykhối. Tuy nhiên, trong câu hỏi, nó cố gắng tiếp tục một vòng lặp "bên ngoài". Các câu lệnh tương tự mà bạn không thể có bên trong một finallykhối là return, break(khi thoát ra khỏi khối) và goto(khi đi đến một nhãn bên ngoài finallykhối). Đối với thảo luận Java liên quan, hãy xem Quay lại từ một khối cuối cùng trong Java .
Jeppe Stig Nielsen

32

Đây là một nguồn đáng tin cậy:

Câu lệnh continue không thể thoát khỏi khối cuối cùng (Phần 8.10). Khi một câu lệnh continue xảy ra trong một khối cuối cùng, mục tiêu của câu lệnh continue phải nằm trong cùng một khối cuối cùng; nếu không, lỗi thời gian biên dịch xảy ra.

Nó được lấy từ MSDN, 8.9.2 Câu lệnh tiếp tục .

Tài liệu nói rằng:

Các câu lệnh của khối cuối cùng luôn được thực thi khi điều khiển rời khỏi một câu lệnh try. Điều này đúng cho dù việc chuyển điều khiển xảy ra do thực hiện bình thường, do thực hiện câu lệnh break, continue, goto, hoặc return, hay kết quả của việc truyền một ngoại lệ ra khỏi câu lệnh try. Nếu một ngoại lệ được ném ra trong khi thực thi một khối cuối cùng, thì ngoại lệ đó sẽ được truyền sang câu lệnh try kèm theo tiếp theo. Nếu một ngoại lệ khác đang trong quá trình được truyền bá, thì ngoại lệ đó sẽ bị mất. Quá trình lan truyền một ngoại lệ được thảo luận thêm trong phần mô tả của câu lệnh ném (Phần 8.9.5).

Đó là từ đây 8.10 Câu lệnh thử .


31

Bạn có thể nghĩ nó có lý, nhưng thực ra nó không có ý nghĩa .

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

Bạn định làm gì để nghỉ hoặc tiếp tục khi một ngoại lệ được đưa ra? Nhóm biên dịch C # không muốn tự mình đưa ra quyết định bằng cách giả định breakhoặc continue. Thay vào đó, họ quyết định phàn nàn rằng tình hình nhà phát triển sẽ mơ hồ để chuyển giao quyền kiểm soát finally block.

Vì vậy, nhiệm vụ của nhà phát triển là phải trình bày rõ ràng những gì anh ta định làm thay vì trình biên dịch giả định một cái gì đó khác.

Tôi hy vọng bạn hiểu tại sao điều này không biên dịch!


Trên một lưu ý liên quan, ngay cả khi không có "catch", đường dẫn thực thi theo sau một finallycâu lệnh sẽ bị ảnh hưởng bởi liệu câu lệnh catchđã thoát qua ngoại lệ hay chưa và không có cơ chế nào để nói điều đó sẽ tương tác với a continue. Tôi thích ví dụ của bạn, vì nó cho thấy một vấn đề còn lớn hơn.
supercat

@supercat Đồng ý với bạn, câu trả lời của tôi cho thấy một ví dụ về tình huống không rõ ràng cho trình biên dịch vẫn còn rất nhiều vấn đề với cách tiếp cận này.
Sriram Sakthivel

16

Như những người khác đã nêu, nhưng tập trung vào các trường hợp ngoại lệ, đó thực sự là về việc xử lý mơ hồ đối với việc chuyển giao quyền kiểm soát.

Trong đầu bạn, có lẽ bạn đang nghĩ đến một kịch bản như sau:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

Về mặt lý thuyết, bạn có thể theo dõi luồng điều khiển và nói, có, điều này là "ok". Không có ngoại lệ nào được ném ra, không có quyền kiểm soát nào được chuyển giao. Nhưng các nhà thiết kế ngôn ngữ C # có những vấn đề khác trong tâm trí.

Trường hợp ngoại lệ ném

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

The Dreaded Goto

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

Sự trở lại

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

Sự chia tay

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

Vì vậy, trong kết luận, có, trong khi đó một chút khả năng sử dụng một continuetrong các trường hợp kiểm soát không được chuyển nhượng, nhưng một việc tốt (đa số?) Các trường hợp liên quan đến trường hợp ngoại lệ hoặc returnkhối. Các nhà thiết kế ngôn ngữ cảm thấy điều này quá mơ hồ và (có khả năng) không thể đảm bảo tại thời điểm biên dịch rằng của bạn chỉcontinue được sử dụng trong trường hợp luồng điều khiển không được chuyển.


Gặp phải tình huống tương tự trong java khi proguard bị lỗi trong khi giải mã. Giải thích của bạn là khá tốt. C # đưa ra lỗi thời gian biên dịch - hoàn toàn có lý.
Dev_Vikram

11

Nói chung continuekhông có ý nghĩa khi sử dụng trong finallykhối. Hãy xem này:

foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}

"Nói chung" rất nhiều thứ không có ý nghĩa. Và nó không có nghĩa là nó không nên hoạt động "cụ thể". IE: continuekhông có ý nghĩa gì bên ngoài câu lệnh lặp, nhưng không có nghĩa là nó không được hỗ trợ.
zerkms

Tôi nghĩ nó có ý nghĩa. itemcó thể là tệp, không đọc được -> finallyđóng tệp. continuengăn phần còn lại của quá trình xử lý.
jnovacho

5

"Điều này sẽ không biên dịch và tôi nghĩ rằng nó hoàn toàn có ý nghĩa"

Tôi nghĩ là không.

Khi bạn thực sự có catch(Exception)thì bạn không cần cuối cùng (và có thể thậm chí không continue).

Khi bạn có thực tế hơn catch(SomeException), điều gì sẽ xảy ra khi một ngoại lệ không bị bắt? Bạn continuemuốn đi một cách, ngoại lệ xử lý một cách khác.


2
Tôi nghĩ bạn có thể cần finallykhi bạn có catch. Nó thường được sử dụng để đóng tài nguyên một cách đáng tin cậy.
jnovacho

Nó không chỉ là những trường hợp ngoại lệ. Bạn có thể dễ dàng có một returntuyên bố trong tryhoặc catchcác khối của bạn . Chúng ta làm gì bây giờ? Quay lại hay tiếp tục vòng lặp? (và nếu chúng ta tiếp tục, những gì nếu đó là mục cuối cùng để lặp Sau đó chúng ta chỉ cần tiếp tục bên ngoài? foreachvà sự trở lại không bao giờ xảy ra?) EDIT: Hoặc thậm chí gotođến một nhãn bên ngoài vòng lặp, khá nhiều bất kỳ hành động mà chuyển kiểm soát bên ngoài foreachvòng lặp .
Chris Sinclair

2
Tôi không đồng ý với "Khi bạn thực sự có bắt (Ngoại lệ) thì bạn không cần cuối cùng (và thậm chí không cần tiếp tục).". Điều gì sẽ xảy ra nếu tôi cần thực hiện một thao tác bất kỳ ngoại lệ nào đã nêu ra hoặc không?
lpaloub

OK, cuối cùng phục vụ cho các bổ sung returnbên trong khối. Nhưng thông thường việc thực thi vẫn tiếp tục sau khi bắt hết.
Henk Holterman

'cuối cùng' không phải là 'bắt'. Nó nên được sử dụng cho mã dọn dẹp. một khối cuối cùng chạy bất kể ngoại lệ có được ném ra hay không . Những thứ như đóng tệp hoặc giải phóng bộ nhớ (nếu bạn đang sử dụng mã không được quản lý), v.v. đây là những thứ bạn đưa vào một khối cuối cùng
Robotnik

3

Bạn không thể rời khỏi phần thân của một khối cuối cùng. Điều này bao gồm các từ khóa break, return và trong trường hợp của bạn là tiếp tục.


3

Các finallykhối có thể được thực hiện với một ngoại lệ chờ đợi để được rethrown. Sẽ không thực sự có ý nghĩa nếu có thể thoát khỏi khối (bằng cáchcontinue hoặc bất cứ điều gì khác) mà không lặp lại ngoại lệ.

Nếu bạn muốn tiếp tục vòng lặp của mình bất cứ điều gì xảy ra, bạn không cần câu lệnh cuối cùng: Chỉ cần bắt ngoại lệ và không lặp lại.


1

finallychạy cho dù có ném ra một ngoại lệ không cần thiết hay không. Những người khác đã giải thích lý do tại sao điều này trở nên continuephi logic, nhưng đây là một giải pháp thay thế tuân theo tinh thần của những gì mã này dường như đang yêu cầu. Về cơ bản, finally { continue; }đang nói:

  1. Khi có các ngoại lệ bắt được, hãy tiếp tục
  2. Khi có những trường hợp ngoại lệ chưa được ghi nhận, hãy cho phép chúng được ném, nhưng vẫn tiếp tục

(1) có thể được thỏa mãn bằng cách đặt continueở cuối mỗi catch, và (2) có thể được thỏa mãn bằng cách lưu trữ các ngoại lệ không cần thiết để ném sau này. Bạn có thể viết nó như thế này:

var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

Trên thực tế, finallycũng sẽ được thực hiện trong trường hợp thứ ba, nơi không có trường hợp ngoại lệ nào được ném ra, bị bắt hoặc không được giải quyết. Nếu điều đó được mong muốn, tất nhiên bạn có thể chỉ đặt một khối duy nhất continuesau khối try-catch thay vì bên trong mỗi khối catch.


1

Về mặt kỹ thuật, đó là một hạn chế của CIL cơ bản. Từ thông số ngôn ngữ :

Việc chuyển quyền kiểm soát không bao giờ được phép nhập vào mệnh đề xử lý bắt hoặc cuối cùng ngoại trừ thông qua cơ chế xử lý ngoại lệ.

Chuyển giao kiểm soát ra khỏi một khu vực được bảo vệ chỉ được phép thông qua một lệnh ngoại lệ ( leave, end.filter, end.catch, hay end.finally)

Trên trang tài liệu để xem brhướng dẫn :

Lệnh này không thể thực hiện các chuyển giao kiểm soát vào và ra khỏi khối try, catch, filter và cuối cùng.

Cuối cùng này đúng với tất cả các lệnh rẽ nhánh, bao gồm beq, brfalsevv


-1

Các nhà thiết kế ngôn ngữ chỉ đơn giản là không muốn (hoặc không thể) giải thích về ngữ nghĩa của một khối cuối cùng bị kết thúc bởi một chuyển giao điều khiển.

Một vấn đề, hoặc có lẽ là vấn đề quan trọng, là finally khối được thực thi như một phần của một số chuyển điều khiển không cục bộ (xử lý ngoại lệ). Mục tiêu của việc chuyển giao quyền kiểm soát đó không phải là vòng lặp bao quanh; xử lý ngoại lệ hủy bỏ vòng lặp và tiếp tục giải nén thêm.

Nếu chúng tôi có sự chuyển giao quyền kiểm soát ra khỏi finally khối dọn dẹp, thì chuyển giao kiểm soát ban đầu đang bị "chiếm quyền điều khiển". Nó bị hủy bỏ và quyền kiểm soát sẽ chuyển sang chỗ khác.

Các ngữ nghĩa có thể được giải quyết. Các ngôn ngữ khác có nó.

Các nhà thiết kế của C # đã quyết định đơn giản là không cho phép chuyển giao điều khiển tĩnh, "giống như goto", do đó đơn giản hóa mọi thứ phần nào.

Tuy nhiên, ngay cả khi bạn làm điều đó, nó không giải quyết được câu hỏi điều gì sẽ xảy ra nếu quá trình chuyển động được bắt đầu từ finally : điều khối cuối cùng gọi một hàm và hàm đó ném? Xử lý ngoại lệ ban đầu sau đó bị "chiếm quyền điều khiển".

Nếu bạn tìm ra ngữ nghĩa của hình thức chiếm quyền điều khiển thứ hai này, không có lý do gì để loại bỏ kiểu đầu tiên. Chúng thực sự giống nhau: chuyển giao quyền kiểm soát là chuyển giao quyền kiểm soát, cho dù nó có cùng phạm vi từ vựng hay không.


Sự khác biệt là finallytheo định nghĩa phải được theo sau ngay lập tức bằng cách tiếp tục chuyển giao đã kích hoạt nó (một trường hợp ngoại lệ không cần thiết return, v.v.). Sẽ rất dễ dàng cho phép chuyển "giống như goto" bên trong finally(nhân tiện, ngôn ngữ nào làm được điều này?), Nhưng làm như vậy có nghĩa là nó try { return } finally { ... } có thể không quay lại , điều này hoàn toàn không mong đợi. Nếu finallygọi một hàm ném, điều đó không thực sự giống như vậy, bởi vì chúng tôi đã dự đoán rằng các ngoại lệ có thể xảy ra bất cứ lúc nào và làm gián đoạn luồng thông thường.
nmclean

@nmclean: Trên thực tế, một ngoại lệ thoát ra finallycũng tương tự như vậy, ở chỗ nó có thể dẫn đến luồng chương trình bất ngờ và phi logic. Sự khác biệt duy nhất là các nhà thiết kế ngôn ngữ, không thể từ chối tất cả các chương trình trong đó một khối cuối cùng có thể tạo ra một ngoại lệ không được xử lý, thay vào đó cho phép các chương trình đó biên dịch và hy vọng chương trình có thể chấp nhận bất kỳ hậu quả nào có thể xảy ra sau việc bỏ chạy ngoại lệ trước đó sự nối tiếp.
supercat

@supercat Tôi đồng ý rằng nó phá vỡ luồng dự kiến, nhưng quan điểm của tôi là đó luôn là trường hợp cho các trường hợp ngoại lệ không được xử lý, cho dù luồng hiện tại có phải là một finallyhay không. Theo logic của đoạn cuối cùng của Kaz, các hàm cũng nên được tự do ảnh hưởng đến luồng trong phạm vi người gọi của chúng theo cách continue, v.v., vì chúng được phép làm như vậy theo cách ngoại lệ. Nhưng điều này tất nhiên là không được phép; ngoại lệ là ngoại lệ duy nhất cho quy tắc này, do đó có tên "ngoại lệ" (hoặc "ngắt").
nmclean

@nmclean: Các ngoại lệ không lồng nhau thể hiện luồng điều khiển khác với việc thực thi thông thường, nhưng vẫn có cấu trúc. Câu lệnh theo sau một khối chỉ nên thực thi nếu tất cả các ngoại lệ xảy ra trong khối đó đều bị bắt trong nó. Điều đó có vẻ giống như một kỳ vọng hợp lý, nhưng một ngoại lệ xảy ra trong một finallykhối có thể vi phạm nó. Thực sự là một tình huống khó chịu, mà IMHO lẽ ra phải được giải quyết bằng cách tối thiểu cho finallycác khối biết về bất kỳ trường hợp ngoại lệ nào đang chờ xử lý để họ có thể xây dựng các đối tượng ngoại lệ tổng hợp.
supercat

@mmclean Xin lỗi, tôi không đồng ý rằng tên "ngoại lệ" xuất phát từ một số "ngoại lệ đối với quy tắc" (liên quan đến kiểm soát) cụ thể đối với C #, LOL. Khía cạnh thay đổi luồng điều khiển của một ngoại lệ chỉ đơn giản là chuyển giao điều khiển động không cục bộ. Việc chuyển giao quyền kiểm soát thường xuyên trong cùng một phạm vi từ vựng (không diễn ra liên tục) có thể được coi là một trường hợp đặc biệt của điều đó.
Kaz
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.