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.
answer
- returns(false) implies (this != null)
- Smart cast lives in the FALSE / else branch
- true branch = null OR empty, no cast
- Same for isNullOrBlank()
- Receiver referenced as this@isNullOrEmpty
basics
~20 sisNullOrEmpty() 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 sisNullOrEmpty() (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 linesfun firstChar(s: String?): Char? {
if (s.isNullOrEmpty()) return null
// s smart-cast to String after the early return
return s[0]
}go deeper
Knows the else/negated branch lets you use the string as non-null.
Names the returns(false) implies (this != null) contract and explains why the true branch is excluded.
Connects it to control-flow smart casting (early returns) and the broader returns(Boolean) family.
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)