Tại sao lợi nhuận trả về không thể xuất hiện bên trong khối thử với một lệnh bắt?


95

Sau đây là ổn:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

Các finallykhối chạy khi toàn bộ điều đã hoàn tất thi công ( IEnumerator<T>hỗ trợ IDisposableđể cung cấp một cách để đảm bảo điều này ngay cả khi liệt kê là bị bỏ rơi trước khi nó kết thúc).

Nhưng điều này không ổn:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Giả sử (vì lợi ích của đối số) rằng một ngoại lệ được ném bởi một hoặc một số lệnh WriteLinegọi bên trong khối try. Vấn đề với việc tiếp tục thực thi trong catchkhối là gì?

Tất nhiên, phần trả về lợi nhuận (hiện tại) không thể ném bất cứ thứ gì, nhưng tại sao điều đó lại ngăn chúng ta có một bao vây try/ catchđể đối phó với các ngoại lệ được ném trước hoặc sau yield return?

Cập nhật: Có một nhận xét thú vị từ Eric Lippert ở đây - có vẻ như họ đã gặp đủ vấn đề khi triển khai hành vi try / last một cách chính xác!

CHỈNH SỬA: Trang MSDN về lỗi này là: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Nó không giải thích tại sao, mặc dù.


2
Liên kết trực tiếp đến Eric Lippert của bình luận: blogs.msdn.com/oldnewthing/archive/2008/08/14/...
La Mã Starkov

