Kotlin - Khởi tạo thuộc tính bằng cách sử dụng bởi người lười biếng


278

Trong Kotlin nếu bạn không muốn khởi tạo một thuộc tính lớp bên trong hàm tạo hoặc ở phần trên cùng của thân lớp, về cơ bản bạn có hai tùy chọn này (từ tham chiếu ngôn ngữ):

  1. Khởi tạo lười biếng

Lazy () là một hàm lấy lambda và trả về một thể hiện của Lazy, có thể đóng vai trò là đại biểu để thực hiện một thuộc tính lười biếng: lệnh gọi đầu tiên để nhận () thực hiện lambda được chuyển đến lazy () và ghi nhớ kết quả, các cuộc gọi tiếp theo để có được () chỉ cần trả về kết quả đã nhớ.

Thí dụ

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Vì vậy, cuộc gọi đầu tiên và các cuộc gọi phụ, mọi lúc mọi nơi, đến myLazyString sẽ trả về "Xin chào"

  1. Khởi tạo muộn

Thông thường, các thuộc tính được khai báo là có loại không null phải được khởi tạo trong hàm tạo. Tuy nhiên, khá thường xuyên điều này là không thuận tiện. Ví dụ, các thuộc tính có thể được khởi tạo thông qua phép nội xạ phụ thuộc hoặc trong phương thức thiết lập của kiểm tra đơn vị. Trong trường hợp này, bạn không thể cung cấp trình khởi tạo không null trong hàm tạo, nhưng bạn vẫn muốn tránh kiểm tra null khi tham chiếu thuộc tính bên trong phần thân của một lớp.

Để xử lý trường hợp này, bạn có thể đánh dấu thuộc tính bằng công cụ sửa đổi lateinit:

public class MyTest {

   lateinit var subject: TestSubject

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

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

Công cụ sửa đổi chỉ có thể được sử dụng trên các thuộc tính var được khai báo bên trong phần thân của một lớp (không phải trong hàm tạo chính) và chỉ khi thuộc tính không có getter hoặc setter tùy chỉnh. Loại tài sản phải là không có giá trị và nó không phải là loại nguyên thủy.

Vậy, làm thế nào để chọn chính xác giữa hai tùy chọn này, vì cả hai đều có thể giải quyết cùng một vấn đề?

Câu trả lời:


333

Dưới đây là sự khác biệt đáng kể giữa lateinit varby lazy { ... }tài sản được ủy quyền:

  • lazy { ... }ủy nhiệm chỉ có thể được sử dụng cho valcác thuộc tính, trong khi lateinitchỉ có thể được áp dụng cho vars, vì nó không thể được biên dịch thành một finaltrường, do đó không thể đảm bảo tính bất biến;

  • lateinit varcó một trường sao lưu lưu trữ giá trị và by lazy { ... }tạo một đối tượng ủy nhiệm trong đó giá trị được lưu trữ sau khi tính toán, lưu trữ tham chiếu đến đối tượng ủy nhiệm trong đối tượng lớp và tạo getter cho thuộc tính hoạt động với thể hiện ủy nhiệm. Vì vậy, nếu bạn cần trường sao lưu trong lớp, hãy sử dụng lateinit;

  • Ngoài vals, lateinitkhông thể được sử dụng cho các thuộc tính không null và các kiểu nguyên thủy Java (điều này là do nullđược sử dụng cho giá trị chưa được khởi tạo);

  • lateinit varcó thể được khởi tạo từ bất cứ nơi nào mà đối tượng được nhìn thấy, ví dụ từ bên trong mã khung và có thể có nhiều kịch bản khởi tạo cho các đối tượng khác nhau của một lớp. by lazy { ... }lần lượt, định nghĩa trình khởi tạo duy nhất cho thuộc tính, chỉ có thể được thay đổi bằng cách ghi đè thuộc tính trong một lớp con. Nếu bạn muốn tài sản của mình được khởi tạo từ bên ngoài theo cách có thể chưa biết trước, hãy sử dụng lateinit.

  • Khởi tạo by lazy { ... }là an toàn luồng theo mặc định và đảm bảo rằng trình khởi tạo được gọi nhiều nhất một lần (nhưng điều này có thể được thay đổi bằng cách sử dụng quá tải kháclazy ). Trong trường hợp lateinit var, tùy thuộc vào mã người dùng để khởi tạo thuộc tính chính xác trong môi trường đa luồng.

  • Một Lazythể hiện có thể được lưu, chuyển qua và thậm chí được sử dụng cho nhiều thuộc tính. Ngược lại, lateinit vars không lưu trữ bất kỳ trạng thái thời gian chạy bổ sung nào (chỉ nulltrong trường cho giá trị chưa được khởi tạo).

