When I heard about guard
in Kotlin 2.1 announcement, I was confused. Why do we even need this? Isn't a simple if/return
enough?
fun foo(data: Data) {
if (!data.valid) {
return
}
// foo main logic
}
Or is it similar to Swift guard
?
func foo(data: Data) {
guard let isValid = data.valid, isValid else {
return
}
// foo main logic
}
I had to re-read the new feature name and it's actually called when guards
. I told myself stop just reading the keyword and look deeper into what it offers
What is when guards
Feature?
According to Kotlin doc:
Starting from 2.1.0, you can use guard conditions in when expressions or statements with subjects.
Guard conditions allow you to include more than one condition for the branches of a when expression, making complex control flows more explicit and concise as well as flattening the code structure.
This means you can replace nested conditions in a when branch with a flatter, more readable structure. It's a great way to improve code clarity and reduce unnecessary indentation(s). Let's take a look at an example to demonstrate this.
Prerequisite
But first you need to upgrade your Kotlin to the 2.1.0 (or later) and enable the experimental feature by adding the following compiler flag to your build.gradle.kts
file:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xwhen-guards")
}
}
Otherwise you will run into a compiler error asking you to add the compiler flag
The feature "when guards" is experimental and should be enabled explicitly
Silly Example
Imagine you have when
statement with a sealed class as a subject and you want to generate different string value depending on the subject's class type:
sealed class Dessert {
data class CheeseCake(val type: Type): Dessert() {
enum class Type {
NEW_YORK,
JAPANESE,
NORMAL,
}
}
object Kitkat: Dessert()
class Oreo(): Dessert()
}
fun foo(dessert: Dessert) {
when(dessert) {
is Dessert.CheeseCake -> "This is cheesecake"
Dessert.Kitkat -> "This is Kitkat"
is Dessert.Oreo -> "This is Oreo"
}
}
Straightforward, right? But what if you want more granular logic for CheeseCake, such as displaying a special message for the NEW_YORK type? Before Kotlin 2.1, this would look like:
fun foo(dessert: Dessert) {
when(dessert) {
is Dessert.CheeseCake -> {
if (dessert.type == Type.NEW_YORK) {
"This is my favorite cheesecake"
} else {
"This is cheesecake"
}
}
Dessert.Kitkat -> "This is Kitkat"
is Dessert.Oreo -> "This is Oreo"
}
}
With more complex conditions, this structure quickly becomes cluttered.
Simplifying with when guards
This is where the new feature comes in (Btw it's not actual guard
keyword but concept). Instead of having this nested block of code, we can rewrite this into:
when (dessert) {
is Dessert.CheeseCake if dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
Dessert.Kitkat -> "This is Kitkat"
is Dessert.Oreo -> "This is Oreo"
is Dessert.CheeseCake -> "This is cheesecake"
}
This is cool and looks a lot nicer than before but you might want to ask yourself what if we don't use subject for when
statement and achieve the same thing? It would look something like this:
when {
dessert is Dessert.CheeseCake && dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
dessert is Dessert.CheeseCake -> "This is cheesecake"
dessert is Dessert.Kitkat -> "This is Kitkat"
dessert is Dessert.Oreo -> "This is Oreo"
}
This works well but does have downsides. Not only Repeating dessert is
checks makes the code verbose, but using when
as an expression requires an else
branch:
fun foo(dessert: Dessert): String {
return when {
dessert is Dessert.CheeseCake && dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
dessert is Dessert.CheeseCake -> "This is cheesecake"
dessert is Dessert.Kitkat -> "This is Kitkat"
dessert is Dessert.Oreo -> "This is Oreo"
else -> "This is dessert" // Required by the compiler
}
}
Adding an else branch for unreachable cases feels redundant and clutters the code.
With this new guard feature, the final code would look like this:
fun foo(dessert: Dessert): String {
return when (dessert) {
is Dessert.CheeseCake if dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
Dessert.Kitkat -> "This is Kitkat"
is Dessert.Oreo -> "This is Oreo"
is Dessert.CheeseCake -> "This is cheesecake"
}
}
This approach is concise, readable, and avoids unnecessary branching. If you’ve struggled with tools like Detekt complaining about nested blocks, try this feature out!
Top comments (2)
Nit: I think the Swift code you meant to do this
func foo(data: Data) {
guard let isValid = data.valid, isValid else {
return
}
// foo main logic
}
K thanks!
Your friend,
Steve
Thanks for that. Addressed!