Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How does the Kotlin inline class work

2025-02-28 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly explains "what is the working principle of Kotlin inline class". Interested friends may wish to take a look at it. The method introduced in this paper is simple, fast and practical. Next, let the editor take you to learn "what is the working principle of Kotlin inline class"?

At first glance, the inline class

Inline classes are very simple. You only need to add the inline keyword to the front of the class:

Inline class WrappedInt (val value: Int)

Inline classes have some more or less obvious limitations: you need to specify an attribute precisely in the main constructor, as shown in value. You cannot wrap multiple values in an inline class. Inline classes also prohibit the inclusion of init blocks and cannot have attributes with behind-the-scenes fields. Inline classes can have simple computable properties, but we'll see later in this article.

At run time, the wrapper type of the inline class will be used as much as possible instead of its wrapper. This is similar to Java's box types, such as Integer or Boolean, which are represented as their corresponding primitive types as long as the compiler can do so. This is a big selling point of inline classes in Kotlin: when inlining classes, the class itself will not be used in bytecode unless absolutely necessary. Inline classes greatly reduce the space overhead at run time.

Run time

At run time, inline classes can be represented as wrapper types and base types. As mentioned in the previous paragraph, the compiler prefers to use the base (wrapper) types of inline classes to optimize the code as much as possible. This is similar to boxing between int and Integer. However, in some cases, the compiler needs to use the wrapper itself, so it will generate during compilation:

Public final class WrappedInt {

Private final int value

Public final int getValue () {return this.value;}

/ / $FF: synthetic method

Private WrappedInt (int value) {this.value = value;}

Public static int constructor_impl (int value) {return value;}

/ / $FF: synthetic method

@ NotNull

Public static final WrappedInt box_impl (int v) {return new WrappedInt (v);}

/ / $FF: synthetic method

Public final int unbox_impl () {return this.value;}

/ / more Object related implementations

}

This code snippet shows the simplified Java bytecode for the inline class. Except for something obvious, such as the value field and its getter, the constructor is private, and the new object is created through Constructor_impl, which does not actually use the wrapper type, but only returns the passed-in base type. Finally, you can see the box_impl and unbox_impl functions, which, as you might expect, are intended for unpacking operations. Now, let's look at how to use inline classes in your code.

Use the inline class fun take (w: WrappedInt) {

Println (w.value)

}

Fun main () {

Val inlined = WrappedInt (5)

Take (inlined)

}

In this code snippet, you are creating a WrappedInt and passing it to the function that prints its wrapper value. The corresponding Java bytecode, as follows:

Public static final void take_hqTGqkw (int w) {

System.out.println (w)

}

Public static final void main () {

Int inlined = WrappedInt.constructor_impl (5)

Take_hqTGqkw (inlined)

}

In the compiled code, no WrappedInt instance is created. Although the static builder_impl function is used, it simply returns an int value and passes it to the take function, which also knows nothing about the type of inline class we originally had in the source code. Note that the name of the function that accepts inline class parameters is extended with the hash code generated in the bytecode. In this way, they can be distinguished from overloaded functions that accept the underlying type as parameters:

Fun take (w: WrappedInt) = println (w.value)

Fun take (v: Int) = println (v.value)

To make these two take methods available in JVM bytecode and avoid signature conflicts, the compiler renamed the first method to something like take-hqTGqkw. Note that the above example does show "_" instead of "-" because Java does not allow method names to contain dashes, which is why methods that accept inline classes cannot be called from Java.

Boxing of inline classes

As we saw earlier, the box_impl and unbox_impl functions are created for inline classes, so when do you need them? Kotlin's documentation cites a rule of thumb:

Inline classes are boxed when used as other types.

For example, boxing occurs when you use an inline class as a generic type or a nullable type:

Inline class WrappedInt (val value: Int)

Fun take (w: WrappedInt?) {

If (w! = null) println (w.value)

}

Fun main () {

Take (WrappedInt (5))

}

In this code, we modify the take function to take a nullable WrappedInt and display the underlying type when the parameter is not null.

Public static final void take_G1XIRLQ (@ Nullable WrappedInt w) {

If (Intrinsics.areEqual (w, (Object) null) ^ true) {

Int var1 = w.unbox_impl ()

System.out.println (var1)

}

}

