skip to content

How does isNullOrEmpty() let the compiler smart-cast a String? to String in only one branch of an if? Show the contract that makes it work.

level: middleimportance: should knowfreq 45%

answer

  1. returns(false) implies (this != null)
  2. Smart cast lives in the FALSE / else branch
  3. true branch = null OR empty, no cast
  4. Same for isNullOrBlank()
  5. Receiver referenced as this@isNullOrEmpty

basics

~20 s

isNullOrEmpty() carries a contract saying 'if I return false, the receiver is not null and not empty'. So in the else branch (or after a negated check) the compiler knows the string is non-null and smart-casts it.

solid answer

~40 s

isNullOrEmpty() (and isNullOrBlank()) are extension functions on CharSequence? declared with contract { returns(false) implies (this@isNullOrEmpty != null) }. returns(false) is a ReturnsEffect tied to the literal Boolean result false. So when the call evaluates to false, the receiver is guaranteed non-null and the compiler smart-casts it to the non-null type — typically inside the else of if (s.isNullOrEmpty()), or directly in if (!s.isNullOrEmpty()) { ... }. Note it does NOT smart-cast in the true branch, because a true result allows either null or empty — non-nullity isn't guaranteed there. This returns(Boolean) form is the same mechanism behind String?.isNullOrBlank() and is the reason these read more cleanly than s != null && s.isNotEmpty().

code

kotlin · 5 lines
kotlin
fun firstChar(s: String?): Char? {
    if (s.isNullOrEmpty()) return null
    // s smart-cast to String after the early return
    return s[0]
}

go deeper

for a junior

Knows the else/negated branch lets you use the string as non-null.

for a middle

Names the returns(false) implies (this != null) contract and explains why the true branch is excluded.

for a senior

Connects it to control-flow smart casting (early returns) and the broader returns(Boolean) family.

for a principal

Reasons about API design: exposing predicate+contract helpers vs separate checks, and readability/maintainability tradeoffs.

## The returns(Boolean) contract form For a function returning `Boolean`, a contract can tie a condition to a **specific** result value using `returns(true)` or `returns(false)`. `isNullOrEmpty` is declared (simplified) as: ```kotlin @OptIn(ExperimentalContracts::class) public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 } ``` - **`returns(false)`** — the effect 'this function returned the value `false`'. - **`implies (this@isNullOrEmpty != null)`** — given a `false` result, the receiver is non-null. ## Which branch gets the smart cast ```kotlin fun render(s: String?) { if (s.isNullOrEmpty()) { // true branch: s could be null OR empty -> NO smart cast println("empty or absent") } else { // false branch: contract guarantees s != null -> smart cast to String println(s.length) } } ``` Equivalently, negate to put the safe case first: ```kotlin if (!s.isNullOrEmpty()) { println(s.length) // smart-cast here } ``` ## Why not the true branch? Returning `true` means "null **or** empty". An empty non-null string still returns `true`, so non-nullity is **not** implied — there is no `returns(true) implies ...` clause. The compiler correctly refuses to smart-cast there. ## Same family - `CharSequence?.isNullOrBlank()` — same `returns(false) implies (this != null)`. - `Collection?.isNullOrEmpty()` — same idea for collections. These are why `s.isNullOrEmpty()`-style guards both validate and unlock the non-null type without a separate `!= null` check.

  • Why isn't the variable smart-cast inside the true branch of if (s.isNullOrEmpty())?
    A true result means null OR empty; an empty non-null string still returns true, so non-nullity isn't guaranteed. Only returns(false) implies non-null.
  • Does an early return like 'if (s.isNullOrEmpty()) return' also enable the smart cast afterward?
    Yes. Control-flow analysis sees that reaching the code below means the call returned false, so s is smart-cast to non-null below the return.

saying these in an interview costs you the question

  • Claiming smart cast happens in the true branch
  • Saying isNullOrEmpty guarantees the string is non-empty everywhere after the call
  • Not knowing the returns(false) form exists
  • Confusing isNullOrEmpty (null or length 0) with isNullOrBlank (null or whitespace-only)

context