Kotlin - การเริ่มต้นคุณสมบัติโดยใช้“ โดยขี้เกียจ” กับ“ ล่าช้า”


280

ใน Kotlin หากคุณไม่ต้องการเริ่มต้นคุณสมบัติคลาสภายในตัวสร้างหรือในส่วนบนของคลาสคุณมีสองตัวเลือกโดยทั่วไป (จากการอ้างอิงภาษา):

  1. การเริ่มต้น Lazy

lazy () คือฟังก์ชั่นที่รับแลมบ์ดาและส่งคืนอินสแตนซ์ของ Lazy ซึ่งสามารถทำหน้าที่เป็นตัวแทนในการดำเนินการกับคุณสมบัติ lazy: การเรียกครั้งแรกเพื่อรับ () เรียกใช้แลมบ์ดาที่ส่งผ่านขี้เกียจ () และจดจำผลลัพธ์ เพื่อรับ () เพียงแค่ส่งคืนผลลัพธ์ที่จดจำ

ตัวอย่าง

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

ดังนั้นการโทรแรกและการโทรย่อยทุกที่ไปยังmyLazyStringจะส่งคืน"Hello"

  1. การเริ่มต้นช้า

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

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

public class MyTest {
   
   lateinit var subject: TestSubject

   @SetUp fun setup() { subject = TestSubject() }

   @Test fun test() { subject.method() }
}

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

ดังนั้นวิธีการเลือกอย่างถูกต้องระหว่างตัวเลือกทั้งสองนี้เนื่องจากทั้งคู่สามารถแก้ปัญหาเดียวกันได้

คำตอบ:


334

