เมื่อใดที่ฉันควรใช้ Lazy <T>


327

ฉันพบบทความนี้เกี่ยวกับLazy: ความเกียจคร้านใน C # 4.0 - Lazy

แนวปฏิบัติที่ดีที่สุดในการใช้งานวัตถุ Lazy ให้ได้ประสิทธิภาพสูงสุดคืออะไร ใครบางคนชี้ให้ฉันเห็นการใช้งานจริงในแอปพลิเคชันจริง กล่าวอีกนัยหนึ่งฉันควรใช้เมื่อใด


42
แทนที่: get { if (foo == null) foo = new Foo(); return foo; }. และมีสถานที่ที่เป็นไปได้มากมายที่จะใช้ ...
Kirk Woll

57
โปรดทราบว่าget { if (foo == null) foo = new Foo(); return foo; }ไม่ปลอดภัยสำหรับเธรดในขณะที่Lazy<T>ปลอดภัยตามเธรดโดยค่าเริ่มต้น
Matthew

23
จาก MSDN: สำคัญ: การเริ่มต้น Lazy นั้นปลอดภัยต่อเธรด แต่ไม่ได้ป้องกันวัตถุหลังจากการสร้าง คุณต้องล็อควัตถุก่อนที่จะเข้าถึงเว้นแต่ว่าประเภทนี้จะปลอดภัยสำหรับเธรด
Pedro.The.Kid

คำตอบ:


237

โดยทั่วไปคุณจะใช้มันเมื่อคุณต้องการยกตัวอย่างบางสิ่งในครั้งแรกที่ใช้จริง การทำเช่นนี้จะทำให้เกิดค่าใช้จ่ายในการสร้างจนกว่าจะถึงเวลาที่จำเป็นแทนที่จะต้องเสียค่าใช้จ่าย

โดยปกติสิ่งนี้จะดีกว่าเมื่อวัตถุอาจหรืออาจไม่ได้ใช้และค่าใช้จ่ายในการสร้างมันเป็นเรื่องไม่สำคัญ


121
ทำไมไม่เพียง แต่ใช้ Lazy เสมอ
TruthOf42

44
มีค่าใช้จ่ายในการใช้งานครั้งแรกและอาจใช้ค่าใช้จ่ายในการล็อค ดังนั้นจึงควรเลือกอย่างระมัดระวังและไม่ได้ใช้เว้นแต่จำเป็น
James Michael Hare

3
เจมส์คุณช่วยกรุณาขยายความ "และค่าใช้จ่ายในการสร้างนั้นไม่ใช่เรื่องเล็กน้อย"? ในกรณีของฉันฉันมี 19 คุณสมบัติในชั้นเรียนของฉันและในกรณีส่วนใหญ่เพียง 2 หรือ 3 จะต้องได้รับการดู Lazy<T>ดังนั้นฉันกำลังพิจารณาการดำเนินการของสถานที่ที่ใช้แต่ละ อย่างไรก็ตามเพื่อสร้างสถานที่ให้บริการแต่ละฉันกำลังทำการแก้ไขเชิงเส้น (หรือการแก้ไข bilinear) ซึ่งเป็นเรื่องเล็กน้อย แต่มีค่าใช้จ่ายบางอย่าง (คุณจะแนะนำให้ฉันไปและทำการทดลองด้วยตัวเองหรือไม่?)
เบ็

3
เจมส์ทำตามคำแนะนำของตัวเองฉันทำการทดลองของตัวเอง เห็นโพสต์ของฉัน
Ben

17
คุณอาจต้องการเริ่มต้น / สร้างอินสแตนซ์ทุกอย่าง "ระหว่าง" การเริ่มต้นระบบเพื่อป้องกันไม่ให้ผู้ใช้เกิดความล่าช้าในการรับส่งข้อมูลสูงและระบบเวลาแฝงต่ำ นี่เป็นเพียงหนึ่งในหลายเหตุผลที่ไม่ใช้ "Lazy" เสมอ
Derrick

126

