4

Why does this give a compile error?

val autoClosable = MyAutoClosable()
var myVar: MyType
autoClosable.use {
    myVar= it.foo()
}
println(myVar) // Error: Variable 'myVar' must be initialized

Maybe the compiler just sees { myVar= it.foo() } as a function that is passed to another function and has no knowledge about when or even if it will be executed?

But since use is not just a function but Kotlin's replacement for Java's try-with-resource, some special knowledge about it would be appropriate, wouldn't it? Right now, I am forced to initialize myVar with some dummy value, which is not in the spirit of Kotlin at all.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Marco Eckstein
  • 4,448
  • 4
  • 37
  • 48

3 Answers3

6

Since use { ... } is not a language construct but is just a library function, the compiler doesn't know (and, currently, does not make effort to prove) that the lambda you pass is ever executed. Therefore the usage of the variable that might not be initialized is prohibited.

For example, compare your code to this function call. Without additional code analysis, they are identical for the compiler:

inline fun ignoreBlock(block: () -> Unit) = Unit

var myVar: MyType
ignoreBlock { myVar = it.foo() }
println(myVar) // Expectedly, `myVar` stays uninitialized, and the compiler prohibits it

To bypass this restriction, you can use the value returned from use (this is the value your block returns) to initialize your variable:

val myVar = autoClosable.use {
    it.foo()
}

And if you also want to handle the exception it might throw, then use try as an expression:

val myVar = try {
    autoClosable.use {
        it.foo()
    }
} catch (e: SomeException) {
    otherValue   
}

Theoretically, inline functions can actually be checked to invoke a lambda exactly once, and if the Kotlin compiler could do that, it would allow your use case and some others. But this has not been implemented yet.

hotkey
  • 140,743
  • 39
  • 371
  • 326
  • if it is lazy why this code can't working? `var myVar: MyType ;val b = { println(myVar) }; myVar = autoClosable.foo()`, Otherwise, there's my down-vote. :) – holi-java Jul 21 '17 at 15:01
  • @holi-java, and why should it work? It's much easier for the compiler to prohibit `val b = { println(myVar) }` (which can be called **before** `myVar` is assigned -- `myVar` is unitialized up to the moment `b` is created) than to check that at each call site of `b` the `myVar` has already been initialized. – hotkey Jul 21 '17 at 15:06
  • @holi-java, if that's not what you were asking about, then I must have misunderstood you, could you please reformulate the question? – hotkey Jul 21 '17 at 15:07
  • Hi, you ask me to post it as question? – holi-java Jul 21 '17 at 15:08
  • but it should be initialized `myVar` with `ObjectRef` since as you said the lambda is lazily, the compiler can't be sure the lambda is invoked. – holi-java Jul 21 '17 at 15:11
  • we know any language such as javascript, the function can access the variable out side of its scope, and can be initialized later. but in java if you want both to write the value out of the lambda scope & read the value in lambda scope, we must using an `ObjectRef`. since the variable out of the lambda scope is **effective-final**. and I also can see the kotlin bytecode that the type exactly is a `ObjectRef`. why can't be compiled? – holi-java Jul 21 '17 at 15:14
  • @holi-java, well, it's just how it works under the hood, but look at the semantics of the code. When you write `println(myVar)`, you expect the value of `MyType` to be printed, not the `ObjectRef` the compiler created to store that. Actually, the compiler makes everything to make a variable wrapped into an `ObjectRef` look like an ordinary `var`, the `ObjectRef` is never shown to the user (unless they look into the bytecode). So, by saying *`myVar` stays uninitialized*, I mean, *either the local variable or the `ObjectRef` wrapper doesn't hold the value that should be assigned*. – hotkey Jul 21 '17 at 15:15
  • Actually, in my answer I abstract away from the `ObjectRef`, which is completely an implementation detail and is not necessary to explain the code semantics -- we can just think of the `var` as a container for a variable, no matter if it is wrapped or not, the compiler makes sure we assign the value before using the `var`. – hotkey Jul 21 '17 at 15:16
  • hi, the first time I also think it is lazily. but when I dump the code, I found I can't say it is lazily correctly.I think you should give the thoroughly answer. :) – holi-java Jul 21 '17 at 15:22
  • invoke the lambda exactly **once**. so what's wrong of my answer, sir? – holi-java Jul 21 '17 at 15:24
  • What do you mean by **lazy**? The actual reason for the restriction the OP faced is that the compiler does not prove that the lambda is ever called. Once the *exactly-once* check I mentioned is implemented in the compiler, the code in the question will be allowed. – hotkey Jul 21 '17 at 15:26
  • I'm sorry I'm not good at english. I said I also found the lambda is lazily, but when I dump the byte code, I found I can't say it is lazily correctly. since the byte code of the lambda body is analyzed by compiler. **lazy** I means the code in lambda body is never invoked, if the lambda is not called. – holi-java Jul 21 '17 at 15:27
  • sir, due to my bad english, I have create it as a [question](https://stackoverflow.com/questions/45243255/why-the-variable-cant-be-initialized-correctly-in-inline-function-as-in-java). I'm glad you to go to help me solve my confusion. first, thanks. – holi-java Jul 21 '17 at 17:17
  • Regarding the original answer: Great! Writing it this way, you can even omit the explicit type declaration `MyType`. You might want to edit your answer. – Marco Eckstein Jul 28 '17 at 13:04
-1

In case an exception occurs while executing it.foo(), the use block will catch the exception, close your autoClosable, and then return. In this case, myVar will be left uninitialized.

This is why the compiler won't let you do what you're trying to do.

zsmb13
  • 85,752
  • 11
  • 221
  • 226
  • 1
    Actually, `use` re-throws the exception it catches. If an exception is thrown inside the `use { ... }` block, it is thrown outside the `use` call, and the `println(myVar)` line is not reached at all. A *perfect compiler* would not prohibit this code because it would guess that `use` calls the block exactly once and rethrows the exception back into the calling code. :) The Kotlin compiler just does perform the *exactly-once* analysis for the lambdas. – hotkey Jul 21 '17 at 15:48
  • Oh, right, I didn't check that. Thanks for the explanation! – zsmb13 Jul 21 '17 at 17:11
