Lợi thế nào đạt được bằng cách triển khai LINQ theo cách không lưu trữ kết quả?


20

Đây là một cạm bẫy được biết đến đối với những người bị ướt chân khi sử dụng LINQ:

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

Điều này sẽ in "Sai", vì với mỗi tên được cung cấp để tạo bộ sưu tập gốc, hàm select sẽ tiếp tục được đánh giá lại và Recordđối tượng kết quả được tạo lại. Để khắc phục điều này, một cuộc gọi đơn giản ToListcó thể được thêm vào cuối GenerateRecords.

Microsoft đã hy vọng đạt được lợi thế gì khi thực hiện theo cách này?

Tại sao việc thực hiện không chỉ đơn giản là lưu trữ kết quả một mảng nội bộ? Một phần cụ thể của những gì đang xảy ra có thể bị hoãn thực thi, nhưng điều đó vẫn có thể được thực hiện mà không có hành vi này.

Khi một thành viên nhất định của bộ sưu tập được LINQ trả về đã được đánh giá, lợi thế nào được cung cấp bằng cách không giữ tham chiếu / bản sao nội bộ, mà thay vào đó tính toán lại kết quả tương tự, như một hành vi mặc định?

Trong các tình huống có nhu cầu đặc biệt về logic cho cùng một thành viên của bộ sưu tập được tính toán lại nhiều lần, có vẻ như điều đó có thể được chỉ định thông qua một tham số tùy chọn và hành vi mặc định có thể làm khác. Ngoài ra, lợi thế về tốc độ đạt được khi thực hiện hoãn lại cuối cùng bị cắt giảm theo thời gian cần thiết để liên tục tính toán lại các kết quả tương tự. Cuối cùng, đây là khối khó hiểu đối với những người mới sử dụng LINQ và nó có thể dẫn đến các lỗi tinh vi trong chương trình của bất kỳ ai.

Lợi thế nào cho điều này, và tại sao Microsoft lại đưa ra quyết định dường như rất có chủ ý này?


1
Chỉ cần gọi ToList () trong phương thức GenerateRecords () của bạn. return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); Điều đó cung cấp cho bạn "bản sao lưu trữ." Vấn đề được giải quyết.
Robert Harvey

1
Tôi biết, nhưng tôi đã tự hỏi tại sao họ lại làm điều này cần thiết ngay từ đầu.
Panzercrisis

11
Bởi vì đánh giá lười biếng có lợi ích đáng kể, không phải ít nhất là "ồ, nhân tiện, hồ sơ này đã thay đổi kể từ lần cuối bạn yêu cầu; đây là phiên bản mới", đó chính xác là những gì ví dụ mã của bạn minh họa.
Robert Harvey

Tôi có thể thề rằng tôi đã đọc một câu hỏi gần như giống nhau ở đây trong 6 tháng qua, nhưng tôi không tìm thấy nó bây giờ. Lần gần nhất tôi có thể tìm thấy là từ năm 2016 trên stackoverflow: stackoverflow.com/q/37437893/391656
Mr.Mindor 23/03/18

29
Chúng tôi có một tên cho bộ đệm mà không có chính sách hết hạn: "rò rỉ bộ nhớ". Chúng tôi có một tên cho bộ đệm mà không có chính sách vô hiệu: "bug farm". Nếu bạn sẽ không đề xuất một chính sách hết hạn và vô hiệu hóa luôn luôn chính xác, phù hợp với mọi truy vấn LINQ có thể thì câu hỏi của bạn sẽ tự trả lời.
Eric Lippert

Câu trả lời:


51

Lợi thế nào đạt được bằng cách triển khai LINQ theo cách không lưu trữ kết quả?

Bộ nhớ đệm kết quả sẽ không hoạt động cho tất cả mọi người. Miễn là bạn có lượng dữ liệu nhỏ, thật tuyệt. Tốt cho bạn. Nhưng nếu dữ liệu của bạn lớn hơn RAM thì sao?

Nó không có gì để làm với LINQ, nhưng với IEnumerable<T>giao diện nói chung.

Đó là sự khác biệt giữa File.ReadAllLinesFile.ReadLines . Một người sẽ đọc toàn bộ tệp vào RAM và người kia sẽ đưa nó cho bạn từng dòng, do đó bạn có thể làm việc với các tệp lớn (miễn là chúng ngắt dòng).

Bạn có thể dễ dàng lưu trữ mọi thứ bạn muốn lưu vào bộ đệm bằng cách thực hiện cuộc gọi trình tự của bạn .ToList()hoặc .ToArray()trên đó. Nhưng những người trong chúng ta không muốn lưu trữ nó, chúng ta có cơ hội không làm như vậy.

Và trên một lưu ý liên quan: làm thế nào để bạn lưu trữ sau đây?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

Bạn không thể. Đó là lý do tại sao IEnumerable<T>nó tồn tại như nó.


2
Ví dụ cuối cùng của bạn sẽ hấp dẫn hơn nếu đó là một chuỗi vô hạn thực sự (chẳng hạn như Fibonnaci), và không chỉ là một chuỗi số không vô tận, không đặc biệt thú vị.
Robert Harvey

23
@RobertHarvey Điều đó đúng, tôi chỉ nghĩ rằng sẽ dễ dàng hơn khi phát hiện ra rằng đó là một dòng số 0 vô tận khi không có logic nào để hiểu.
nvoigt

2
int i=1; while(true) { i++; yield fib(i); }
Robert Harvey

2
Ví dụ tôi đã nghĩ đến là Enumerable.Range(1,int.MaxValue)- rất dễ dàng để tìm ra giới hạn thấp hơn cho bao nhiêu bộ nhớ sẽ sử dụng.
Chris