คุณควรพยายามหลีกเลี่ยงการใช้ Singletons แต่ถ้าคุณจำเป็นต้องLazy<T>ทำให้การใช้ singletons ที่ปลอดภัยต่อเธรดเป็นเรื่องขี้เกียจง่าย:

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
ฉันเกลียดการอ่านคุณควรพยายามหลีกเลี่ยงการใช้ซิงเกิลตอนที่ฉันใช้: D ... ตอนนี้ฉันต้องเรียนรู้ว่าทำไมฉันควรพยายามหลีกเลี่ยงพวกเขา: D
Bart Calixto

24
ฉันจะหยุดใช้ Singletons เมื่อ Microsoft หยุดใช้ในตัวอย่างของพวกเขา
eaglei22

4
ฉันมักจะไม่เห็นด้วยกับแนวคิดที่ต้องการหลีกเลี่ยงซิงเกิล เมื่อทำตามกระบวนทัศน์การฉีดของการพึ่งพาอาศัยกันมันไม่ควรมีความสำคัญ ตามหลักการแล้วการอ้างอิงทั้งหมดของคุณควรสร้างเพียงครั้งเดียว สิ่งนี้จะช่วยลดแรงกดดันต่อ GC ในสถานการณ์โหลดสูง ดังนั้นการทำให้พวกเขาเป็นโสดจากภายในชั้นเรียนของตัวเองเป็นเรื่องปกติ ภาชนะ DI สมัยใหม่ส่วนใหญ่ (ถ้าไม่ใช่ทั้งหมด) สามารถจัดการกับมันได้ตามที่คุณเลือก
Lee Grissom

1
คุณไม่จำเป็นต้องใช้รูปแบบซิงเกิลแบบนั้นแทนใช้คอนเทนเนอร์ di ใด ๆ กำหนดคลาสของคุณสำหรับซิงเกิลแทน ภาชนะจะดูแลค่าใช้จ่ายสำหรับคุณ
VivekDev

ทุกอย่างมีจุดมุ่งหมายมีสถานการณ์ที่ซิงเกิลตันเป็นแนวทางที่ดีและสถานการณ์ที่ไม่ใช่ :)
Hawkzey

86

ตัวอย่างจริงของโลกที่มีการโหลดแบบขี้เกียจมีประโยชน์กับ ORM (Object Relation Mappers) เช่น Entity Framework และ NHibernate

สมมติว่าคุณมีลูกค้านิติบุคคลซึ่งมีคุณสมบัติสำหรับชื่อหมายเลขโทรศัพท์และคำสั่งซื้อ ชื่อและ PhoneNumber เป็นสตริงปกติ แต่คำสั่งซื้อเป็นคุณสมบัติการนำทางที่ส่งคืนรายการคำสั่งซื้อทุกรายการที่ลูกค้าเคยทำ

คุณมักจะต้องการผ่านลูกค้าทั้งหมดของคุณและรับชื่อและหมายเลขโทรศัพท์ของพวกเขาเพื่อเรียกพวกเขา นี่เป็นงานที่ง่ายและรวดเร็วมาก แต่ลองคิดดูว่าในแต่ละครั้งที่คุณสร้างลูกค้าลูกค้าจะไปโดยอัตโนมัติและเข้าร่วมที่ซับซ้อนเพื่อส่งคืนคำสั่งซื้อนับพันรายการ ส่วนที่แย่ที่สุดคือคุณไม่ต้องใช้คำสั่งซื้อดังนั้นจึงเป็นการสิ้นเปลืองทรัพยากรอย่างสมบูรณ์!

นี่คือสถานที่ที่เหมาะสำหรับการโหลดขี้เกียจเพราะถ้าคุณสมบัติการสั่งซื้อเป็นสันหลังยาวมันจะไม่ไปดึงคำสั่งของลูกค้าทั้งหมดเว้นแต่คุณจะต้องการมันจริง ๆ คุณสามารถระบุวัตถุของลูกค้าที่ได้รับเพียงชื่อและหมายเลขโทรศัพท์ของพวกเขาในขณะที่คุณสมบัติการสั่งซื้อกำลังหลับอย่างอดทนพร้อมสำหรับเมื่อคุณต้องการ


34
ตัวอย่างที่ไม่ดีเช่นการโหลดแบบสันหลังยาวมักจะสร้างไว้ใน ORM แล้ว คุณไม่ควรเริ่มเพิ่มค่า Lazy <T> ใน POCO ของคุณเพื่อรับการโหลดที่ขี้เกียจ แต่ใช้วิธีเฉพาะ ORM เพื่อทำเช่นนั้น
Dynalon

