Translation Instructions:
An Introduction to Inline Classes in Kotlin
Original address:Typealias.com/guides/intr…
Original author:Dave Leeds
Whether you’re writing a massive data flow program running in the cloud or an application running on a low-power phone, most developers want their code to run fast. Now, Kotlin’s latest experimental feature, inline classes, allows you to create the data types you want, without sacrificing the performance you need!
In this new series of articles, we’ll take a thorough look at inline classes from top to bottom!
In this article, we’ll look at what inline class is, how it works, and how we can weigh our options when using it. Then, in the following articles, we’ll take a closer look at the inline class to see exactly how it’s implemented and examine how it interoperates with Java.
Keep in mind – this is an experimental syntax feature, and it is being actively developed and refined. The current article is based on kotlin-1.3M1 version of the inline class implementation.
If you want to try it out for yourself, I’ve also written a companion article how to Enable them in your IDE so you can start using inline classes and other Kotlin 1.3 features right away!
Strong typing and common values: The case for inline classes
At 8am on Monday, after pouring myself a fresh, steaming cup of coffee, I was given an assignment in the project management system. It read:
Send a welcome email to new users – four days after registration
Now that your mail system is written, you can launch the mail scheduler interface, as you can see below:
interface MailScheduler {
fun sendEmail(email: Email, delay: Int)
}
Copy the code
Look at this function, you know you need to call it… But what parameters do you pass in order to delay an email for four days?
The delay parameter is of type Int. So we only know that this is an Integer, but we don’t know what its units are – should you pass in 4 days? Or it stands for hours, in which case you should pass in 96 times 24 times 4. Or maybe it’s in minutes, seconds, milliseconds…
How can we optimize this code?
How can this code be made better?
If the compiler can force the correct unit of time. For example, assuming that the receiving parameter type is not Int, let’s update the interface function to receive a strong type Minutes
interface MailScheduler {
fun sendEmail(email: Email, delay: Minutes)
}
Copy the code
Now we have a strong typing system working for us! It is impossible to send a Seconds parameter to this function because it only accepts arguments of type Minutes! Consider how the following code can significantly reduce errors compared to previous versions:
val defaultDelay = Days(2)
fun send(email: Email) {
mailScheduler.sendEmail(email, defaultDelay.toMinutes())
}
Copy the code
When we can take full advantage of the type system, we improve the robustness of our code.
But developers often do not choose to make a wrapper class for a single common value, but rather pass basic types like Int, Float, and Boolean.
Why is that?
In general, we discourage the creation of such strong types for performance reasons. As you may recall, memory on the JVM looks like this:
When we create local variables of a primitive type (that is, function parameters and variables defined within a function) – such as Int, Float, Boolean – these values are stored in the partial JVM memory stack. The performance overhead involved in storing values of these basic types on the stack is not significant.
On the other hand, whenever we instantiate an object, the object instance is stored on the JVM heap. We have a performance penalty when we store and use object instances – heap allocation and memory extraction have high performance costs. While the per-object performance overhead may seem trivial, it can add up to a serious impact on the speed of your code.
Wouldn’t it be great if we could get all the benefits of a strongly typed system without being compromised by performance?
In fact, Kotlin’s new feature inline Class is designed to solve just such a problem.
Let’s take a look
Introduction to inline classes
Inline classes are easy to create – just prefix the class you define with the inline keyword.
inline class Hours(val value: Int) {
fun toMinutes(a) = Minutes(value * 60)}Copy the code
That’s it! This class will now serve as a strong type for the values you define, and in many cases it has almost the same performance cost as a regular non-inline class.
You can instantiate and use inline classes just like any other class. You may eventually need to reference the wrapped generic values somewhere in your code — usually at the boundary with another library or system. Of course, at this point, you can access this value just as you would normally use any other class.
Key terms you should know
The inline class wraps the value of the underlying type. And this value also has a type, which we call the base type
Why can inline classes perform well
So why should an inline class perform better than a normal class?
You can instantiate an inline class like this
val period = Hours(24)
Copy the code
. This class is not actually instantiated in compiled code! In fact, as far as the JVM is concerned, it actually amounts to the following code……
int period = 24;
Copy the code
As you can see, there is no concept of Hours in this compiled version of the code – it just assigns base values to variables of type int! The same is true when you use inline classes as the types of function arguments:
fun wait(period: Hours) { / *... * / }
Copy the code
. It can be effectively compiled as……
void wait(int period) { / *... * / }
Copy the code
Therefore, our code inlines base types and base values. In other words, the compiled code uses only the int integer type, so we avoid the overhead of creating and accessing objects on the heap memory.
But wait!
Remember that the Hours class has a function called toMinutes ()? Because the compiled code uses an int instead of an instance of the Hours object, imagine what happens when toMinutes () is called.
inline class Hours(val value: Int) {
fun toMinutes(a) = Minutes(value * 60)}Copy the code
The compiled code for hours.tominutes () looks like this:
public static final int toMinutes(int $this) {
return $this * 60;
}
Copy the code
If we call Hours(24).tominutes () in Kotlin, it will effectively compile to toMinutes(24).
Sure, you can handle functions like this, but what about class member attributes? What if we wanted Hours to include some other data besides the main base value?
Everything has its tradeoffs, so this is one of them – inline classes cannot have any member attributes other than the base value. Let’s talk about others.
Trade-offs and use limits
Now that we know that inline classes can be represented by base values in compiled code, we are ready to learn what usage restrictions to be aware of when using them.
First, the inline class must contain an underlying value, which means it needs a primary constructor to receive the underlying value, and it must be read-only (val). You can define the base value variable name as you want.
inline class Seconds(a)// nope - needs to accept a value!
inline class Minutes(value: Int) // nope - value needs to be a property
inline class Hours(var value: Int) // nope - property needs to be read-only
inline class Days(val value: Int) // yes!
inline class Months(val count: Int) // yes! - name it what you want
Copy the code
You can make this property private if necessary, but the constructor must be public.
inline class Years private constructor(val value: Int) // nope - constructor must be public
inline class Decades(private val value: Int) // yes!
Copy the code
An inline class cannot contain an init block initialization block. I’ll explore how inline classes interoperate with Java in my next post, which will explain this once and for all.
inline class Centuries(val value: Int) {
// nope - "Inline class cannot have an initializer block"
init {
require(value >= 0)}}Copy the code
As we discovered above, our inline class main constructor cannot contain any member attributes other than a base value.
// nope - "Inline class must have exactly one primary constructor parameter"
inline class Years(val count: Int.val startYear: Int)
Copy the code
However, it is possible to have member attributes internally, as long as they are evaluated only from the base value in the constructor, or from some value or object that can be resolved statically – from singletons, top-level objects, constants, and so on.
object Conversions {
const val MINUTES_PER_HOUR = 60
}
inline class Hours(val value: Int) {
val valueAsMinutes get() = value * Conversions.MINUTES_PER_HOUR
}
Copy the code
Class inheritance not allowed – Inline classes cannot inherit from another class, and they cannot be inherited by another class. (Kotlin 1.3-M1 does technically allow an inline class to inherit from another class, but this will be corrected in an upcoming release)
open class TimeUnit
inline class Seconds(val value: Int) : TimeUnit() // nope - cannot extend classes
open inline class Minutes(val value: Int) // nope - "Inline classes can only be final"
Copy the code
If you need an inline class as a subtype, that’s fine – you can implement the interface instead of inheriting the base class.
interface TimeUnit {
val value: Int
}
inline class Hours(override val value: Int) : TimeUnit // yes!
Copy the code
Inline classes must be declared at the top level. Nested/inner classes are not inline.
class Outer {
// nope - "Inline classes are only allowed on top level"
inline class Inner(val value: Int)}inline class TopLevelInline(val value: Int) // yes!
Copy the code
Currently, inline enumerated classes are also not supported.
// nope - "Modifier 'inline' is not applicable to 'enum class'"
inline enum class TimeUnits(val value: Int) {
SECONDS_PER_MINUTE(60),
MINUTES_PER_HOUR(60),
HOURS_PER_DAY(24)}Copy the code
Contrast Type Aliases with Inline Classes
Because they both contain underlying types, inline classes can easily be confused with type aliases. But there are some key differences that make them useful in different scenarios.
A type alias provides an alternate name for the underlying type. For example, you can alias a common type like String and give it a descriptive name that makes sense in a particular context, such as Username. A Username variable is actually the same thing as a String variable in the compiled code, just with a different name. For example, you could do this:
typealias Username = String
fun validate(name: Username) {
if(name.length < 5) {
println("Username $name is too short.")}}Copy the code
Notice that we can call.length directly on name, because name is actually a String, even though we’re using the alias Username when we declare the parameter type.
On the other hand, inline classes are actually wrappers for base types, so you need to unpack them when you need to use base values. For example, we used an inline class to override the above alias:
inline class Username(val value: String)
fun validate(name: Username) {
if (name.value.length < 5) {
println("Username ${name.value} is too short.")}}Copy the code
Notice that we have to do name.value.length instead of name.length, we have to unwrap the wrapper to get the value inside.
But the biggest difference has to do with distributive compatibility. Inline classes give you type security; type aliases do not. A type alias is the same as its underlying type. For example, look at the following code:
typealias Username = String
typealias Password = String
fun authenticate(user: Username, pass: Password) { / *... * / }
fun main(args: Array<String>) {
val username: Username = "joe.user"
val password: Password = "super-secret"
authenticate(password, username)
}
Copy the code
In this case, Username and Password are just different names for strings, and you can even swap Username and Password. In fact, this is exactly what we did in the above code – when we called authenticate (), the compiler considered it valid even though we had the Username and Password positions reversed.
On the other hand, if you use an inline class for the same case above, the compiler will be lucky to tell you that it is illegal:
inline class Username(val value: String)
inline class Password(val value: String)
fun authenticate(user: Username, pass: Password) { / *... * / }
fun main(args: Array<String>) {
val username: Username = Username("joe.user")
val password: Password = Password("super-secret")
authenticate(password, username) // <--- Compiler error here! =)
}
Copy the code
This is very powerful! This is powerful enough to tell us when we’re writing code that we’ve written a bug. We don’t have to wait for automated testing, QA engineers, or users to tell us. Very good!
packaging
Are you ready to start experimenting with inline classes yourself? If so, read how to Enable inline classes immediately
While we’ve covered the basics, there are some confusing caveats and limitations to keep in mind when using them. In fact, if you don’t understand what’s going on inside, you may end up writing slower code than normal classes.
In the next article, we’ll delve into the underlying workings of inline classes so you can use them more efficiently.
Translator has something to say
Remember from Kotlin’s last article about the new feature, we were pretty clear about inline class. Inline class inline class inline class inline class inline class inline class inline class inline class In addition, the author has written a series of articles about inline class, from its use to a basic introduction and finally to the anatomy of the internals. I will, of course, continue to translate his last in-depth article on inline class internals. Welcome to continue to pay attention to ~~~
Welcome to Kotlin’s series of articles:
Original series:
- Jetbrains developer briefing (3) Kotlin1.3 new features (inline class)
- JetBrains developer briefing (2) new features of Kotlin1.3 (Contract and coroutine)
- Kotlin/Native: JetBrains Developer’s Day (Part 1
- How to overcome the difficulties of generic typing in Kotlin
- How to overcome the difficulties of Generic typing in Kotlin (Part 2)
- How to overcome the difficulties of Generic typing in Kotlin (Part 1)
- Kotlin’s trick of Reified Type Parameter (Part 2)
- Everything you need to know about the Kotlin property broker
- Source code parsing for Kotlin Sequences
- Complete analysis of Sets and functional apis in Kotlin – Part 1
- Complete parsing of lambdas compiled into bytecode in Kotlin syntax
- On the complete resolution of Lambda expressions in Kotlin’s Grammar
- On extension functions in Kotlin’s Grammar
- A brief introduction to Kotlin’s Grammar article on top-level functions, infix calls, and destruct declarations
- How to Make functions call Better
- On variables and Constants in Kotlin’s Grammar
- Elementary Grammar in Kotlin’s Grammar Essay
Translation series:
- Kotlin’s trick of Reified Type Parameter
- When should type parameter constraints be used in Kotlin generics?
- An easy way to remember Kotlin’s parameters and arguments
- Should Kotlin define functions or attributes?
- How to remove all of them from your Kotlin code! (Non-empty assertion)
- Master Kotlin’s standard library functions: run, with, let, also, and apply
- All you need to know about Kotlin type aliases
- Should Sequences or Lists be used in Kotlin?
- Kotlin’s turtle (List) rabbit (Sequence) race
- The Effective Kotlin series considers using static factory methods instead of constructors
- The Effective Kotlin series considers using a builder when encountering multiple constructor parameters
Actual combat series:
- Use Kotlin to compress images with ImageSlimming.
- Use Kotlin to create a picture compression plugin.
- Use Kotlin to compress images.
- Simple application of custom View picture fillet in Kotlin practice article
Welcome to the Kotlin Developer Association, where the latest Kotlin technical articles are published, and a weekly Kotlin foreign technical article is translated from time to time. If you like Kotlin, welcome to join us ~~~