นี่คือความแตกต่างที่สำคัญระหว่างlateinit varและby lazy { ... }คุณสมบัติที่ได้รับมอบหมาย:

  • lazy { ... }ผู้รับมอบสิทธิ์สามารถใช้สำหรับvalคุณสมบัติเท่านั้นในขณะที่lateinitสามารถนำไปใช้กับvars เท่านั้นเนื่องจากไม่สามารถรวบรวมไปยังfinalเขตข้อมูลได้ดังนั้นจึงไม่สามารถรับประกันการเปลี่ยนไม่ได้

  • lateinit varมีเขตข้อมูลสำรองที่เก็บค่าและby lazy { ... }สร้างวัตถุผู้รับมอบสิทธิ์ซึ่งจะถูกเก็บค่าเมื่อคำนวณแล้วเก็บการอ้างอิงไปยังอินสแตนซ์ของผู้ร่วมประชุมในวัตถุคลาสและสร้าง getter สำหรับคุณสมบัติที่ทำงานกับอินสแตนซ์ของตัวแทน ดังนั้นหากคุณต้องการเขตข้อมูลสำรองอยู่ในชั้นเรียนให้ใช้lateinit;

  • นอกเหนือจากvals แล้วlateinitไม่สามารถใช้สำหรับคุณสมบัติที่ไม่เป็นโมฆะและประเภท Java ดั้งเดิม (นี่เป็นเพราะnullใช้สำหรับค่าเริ่มต้น)

  • lateinit varสามารถเริ่มต้นได้จากที่ใดก็ตามที่วัตถุถูกมองเห็นเช่นจากภายในโค้ดเฟรมเวิร์กและสถานการณ์การเริ่มต้นหลายอย่างนั้นเป็นไปได้สำหรับวัตถุที่แตกต่างของคลาสเดียว by lazy { ... }ในทางกลับกันกำหนด initializer เท่านั้นสำหรับคุณสมบัติซึ่งสามารถเปลี่ยนแปลงได้โดยการแทนที่คุณสมบัติในคลาสย่อย lateinitหากคุณต้องการทรัพย์สินของคุณจะต้องเริ่มต้นจากนอกในทางที่อาจจะไม่รู้จักก่อนการใช้งาน

  • การกำหนดค่าเริ่มต้นby lazy { ... }คือความปลอดภัยของเธรดโดยค่าเริ่มต้นและรับประกันว่า initializer จะถูกเรียกใช้มากที่สุดหนึ่งครั้ง (แต่สามารถแก้ไขได้โดยใช้โอเวอร์โหลดอื่นlazy ) ในกรณีของlateinit varมันขึ้นอยู่กับรหัสของผู้ใช้เพื่อเริ่มต้นคุณสมบัติได้อย่างถูกต้องในสภาพแวดล้อมแบบมัลติเธรด

  • Lazyเช่นสามารถบันทึกผ่านรอบและแม้กระทั่งใช้สำหรับคุณสมบัติหลาย ในทางตรงกันข้ามlateinit vars จะไม่เก็บสถานะรันไทม์เพิ่มเติมใด ๆ (เฉพาะnullในฟิลด์สำหรับค่าที่ไม่ได้กำหนดค่าเริ่มต้น)

  • ถ้าคุณถืออ้างอิงถึงตัวอย่างของLazy, isInitialized()ช่วยให้คุณสามารถตรวจสอบว่าได้รับการเริ่มต้น (และคุณสามารถขอรับตัวอย่างเช่นมีการสะท้อนจากคุณสมบัติที่ได้รับการแต่งตั้ง) เพื่อตรวจสอบว่ามีคุณสมบัติ lateinit ได้รับการเริ่มต้นคุณสามารถใช้property::isInitializedตั้งแต่ Kotlin 1.2

  • แลมบ์ดาที่ผ่านไปby lazy { ... }อาจจับภาพการอ้างอิงจากบริบทที่ใช้ในการปิด .. จากนั้นจะจัดเก็บการอ้างอิงและปล่อยเมื่อมีการเริ่มต้นคุณสมบัติเท่านั้น สิ่งนี้อาจนำไปสู่ลำดับชั้นวัตถุเช่นกิจกรรม Android ไม่ถูกปล่อยออกมานานเกินไป (หรือตลอดไปหากทรัพย์สินยังคงสามารถเข้าถึงได้และไม่เคยเข้าถึง) ดังนั้นคุณควรระมัดระวังเกี่ยวกับสิ่งที่คุณใช้ภายในแลมบ์เริ่มต้น

นอกจากนี้ยังมีวิธีอื่นที่ไม่ได้กล่าวถึงในคำถาม: Delegates.notNull()ซึ่งเหมาะสำหรับการเริ่มต้นรอการตัดบัญชีของคุณสมบัติที่ไม่เป็นโมฆะรวมถึงประเภทดั้งเดิมของ Java


9
คำตอบที่ดี! ฉันจะเพิ่มที่lateinitexposes เขตข้อมูลสำรองด้วยการมองเห็นของ setter ดังนั้นวิธีการเข้าถึงทรัพย์สินจาก Kotlin และ Java แตกต่างกัน และจากรหัส Java คุณสมบัตินี้สามารถตั้งค่าได้แม้จะnullไม่มีการตรวจสอบใด ๆ ใน Kotlin ดังนั้นจึงlateinitไม่เหมาะสำหรับการเริ่มต้นแบบขี้เกียจ แต่สำหรับการเริ่มต้นไม่จำเป็นต้องมาจากรหัส Kotlin
Michael