4
Một điều khác mà tôi đã thấy dọc theo dòng while (true) return ...while (true) return _random.Next();tạo ra một dòng vô số các số ngẫu nhiên.
Chris

24

Microsoft đã hy vọng đạt được lợi thế gì khi thực hiện theo cách này?

Đúng không? Ý tôi là, vô số cốt lõi có thể thay đổi giữa các cuộc gọi. Bộ nhớ đệm sẽ tạo ra kết quả không chính xác và mở toàn bộ hệ thống khi tôi vô hiệu hóa bộ đệm đó?

Và nếu bạn xem xét LINQ đầu được thiết kế như một phương tiện để làm LINQ to nguồn dữ liệu (như khuôn khổ tổ chức nào, hoặc SQL trực tiếp), các đếm được sẽ thay đổi vì đó là những gì cơ sở dữ liệu làm .

Trên hết, có mối quan tâm Nguyên tắc Trách nhiệm duy nhất. Việc tạo một số mã truy vấn hoạt động và xây dựng bộ đệm trên đầu trang dễ dàng hơn nhiều so với xây dựng mã truy vấn và lưu trữ nhưng sau đó xóa bộ đệm.


3
Có thể đáng nói đến điều đó ICollectiontồn tại và có lẽ hành xử theo cách OP đang mong đợi IEnumerableđể hành xử
Caleth 23/03/18

Nếu bạn đang sử dụng IEnumerable <T> để đọc con trỏ cơ sở dữ liệu mở, kết quả của bạn sẽ không thay đổi nếu bạn đang sử dụng cơ sở dữ liệu với các giao dịch ACID.
Doug

4

Bởi vì LINQ đã và được dự định ngay từ đầu, nên việc triển khai chung mẫu Monad phổ biến trong các ngôn ngữ lập trình chức năng và Monad không bị hạn chế để luôn mang lại các giá trị giống nhau cho cùng một chuỗi các cuộc gọi (trên thực tế, việc sử dụng nó trong lập trình chức năng là phổ biến chính xác vì thuộc tính này, cho phép thoát khỏi hành vi xác định của các hàm thuần túy).


4

Một lý do khác chưa được đề cập là, khả năng kết hợp các bộ lọc và biến đổi khác nhau mà không tạo ra kết quả rác giữa.

Lấy ví dụ này:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

Nếu các phương thức LINQ tính toán kết quả ngay lập tức, chúng tôi sẽ có 3 bộ sưu tập:

  • Kết quả ở đâu
  • Chọn kết quả
  • Kết quả nhóm

Trong đó chúng tôi chỉ quan tâm đến người cuối cùng. Không có điểm nào trong việc lưu kết quả giữa vì chúng tôi không có quyền truy cập vào chúng và chúng tôi chỉ muốn biết về những chiếc xe đã được lọc và nhóm theo năm.

Nếu có nhu cầu lưu bất kỳ kết quả nào trong số này, thì giải pháp rất đơn giản: tách các cuộc gọi ra và gọi .ToList()chúng và lưu chúng trong một biến.


Cũng như một ghi chú bên lề, trong JavaScript, các phương thức Array thực sự trả về kết quả ngay lập tức, điều này có thể dẫn đến việc tiêu thụ nhiều bộ nhớ hơn nếu không cẩn thận.


3

Về cơ bản, mã này - đặt Guid.NewGuid ()một Selecttuyên bố bên trong - rất đáng ngờ. Đây chắc chắn là một mùi mã của một số loại!

Về lý thuyết, chúng ta không nhất thiết mong đợi một Selecttuyên bố sẽ tạo ra dữ liệu mới mà là lấy dữ liệu hiện có. Mặc dù hợp lý khi Chọn tham gia dữ liệu từ nhiều nguồn để tạo ra nội dung được nối có hình dạng khác nhau hoặc thậm chí tính toán các cột bổ sung, chúng tôi vẫn có thể hy vọng nó có chức năng & thuần túy. Đặt NewGuid ()bên trong làm cho nó không chức năng và không tinh khiết.

Việc tạo dữ liệu có thể bị trêu chọc ngoài việc chọn và đưa vào hoạt động tạo một loại nào đó, để việc chọn có thể được giữ nguyên và có thể sử dụng lại, nếu không thì việc chọn chỉ nên được thực hiện một lần và được bọc / bảo vệ - điều này là .ToList ()gợi ý.

Tuy nhiên, rõ ràng, vấn đề đối với tôi là sự pha trộn giữa sáng tạo bên trong lựa chọn thay vì thiếu bộ nhớ đệm. Đặt phần NewGuid()bên trong lựa chọn dường như là một sự pha trộn không phù hợp của các mô hình lập trình.


0

Việc thực thi hoãn lại cho phép những người viết mã LINQ (chính xác, sử dụng IEnumerable<T>) chọn rõ ràng liệu kết quả có được tính toán và lưu trữ ngay lập tức trong bộ nhớ hay không. Nói cách khác, nó cho phép các lập trình viên chọn thời gian tính toán so với sự đánh đổi không gian lưu trữ phù hợp nhất với ứng dụng của họ.

Có thể lập luận rằng phần lớn các ứng dụng muốn có kết quả ngay lập tức, do đó, đó phải là hành vi mặc định của LINQ. Nhưng có rất nhiều API khác (ví dụ List<T>.ConvertAll) cung cấp hành vi này và đã được thực hiện kể từ khi Framework được tạo, trong khi cho đến khi LINQ được giới thiệu, không có cách nào để thực hiện hoãn lại. Mà, như các câu trả lời khác đã chứng minh, là điều kiện tiên quyết để cho phép một số loại tính toán nhất định mà không thể (bằng cách làm cạn kiệt tất cả bộ nhớ có sẵn) khi sử dụng thực thi ngay lập tức.

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.