56
@Dyna ตัวอย่างนี้อ้างถึงการโหลดแบบ lazy ในตัวของ ORM เพราะฉันคิดว่านี่เป็นตัวอย่างของประโยชน์ของการโหลดแบบ lazy ในวิธีที่ชัดเจนและเรียบง่าย
Despertar

ดังนั้นหากคุณใช้ประโยชน์จาก Entity Framework ควรบังคับใช้ความขี้เกียจของตัวเองหรือไม่? หรือ EF ทำเพื่อคุณ
Zapnologica

7
@Zapnologica EF ทำทุกอย่างให้คุณตามค่าเริ่มต้น ในความเป็นจริงถ้าคุณต้องการโหลดกระตือรือร้น (ตรงข้ามของการโหลดขี้เกียจ), คุณต้องบอกอย่างชัดเจน EF Db.Customers.Include("Orders")โดยใช้ สิ่งนี้จะทำให้การสั่งซื้อการเข้าร่วมถูกดำเนินการในขณะนั้นมากกว่าเมื่อCustomer.Ordersมีการใช้งานคุณสมบัติครั้งแรก Lazy Loading ยังสามารถปิดการใช้งานได้ผ่าน DbContext
Despertar

2
อันที่จริงนี่เป็นตัวอย่างที่ดีเนื่องจากอาจต้องการเพิ่มฟังก์ชั่นนี้เมื่อใช้งานอะไรบางอย่างเช่น Dapper
tbone

41

ฉันได้พิจารณาใช้Lazy<T>คุณสมบัติเพื่อช่วยปรับปรุงประสิทธิภาพของรหัสของฉัน (และเรียนรู้เพิ่มเติมเล็กน้อยเกี่ยวกับมัน) ฉันมาที่นี่เพื่อค้นหาคำตอบเกี่ยวกับเวลาที่จะใช้ แต่ดูเหมือนว่าทุกที่ที่ฉันไปมีวลีเช่น:

ใช้การเริ่มต้นแบบขี้เกียจเพื่อเลื่อนการสร้างวัตถุที่มีขนาดใหญ่หรือใช้ทรัพยากรมากหรือการดำเนินการของงานที่ใช้ทรัพยากรมากโดยเฉพาะอย่างยิ่งเมื่อการสร้างหรือการดำเนินการดังกล่าวอาจไม่เกิดขึ้นในช่วงอายุการใช้งานของโปรแกรม

จากMSDN Lazy <T> คลาส

ฉันสับสนเล็กน้อยเพราะฉันไม่แน่ใจว่าจะวาดเส้นตรงไหน ตัวอย่างเช่นฉันคิดว่าการแก้ไขเชิงเส้นเป็นการคำนวณที่ค่อนข้างเร็ว แต่ถ้าฉันไม่จำเป็นต้องทำมันการเริ่มต้นแบบขี้เกียจสามารถช่วยฉันหลีกเลี่ยงการทำมันและมันคุ้มค่าหรือไม่

ในที่สุดฉันก็ตัดสินใจลองทำแบบทดสอบของตัวเองและฉันคิดว่าฉันจะแบ่งปันผลที่นี่ น่าเสียดายที่ฉันไม่ใช่ผู้เชี่ยวชาญในการทำแบบทดสอบเหล่านี้ดังนั้นฉันจึงยินดีรับความคิดเห็นที่แนะนำการปรับปรุง

ลักษณะ

สำหรับกรณีของฉันฉันสนใจเป็นพิเศษเพื่อดูว่า Lazy Properties สามารถช่วยปรับปรุงส่วนของรหัสของฉันที่ทำการแก้ไขจำนวนมาก (ส่วนใหญ่ไม่ได้ใช้งาน) และดังนั้นฉันจึงได้สร้างการทดสอบที่เปรียบเทียบ 3 แนวทาง

