- Introduction
- Part 1 Good Code
- Chapter 1 Safety
- 引言
- 第1条:限制可变性
- 第2条:最小化变量作用域
- 第3条:尽快消除平台类型
- 第4条:不要把推断类型暴露给外部
- Item 5 Specify Your Expectations On Arguments And State
- 第6条:尽可能使用标准库中提供的异常
- 第7条:当不能返回预期结果时,优先使用null o或Failure 作为返回值
- Item 8 Handle Nulls Properly
- 第9条:使用use关闭资源
- Item 10 Write Unit Tests
- Chapter 2 Readability
- Introduction
- Item 11 Design For Readability
- Item 12 Operator Meaning Should Be Consistent With Its Function Name
- Item 13 Avoid Returning Or Operating On Unit
- Item 14 Specify The Variable Type When It Is Not Clear
- Item 15 Consider Referencing Receivers Explicitly
- Item 16 Properties Should Represent State Not Behavior
- Item 17 Consider Naming Arguments
- Item 18 Respect Coding Conventions
- Part 2 Code Design
- Chapter 3 Reusability
- Introduction
- Item 19 Do Not Repeat Knowledge
- Item 20 Do Not Repeat Common Algorithms
- Item 21 Use Property Delegation To Extract Common Property Patterns
- Item 22 Use Generics When Implementing Common Algorithms
- Item 23 Avoid Shadowing Type Parameters
- Item 24 Consider Variance For Generic Types
- Item 25 Reuse Between Different Platforms By Extracting Common Modules
- Chapter 4 Abstraction Design
- Introduction
- Item 26 Each Function Should Be Written In Terms Of A Single Level Of Abstraction
- Item 27 Use Abstraction To Protect Code Against Changes
- Item 28 Specify API Stability
- Item 29 Consider Wrapping External API
- Item 30 Minimize Elements Visibility
- Item 31 Define Contract With Documentation
- Item 32 Respect Abstraction Contracts
- Chapter 5 Object Creation
- Introduction
- Item 33 Consider Factory Functions Instead Of Constructors
- Item 34 Consider A Primary Constructor With Named Optional Arguments
- Item 35 Consider Defining A DSL For Complex Object Creation
- Chapter 6 Class Design
- Introduction
- Item 36 Prefer Composition Over Inheritance
- Item 37 Use The Data Modifier To Represent A Bundle Of Data
- Item 38 Use Function Types Instead Of Interfaces To Pass Operations And Actions
- Item 39 Prefer Class Hierarchies To Tagged Classes
- Item 40 Respect The Contract Of Equals
- Item 41 Respect The Contract Of Hash Code
- Item 42 Respect The Contract Of Compare To
- Item 43 Consider Extracting Non Essential Parts Of Your API Into Extensions
- Item 44 Avoid Member Extensions
- Part 3 Efficiency
- Chapter 7 Make It Cheap
- Introduction
- Item 45 Avoid Unnecessary Object Creation
- Item 46 Use Inline Modifier For Functions With Parameters Of Functional Types
- Item 47 Consider Using Inline Classes
- Item 48 Eliminate Obsolete Object References
- Chapter 8 Efficient Collection Processing
- Introduction
- Item 49 Prefer Sequence For Big Collections With More Than One Processing Step
- Item 50 Limit The Number Of Operations
- Item 51 Consider Arrays With Primitives For Performance Critical Processing
- Item 52 Consider Using Mutable Collections
- Published with GitBook
Item 47 Consider Using Inline Classes
Item 47: Consider using inline classes
Not only functions can be inlined, but also objects holding a single value can be replaced with this value. Such possibility was introduced as experimental in Kotlin 1.3, and to make it possible, we need to place the inline
modifier before a class with a single primary constructor property:
inline class Name(private val value: String) {
// ...
}
Such a class will be replaced with the value it holds whenever possible:
// Code
val name: Name = Name("Marcin")
// During compilation replaced with code similar to:
val name: String = "Marcin"
Methods from such a class will be evaluated as static methods:
inline class Name(private val value: String) {
// ...
fun greet() {
print("Hello, I am $value")
}
}
// Code
val name: Name = Name("Marcin")
name.greet()
// During compilation replaced with code similar to:
val name: String = "Marcin"
Name.`greet-impl`(name)
We can use inline classes to make a wrapper around some type (like String
in the above example) with no performance overhead (Item 45: Avoid unnecessary object creation). Two especially popular uses of inline classes are:
- To indicate a unit of measure
- To use types to protect user from misuse
Let’s discuss them separately.
Indicate unit of measure
Imagine that you need to use a method to set up timer:
interface Timer {
fun callAfter(time: Int, callback: ()->Unit)
}
What is this time
? Might be time in milliseconds, seconds, minutes… it is not clear at this point and it is easy to make a mistake. A serious mistake. One famous example of such a mistake is Mars Climate Orbiter that crashed into Mars atmosphere. The reason behind that was that the software used to control it was developed by an external company and it produced outputs in different units of measure than the ones expected by NASA. It produced results in pound-force seconds (lbf·s), while NASA expected newton-seconds (N·s). The total cost of the mission was 327.6 million USD and it was a complete failure. As you can see, confusion of measurement units can be really expensive.
One common way for developers to suggest a unit of measure is by including it in the parameter name:
interface Timer {
fun callAfter(timeMillis: Int, callback: ()->Unit)
}
It is better but still leaves some space for mistakes. The property name is often not visible when a function is used. Another problem is that indicating the type this way is harder when the type is returned. In the example below, time is returned from decideAboutTime
and its unit of measure is not indicated at all. It might return time in minutes and time setting would not work correctly then.
interface User {
fun decideAboutTime(): Int
fun wakeUp()
}
interface Timer {
fun callAfter(timeMillis: Int, callback: ()->Unit)
}
fun setUpUserWakeUpUser(user: User, timer: Timer) {
val time: Int = user.decideAboutTime()
timer.callAfter(time) {
user.wakeUp()
}
}
We might introduce the unit of measure of the returned value in the function name, for instance by naming it decideAboutTimeMillis
, but such a solution is rather rare as it makes a function longer every time we use it, and it states this low-level information even when we don’t need to know about it.
A better way to solve this problem is to introduce stricter types that will protect us from misusing more generic types, and to make them efficient we can use inline classes:
inline class Minutes(val minutes: Int) {
fun toMillis(): Millis = Millis(minutes * 60 * 1000)
// ...
}
inline class Millis(val milliseconds: Int) {
// ...
}
interface User {
fun decideAboutTime(): Minutes
fun wakeUp()
}
interface Timer {
fun callAfter(timeMillis: Millis, callback: ()->Unit)
}
fun setUpUserWakeUpUser(user: User, timer: Timer) {
val time: Minutes = user.decideAboutTime()
timer.callAfter(time) { // ERROR: Type mismatch
user.wakeUp()
}
}
This would force us to use the correct type:
fun setUpUserWakeUpUser(user: User, timer: Timer) {
val time = user.decideAboutTime()
timer.callAfter(time.toMillis()) {
user.wakeUp()
}
}
It is especially useful for metric units, as in frontend, we often use a variety of units like pixels, millimeters, dp, etc. To support object creation, we can define DSL-like extension properties (and you can make them inline functions):
inline val Int.min
get() = Minutes(this)
inline val Int.ms
get() = Millis(this)
val timeMin: Minutes = 10.min
Protect us from type misuse
In SQL databases, we often identify elements by their IDs, which are all just numbers. Therefore, let’s say that you have a student grade in a system. It will probably need to reference the id of a student, teacher, school etc.:
@Entity(tableName = "grades")
class Grades(
@ColumnInfo(name = "studentId")
val studentId: Int,
@ColumnInfo(name = "teacherId")
val teacherId: Int,
@ColumnInfo(name = "schoolId")
val schoolId: Int,
// ...
)
The problem is that it is really easy to later misuse all those ids, and the typing system does not protect us because they are all of type Int
. The solution is to wrap all those integers into separate inline classes:
inline class StudentId(val studentId: Int)
inline class TeacherId(val teacherId: Int)
inline class SchoolId(val studentId: Int)
@Entity(tableName = "grades")
class Grades(
@ColumnInfo(name = "studentId")
val studentId: StudentId,
@ColumnInfo(name = "teacherId")
val teacherId: TeacherId,
@ColumnInfo(name = "schoolId")
val schoolId: SchoolId,
// ...
)
Now those id uses will be safe, and at the same time, the database will be generated correctly because during compilation, all those types will be replaced with Int
anyway. This way, inline classes allow us to introduce types where they were not allowed before, and thanks to that, we have safer code with no performance overhead.
Inline classes and interfaces
Inline classes just like other classes can implement interfaces. Those interfaces could let us properly pass time in any unit of measure we want.
interface TimeUnit {
val millis: Long
}
inline class Minutes(val minutes: Long): TimeUnit {
override val millis: Long get() = minutes * 60 * 1000
// ...
}
inline class Millis(val milliseconds: Long): TimeUnit {
override val millis: Long get() = milliseconds
}
fun setUpTimer(time: TimeUnit) {
val millis = time.millis
//...
}
setUpTimer(Minutes(123))
setUpTimer(Millis(456789))
The catch is that when an object is used through an interface, it cannot be inlined. Therefore in the above example there is no advantage to using inline classes, since wrapped objects need to be created to let us present a type through this interface. When we present inline classes through an interface, such classes are not inlined.
Typealias
Kotlin typealias lets us create another name for a type:
typealias NewName = Int
val n: NewName = 10
Naming types is a useful capability used especially when we deal with long and repeatable types. For instance, it is popular practice to name repeatable function types:
typealias ClickListener =
(view: View, event: Event) -> Unit
class View {
fun addClickListener(listener: ClickListener) {}
fun removeClickListener(listener: ClickListener) {}
//...
}
What needs to be understood though is that typealiases do not protect us in any way from type misuse. They are just adding a new name for a type. If we would name Int
as both Millis
and Seconds
, we would make an illusion that the typing system protects us while it does not:
typealias Seconds = Int
typealias Millis = Int
fun getTime(): Millis = 10
fun setUpTimer(time: Seconds) {}
fun main() {
val seconds: Seconds = 10
val millis: Millis = seconds // No compiler error
setUpTimer(getTime())
}
In the above example it would be easier to find what is wrong without type aliases. This is why they should not be used this way. To indicate a unit of measure, use a parameter name or classes. A name is cheaper, but classes give better safety. When we use inline classes, we take the best from both options - it is both cheap and safe.
Summary
Inline classes let us wrap a type without performance overhead. Thanks to that, we improve safety by making our typing system protect us from value misuse. If you use a type with unclear meaning, especially a type that might have different units of measure, consider wrapping it with inline classes.