  • Nếu bạn giữ một tham chiếu đến một thể hiện của Lazy, isInitialized()cho phép bạn kiểm tra xem nó đã được khởi tạo chưa (và bạn có thể có được ví dụ đó với sự phản ánh từ một thuộc tính được ủy quyền). Để kiểm tra xem một thuộc tính lateinit đã được khởi tạo chưa, bạn có thể sử dụng property::isInitializedkể từ Kotlin 1.2 .

  • Một lambda được chuyển đến by lazy { ... }có thể nắm bắt các tham chiếu từ ngữ cảnh nơi nó được sử dụng để đóng nó .. Sau đó, nó sẽ lưu trữ các tham chiếu và chỉ phát hành chúng khi tài sản đã được khởi tạo. Điều này có thể dẫn đến hệ thống phân cấp đối tượng, chẳng hạn như các hoạt động của Android, không được phát hành quá lâu (hoặc bao giờ, nếu thuộc tính vẫn có thể truy cập và không bao giờ được truy cập), vì vậy bạn nên cẩn thận về những gì bạn sử dụng bên trong lambda trình khởi tạo.

Ngoài ra, có một cách khác không được đề cập trong câu hỏi Delegates.notNull():, phù hợp cho việc khởi tạo hoãn lại các thuộc tính không null, bao gồm cả các kiểu nguyên thủy Java.


9
Câu trả lời chính xác! Tôi sẽ thêm rằng lateinithiển thị trường sao lưu của nó với khả năng hiển thị của trình thiết lập để cách các thuộc tính được truy cập từ Kotlin và từ Java là khác nhau. Và từ mã Java, thuộc tính này có thể được đặt ngay cả khi nullkhông có bất kỳ kiểm tra nào trong Kotlin. Do đó, lateinitkhông phải để khởi tạo lười biếng mà là khởi tạo không nhất thiết phải từ mã Kotlin.
Michael

Có gì tương đương với "!" Của Swift không ?? Nói cách khác, đó là thứ gì đó được khởi tạo muộn nhưng CÓ THỂ được kiểm tra null mà không bị lỗi. 'Lateinit' của Kotlin không thành công với "thuộc tính lateinit hiện tại Người dùng chưa được khởi tạo" nếu bạn kiểm tra 'theObject == null'. Điều này cực kỳ hữu ích khi bạn có một đối tượng không phải là null trong kịch bản sử dụng cốt lõi của nó (và do đó muốn mã chống lại sự trừu tượng trong đó không phải là null), nhưng là null trong các kịch bản ngoại lệ / giới hạn (ví dụ: truy cập vào bản ghi hiện tại trong người dùng, không bao giờ là null ngoại trừ khi đăng nhập ban đầu / trên màn hình đăng nhập)
Marchy

@Marchy, bạn có thể sử dụng được lưu trữ rõ ràng Lazy+ .isInitialized()để làm điều đó. Tôi đoán không có cách đơn giản nào để kiểm tra một tài sản như vậy nullvì đảm bảo rằng bạn không thể nhận được nulltừ nó. :) Xem bản demo này .
phím nóng

@hotkey Có điểm nào về việc sử dụng quá nhiều by lazycó thể làm chậm thời gian xây dựng hoặc thời gian chạy không?
Dr.jacky

Tôi thích ý tưởng sử dụng lateinitđể phá vỡ việc sử dụng nullcho giá trị chưa được khởi tạo. Khác với điều đó nullkhông bao giờ nên được sử dụng, và với lateinitnull có thể được loại bỏ. Đó là cách tôi yêu Kotlin :)
KenIchi

25

Ngoài ra hotkey, đây là cách tôi chọn trong số hai câu trả lời:

lateinit dành cho khởi tạo bên ngoài: khi bạn cần công cụ bên ngoài để khởi tạo giá trị của mình bằng cách gọi một phương thức.

ví dụ: bằng cách gọi:

private lateinit var value: MyClass

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

Trong khi đó lazylà khi nó chỉ sử dụng phụ thuộc nội bộ vào đối tượng của bạn.


1
Tôi nghĩ rằng chúng ta vẫn có thể lười biếng khởi tạo ngay cả khi nó phụ thuộc vào một đối tượng bên ngoài. Chỉ cần truyền giá trị cho một biến nội bộ. Và sử dụng biến nội bộ trong quá trình khởi tạo lười biếng. Nhưng nó tự nhiên như lateinit.
Elye

Cách tiếp cận này ném UninitializedPropertyAccessException, tôi đã kiểm tra lại rằng tôi đang gọi một hàm setter trước khi sử dụng giá trị. Có quy tắc cụ thể tôi đang thiếu với lateinit? Trong câu trả lời của bạn thay thế MyClass và Any bằng Android Context, đó là trường hợp của tôi.
Talha

24

Câu trả lời rất ngắn gọn và súc tích

lateinit: Nó khởi tạo các thuộc tính không null gần đây

Không giống như khởi tạo lười biếng, lateinit cho phép trình biên dịch nhận ra rằng giá trị của thuộc tính không null không được lưu trữ trong giai đoạn hàm tạo để biên dịch bình thường.

