10

Is Kotlin ?.let thread-safe?

Let's say a variable can be changed in different thread. Is using a?.let { /* */ } thread-safe? If it's equal to if (a != null) { block() } can it happen that in if it's not null and in block it's already null?

4ntoine
  • 19,816
  • 21
  • 96
  • 220
  • I guess that it would be too much to make this operator thread safe – Valeriy Katkov Aug 02 '19 at 10:07
  • 3
    `a` can be null when the block is executed, but `it` can not. I.e. it's equivalent to `val copy = a; if (copy != null) { block(copy) }` – JB Nizet Aug 02 '19 at 10:11
  • @4ntoine When the Kotlin compiler smart casts a nullable type to a non-nullable type, you can be sure that it is really non-null. If the code was not thread safe, the compiler would have given you a compiler error (like it does if you do `if (a != null) { a.someFunction() }`) – marstran Aug 02 '19 at 10:15
  • The compiler error it would have given you is this (if `a` was of type `Int?`): `Smart cast to 'Int' is impossible, because 'a' is a mutable property that could have been changed by this time` – marstran Aug 02 '19 at 10:16

1 Answers1

12

a?.let { block() } is indeed equivalent to if (a != null) block().

This also means that if a is a mutable variable, then:

  1. If a is a mutable variable, it might be reassigned after the null check and hold a null value at some point during block() execution;

  2. All concurrency-related effects are in power, and proper synchronization is required if a is shared between threads to avoid a race condition, if block() accesses a again;

However, as let { ... } actually passes its receiver as the single argument to the function it takes, it can be used to capture the value of a and use it inside the lambda instead of accessing the property again in the block(). For example:

a?.let { notNullA -> block(notNullA) }

// with implicit parameter `it`, this is equivalent to:
a?.let { block(it) }

Here, the value of a passed as the argument into the lambda is guaranteed to be the same value that was checked for null. However, observing a again in the block() might return a null or a different value, and observing the mutable state of the given instance should also be properly synchronized.

hotkey
  • 140,743
  • 39
  • 371
  • 326
  • 3
    …and the latter case _is_ thread-safe.  (In the sense that the `let` parameter is always the same as the value that was checked by `?.`, so can never be null.  However, if `a` has changed in the meantime, it won't reflect that.) – gidds Aug 02 '19 at 11:59
  • @gidds that's interesting. any proof? – 4ntoine Aug 02 '19 at 12:11
  • 1
    I think it follows from the way the `?.` operator works: it takes the value from the expression to its left (which may mean calling getters or whatever), checks whether it's null, and if not, calls the following method on that value.  In this case, `let()` then passes that value as a parameter to the function.  So there's no opportunity to fetch a different value at any point.  Of course, if the value is a mutable object, then its _state_ can change.  But it must be the same object. – gidds Aug 02 '19 at 13:46
  • @gidds, thanks, I've added a remark about it to the answer. Indeed, `?.` will only access the variable once and call the function on the returned value if it's not null. – hotkey Aug 02 '19 at 13:57
  • The first sentence is not quite true, JB Nizet's comment gives the actual equivalence. – Alexey Romanov Aug 02 '19 at 15:50
  • 1
    @AlexeyRomanov My statement assumes that the `block` is parameterless, so I'd say it holds but doesn't explain the whole semantics of `?.let { ... }`. Thanks for the remark! – hotkey Aug 02 '19 at 16:24
  • The last question i have is "is `a != null` check atomic and synchronized"? It looks like it's not. – 4ntoine Nov 27 '19 at 10:43