มีอะไรที่เทียบเท่ากับ "!" ของ Swift ?? กล่าวอีกนัยหนึ่งมันเป็นสิ่งที่เริ่มต้นช้า แต่สามารถตรวจสอบได้ว่าเป็นโมฆะโดยไม่ล้มเหลว Kotlin's 'ล่าช้า' ล้มเหลวด้วย "คุณสมบัติล่าช้าของ currentinitUser ไม่ได้รับการเริ่มต้น" ถ้าคุณตรวจสอบ 'theObject == null' สิ่งนี้มีประโยชน์มากเมื่อคุณมีวัตถุที่ไม่เป็นโมฆะในสถานการณ์การใช้งานหลักของมัน (และต้องการโค้ดกับนามธรรมที่ไม่เป็นโมฆะ) แต่เป็นโมฆะในสถานการณ์พิเศษ / จำกัด (เช่นการเข้าถึงบันทึกปัจจุบัน ในผู้ใช้ซึ่งไม่เป็นโมฆะยกเว้นเมื่อเข้าสู่ระบบครั้งแรก / บนหน้าจอเข้าสู่ระบบ)
Marchy

@Marchy คุณสามารถใช้Lazy+ ที่เก็บไว้อย่างชัดเจน.isInitialized()เพื่อทำเช่นนั้น ฉันคิดว่าไม่มีวิธีที่ตรงไปตรงมาเพื่อตรวจสอบสถานที่ให้บริการnullเพราะรับประกันว่าคุณจะไม่ได้รับnullจากมัน :) ดูตัวอย่างนี้
ฮอต

@hotkey มีจุดใดบ้างที่เกี่ยวกับการใช้งานจำนวนมากเกินไปที่by lazyจะชะลอเวลาการสร้างหรือรันไทม์?
Dr.jacky

ฉันชอบความคิดในการใช้lateinitเพื่อหลีกเลี่ยงการใช้nullค่าที่ไม่กำหนดค่าเริ่มต้น นอกเหนือจากที่nullไม่ควรใช้และมีlateinitค่าเป็นโมฆะสามารถกำจัดออกไป นั่นเป็นวิธีที่ฉันรัก Kotlin :)
KenIchi

26

ยิ่งไปกว่าhotkeyนั้นคือคำตอบที่ดีนี่คือวิธีที่ฉันเลือกระหว่างคนสองคนในทางปฏิบัติ:

lateinit มีไว้สำหรับการเริ่มต้นภายนอก: เมื่อคุณต้องการสิ่งภายนอกเพื่อเริ่มต้นค่าของคุณโดยการเรียกวิธีการ

เช่นโดยการโทร:

private lateinit var value: MyClass

fun init(externalProperties: Any) {
   value = somethingThatDependsOn(externalProperties)
}

ในขณะที่lazyเมื่อมันใช้อ้างอิงภายในวัตถุของคุณเท่านั้น


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

วิธีนี้จะพ่น UninitializedPropertyAccessException ฉันตรวจสอบอีกครั้งว่าฉันกำลังเรียกใช้ฟังก์ชัน setter ก่อนที่จะใช้ค่า มีกฎเฉพาะที่ฉันขาดหายไปกับล่าช้าหรือไม่ ในคำตอบของคุณแทนที่ MyClass และอื่น ๆ ด้วยบริบท Android นั่นคือกรณีของฉัน
Talha

24

คำตอบสั้นและกระชับ

ล่าช้า: มันเริ่มต้นคุณสมบัติที่ไม่ใช่ null เมื่อเร็ว ๆ นี้

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

การเริ่มต้นขี้เกียจ

โดย lazyอาจมีประโยชน์มากเมื่อใช้คุณสมบัติread-only (val) ที่ดำเนินการ initialization lazy ใน Kotlin

โดย lazy {... } ดำเนินการ initializer โดยที่คุณสมบัติที่กำหนดถูกใช้ครั้งแรกไม่ใช่การประกาศ


คำตอบที่ดีโดยเฉพาะอย่างยิ่ง "ดำเนินการ initializer ที่คุณสมบัติที่กำหนดจะใช้ครั้งแรกไม่ประกาศ"
user1489829

17

