Trong C #, tại sao một phương thức ẩn danh không thể chứa một câu lệnh lợi nhuận?


87

Tôi nghĩ sẽ rất tuyệt nếu làm điều gì đó như thế này (với lambda thực hiện trả về lợi nhuận):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Tuy nhiên, tôi phát hiện ra rằng tôi không thể sử dụng lợi nhuận trong phương pháp ẩn danh. Tôi tự hỏi tại sao. Tài liệu lợi nhuận chỉ nói rằng nó không được phép.

Vì nó không được phép nên tôi chỉ tạo Danh sách và thêm các mục vào đó.


Bây giờ chúng ta có thể có các asynclambdas ẩn danh cho phép awaitbên trong C # 5.0, tôi muốn biết tại sao họ vẫn chưa triển khai các trình lặp ẩn danh với yieldbên trong. Ít nhiều thì nó cũng là một máy phát điện trạng thái.
mũi,

Câu trả lời:


113

Eric Lippert gần đây đã viết một loạt bài đăng trên blog về lý do tại sao không cho phép lợi nhuận trong một số trường hợp.

EDIT2:

  • Phần 7 (phần này được đăng sau và giải quyết cụ thể câu hỏi này)

Bạn có thể sẽ tìm thấy câu trả lời ở đó ...


EDIT1: điều này được giải thích trong các nhận xét của Phần 5, trong câu trả lời của Eric cho nhận xét của Abhijeet Patel:

Q:

Eric,

Bạn cũng có thể cung cấp một số thông tin chi tiết về lý do tại sao "sản lượng" không được phép bên trong một phương thức ẩn danh hoặc biểu thức lambda

A:

Câu hỏi hay. Tôi rất thích có các khối vòng lặp ẩn danh. Sẽ hoàn toàn tuyệt vời nếu bạn có thể tự tạo cho mình một trình tạo trình tự nhỏ tại chỗ đóng trên các biến cục bộ. Lý do tại sao lại không đơn giản: lợi ích không lớn hơn chi phí. Sự tuyệt vời của việc tạo trình tự tạo trình tự tại chỗ thực sự là khá nhỏ trong sơ đồ tổng thể của mọi thứ và các phương pháp danh nghĩa thực hiện công việc đủ tốt trong hầu hết các tình huống. Vì vậy, lợi ích không phải là hấp dẫn.

Các chi phí lớn. Viết lại lặp lại là biến đổi phức tạp nhất trong trình biên dịch và viết lại theo phương thức ẩn danh là phức tạp thứ hai. Các phương thức ẩn danh có thể nằm bên trong các phương thức ẩn danh khác và các phương thức ẩn danh có thể nằm bên trong các khối trình vòng lặp. Do đó, những gì chúng tôi làm là đầu tiên chúng tôi viết lại tất cả các phương thức ẩn danh để chúng trở thành các phương thức của một lớp đóng. Đây là điều cuối cùng mà trình biên dịch làm trước khi phát ra IL cho một phương thức. Khi bước đó được thực hiện, trình ghi lại trình lặp có thể giả định rằng không có phương thức ẩn danh nào trong khối trình lặp; tất cả chúng đã được viết lại rồi. Do đó, người viết lại trình lặp có thể chỉ tập trung vào việc viết lại trình lặp mà không cần lo lắng rằng có thể có một phương thức ẩn danh chưa được thực hiện trong đó.

Ngoài ra, các khối vòng lặp không bao giờ "lồng", không giống như các phương thức ẩn danh. Trình ghi lại trình lặp có thể giả định rằng tất cả các khối trình lặp là "cấp cao nhất".

Nếu các phương thức ẩn danh được phép chứa các khối trình vòng lặp, thì cả hai giả định đó đều bị loại khỏi cửa sổ. Bạn có thể có một khối trình vòng lặp chứa một phương thức ẩn danh chứa một phương thức ẩn danh chứa một khối trình vòng lặp có chứa một phương thức ẩn danh, và ... yuck. Bây giờ chúng ta phải viết một pass viết lại có thể xử lý các khối trình lặp lồng nhau và các phương thức ẩn danh lồng nhau cùng một lúc, hợp nhất hai thuật toán phức tạp nhất của chúng ta thành một thuật toán phức tạp hơn nhiều. Nó sẽ thực sự khó khăn để thiết kế, triển khai và thử nghiệm. Tôi chắc chắn rằng chúng tôi đủ thông minh để làm như vậy. Chúng tôi có một đội ngũ thông minh ở đây. Nhưng chúng tôi không muốn gánh vác gánh nặng lớn đó cho một tính năng "tốt khi có nhưng không cần thiết". - Eric


2
Thú vị, đặc biệt là vì bây giờ có các chức năng địa phương.
Mafii

4
Tôi tự hỏi liệu câu trả lời này có lỗi thời hay không vì nó sẽ trả về lợi nhuận trong một hàm cục bộ.
Joshua

2
@Joshua nhưng một hàm cục bộ không giống với một phương thức ẩn danh ... lợi nhuận vẫn không được phép trong các phương thức ẩn danh.
Thomas Levesque

21

Eric Lippert đã viết một loạt bài xuất sắc về những hạn chế (và các quyết định thiết kế ảnh hưởng đến những lựa chọn đó) trên các khối trình lặp