Public static final void main () {

Take_G1XIRLQ (WrappedInt.box_impl (WrappedInt.constructor_impl (5)

}

In bytecode, the take function no longer accepts the underlying type directly. It must use the boxing type instead. Unbox_impl is called when its contents are printed. At the place of call, we can see the boxed instance that box_impl uses to create WrappedInt.

Obviously, we want to avoid packing as much as possible. Keep in mind that specific uses of inline classes and primitive types often depend on this technique, so you may have to reconsider whether to do so.

Use case

We see that inline classes have a huge advantage: at best, they can greatly reduce runtime overhead by avoiding additional heap allocation. But when is it appropriate for us to use this type of packaging?

Better differentiation of types

If you have an authentication method, API, as follows:

Fun auth (userName: String, password: String) {println ("authenticating $userName.")}

In a beautiful world, everyone will call it by user name and password. However, it is not difficult for some users to call this method in different ways:

Auth ("12345", "user1")

Because both parameters are of type String, you may mess up their order, which is more likely as the number of parameters increases. These types of wrappers can help you mitigate this risk, so inline classes are a great tool:

Inline class Password (val value: String)

Inline class UserName (val value: String)

Fun auth (userName: UserName, password: Password) {println ("authenticating $userName.")}

Fun main () {

Auth (UserName ("user1"), Password ("12345"))

/ / does not compile due to type mismatch

Auth (Password ("12345"), UserName ("user1"))

}

The parameter list is becoming more and more confusing, and from the caller's point of view, the compiler does not allow mismatches. What was previously described is probably the most common scenario for using inline classes. They provide you with a simple type-safe wrapper without introducing other heap allocations. For these cases, you should choose inline classes as much as possible. However, inline classes can be even smarter, which will be demonstrated in the next use case.

No extra space is needed

Let's consider a way to take a numeric string and parse it to BigDecimal and adjust its scale at the same time:

/ * *

* parses string number into BigDecimal with a scale of 2

, /

Fun parseNumber (number: String): BigDecimal {

Return number.toBigDecimal () .setScale (2 RoundingMode.HALF_UP)

}

Fun main () {

Println (parseNumber ("100.12212"))

}

The code is very simple and works well, but one requirement may be that you need to track the original string used to parse the number in some way. To solve this problem, you might create a wrapper type, or use an existing Pair class to return a pair of values from the function. Although these methods obviously allocate additional space, they are still effective and should be avoided in special circumstances. Inline classes can help you. We have noticed that inline classes cannot have multiple properties with behind-the-scenes fields. However, they can have simple calculated members in the form of properties and functions. We can create an inline class for our use case that wraps the original String and provides methods or properties to analyze our values on demand. To the user, this looks like a normal data wrapper around two types, and at best it does not add any runtime overhead:

Inline class ParsableNumber (val original: String) {

Val parsed: BigDecimal

Get () = original.toBigDecimal () .setScale (2, RoundingMode.HALF_UP)

}

Fun getParsableNumber (number: String): ParsableNumber {

Return ParsableNumber (number)

}

Fun main () {

Val parsableNumber = getParsableNumber ("100.12212")

Println (parsableNumber.parsed)

Println (parsableNumber.original)

}

As you can see, the getParsableNumber method returns an instance of our inline class, which provides both raw (base type) and parsed (calculated parsed quantity) properties. This is an interesting use case that is worth observing again at the bytecode level:

Public final class ParsableNumber {

@ NotNull

Private final String original

@ NotNull

Public final String getOriginal () {return this.original;}

/ / $FF: synthetic method

Private ParsableNumber (@ NotNull String original) {

Intrinsics.checkParameterIsNotNull (original, "original")

Super ()

This.original = original

}

@ NotNull

Public static final BigDecimal getParsed_impl (String $this) {

BigDecimal var10000 = (new BigDecimal ($this)) .setScale (2, RoundingMode.HALF_UP)

Intrinsics.checkExpressionValueIsNotNull (var10000, "original.toBigDecimal ()... (2, RoundingMode.HALF_UP)")

Return var10000

}

@ NotNull

Public static String constructor_impl (@ NotNull String original) {

Intrinsics.checkParameterIsNotNull (original, "original")

Return original

}

/ / $FF: synthetic method

@ NotNull

Public static final ParsableNumber box_impl (@ NotNull String v) {

Intrinsics.checkParameterIsNotNull (v, "v")

Return new ParsableNumber (v)

}

/ / $FF: synthetic method

@ NotNull

Public final String unbox_impl () {return this.original;}

/ / more Object related implementations

}

The generated wrapper class ParsableNumber is almost similar to the WrappedInt class shown earlier. However, an important difference is the getParsed_impl function, which represents a computable attribute that has been parsed. As you can see, this function is implemented as a static function that takes a string and returns BigDecimal. So how do you use it in the caller code?

@ NotNull

Public static final String getParsableNumber (@ NotNull String number) {

Intrinsics.checkParameterIsNotNull (number, "number")

Return ParsableNumber.constructor_impl (number)

}

Public static final void main () {

String parsableNumber = getParsableNumber ("100.12212")

BigDecimal var1 = ParsableNumber.getParsed_impl (parsableNumber)

System.out.println (var1)

System.out.println (parsableNumber)

}

As expected, getParsableNumber did not refer to our packaging type. It just returns String without introducing any new types. In the body, we see that the static getParsed_impl is used to parse the given String into BigDecimal. Again, do not use ParsableNumber.

Narrow the scope of the extension function

A common problem with extension functions is that if they are defined on regular types such as String, they can pollute your namespace. For example, you might need an extension function to convert the JSON string to the appropriate type:

Inline fun String.asJson () = jacksonObjectMapper () .readValue (this)

To convert a given string to data JsonData, you can do the following:

Val jsonString = "" {"x": 200,300} ""

Val data: JsonData = jsonString.asJson ()

However, the extension feature can also be used to represent strings of other data, although it may not make much sense:

"whatever" .asjson / / will fail

This code will fail because the string does not contain valid JSON data. What can we do to make the extension shown above apply only to certain strings? Yes, what you need is an inline class:

Narrow the scope of expansion inline class JsonString (val value: String)

Inline fun JsonString.asJson () = jacksonObjectMapper () .readValue (this.value)

The above problem was resolved when we introduced a wrapper around the string used to hold JSON data and changed the extension accordingly to use a JsonString receiver. The extension will no longer appear on any String, but only on those strings that we consciously wrap in JsonString.

Unsigned type

When you look at the unsigned integer types added to the language in version 1.3, another good example of inline classes becomes obvious, which is also an experimental feature:

Public inline class UInt @ PublishedApi internal constructor (@ PublishedApi internal val data: Int): Comparable

As you can see, the Uint class is defined as an unsigned class that wraps regular signed integer data.

At this point, I believe that everyone on the "Kotlin inline class working principle is what" have a deeper understanding, might as well to the actual operation of it! Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report