ฉันสร้างคลาสการทดสอบแยกต่างหากพร้อมด้วยคุณสมบัติการทดสอบ 20 รายการ (ให้เรียกว่าคุณสมบัติที) สำหรับแต่ละวิธี

  • คลาส GetInterp: ทำการแก้ไขเชิงเส้นทุกครั้งที่มีคุณสมบัติ t
  • คลาส InitInterp:เริ่มต้นคุณสมบัติ t โดยเรียกใช้การแก้ไขเชิงเส้นสำหรับแต่ละรายการในตัวสร้าง รับเพียงแค่ส่งกลับสองครั้ง
  • InitLazy Class:ตั้งค่าคุณสมบัติ t- เป็นคุณสมบัติ Lazy เพื่อให้การแก้ไขเชิงเส้นทำงานครั้งเดียวเมื่อคุณสมบัติได้รับครั้งแรก การได้รับครั้งต่อมาควรจะคืนค่า double ที่คำนวณแล้ว

ผลการทดสอบวัดเป็นมิลลิวินาทีและมีค่าเฉลี่ย 50 อินสแตนซ์หรือ 20 คุณสมบัติที่ได้รับ การทดสอบแต่ละครั้งจะทำงาน 5 ครั้ง

ทดสอบผลลัพธ์ 1:อินสแตนซ์ (เฉลี่ย 50 instantiations)

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

ทดสอบผลลัพธ์ 2: รับครั้งแรก (โดยเฉลี่ย 20 คุณสมบัติได้รับ)

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

ทดสอบ 3 ผลลัพธ์: รับครั้งที่สอง (โดยเฉลี่ย 20 คุณสมบัติได้รับ)

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

ข้อสังเกต

GetInterpเร็วที่สุดในการยกตัวอย่างตามที่คาดไว้เพราะมันไม่ได้ทำอะไรเลย InitLazyเร็วกว่า instantiate กว่าInitInterpแนะนำว่าค่าใช้จ่ายในการตั้งค่าคุณสมบัติขี้เกียจนั้นเร็วกว่าการคำนวณการแก้ไขเชิงเส้น อย่างไรก็ตามฉันสับสนเล็กน้อยที่นี่เพราะInitInterpควรทำการประมาณ 20 เชิงเส้น (เพื่อตั้งค่าเป็นคุณสมบัติ t) แต่ใช้เวลาเพียง 0.09 ms ในการสร้างอินสแตนซ์ (ทดสอบ 1) เมื่อเทียบกับGetInterpที่ใช้เวลา 0.28 มิลลิวินาทีในการแก้ไขเชิงเส้นเดียว ครั้งแรก (ทดสอบ 2) และ 0.1 ms เพื่อทำครั้งที่สอง (ทดสอบ 3)

