การเริ่มต้นแบบเลื่อนเวลา (lazy initialization) เป็นกลไกใน Kotlin ซึ่งคุณสมบัติของอ็อบเจกต์ถูกเริ่มต้นไม่ใช่ในขณะที่สร้าง แต่เมื่อมีการเข้าถึงครั้งแรก ตามข้อมูลจาก JetBrains, 2024, lateinit และ lazy เป็นเครื่องมือในตัวสองอย่างสำหรับใช้กลยุทธ์นี้ ทั้งสองแก้ปัญหาการเริ่มต้นแบบเลื่อนเวลา แต่แตกต่างกันโดยพื้นฐานในกลไกการทำงานและขอบเขตการใช้งาน
ประเด็นสำคัญ
การเริ่มต้นแบบเลื่อนเวลา เป็นรูปแบบที่คุณสมบัติของคลาสได้รับค่าไม่ใช่ในขณะที่สร้างอ็อบเจกต์ แต่ในภายหลังเมื่อจำเป็น ใน Kotlin รูปแบบนี้ถูกนำไปใช้ด้วยสองวิธีที่แตกต่างกันโดยพื้นฐาน: ตัวปรับแต่ง lateinit และตัวแทน lazy
ทั้งสองกลไกแก้ปัญหาทั่วไป — คุณสมบัติต้องมีอยู่ในคลาส แต่ค่าของมันไม่เป็นที่รู้จักในขณะที่สร้างอ็อบเจกต์ หรือการคำนวณมันใช้ทรัพยากรมากเกินไปที่จะทำโดยไม่จำเป็น ตามข้อมูลจาก Google I/O 2023 คุณสมบัติมากถึง 40% ในแอปพลิเคชัน Android ทั่วไปสามารถปรับให้เหมาะสมผ่านการเริ่มต้นแบบเลื่อนเวลา ซึ่งลดเวลาเริ่มต้นลง 15–25%
การเลือกระหว่าง lateinit และ lazy ถูกกำหนดโดยสามปัจจัย: ความเปลี่ยนแปลงได้ของคุณสมบัติ (var หรือ val) วงจรชีวิตของมัน (การกำหนดค่าเดียวหรือหลายครั้ง) และข้อกำหนดด้านความปลอดภัยของเธรด (การเข้าถึงแบบเธรดเดียวหรือหลายเธรด)
สถานการณ์แรกและพบบ่อยที่สุดคือ การฉีดพึ่งพา เฟรมเวิร์ก (Dagger, Hilt, Koin) ฉีดพึ่งพาหลังจากการสร้างอ็อบเจกต์ ดังนั้นคุณสมบัติจึงไม่สามารถเริ่มต้นในคอนสตรักเตอร์ได้ หากไม่มี lateinit พึ่งพาทั้งหมดจะต้องถูกประกาศเป็น nullable และตรวจสอบทุกครั้งที่ใช้งาน
สถานการณ์ที่สองคือ ทรัพยากรหนัก: ฐานข้อมูล, ไคลเอนต์เครือข่าย, ตัวจัดการไฟล์ การสร้างมันต้องใช้เวลาและหน่วยความจำ ดังนั้นควรเริ่มต้นเมื่อใช้งานจริงเท่านั้น lazy เหมาะสำหรับกรณีเหล่านี้ รับประกันการสร้างครั้งเดียว
สถานการณ์ที่สามคือ คอมโพเนนต์ Android (Activity, Fragment, ViewModel) ซึ่งวงจรชีวิตถูกจัดการโดยระบบปฏิบัติการ คุณสมบัติที่ขึ้นอยู่กับ onCreate, onViewCreated หรือบล็อก init ของ ViewModel ไม่สามารถเริ่มต้นในคอนสตรักเตอร์ได้
lateinit เป็นตัวปรับแต่งสำหรับคุณสมบัติ var ที่อนุญาตให้คอมไพเลอร์ Kotlin เลื่อนการเริ่มต้น คอมไพเลอร์ไม่ต้องการการกำหนดค่าในคอนสตรักเตอร์ แต่สร้างการตรวจสอบขณะรันไทม์ทุกครั้งที่มีการเข้าถึง: หากคุณสมบัติไม่ได้ถูกเริ่มต้น มันจะขว้าง UninitializedPropertyAccessException
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
ข้อจำกัดของ lateinit: คุณสมบัติต้องถูกประกาศเป็น var (ไม่ใช่ val), ไม่เป็น nullable, และไม่ใช่ชนิดพื้นฐาน (Int, Double, Boolean ฯลฯ) สาเหตุคือชนิดพื้นฐานถูกคอมไพล์เป็นพื้นฐาน JVM ซึ่งไม่มีสถานะ “ไม่ได้เริ่มต้น” สำหรับคุณสมบัติที่ nullable การเริ่มต้นแบบเลื่อนเวลาไม่จำเป็น: null หมายถึงการไม่มีค่าอยู่แล้ว
ในการตรวจสอบสถานะของคุณสมบัติ lateinit ให้ใช้การอ้างอิงในตัวผ่านโอเปอเรเตอร์ ::: ::propertyName.isInitialized นี่เป็นวิธีที่ปลอดภัยเพียงวิธีเดียวในการตรวจสอบว่าคุณสมบัติถูกเริ่มต้นแล้วหรือไม่โดยไม่เสี่ยงต่อข้อยกเว้น การตรวจสอบสามารถใช้ได้จากคลาสเดียวกันหรือคลาสภายในเท่านั้น ไม่ใช่จากโค้ดภายนอก
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit ไม่เพิ่มโอเวอร์เฮดหลังจากการเริ่มต้น: เมื่อกำหนดค่าแล้ว การเข้าถึงคุณสมบัติจะเหมือนกับการเข้าถึงฟิลด์โดยตรง ค่าใช้จ่ายเดียวคือ การตรวจสอบการเริ่มต้นในการอ่านทุกครั้งก่อนการกำหนดค่า หลังจากการเริ่มต้น คอมไพเลอร์ JIT จะปรับการตรวจสอบให้เหมาะสม
หมายเหตุสำคัญ: คุณสมบัติ lateinit ไม่สามารถใช้ใน คลาสอินไลน์ และไม่รองรับสำหรับคุณสมบัติที่มี getter/setter ที่กำหนดเอง หากคุณสมบัติต้องการการเข้าถึงแบบคำนวณ ให้ใช้ lazy แทน lateinit
lazy เป็นตัวแทนคุณสมบัติที่สร้างในไลบรารีมาตรฐานของ Kotlin มันคำนวณค่าเมื่อเข้าถึงคุณสมบัติครั้งแรกและแคชผลลัพธ์สำหรับการเรียกทั้งหมดที่ตามมา ต่างจาก lateinit, lazy ทำงานกับ val เท่านั้น ทำให้คุณสมบัติไม่สามารถเปลี่ยนแปลงได้หลังจากการเริ่มต้น
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy รับพารามิเตอร์เสริม LazyThreadSafetyMode ที่ควบคุมกลไกความปลอดภัยของเธรด ค่าเริ่มต้นคือ SYNCHRONIZED — การตรวจสอบซ้อนกับการล็อก รับประกันการเริ่มต้นครั้งเดียวแม้ภายใต้การเข้าถึงพร้อมกันจากหลายเธรด
โหมด PUBLICATION อนุญาตให้เริ่มต้นแบบขนาน: หลายเธรดสามารถดำเนินการบล็อกเริ่มต้นพร้อมกันได้ แต่ผลลัพธ์จะถูกยอมรับจากเธรดที่เสร็จก่อนเท่านั้น ซึ่งเร็วกว่า SYNCHRONIZED เมื่อมีการแข่งขันสูง แต่เพิ่มการใช้ทรัพยากร
โหมด NONE ปิดการซิงโครไนซ์โดยสมบูรณ์ ใช้สำหรับคุณสมบัติที่รับประกันการเข้าถึงจากเธรดเดียวเท่านั้น ในโหมดนี้ lazy ทำงานด้วยโอเวอร์เฮดน้อยที่สุด — เกือบเหมือนการกำหนดค่าโดยตรง
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy เป็นตัวเลือกที่ถูกต้องสำหรับพึ่งพาที่ เริ่มต้นครั้งเดียว: ที่เก็บข้อมูล, ไคลเอนต์เครือข่าย, แคช, ฐานข้อมูล ความหมายของ val ป้องกันการเขียนทับโดยไม่ตั้งใจ และความปลอดภัยของเธรดโดยค่าเริ่มต้นทำให้โค้ดปลอดภัยในสภาพแวดล้อมแบบหลายเธรด lazy ยังทำงานถูกต้องกับ ชนิดพื้นฐาน ซึ่งเป็นไปไม่ได้กับ lateinit
ใน Android lazy มักใช้สำหรับเริ่มต้น พึ่งพาของ ViewModel ผ่าน by viewModels() หรือสำหรับสร้างไคลเอนต์ Retrofit อย่างไรก็ตาม ระวัง: หากบล็อก lazy จับการอ้างอิงไปยัง Activity หรือ Fragment มันอาจทำให้ หน่วยความจำรั่ว เนื่องจากตัวแทนเก็บการปิดไว้ตลอดอายุของคุณสมบัติ
การเลือกระหว่าง lateinit และ lazy ไม่ใช่เรื่องของความชอบ แต่เป็นการตัดสินใจทางสถาปัตยกรรมที่กำหนดโดยธรรมชาติของคุณสมบัติ แต่ละกลไกแก้ปัญหาของตัวเอง และพื้นที่การใช้งานของมันทับซ้อนกันเพียงบางส่วน
| เกณฑ์ | lateinit | lazy |
|---|---|---|
| ชนิดคุณสมบัติ | เฉพาะ var | เฉพาะ val |
| Nullable | ไม่อนุญาต | อนุญาต |
| ชนิดพื้นฐาน | ไม่อนุญาต | อนุญาต |
| ความปลอดภัยของเธรด | ไม่รับประกัน | SYNCHRONIZED โดยค่าเริ่มต้น |
| การตรวจสอบสถานะ | ::x.isInitialized | ไม่จำเป็น |
| ข้อยกเว้นเมื่อผิดพลาด | UninitializedPropertyAccessException | ข้อผิดพลาดในบล็อก init |
| การแคช | ใช้ไม่ได้ | คำนวณครั้งเดียว |
| Android Binding | View Binding, Data Binding | ไม่ใช้ |
| เฟรมเวิร์ก DI | Dagger, Hilt, Koin | ฉีดด้วยตนเอง |
ใช้ lateinit เมื่อคุณสมบัติต้องเปลี่ยนแปลงหลังจากการเริ่มต้นหรือการสร้างถูกจัดการโดยโค้ดภายนอก ตัวอย่างทั่วไปคือ View Binding ใน Android Activity: binding ถูกสร้างใน onCreate แต่ยังคงเป็น var เพราะเฟรมเวิร์กไม่รองรับ val สำหรับสถานการณ์นี้
ใช้ lazy เมื่อคุณสมบัติถูกเริ่มต้นครั้งเดียว การคำนวณมีราคาแพง และค่าไม่เปลี่ยนแปลงตลอดอายุของอ็อบเจกต์ ตัวอย่างคลาสสิกคือการสร้าง ไคลเอนต์ Retrofit หรือฐานข้อมูล Room แบบขี้เกียจเมื่อเข้าถึงที่เก็บข้อมูลครั้งแรก
ทั้งสองกลไกสามารถใช้พร้อมกันภายในคลาสเดียวกัน ตัวอย่างเช่น lateinit สำหรับ View Binding และ lazy สำหรับที่เก็บข้อมูล นี่เป็นปฏิบัติปกติที่สะท้อนความต้องการที่แตกต่างกันสำหรับคุณสมบัติที่แตกต่างกัน สิ่งสำคัญคือไม่สับสนความหมาย: อย่าใช้ lateinit ในที่ที่ต้องการ val และอย่าใช้ lazy สำหรับคุณสมบัติที่ต้องกำหนดค่าใหม่
ข้อผิดพลาดที่พบบ่อยที่สุดกับ lateinit คือการเข้าถึงคุณสมบัติก่อนที่จะถูกเริ่มต้น นำไปสู่ UninitializedPropertyAccessException ซึ่งไม่ถูกจับในขณะคอมไพล์เพราะ Kotlin เชื่อถือว่านักพัฒนาจะรับประกันลำดับการเริ่มต้นที่ถูกต้อง วิธีแก้ไขคือตรวจสอบสถานะผ่าน ::property.isInitialized ก่อนเข้าถึงในสถานการณ์ที่ไม่ชัดเจน
ปัญหาที่พบบ่อยที่สองคือการใช้ lateinit สำหรับคุณสมบัติที่มีความหมายเป็น val หากค่าถูกตั้งครั้งเดียวและไม่เคยเปลี่ยนแปลง lazy เป็นตัวเลือกที่ถูกต้องกว่า มันทำให้คุณสมบัติไม่เปลี่ยนแปลง ป้องกันการเขียนทับโดยไม่ตั้งใจ และเพิ่มความปลอดภัยของเธรดฟรี
ข้อผิดพลาดที่สามคือ lazy ที่มีผลข้างเคียง บล็อกการเริ่มต้น lazy ไม่ควรแก้ไขสถานะภายนอกหรือขึ้นอยู่กับลำดับการเริ่มต้นของคุณสมบัติ lazy อื่น เพราะลำดับการคำนวณขึ้นอยู่กับการเข้าถึงครั้งแรกและอาจไม่ชัดเจน หากคุณสมบัติ lazy อ้างอิงถึงกัน มันนำไปสู่การพึ่งพาแบบวงกลมและ StackOverflowError
ปัญหาที่สี่คือ หน่วยความจำรั่วผ่าน lazy ใน Android หากบล็อก lazy จับการอ้างอิงไปยัง Activity หรือ Fragment ตัวแทนจะเก็บการปิดไว้และตัวเก็บขยะไม่สามารถปล่อยคอมโพเนนต์ได้แม้หลังจากการทำลาย วิธีแก้คือใช้ lazy กับอ็อบเจกต์อายุสั้นเท่านั้นหรือส่งบริบท Application แทน Activity
ข้อผิดพลาดทั่วไปที่ห้าคือการพยายามใช้ lateinit กับชนิดพื้นฐาน คอมไพเลอร์ Kotlin ปิดกั้นสิ่งนี้ในระดับไวยากรณ์ แต่นักพัฒนาพยายามหลีกเลี่ยงข้อจำกัดผ่านตัวห่อ nullable สิ่งนี้นำไปสู่การตรวจสอบ null ที่ไม่จำเป็นและทำให้ข้อดีของการเริ่มต้นแบบเลื่อนเวลาเป็นโมฆะโดยสมบูรณ์
คำถามที่พบบ่อย
lateinit เป็นตัวปรับแต่งสำหรับคุณสมบัติ var อนุญาตให้เริ่มต้นหลังคอนสตรักเตอร์ lazy เป็นตัวแทนสำหรับคุณสมบัติ val คำนวณค่าเมื่อเข้าถึงครั้งแรกและแคชไว้ lateinit ไม่รองรับชนิดพื้นฐานและ nullable ในขณะที่ lazy ปลอดภัยต่อเธรดโดยค่าเริ่มต้น
ได้ ผ่านการอ้างอิงคุณสมบัติในตัว: ::propertyName.isInitialized เมธอดคืนค่า true หากคุณสมบัติถูกเริ่มต้นแล้ว นี่เป็นวิธีที่ปลอดภัยเพียงวิธีเดียวในการหลีกเลี่ยง UninitializedPropertyAccessException เมื่อทำงานกับฟิลด์ lateinit
ชนิดพื้นฐาน — Int, Double, Boolean และอื่น ๆ — ถูกคอมไพล์เป็นพื้นฐาน JVM (int, double, boolean) ซึ่งไม่มีสถานะ “ไม่ได้เริ่มต้น” lateinit ใช้ null เป็นตัวบ่งชี้ และพื้นฐานไม่สามารถเป็น null ดังนั้นกลไกนี้จึงเป็นไปไม่ได้ทางกายภาพสำหรับชนิดเหล่านี้
ค่าเริ่มต้นคือ LazyThreadSafetyMode.SYNCHRONIZED — การตรวจสอบซ้อนกับการล็อก รับประกันการเริ่มต้นครั้งเดียวภายใต้การเข้าถึงพร้อมกันจากหลายเธรด สำหรับสถานการณ์เธรดเดียวใช้ NONE สำหรับการแข่งขันสูงใช้ PUBLICATION
เมื่อคุณสมบัติต้องเปลี่ยนหลังจากการเริ่มต้นหรือการสร้างถูกจัดการโดยเฟรมเวิร์ก ตัวอย่างทั่วไปคือ View Binding ใน Android Activity: binding ถูกสร้างใน onCreate และต้องเป็น var สำหรับพึ่งพา val ที่เริ่มต้นครั้งเดียวให้ใช้ lazy
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม