Reputation: 3026
The let
function guarantees the value inside the block won't be null, even if another thread changes the original value by the time the block is run... Sounds good, but how does it do that?
var user: User?
user?.let { it ->
// `user` can be null at this point, but not `it`.
}
How does the compiler prevent the underlying object from being deallocated while this block is running? Strong reference? Critical section? Something else?
Upvotes: 2
Views: 930
Reputation: 19595
?.
This behavior actually has little to do with the let
function and has more to do with the ?.
, also known as a null-safe operator.
If you have a nullable reference user
as in your example, a statement such as the following which includes the null-safe operator ?.
:
val n = user?.name
says: if user
is not null, use it for the next part of the call chain (in this case referencing the name
property) and assign the result of that to the val n
, otherwise assign null
to n
.
let
is simply one of the Kotlin scoping functions -- its definition in code is simple. let
takes its "receiver", or the value it is being called on, and passes that value as the first parameter to the function that is passed to let
i.e. the { it -> ... }
lambda. That lambda parameter name is by default it
, so the it ->
part is actually redundant.
When the lambda block ends, that variable goes out of scope, hence it is "scoped" to that specific block i.e. it simply assures that the variable does not exist beyond that block.
Now, combining the idea of the null-safe operator with the let
scoping function, we have the code you wrote:
user?.let {
// `user` can be null at this point, but not `it`.
}
The null-safe operator ?.
guarantees that if user is not null
, then the next part of the chain is executed, which in this case is the let
scoping function. If user
is not null, then the not-null user
becomes the receiver of the let
and its value is assigned to it
.
The receiver of the let
function is a copy of the non-null reference, so it is not affected by changes in the reference of the original user
var e.g. if user
is set to null by another thread.
If the user is already null, then the let
function is never even called. In other words, the let
block is never executed because of the null-safe operator.
This code is equivalent to a copy of the user variable into a local variable, then an if
statement checking if that copy is not null, which Kotlin handles with a smart cast for this situation:
val u = user
if (u != null) {
// u cannot be null inside this block, due to smart cast
}
However, ?.let { ... }
is a bit more succint by avoiding the explicit assignment, and can read better when its return value is used to continue the call chain.
One last thing... if you refer back to the definition of let
, you can see it allows a nullable receiver. That means it is valid to call a let
function on a possibly null value, and when doing so, the it
is nullable too:
user.let {
// it can be null here
}
Upvotes: 3
Reputation: 18577
The short answer is that let
makes no such guarantee — in fact, like many extension functions, it can be called on a null value.
What prevents a null in this case is the ?.
safe-call operator. That calls the following function only if the previous value is non-null. So if user
is null, it won't even call let
. That's why the compiler knows that if it gets into the let
lambda, the receiver must have been non-null.
And yes, while the lambda is being executed, the it
reference makes the user
object reachable, and so not eligible for garbage collection.
Upvotes: 1
Reputation: 93649
It is equivalent to copying the value to a local variable and then using that. Your code is equivalent to:
val it = user
if (it != null) {
// `user` can be null at this point, but not `it`.
}
except that it
is confined to the scope of the lambda. So, yes, it creates a strong reference.
By the way, it only guarantees it
is not null if using the null-safe call ?.
. If you use a standard .let
call on a nullable, it very well could be null inside the lambda.
Upvotes: 5