Khi nào tôi nên sử dụng Lazy <T>?


327

Tôi tìm thấy bài viết này về Lazy: Sự lười biếng trong C # 4.0 - Lười

Thực hành tốt nhất để có hiệu suất tốt nhất khi sử dụng các đối tượng Lazy là gì? Ai đó có thể chỉ cho tôi một ứng dụng thực tế trong một ứng dụng thực tế không? Nói cách khác, khi nào tôi nên sử dụng nó?


42
Thay thế : get { if (foo == null) foo = new Foo(); return foo; }. Và có hàng trăm nơi có thể sử dụng nó ...
Kirk Woll

57
Lưu ý rằng mặc định get { if (foo == null) foo = new Foo(); return foo; }không an toàn cho luồng, trong khi đó Lazy<T>là an toàn cho luồng theo mặc định.
Matthew

23
Từ MSDN: QUAN TRỌNG: Khởi tạo lười biếng là an toàn cho luồng, nhưng nó không bảo vệ đối tượng sau khi tạo. Bạn phải khóa đối tượng trước khi truy cập nó, trừ khi loại là luồng an toàn.
Pedro ..Kid

Câu trả lời:


237

Bạn thường sử dụng nó khi bạn muốn khởi tạo một cái gì đó lần đầu tiên nó thực sự được sử dụng. Điều này làm trì hoãn chi phí tạo ra nó cho đến khi / khi cần thiết thay vì luôn luôn phát sinh chi phí.

Thông thường điều này là thích hợp hơn khi đối tượng có thể hoặc không thể được sử dụng và chi phí xây dựng nó là không tầm thường.


121
Tại sao không LUÔN LUÔN sử dụng Lazy?
TruthOf42

44
Nó phải chịu chi phí khi sử dụng lần đầu và có thể sử dụng một số chi phí khóa (hoặc hy sinh sự an toàn của luồng nếu không) để làm như vậy. Vì vậy, nó nên được lựa chọn cẩn thận và không được sử dụng trừ khi cần thiết.
James Michael Hare

3
James bạn có thể vui lòng mở rộng "và chi phí xây dựng là không tầm thường"? Trong trường hợp của tôi, tôi có 19 thuộc tính trong lớp và trong hầu hết các trường hợp, chỉ có 2 hoặc 3 sẽ cần được xem xét. Vì vậy, tôi đang xem xét thực hiện từng tài sản bằng cách sử dụng Lazy<T>. Tuy nhiên, để tạo ra mỗi thuộc tính tôi đang thực hiện phép nội suy tuyến tính (hoặc phép nội suy song tuyến tính) khá tầm thường nhưng có một số chi phí. (Bạn có đề nghị tôi đi và làm thí nghiệm của riêng mình không?)
Ben

3
James, nhận lời khuyên của riêng tôi, tôi đã làm thí nghiệm của riêng tôi. Xem bài viết của tôi .
Ben

17
Bạn có thể muốn khởi tạo / khởi tạo mọi thứ "trong" khởi động hệ thống để ngăn chặn độ trễ của người dùng trong các hệ thống có thông lượng cao, độ trễ thấp. Đây chỉ là một trong nhiều lý do để không "luôn luôn" sử dụng Lazy.
Derrick

126

Bạn nên cố gắng tránh sử dụng Singletons, nhưng nếu bạn cần, Lazy<T>làm cho việc thực hiện các singletons lười biếng, an toàn theo luồng dễ dàng:

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    Singleton()
    {
        // Explicit private constructor to prevent default public constructor.
        ...
    }

    public static Singleton Instance => instanceHolder.Value;
}

38
Tôi ghét đọc Bạn nên cố gắng tránh sử dụng Singletons khi tôi đang sử dụng chúng: D ... bây giờ tôi cần tìm hiểu lý do tại sao tôi nên cố gắng tránh chúng: D
Bart Calixto

24
Tôi sẽ ngừng sử dụng Singletons khi Microsoft ngừng sử dụng chúng trong các ví dụ của họ.
Eaglei22

4
Tôi có xu hướng không đồng ý với khái niệm cần tránh Singletons. Khi theo mô hình tiêm phụ thuộc, nó cũng không thành vấn đề. Lý tưởng nhất, tất cả các phụ thuộc của bạn chỉ nên được tạo một lần. Điều này làm giảm áp lực lên GC trong các kịch bản tải cao. Do đó, biến họ thành một Singleton từ trong lớp là ổn. Hầu hết (nếu không phải tất cả) các container DI hiện đại có thể xử lý theo cách bạn chọn.
Lee Grissom

1
Bạn không phải sử dụng một mẫu singleton như thế, thay vào đó hãy sử dụng bất kỳ bộ chứa di nào cấu hình lớp của bạn cho singleton. Các container sẽ chăm sóc trên cao cho bạn.
VivekDev