ล่าช้าหรือขี้เกียจ

  1. lateinit

    i) ใช้กับตัวแปรที่ไม่แน่นอน [var]

    lateinit var name: String       //Allowed
    lateinit val name: String       //Not Allowed
    

    ii) อนุญาตให้ใช้กับชนิดข้อมูลที่ไม่สามารถยกเลิกได้เท่านั้น

    lateinit var name: String       //Allowed
    lateinit var name: String?      //Not Allowed
    

    iii) มันเป็นสัญญาที่จะคอมไพเลอร์ว่าค่าจะเริ่มต้นในอนาคต

หมายเหตุ : หากคุณพยายามเข้าถึงตัวแปรล่าช้าโดยไม่เริ่มต้นมันจะส่ง UnInitializedPropertyAccessException

  1. ขี้เกียจ

    i) การเริ่มต้น Lazy ถูกออกแบบมาเพื่อป้องกันการเริ่มต้นวัตถุที่ไม่จำเป็น

    ii) ตัวแปรของคุณจะไม่เริ่มต้นเว้นแต่ว่าคุณจะใช้

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

    iv) มันปลอดภัยของเธรด (มันจะเริ่มต้นในเธรดที่ใช้เป็นครั้งแรกเธรดอื่น ๆ ใช้ค่าเดียวกันที่เก็บไว้ในแคช)

    v) ตัวแปรสามารถเป็นvalได้เท่านั้น

    vi) ตัวแปรเท่านั้นที่สามารถจะไม่ใช่nullable


7
ฉันคิดว่าในตัวแปรขี้เกียจไม่สามารถ var
DäñishShärmà

4

นอกจากคำตอบที่ยอดเยี่ยมทั้งหมดแล้วยังมีแนวคิดที่เรียกว่า lazy loading:

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

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

แต่ล่าช้าจะใช้เมื่อคุณแน่ใจว่าตัวแปรจะไม่ว่างเปล่าและจะถูกเตรียมใช้งานก่อนที่คุณจะใช้ -eg ในonResume()วิธีสำหรับ android- และดังนั้นคุณไม่ต้องการที่จะประกาศว่ามันเป็นประเภท nullable


ใช่ฉันเริ่มต้นonCreateViewด้วยonResumeและอื่น ๆ ด้วยlateinitแต่บางครั้งข้อผิดพลาดเกิดขึ้นที่นั่น (เพราะบางเหตุการณ์เริ่มต้นก่อนหน้านี้) ดังนั้นอาจby lazyให้ผลลัพธ์ที่เหมาะสม ฉันใช้lateinitสำหรับตัวแปรที่ไม่เป็นค่าว่างที่สามารถเปลี่ยนแปลงได้ในระหว่างรอบการใช้งาน
CoolMind

2

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


0

หากคุณกำลังใช้ Spring container และคุณต้องการเริ่มต้นฟิลด์ bean ที่ไม่เป็นโมฆะlateinitจะเหมาะกว่า

    @Autowired
    lateinit var myBean: MyBean

1
น่าจะเป็น@Autowired lateinit var myBean: MyBean
Cnfn

0

ถ้าคุณใช้ตัวแปร unchangable แล้วมันจะดีกว่าที่จะเริ่มต้นด้วยหรือby lazy { ... } valในกรณีนี้คุณสามารถมั่นใจได้ว่ามันจะเริ่มต้นได้เสมอเมื่อมีความจำเป็นและมากที่สุด 1 ครั้ง

หากคุณต้องการตัวแปร null lateinit varไม่ใช่ว่าสามารถเปลี่ยนค่าของมันใช้ ในการพัฒนา Android คุณสามารถเริ่มต้นได้ในภายหลังในกิจกรรมเช่นonCreate, onResume. โปรดระวังว่าถ้าคุณเรียกใช้คำขอ REST และเข้าถึงตัวแปรนี้อาจทำให้เกิดข้อยกเว้นUninitializedPropertyAccessException: lateinit property yourVariable has not been initializedเนื่องจากการร้องขอสามารถดำเนินการได้เร็วกว่าตัวแปรนั้นสามารถเริ่มต้นได้

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