Despite possible performance impacts, local vals is the best solution. If the variable is mutable and accessible to outer scopes then the only way to guarantee that it hasn’t changed is making a local copy. Even null checking using ?.let {}
for single variables does this:
fun test() {
name?.let { doSomething(it) }
}
compiles to the equivalent java code:
public final void test() {
String var10000 = this.name;
if(this.name != null) {
String var1 = var10000;
this.doSomething(var1);
}
}
(Note: the kotlin compiler does optimise this when it can be sure that the variable won’t change. If name was a local var instead of a property, the compiler wouldn’t make an additional local variable
Swift has a similar problem with null checking multiple vars and solves this by making it easy to declare local values within if statements:
// Swift code
if let name = name, let age = age {
doSth(name, age)
}
A similar syntax in Kotlin could be the following:
if (val name = name, val age = age) {
doSth(name, age)
}
Which would compile to the equivalent java code:
final String name = this.name;
if (name != null) {
final Integer age = this.age;
if (age != null) {
doSomething(name, age);
}
}
The usage of the comma instead of &&
is to differentiate it from normal if statements in that everything should evaluate to non-null, the ||
operator is not allowed and do not make sense:
if (val name = name || val age == age) { /* Defeats the point of null checking */ }
Like with the ?.let{}
case, it would be possible for the compiler to optimise the creation of the local variables, only creating them if required. Also like the it
in the ?.let{}
block, any values declared in the scope of the of the if statement and it’s following block would be immutable.
The syntax could be generalised as follows:
if (val var1 = <expression1> ,val var2 = <expression2>, val varN = <expressionN>) {
// Do something with var1, var2, varN
}
Where <expressionX>
is any expression that returns a nullable type
Which compiles to the equivalent java code:
final Type1 var1 = <expression1>;
if (var1 != null) {
final Type2 var2 = <expression2>;
if (var2 != null) {
final TypeN varN = <expressionN>;
if (varN != null) {
// Do something with var1, var2, varN
}
}
}
It may seem a bit much, but it is almost identical to how multiple consecutive safe null calls are compiled. e.g: foo?.bar?.baz
The benefit however is that is that you can use earlier values in later expressions. In the example below p1 and p2 have already been null checked so we can access their age property directly.
fun whoIsOldest(person1: Person?, person2: Person?) {
if (val p1 = person1, val p2 = person2, val age1 = p1.age, age2 = p2.age) {
when {
age1 > age2 -> print("$p1 is oldest")
age1 < age2 -> print("$p2 is oldest")
else -> print("$p1 and $p1 are the same age")
}
} else {
print("Your need two people to compare ages")
}
}
And because the expressions can be anything that returns a nullable type it can also be used with safe casts:
if (val child = person as? Child, val car = child.favouriteToy as? Car) {
car.race()
print("$child is racing their toy $car")
}
A additional improvement that could be made which is also available in swift is allowing boolean expressions alongside the nullable expressions. It’s a fairly common use case to check if something is non-null and then perform an additional check to see if it is appropriate to use.
if (val childAge = person.child?.age, childAge >= 6 && childAge < 18) {
print("$person has to drop their child at school each weekday")
}
Here the use of the comma instead of &&
reinforces the requirement that every expression needs to evaluate to non-null and true, no ||
operators are allowed.