Mọi thứ đều có mục đích, có những tình huống trong đó singletons là một cách tiếp cận tốt và những tình huống không như vậy :).
Hawkzey

86

Một ví dụ tuyệt vời trong thế giới thực về việc tải lười biếng có ích là với ORM (Object Relation Mappers) như Entity Framework và NHibernate.

Giả sử bạn có một thực thể Khách hàng có các thuộc tính cho Tên, Số điện thoại và Đơn hàng. Tên và số điện thoại là các chuỗi thông thường nhưng Đơn hàng là thuộc tính điều hướng trả về danh sách mọi đơn hàng mà khách hàng đã thực hiện.

Bạn thường có thể muốn đi qua tất cả các khách hàng của bạn và nhận được tên và số điện thoại của họ để gọi cho họ. Đây là một nhiệm vụ rất nhanh chóng và đơn giản, nhưng hãy tưởng tượng nếu mỗi lần bạn tạo một khách hàng, nó sẽ tự động thực hiện và tham gia phức tạp để trả lại hàng ngàn đơn đặt hàng. Điều tồi tệ nhất là bạn thậm chí sẽ không sử dụng các đơn đặt hàng nên nó hoàn toàn lãng phí tài nguyên!

Đây là nơi hoàn hảo để tải lười biếng vì nếu thuộc tính Đơn hàng lười biếng, nó sẽ không lấy tất cả đơn đặt hàng của khách hàng trừ khi bạn thực sự cần chúng. Bạn có thể liệt kê các đối tượng Khách hàng chỉ nhận Tên và Số điện thoại của họ trong khi thuộc tính Đơn hàng đang kiên nhẫn ngủ, sẵn sàng khi bạn cần.


34
Ví dụ xấu, vì tải lười biếng như vậy thường đã được tích hợp vào ORM. Bạn không nên bắt đầu thêm các giá trị <T> lười biếng vào POCO của mình để tải lười biếng, nhưng hãy sử dụng cách cụ thể ORM để làm điều đó.
Dynalon

56
@Dyna Ví dụ này đề cập đến việc tải ORM tích hợp sẵn vì tôi nghĩ rằng điều này minh họa cho tính hữu ích của việc tải lười biếng một cách rõ ràng và đơn giản.
Despertar

Vậy nếu bạn đang sử dụng Entity Framework thì ai nên thực thi sự lười biếng của chính họ? Hay là EF làm điều đó cho bạn?
Zapnologica

7
@Zapnologica EF làm tất cả điều này cho bạn theo mặc định. Trong thực tế, nếu bạn muốn tải háo hức (ngược lại với tải lười biếng), bạn phải nói rõ ràng với EF bằng cách sử dụng Db.Customers.Include("Orders"). Điều này sẽ khiến lệnh tham gia được thực thi tại thời điểm đó thay vì khi thuộc Customer.Orderstính được sử dụng lần đầu tiên. Lazy Loading cũng có thể bị vô hiệu hóa thông qua DbContext.
Despertar

2
Trên thực tế đây là một ví dụ tốt, vì người ta có thể muốn thêm chức năng này khi sử dụng một cái gì đó như Dapper.
xương sống

41

Tôi đã xem xét việc sử dụng Lazy<T>các thuộc tính để giúp cải thiện hiệu suất của mã của riêng tôi (và để tìm hiểu thêm một chút về nó). Tôi đến đây để tìm câu trả lời về thời điểm sử dụng nó nhưng dường như mọi nơi tôi đến đều có những cụm từ như:

Sử dụng khởi tạo lười biếng để trì hoãn việc tạo ra một đối tượng lớn hoặc sử dụng nhiều tài nguyên hoặc thực thi một nhiệm vụ sử dụng nhiều tài nguyên, đặc biệt khi việc tạo hoặc thực thi đó có thể không xảy ra trong suốt thời gian của chương trình.

từ MSDN Lazy <T> Class

Tôi có một chút bối rối vì tôi không chắc chắn nơi để vẽ đường. Ví dụ, tôi coi phép nội suy tuyến tính là một phép tính khá nhanh nhưng nếu tôi không cần phải làm điều đó thì việc khởi tạo lười biếng có thể giúp tôi tránh làm điều đó không và nó có đáng không?

Cuối cùng, tôi quyết định thử làm bài kiểm tra của riêng mình và tôi nghĩ rằng mình sẽ chia sẻ kết quả ở đây. Thật không may, tôi không thực sự là một chuyên gia trong việc thực hiện các loại thử nghiệm này và vì vậy tôi rất vui khi nhận được ý kiến ​​đề xuất cải tiến.