khởi tạo lười biếng

bởi sự lười biếng có thể rất hữu ích khi thực hiện các thuộc tính chỉ đọc (val) thực hiện khởi tạo lười biếng trong Kotlin.

bởi lazy {...} thực hiện trình khởi tạo của nó trong đó thuộc tính được xác định được sử dụng lần đầu tiên, không phải khai báo.


câu trả lời tuyệt vời, đặc biệt là "thực hiện trình khởi tạo của nó trong đó thuộc tính được xác định được sử dụng lần đầu tiên, không phải khai báo của nó"
user1489829

17

lateinit vs lười biếng

  1. muộn

    i) Sử dụng nó với biến có thể thay đổi [var]

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

    ii) Chỉ được phép với các loại dữ liệu không có giá trị

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

    iii) Lời hứa với trình biên dịch rằng giá trị sẽ được khởi tạo trong tương lai.

LƯU Ý : Nếu bạn cố gắng truy cập biến lateinit mà không khởi tạo nó thì nó sẽ ném UnInitializedPropertyAccessException.

  1. lười biếng

    i) Khởi tạo lười biếng được thiết kế để ngăn chặn việc khởi tạo các đối tượng không cần thiết.

    ii) Biến của bạn sẽ không được khởi tạo trừ khi bạn sử dụng nó.

    iii) Nó chỉ được khởi tạo một lần. Lần tới khi bạn sử dụng nó, bạn sẽ nhận được giá trị từ bộ nhớ cache.

    iv) Nó là luồng an toàn (Nó được khởi tạo trong luồng nơi nó được sử dụng lần đầu tiên. Các luồng khác sử dụng cùng một giá trị được lưu trong bộ đệm).

    v) Biến chỉ có thể là val .

    vi) Biến chỉ có thể là không thể rỗng .


7
Tôi nghĩ trong biến lười biếng không thể là var.
Däñish Shärmà

4

Ngoài tất cả các câu trả lời hay, còn có một khái niệm gọi là lười tải:

Lazy load là một mẫu thiết kế thường được sử dụng trong lập trình máy tính để trì hoãn việc khởi tạo một đối tượng cho đến thời điểm cần thiết.

Sử dụng đúng cách, bạn có thể giảm thời gian tải ứng dụng của mình. Và cách thức triển khai của Kotlin là bằng cách lazy()tải giá trị cần thiết cho biến của bạn bất cứ khi nào cần.

Nhưng lateinit được sử dụng khi bạn chắc chắn một biến sẽ không có giá trị rỗng hoặc sẽ được khởi tạo trước khi bạn sử dụng onResume()phương thức này cho phương thức android - và vì vậy bạn không muốn khai báo nó là loại không thể.


Vâng, tôi cũng khởi tạo trong onCreateView, onResumevà khác với lateinit, nhưng đôi khi sai sót xảy ra ở đó (vì một số sự kiện bắt đầu sớm hơn). Vì vậy, có by lazythể cho một kết quả thích hợp. Tôi sử dụng lateinitcho các biến không null có thể thay đổi trong vòng đời.
CoolMind

2

Tất cả mọi thứ là chính xác ở trên, nhưng một trong những sự thật giải thích đơn giản LAZY ---- Có những trường hợp khi bạn muốn trì hoãn việc tạo một thể hiện của đối tượng cho đến khi sử dụng lần đầu tiên. Kỹ thuật này được gọi là khởi tạo lười biếng hoặc khởi tạo lười biếng. Mục đích chính của việc khởi tạo lười biếng là để tăng hiệu suất và giảm dung lượng bộ nhớ của bạn. Nếu khởi tạo một thể hiện của loại của bạn có chi phí tính toán lớn và chương trình có thể không thực sự sử dụng nó, bạn sẽ muốn trì hoãn hoặc thậm chí tránh lãng phí chu kỳ CPU.


0

Nếu bạn đang sử dụng Spring container và bạn muốn khởi tạo trường bean không nullable, lateinitthì phù hợp hơn.

    @Autowired
    lateinit var myBean: MyBean

1
nên như thế@Autowired lateinit var myBean: MyBean
Cnfn

0

Nếu bạn sử dụng một biến không thể thay đổi, thì tốt hơn là khởi tạo bằng by lazy { ... }hoặc val. Trong trường hợp này, bạn có thể chắc chắn rằng nó sẽ luôn được khởi tạo khi cần thiết và nhiều nhất là 1 lần.

Nếu bạn muốn một biến không null, có thể thay đổi giá trị của nó, hãy sử dụng lateinit var. Trong phát triển Android, sau này bạn có thể khởi tạo nó trong các sự kiện như onCreate, onResume. Xin lưu ý rằng nếu bạn gọi yêu cầu REST và truy cập vào biến này, nó có thể dẫn đến một ngoại lệ UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, vì yêu cầu có thể thực thi nhanh hơn biến đó có thể khởi 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.