ใช้InitLazyเวลานานกว่า 2 เท่าGetInterpในการรับคุณสมบัติเป็นครั้งแรกในขณะInitInterpที่เร็วที่สุดเนื่องจากมีคุณสมบัติในระหว่างการสร้างอินสแตนซ์ (อย่างน้อยนั่นคือสิ่งที่ควรทำ แต่ทำไมผลของอินสแตนซ์นั้นเร็วกว่าการสอดแทรกเชิงเส้นเดี่ยวมาก?

น่าเสียดายที่ดูเหมือนว่ามีการเพิ่มประสิทธิภาพโค้ดอัตโนมัติในการทดสอบของฉัน ควรใช้เวลาGetInterpเดียวกันเพื่อให้ได้ทรัพย์สินในครั้งแรกเหมือนครั้งที่สอง แต่จะแสดงเร็วกว่า 2x ดูเหมือนว่าการเพิ่มประสิทธิภาพนี้จะมีผลกับคลาสอื่นด้วยเช่นกันเนื่องจากพวกเขาทั้งหมดใช้เวลาในการทดสอบ 3 เท่ากันอย่างไรก็ตามการปรับให้เหมาะสมดังกล่าวอาจเกิดขึ้นในรหัสการผลิตของตัวเองซึ่งอาจเป็นข้อพิจารณาที่สำคัญ

สรุปผลการวิจัย

ในขณะที่ผลลัพธ์บางอย่างเป็นไปตามคาด แต่ก็มีผลลัพธ์ที่ไม่คาดคิดที่น่าสนใจมากซึ่งอาจเกิดจากการปรับโค้ดให้เหมาะสม แม้แต่คลาสที่ดูเหมือนว่าพวกเขากำลังทำงานเป็นจำนวนมากในคอนสตรัคเตอร์ผลลัพธ์ของอินสแตนซ์แสดงว่าพวกเขายังคงสามารถสร้างได้อย่างรวดเร็วเมื่อเทียบกับการได้รับคุณสมบัติสองชั้น ในขณะที่ผู้เชี่ยวชาญในสาขานี้อาจสามารถแสดงความคิดเห็นและตรวจสอบได้อย่างละเอียดยิ่งขึ้นความรู้สึกส่วนตัวของฉันคือฉันต้องทำการทดสอบนี้อีกครั้ง แต่ในรหัสการผลิตของฉันเพื่อตรวจสอบว่ามีการเพิ่มประสิทธิภาพประเภทใดบ้าง อย่างไรก็ตามฉันคาดหวังว่าInitInterpอาจเป็นวิธีที่จะไป


26
บางทีคุณควรโพสต์รหัสทดสอบของคุณเพื่อทำซ้ำผลลัพธ์ของคุณเพราะไม่ทราบรหัสของคุณมันจะเป็นการยากที่จะแนะนำอะไร
WiiMaxx

1
ฉันเชื่อว่าการแลกเปลี่ยนที่สำคัญคือระหว่างการใช้หน่วยความจำ (ขี้เกียจ) และการใช้ cpu (ไม่ขี้เกียจ) เพราะlazyต้องทำหนังสือพิเศษบางอย่างInitLazyจะใช้หน่วยความจำมากกว่าโซลูชันอื่น นอกจากนี้ยังอาจมีประสิทธิภาพเล็กน้อยในการเข้าถึงแต่ละครั้งในขณะที่ตรวจสอบว่ามีค่าหรือไม่; เทคนิคฉลาดสามารถลบค่าใช้จ่ายที่ แต่มันจะต้องมีการสนับสนุนพิเศษใน IL (Haskell ทำสิ่งนี้ด้วยการทำให้ทุกค่าขี้เกียจเรียกใช้ฟังก์ชันเมื่อค่าถูกสร้างขึ้นมันจะถูกแทนที่ด้วยฟังก์ชันที่คืนค่านั้นทุกครั้ง)
jpaugh

14

เพียงชี้ไปที่ตัวอย่างที่โพสต์โดยแม็ตธิว

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; }
    }
}

ก่อนที่ Lazy จะเกิดเราจะทำแบบนี้:

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

6
ฉันมักจะใช้คอนเทนเนอร์ IoC สำหรับสิ่งนี้
Jowen

1
ฉันเห็นด้วยอย่างยิ่งในการพิจารณาคอนเทนเนอร์ IoC สำหรับสิ่งนี้ อย่างไรก็ตามถ้าคุณต้องการให้วัตถุที่เริ่มต้นด้วยสันหลังยาวอย่างง่าย ๆ ต้องพิจารณาด้วยว่าถ้าคุณไม่ต้องการให้เธรดปลอดภัยทำด้วยตนเองด้วยถ้าอาจเป็นการดีที่สุดในการพิจารณาค่าใช้จ่ายด้านประสิทธิภาพของวิธีที่ Lazy จัดการเอง
Thulani Chivandikwa

12

จาก MSDN:

ใช้อินสแตนซ์ของ Lazy เพื่อเลื่อนการสร้างวัตถุที่มีขนาดใหญ่หรือใช้ทรัพยากรมากหรือการดำเนินการของงานที่ใช้ทรัพยากรมากโดยเฉพาะอย่างยิ่งเมื่อการสร้างหรือการดำเนินการดังกล่าวอาจไม่เกิดขึ้นในช่วงอายุการใช้งานของโปรแกรม

นอกจากคำตอบของ James Michael Hare แล้ว Lazy ยังให้การกำหนดค่าเริ่มต้นที่ปลอดภัยสำหรับเธรดของคุณ ลองดูที่การแจงนับLazyThreadSafetyModeรายการ MSDN ที่อธิบายถึงโหมดความปลอดภัยของเธรดประเภทต่างๆสำหรับคลาสนี้


-2

คุณควรดูตัวอย่างนี้เพื่อทำความเข้าใจกับสถาปัตยกรรม 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");
    }
}

-> เอาต์พุต -> 0 1 2

แต่ถ้ารหัสนี้ไม่ได้เขียน "list.Value.Add (0);"

เอาท์พุท -> ค่าไม่ได้สร้าง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.