Sự miêu tả

Đối với trường hợp của tôi, tôi đặc biệt quan tâm xem liệu Lazy Properties có thể giúp cải thiện một phần mã của tôi không thực hiện nhiều phép nội suy (hầu hết không được sử dụng) và vì vậy tôi đã tạo một thử nghiệm so sánh 3 cách tiếp cận.

Tôi đã tạo một lớp thử nghiệm riêng biệt với 20 thuộc tính thử nghiệm (hãy gọi chúng là thuộc tính t) cho mỗi cách tiếp cận.

  • Lớp GetInterp: Chạy nội suy tuyến tính mỗi khi có thuộc tính t.
  • Lớp initInterp: Khởi tạo các thuộc tính t bằng cách chạy phép nội suy tuyến tính cho từng hàm trong hàm tạo. Nhận chỉ trả lại một gấp đôi.
  • Lớp initLazy: Thiết lập các thuộc tính t là thuộc tính Lazy để phép nội suy tuyến tính được chạy một lần khi thuộc tính được nhận lần đầu tiên. Sau đó nhận được chỉ nên trả lại một gấp đôi đã được tính toán.

Các kết quả kiểm tra được đo bằng ms và trung bình là 50 tức thời hoặc 20 tài sản nhận được. Mỗi bài kiểm tra sau đó được chạy 5 lần.

Kết quả kiểm tra 1: Khởi tạo (trung bình 50 lần khởi tạo)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.005668 0.005722 0.006704 0.006652 0.005572 0.0060636 6.72
InitInterp 0.08481  0.084908 0.099328 0.098626 0.083774 0.0902892 100.00
InitLazy   0.058436 0.05891  0.068046 0.068108 0.060648 0.0628296 69.59

Kết quả thử nghiệm 2: Nhận đầu tiên (trung bình 20 tài sản được)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.263    0.268725 0.31373  0.263745 0.279675 0.277775 54.38
InitInterp 0.16316  0.161845 0.18675  0.163535 0.173625 0.169783 33.24
InitLazy   0.46932  0.55299  0.54726  0.47878  0.505635 0.510797 100.00

Kết quả kiểm tra 3: Nhận lần thứ hai (trung bình 20 tài sản được)

Class      1        2        3        4        5        Avg       %
------------------------------------------------------------------------
GetInterp  0.08184  0.129325 0.112035 0.097575 0.098695 0.103894 85.30
InitInterp 0.102755 0.128865 0.111335 0.10137  0.106045 0.110074 90.37
InitLazy   0.19603  0.105715 0.107975 0.10034  0.098935 0.121799 100.00

Quan sát

GetInterplà nhanh nhất để khởi tạo như mong đợi vì nó không làm gì cả. InitLazynhanh hơn để khởi tạo hơn là InitInterpđề xuất rằng chi phí trong việc thiết lập các thuộc tính lười biếng nhanh hơn tính toán nội suy tuyến tính của tôi. Tuy nhiên, tôi hơi bối rối ở đây vì InitInterpphải thực hiện 20 phép nội suy tuyến tính (để thiết lập thuộc tính t) nhưng chỉ mất 0,09 ms để khởi tạo (thử nghiệm 1), so với GetInterpchỉ mất 0,28 ms để thực hiện một phép nội suy tuyến tính lần đầu tiên (thử nghiệm 2) và 0,1 ms để thực hiện lần thứ hai (thử nghiệm 3).

Lần đầu tiên phải mất InitLazygần 2 lần so với GetInterpđể có được một tài sản, trong khi đó InitInterplà nhanh nhất, bởi vì nó chiếm được các thuộc tính của nó trong thời gian khởi tạo. (Ít nhất đó là những gì nó nên làm nhưng tại sao nó lại tạo ra kết quả nhanh hơn nhiều so với phép nội suy tuyến tính đơn lẻ? Khi nào chính xác thì nó thực hiện các phép nội suy này?)

Thật không may, có vẻ như có một số tối ưu hóa mã tự động đang diễn ra trong các thử nghiệm của tôi. Sẽ mất GetInterpcùng thời gian để có được một tài sản lần đầu tiên như lần thứ hai, nhưng nó đang hiển thị nhanh hơn gấp 2 lần. Có vẻ như việc tối ưu hóa này cũng ảnh hưởng đến các lớp khác vì tất cả chúng đều mất cùng thời gian cho thử nghiệm 3. Tuy nhiên, việc tối ưu hóa như vậy cũng có thể diễn ra trong mã sản xuất của riêng tôi cũng có thể là một sự cân nhắc quan trọng.

Kết luận