-1

This is because use is an inline function, which means the lambda body will be inlined to the call-site function, and the actual type of the variable myVar depends on its context.

IF the myVar is used in lambda for reading, the the type is MyType or its supertype. for example:

//      v--- the actual type here is MyType
var myVar: MyType = TODO()

autoClosable.use {
    myVar.todo()
}

IF the myVar is used in lambda for writing, the actual type is an ObjectRef. why? this is because Java don't allow you change the variable out of the annoymous class scope. In fact, myVar is effectively-final. for example:

//  v--- the actual type here is an ObjectRef type.
var myVar: MyType

autoClosable.use {
    myVar = autoClosable.foo()
}

So when the compiler checking at println(myVar), it can't sure the element of the ObjectRef is initialized or not. then a compiler error is raised.

IF you catch anything, the code also can't be compiled, for example:

//  v--- the actual type here is an ObjectRef type.
var myVar: MyType
try {
    autoClosable.use {
        myVar = it.foo()
    }
} catch(e: Throwable) {
    myVar = MyType()
}

//       v--- Error: Variable 'myVar' must be initialized
println(myVar) 

But when the actual type of the myVar is MyType, it works fine. for example:

var myVar: MyType
try {
    TODO()
} catch(e: Throwable) {
    myVar = MyType()
}

println(myVar) // works fine

Why kotlin didn't optimize inline functions to use MyType directly for writing?

the only thing I think, the compiler don't know the myVar whether is used in lambda body of another uninline function in future. or kotlin want to keep semantic consistent for all of the functions.

holi-java
  • 29,655
  • 7
  • 72
  • 83
  • The difference between `use` and `try` is that `try` is a language construct with a known semantics: it will execute the body, and if an exception is thrown, then it will execute the corresponding `catch` block. The compiler can thus prove that `myVar` is assigned by all means. And `use` is a completely different matter: it is just a library function, and it can manage the lambda it is passed in an arbitrary way (e.g. call it multiple times or never call it). Therefore the compiler cannot be sure that the code block is ever executed, and it conservatively prohibits possibly incorrect code. – hotkey Jul 21 '17 at 15:30
  • well, but the code of the lambda body is inlined into call-site function, so it should be right. that I also have test it, nested `try-catch` also can working. but inline function can't. for example: `try { try{ TODO() }catch(e:Throwable){throw e} } catch (e: Throwable) { myVar=autoClosable.foo() }` – holi-java Jul 21 '17 at 15:33
  • so why the inlined function can't working, the only thing I can think is compiler don't sure the `element` of the `ObjectRef` is initialized. and I describe it in answer at the middle. maybe you never see it. – holi-java Jul 21 '17 at 15:35
  • Even though the lambda is inlined, the control flow is not currently analyzed in it. It's just not implemented, and the compiler just prohibits this code as if the lambda is not called. To make it work, the compiler should analyze either the inline function body to prove that it calls the lambda or the resulting bytecode. Neither is done at this moment. – hotkey Jul 21 '17 at 15:42
  • Also, when a `var` is used only in inline lambdas, it is not wrapped into an `ObjectRef` at all. – hotkey Jul 21 '17 at 15:42
  • could you give some source code or some reference let me to see it? I know you have contributed Kotlin. but your answer don't persuade me at the first time. – holi-java Jul 21 '17 at 15:44
  • Ok, sure, I'll do it a little bit later. – hotkey Jul 21 '17 at 15:49