Đặc biệt, các khối trình lặp cụ thể được thực hiện bởi một số biến đổi mã trình biên dịch phức tạp. Những biến đổi này sẽ tác động đến các biến đổi xảy ra bên trong các hàm hoặc lambdas ẩn danh để trong một số trường hợp nhất định, cả hai sẽ cố gắng 'chuyển đổi' mã thành một số cấu trúc khác không tương thích với cấu trúc kia.

Do đó, họ bị cấm tương tác.

Cách khối vòng lặp hoạt động dưới mui xe được giải quyết tốt ở đây .

Như một ví dụ đơn giản về sự không tương thích:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Trình biên dịch đồng thời muốn chuyển đổi điều này thành một cái gì đó như:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

và đồng thời khía cạnh trình lặp đang cố gắng thực hiện nó hoạt động để tạo ra một máy trạng thái nhỏ. Một số ví dụ đơn giản nhất định có thể hoạt động với một lượng lớn kiểm tra tỉnh táo (trước tiên xử lý các đóng lồng nhau (có thể tùy ý)), sau đó xem liệu các lớp kết quả ở cấp cuối cùng có thể được chuyển đổi thành các máy trạng thái trình vòng lặp hay không.

Tuy nhiên đây sẽ là

  1. Khá nhiều công việc.
  2. Không thể hoạt động trong mọi trường hợp nếu không có ít nhất khía cạnh khối trình vòng lặp có thể ngăn khía cạnh đóng áp dụng các phép biến đổi nhất định để có hiệu quả (như quảng bá các biến cục bộ thành các biến cá thể thay vì một lớp đóng hoàn toàn chính thức).
    • Nếu thậm chí có một chút cơ hội trùng lặp mà nó không thể hoặc đủ khó để không được thực hiện thì số lượng các vấn đề hỗ trợ dẫn đến có thể sẽ cao vì sự thay đổi nhỏ sẽ bị mất đối với nhiều người dùng.
  3. Nó có thể rất dễ dàng làm việc xung quanh.

Trong ví dụ của bạn như vậy:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

2
Không có lý do rõ ràng tại sao trình biên dịch không thể, một khi nó đã loại bỏ tất cả các bao đóng, thực hiện chuyển đổi trình lặp thông thường. Bạn có biết về một trường hợp thực sự sẽ gây ra một số khó khăn? Btw, Magiclớp học của bạn nên được Magic<T>.
Qwertie

3

Thật không may, tôi không biết tại sao họ không cho phép điều này, vì tất nhiên hoàn toàn có thể hình dung cách thức hoạt động của nó.

Tuy nhiên, các phương thức ẩn danh đã là một phần của "phép thuật biên dịch" theo nghĩa là phương thức sẽ được trích xuất sang một phương thức trong lớp hiện có, hoặc thậm chí sang một lớp hoàn toàn mới, tùy thuộc vào việc nó có xử lý các biến cục bộ hay không.

Ngoài ra, các phương thức sử dụng trình lặp yieldcũng được thực hiện bằng phép trình biên dịch.

Tôi đoán rằng một trong hai điều này làm cho mã không thể nhận dạng được đối với phần ma thuật khác và người ta đã quyết định không dành thời gian để làm cho nó hoạt động cho các phiên bản hiện tại của trình biên dịch C #. Tất nhiên, nó có thể không phải là một lựa chọn sáng suốt, và nó không hoạt động vì không ai nghĩ đến việc thực hiện nó.

Đối với câu hỏi chính xác 100%, tôi khuyên bạn nên sử dụng trang Microsoft Connect và báo cáo câu hỏi, tôi chắc chắn rằng đổi lại bạn sẽ nhận được thứ gì đó có thể sử dụng được.


1

Tôi sẽ làm điều này:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

Tất nhiên bạn cần System.Core.dll được tham chiếu từ .NET 3.5 cho phương thức Linq. Và bao gồm:

using System.Linq;

Chúc mừng,

Sly


0

Có thể nó chỉ là một giới hạn cú pháp. Trong Visual Basic .NET, tương tự như C #, hoàn toàn có thể xảy ra khi bạn lúng túng khi viết

Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

Cũng lưu ý các dấu ngoặc đơn ' here; hàm lambda Iterator Function... End Function trả về một IEnumerable(Of Integer)nhưng không phải là một đối tượng như vậy. Nó phải được gọi để lấy đối tượng đó.

Mã được chuyển đổi bởi [1] làm phát sinh lỗi trong C # 7.3 (CS0149):

static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}

Tôi thực sự không đồng ý với lý do được đưa ra trong các câu trả lời khác mà trình biên dịch khó xử lý. Các Iterator Function()bạn nhìn thấy trong ví dụ VB.NET được đặc biệt tạo ra cho lặp lambda.

Trong VB, có Iteratortừ khóa; nó không có đối tác C #. IMHO, không có lý do thực sự nào mà đây không phải là một tính năng của C #.

Vì vậy, nếu bạn thực sự, thực sự muốn các hàm trình lặp ẩn danh, hiện đang sử dụng Visual Basic hoặc (tôi chưa kiểm tra nó) F #, như đã nêu trong nhận xét của Phần # 7 trong câu trả lời của @Thomas Levesque (thực hiện Ctrl + F cho F #).

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.