lưu ý: bạn cũng không thể mang lại lợi nhuận trong chính khối bắt :-(
Simon_Weaver

2
Liên kết cũ không còn hoạt động.
Sebastian Redl

Câu trả lời:


50

Tôi nghi ngờ đây là một vấn đề thực tế hơn là tính khả thi. Tôi nghi ngờ rằng có rất, rất ít khi hạn chế này thực sự là một vấn đề không thể giải quyết được - nhưng sự phức tạp thêm vào trình biên dịch sẽ rất đáng kể.

Có một số điều như thế này mà tôi đã gặp phải:

  • Các thuộc tính không thể chung chung
  • Không có khả năng X dẫn xuất từ ​​XY (một lớp được lồng trong X)
  • Các khối lặp lại bằng cách sử dụng các trường công khai trong các lớp được tạo

Trong mỗi trường hợp này, có thể đạt được tự do hơn một chút, với cái giá là trình biên dịch phức tạp hơn. Nhóm đã đưa ra lựa chọn thực dụng, tôi hoan nghênh họ - tôi muốn có một ngôn ngữ hạn chế hơn một chút với trình biên dịch chính xác 99,9% (vâng, có lỗi; tôi đã gặp một lỗi trên SO mới hôm trước) hơn là một ngôn ngữ linh hoạt không thể biên dịch chính xác.

CHỈNH SỬA: Đây là một bằng chứng giả về lý do tại sao nó khả thi.

Xem xét điều đó:

  • Bạn có thể đảm bảo rằng bản thân phần trả về lợi nhuận không đưa ra ngoại lệ (tính toán trước giá trị, sau đó bạn chỉ cần đặt một trường và trả về "true")
  • Bạn được phép thử / bắt mà không sử dụng lợi nhuận trả về trong khối trình lặp.
  • Tất cả các biến cục bộ trong khối trình vòng lặp là các biến thể hiện trong kiểu được tạo, vì vậy bạn có thể tự do di chuyển mã sang các phương thức mới

Bây giờ biến đổi:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

thành (loại mã giả):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

Sự trùng lặp duy nhất là trong việc thiết lập các khối try / catch - nhưng đó là điều mà trình biên dịch chắc chắn có thể làm được.

Tôi có thể đã bỏ lỡ điều gì đó ở đây - nếu vậy, vui lòng cho tôi biết!


11
Một bằng chứng tốt về khái niệm, nhưng chiến lược này trở nên khó khăn (có thể nhiều hơn đối với một lập trình viên C # hơn là đối với một người viết trình biên dịch C #) khi bạn bắt đầu tạo phạm vi với những thứ như usingforeach. Ví dụ:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian

Ngữ nghĩa thông thường của "try / catch" ngụ ý rằng nếu bất kỳ phần nào của khối try / catch bị bỏ qua vì một ngoại lệ, quyền điều khiển sẽ chuyển sang khối "catch" phù hợp nếu tồn tại. Thật không may, nếu một ngoại lệ xảy ra "trong khi" trả về lợi nhuận, không có cách nào để trình lặp phân biệt các trường hợp nó bị Xử lý vì một ngoại lệ với những trường hợp nó bị Xử lý vì chủ sở hữu truy xuất tất cả dữ liệu quan tâm.
supercat

7
"Tôi nghi ngờ có rất, rất ít trường hợp hạn chế này thực sự là một vấn đề không thể khắc phục được" Điều đó giống như nói rằng bạn không cần ngoại lệ vì bạn có thể sử dụng chiến lược trả về mã lỗi thường được sử dụng trong C vậy. nhiều năm về trước. Tôi thừa nhận những khó khăn kỹ thuật có thể là đáng kể, nhưng điều này vẫn hạn chế nghiêm trọng tính hữu ích yield, theo ý kiến ​​của tôi, vì mã spaghetti mà bạn phải viết để giải quyết nó.
jpmc26

@ jpmc26: Không, nói như vậy thực sự không giống chút nào. Tôi không thể nhớ điều này đã bao giờ cắn tôi, và tôi đã sử dụng các khối trình lặp rất nhiều lần. Nó hạn chế một chút tính hữu dụng của yieldIMO - còn lâu mới đạt được mức nghiêm trọng .
Jon Skeet

2
'Tính năng' này thực sự yêu cầu một số mã khá xấu trong một số trường hợp để giải quyết, hãy xem stackoverflow.com/questions/5067188/…
namey

5

Tất cả các yieldcâu lệnh trong định nghĩa trình lặp được chuyển đổi thành một trạng thái trong máy trạng thái sử dụng một cách hiệu quả một switchcâu lệnh để nâng cao trạng thái. Nếu nó đã tạo mã cho các yieldcâu lệnh trong một lần thử / bắt, nó sẽ phải sao chép mọi thứ trong trykhối cho mỗi yield câu lệnh trong khi loại trừ mọi yieldcâu lệnh khác cho khối đó. Điều này không phải lúc nào cũng khả thi, đặc biệt nếu một yieldcâu lệnh phụ thuộc vào câu lệnh trước đó.


2
Tôi không nghĩ rằng tôi mua cái đó. Tôi nghĩ nó sẽ hoàn toàn khả thi - nhưng rất phức tạp.
Jon Skeet

2
Thử / bắt các khối trong C # không có nghĩa là phải tham gia lại. Nếu bạn tách chúng ra thì có thể gọi MoveNext () sau một ngoại lệ và tiếp tục khối thử với trạng thái có thể không hợp lệ.
Mark Cidade

2

Tôi sẽ suy đoán rằng do cách ngăn xếp cuộc gọi bị quấn / không liên kết khi bạn trả lại kết quả từ một người điều tra, nên khối try / catch thực sự "bắt" ngoại lệ sẽ không thể. (bởi vì khối trả về lợi nhuận không có trên ngăn xếp, mặc dù anh ta đã khởi tạo khối lặp lại)

Để có được ý tưởng về những gì tôi đang nói về việc thiết lập một khối trình vòng lặp và bước trước bằng cách sử dụng trình vòng lặp đó. Kiểm tra Ngăn xếp cuộc gọi trông như thế nào bên trong khối foreach và sau đó kiểm tra nó bên trong khối thử / cuối cùng của trình lặp.


Tôi quen thuộc với việc giải nén ngăn xếp trong C ++, nơi các hàm hủy được gọi trên các đối tượng cục bộ nằm ngoài phạm vi. Điều tương ứng trong C # sẽ là thử / cuối cùng. Nhưng việc tháo cuộn đó không xảy ra khi việc trả về năng suất xảy ra. Và để thử / bắt, không cần nó phải tương tác với lợi nhuận thu về.
Daniel Earwicker

Kiểm tra những gì xảy ra đối với các cuộc gọi stack khi Looping trên một iterator và bạn sẽ hiểu những gì tôi có nghĩa là
Radu094

@ Radu094: Không, tôi chắc là có thể. Đừng quên rằng nó đã được xử lý cuối cùng, ít nhất là hơi tương tự.
Jon Skeet

2

Tôi đã chấp nhận câu trả lời của SKEET KHÔNG HỢP LÝ cho đến khi ai đó từ Microsoft đến dội một gáo nước lạnh vào ý tưởng này. Nhưng tôi không đồng ý với phần quan điểm - tất nhiên một trình biên dịch chính xác quan trọng hơn một trình biên dịch hoàn chỉnh, nhưng trình biên dịch C # đã rất thông minh trong việc sắp xếp sự chuyển đổi này cho chúng tôi theo cách của nó. Trong trường hợp này, sự hoàn chỉnh hơn một chút sẽ làm cho ngôn ngữ dễ sử dụng, giảng dạy, giải thích hơn, với ít trường hợp phức tạp hoặc sai lầm hơn. Vì vậy, tôi nghĩ rằng nó sẽ là giá trị nỗ lực thêm. Một vài người ở Redmond vò đầu bứt tai trong hai tuần, và kết quả là hàng triệu lập trình viên trong thập kỷ tới có thể thư giãn hơn một chút.

(Tôi cũng nuôi dưỡng một mong muốn ghê tởm là có một cách để thực hiện yield returnném một ngoại lệ đã được đưa vào máy trạng thái "từ bên ngoài", bằng cách mã điều khiển quá trình lặp lại. Nhưng lý do của tôi muốn điều này khá mù mờ.)

Trên thực tế, một truy vấn tôi có về câu trả lời của Jon là liên quan đến việc ném biểu thức trả về lợi nhuận.

Rõ ràng là lợi nhuận 10 không phải là quá tệ. Nhưng điều này sẽ tệ:

yield return File.ReadAllText("c:\\missing.txt").Length;

Vì vậy, sẽ không hợp lý hơn nếu đánh giá điều này bên trong khối try / catch trước đó:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

Vấn đề tiếp theo sẽ là các khối try / catch được lồng vào nhau và phát triển lại các ngoại lệ:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

Nhưng tôi chắc là có thể ...


1
Có, bạn sẽ đưa đánh giá vào thử / bắt. Việc bạn đặt cài đặt biến ở đâu không thực sự quan trọng. Điểm chính là bạn có thể chia hiệu quả một lần thử / lần bắt với lợi nhuận thu được trong đó thành hai lần thử / lần bắt với lợi nhuận giữa chúng.
Jon Skeet
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.