Mặc dù một số kết quả như mong đợi, nhưng cũng có một số kết quả bất ngờ rất thú vị có lẽ là do tối ưu hóa mã. Ngay cả đối với các lớp trông giống như chúng đang làm rất nhiều công việc trong hàm tạo, kết quả khởi tạo cho thấy chúng vẫn có thể được tạo ra rất nhanh, so với việc có một thuộc tính kép. Mặc dù các chuyên gia trong lĩnh vực này có thể bình luận và điều tra kỹ lưỡng hơn, cảm nhận cá nhân của tôi là tôi cần phải thực hiện thử nghiệm này một lần nữa nhưng trên mã sản xuất của tôi để kiểm tra loại tối ưu hóa nào cũng có thể xảy ra ở đó. Tuy nhiên, tôi đang mong đợi rằng đó InitInterpcó thể là con đường để đi.


26
có lẽ bạn nên đăng mã kiểm tra để tái tạo đầu ra của mình, bởi vì nếu không biết mã của bạn, sẽ rất khó để đề xuất bất cứ điều gì
WiiMaxx

1
Tôi tin rằng sự đánh đổi chính là giữa việc sử dụng bộ nhớ (lười biếng) và sử dụng cpu (không lười biếng). Bởi vì lazyphải thực hiện thêm một số lưu giữ sách, InitLazysẽ sử dụng nhiều bộ nhớ hơn các giải pháp khác. Nó cũng có thể có một hiệu suất nhỏ trên mỗi lần truy cập, trong khi nó kiểm tra xem nó đã có giá trị hay chưa; thủ thuật thông minh có thể loại bỏ chi phí đó, nhưng nó sẽ cần hỗ trợ đặc biệt trong IL. (Haskell thực hiện điều này bằng cách thực hiện mọi giá trị lười biếng gọi hàm; một khi giá trị được tạo, nó sẽ được thay thế bằng hàm trả về giá trị đó mỗi lần.)
jpaugh

14

Chỉ để chỉ vào ví dụ được đăng bởi Mathew

public sealed class Singleton
{
    // Because Singleton's constructor is private, we must explicitly
    // give the Lazy<Singleton> a delegate for creating the Singleton.
    private static readonly Lazy<Singleton> instanceHolder =
        new Lazy<Singleton>(() => new Singleton());

    private Singleton()
    {
        ...
    }

    public static Singleton Instance
    {
        get { return instanceHolder.Value; }
    }
}

trước khi Lazy được sinh ra, chúng ta sẽ làm theo cách này:

private static object lockingObject = new object();
public static LazySample InstanceCreation()
{
    if(lazilyInitObject == null)
    {
         lock (lockingObject)
         {
              if(lazilyInitObject == null)
              {
                   lazilyInitObject = new LazySample ();
              }
         }
    }
    return lazilyInitObject ;
}

6
Tôi luôn sử dụng một container IoC cho việc này.
Jowen 2/2/2015

1
Tôi đồng ý mạnh mẽ về việc xem xét một container IoC cho việc này. Tuy nhiên, nếu bạn muốn một đối tượng đơn giản khởi tạo lười biếng đơn giản cũng xem xét rằng nếu bạn không cần điều này là luồng an toàn thì hãy thực hiện thủ công với If nếu có thể tốt nhất là xem xét chi phí hiệu năng của cách Lazy tự xử lý.
Thulani Chivandikwa

12

Từ MSDN:

Sử dụng một ví dụ của Lazy để trì hoãn việc tạo ra một đối tượng lớn hoặc sử dụng nhiều tài nguyên hoặc thực thi một nhiệm vụ sử dụng nhiều tài nguyên, đặc biệt khi việc tạo hoặc thực thi đó có thể không xảy ra trong suốt thời gian của chương trình.

Ngoài câu trả lời của James Michael Hare, Lazy còn cung cấp khởi tạo an toàn theo chủ đề cho giá trị của bạn. Hãy xem LazyThreadSquilMode liệt kê mục MSDN mô tả các loại chế độ an toàn luồng khác nhau cho lớp này.


-2

Bạn nên xem ví dụ này để hiểu kiến ​​trúc Lazy Loading

private readonly Lazy<List<int>> list = new Lazy<List<int>>(() =>
{
    List<int> configList = new List<int>(Thread.CurrentThread.ManagedThreadId);
    return configList;
});
public void Execute()
{
    list.Value.Add(0);
    if (list.IsValueCreated)
    {
        list.Value.Add(1);
        list.Value.Add(2);

        foreach (var item in list.Value)
        {
            Console.WriteLine(item);
        }
    }
    else
    {
        Console.WriteLine("Value not created");
    }
}

-> đầu ra -> 0 1 2

nhưng nếu mã này không ghi "list.Value.Add (0);"

đầu ra -> Giá trị không được tạo

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.