This question arose while I was experimenting with Kotlin's static typing feature. The scenario is as follows: I wrote a piece of code containing an if/else
expression that returns a value to a variable without an explicit type declaration.
Based on how the Kotlin compiler works, I assumed that the inferred type of the variable would be determined by the return type of the if/else
expression. However, considering Kotlin's static typing characteristic, I expected the compiler to produce an error if the if/else
expression involved ambiguous types.
Consider the following code:
fun main(args: Array<String>) {
var a = args[0].toInt()
var x = if (a < 5) { 5 } else { "hello" }
println(x::class)
x = 38
println(x::class)
x = "test"
println(x::class)
x = 45.9f
println(x::class)
x = 32L
println(x::class)
}
The code was compiled and executed as follows:
kotlin test.kt -include-runtime -d test.jar
java -jar test.jar 45
class kotlin.String
class kotlin.Int
class kotlin.String
class kotlin.Float
class kotlin.Long
As we can see, when running the program with the input 45
, the output shows the class associated with the variable x
based on its different assignments at runtime.
In statically typed languages, shouldn’t there be a compile-time restriction on such dynamic assignments? After all, shouldn’t the Kotlin compiler prevent the inference of an ambiguous type?
This question arose while I was experimenting with Kotlin's static typing feature. The scenario is as follows: I wrote a piece of code containing an if/else
expression that returns a value to a variable without an explicit type declaration.
Based on how the Kotlin compiler works, I assumed that the inferred type of the variable would be determined by the return type of the if/else
expression. However, considering Kotlin's static typing characteristic, I expected the compiler to produce an error if the if/else
expression involved ambiguous types.
Consider the following code:
fun main(args: Array<String>) {
var a = args[0].toInt()
var x = if (a < 5) { 5 } else { "hello" }
println(x::class)
x = 38
println(x::class)
x = "test"
println(x::class)
x = 45.9f
println(x::class)
x = 32L
println(x::class)
}
The code was compiled and executed as follows:
kotlin test.kt -include-runtime -d test.jar
java -jar test.jar 45
class kotlin.String
class kotlin.Int
class kotlin.String
class kotlin.Float
class kotlin.Long
As we can see, when running the program with the input 45
, the output shows the class associated with the variable x
based on its different assignments at runtime.
In statically typed languages, shouldn’t there be a compile-time restriction on such dynamic assignments? After all, shouldn’t the Kotlin compiler prevent the inference of an ambiguous type?
Share edited Mar 7 at 2:49 tyg 16.7k4 gold badges39 silver badges49 bronze badges asked Mar 6 at 21:05 ViniciusVinicius 1011 silver badge8 bronze badges 2- 1 In most languages of this kind common type between 5 and "hello" ("inferred type ... if/else") would be computed to "object" (base type of all types). You seem to have something else in mind - could you please clarify what you expected the type of "x" would be and why? (Or maybe your question is "why I can put value of any type into variable of 'object' type"?) – Alexei Levenkov Commented Mar 6 at 22:12
- 1 Have you used e.g. an IDE to find out what type the compiler infers for ‘x’? – gidds Commented Mar 6 at 23:36
1 Answer
Reset to default 1This doesn't conflict with Kotlin still being a strongly-typed language.
If you do not declare a specific type for a variable, the compiler will infer the type. The compiler will only throw an error if, for some reason, that isn't possible.
In your example, however, it is possible. As you already figured out, though, it cannot be Int
, and it cannot be String
. Since the variable needs to be able to hold both types its type is inferred to be the closest common supertype.
The type hierarchy of Int is this:
Int -> Number -> Any
The type hierarchy of String is this:
String -> Any
So the closest common type in both hierarchies is Any
1.
The Any
type can hold, well, anything that is not null. This doesn't defy Kotlin's type-safety because the compiler now restricts you to only do things with the variable that is guaranteed to succeed at runtime for all types. That isn't much: You can only call equals()
, hashCode()
and toString()
, plus anything that is declared as an extension function for Any
(like let()
, also()
and so on)2. Conversely, the compiler prevents you to do anything that is specific to an Int, like multiplication (*
). You also cannot do anything specific to String, like substring()
.
Any
usually isn't very helpful to work with, why you won't encounter it much in a program. It is completely type-safe, though.
The reason why you see varying types in the console output is that by x::class
you do not retrieve the type of the variable x (which is statically inferred at compile time to always be Any
), you retrieve the type of the actual object that is stored in the variable. Since the variable is of type Any
you can place anything in it that is a subtype of it, like the String
, Int
, Float
and Long
you use in your example3. That only happens at runtime where objects are actually created.
For example, when your program executes x = 45.9f
at runtime, it first creates an object of type Float
with the value 45.9. Then this object is stored in the variable x
. The object itself isn't changed so it remains of type Float
, but it still can be assigned to the variable of type Any
because a Float
also is an Any
(it's in the type hierarchy Float -> Number -> Any
).
The rest of the program won't see the object as a Float
from now on (although it still is), it only sees the variable x
where the object is stored in, and that is of type Any
. Since the variable could also contain a String or a Long or, actually, anything, you won't be able to access anything specific to a Float, like isInfinite()
. So there won't be any ambiguity.
Kotlin allows you to use reflection on runtime to find out more about the actual object stored in a variable, even when the compiler only allows you to see it as an Any
. That is what you do when you call x::class
, you probe the object that can be accessed via the variable x
to retrieve the class of the object, e.g. a Float. You can even cast it to Float so you can access all its specific members again:
val isInf: Boolean? = (x as? Float)?.isInfinite()
This will be true
or false
when x actually contains a Float, and it will be null when x contains anything else (like a String or a Long).
In some cases you won't even need the explicit cast, namely in cases where the compiler knows that the variable contains a Float (although the variable was inferred to be of type Any
), for example here:
var x = if (a < 5) { 5 } else { "hello" }
x = 45.9f
val isInf: Boolean = x.isInfinite()
The compiler sees that the last time before you try to access x
you stored a Float in it, therefore it will smart-cast x
to a Float for you. You could do it explicitly, but since the compiler can guarantee that x
contains a Float, it will conveniently do this behind the scenes so you can directly access isInfinite()
. When you remove x = 45.9f
then the compiler won't do the smart cast and will prevent you to compile the code with the error Unresolved reference 'isInfinite'.
, because it only sees that x
is of type Any
, and that doesn't provide a function named isInfinite
.
Smart casts are a convenience feature that allows you to omit a lot of boilerplate code you would need to write otherwise, but it doesn't compromise type-safety. The Kotlin compiler tries to help you out determining the actual type of an object the best it can by using its sophisticated type inference system, but it won't compromise type-safety at any point. Only inferences that are guaranteed to be correct will be made.
1 Since Kotlin also allows multiple inheritance via interfaces, the actual type of the variable is an intersection of all common interfaces. In your example that is Comparable<*> & Serializable
. That is a type that is only used internally by the compiler, you cannot denote this type explicitly on a variable. You would need to use Any
for that. See here fore more: https://youtrack.jetbrains/issue/KT-13108/Denotable-union-and-intersection-types
2 As established in the previous footnote the actual inferred type will be Comparable<*> & Serializable
, so you could also call the compareTo
method from the Comparable interface. That requires a proper generic type, though, so you cannot actually provide a matching parameter and hence you still cannot call compareTo
, but that is a restriction of Generics.
3 As the type actually is Comparable<*> & Serializable
rather than Any
, you will only be able to store objects that are comparable and serializable (like what you used in your example). When you would explcitly declare x
as Any
to prevent type inference you could also do x = Unit
. That won't be possible with the inferred type and the compiler errors out with Assignment type mismatch: actual type is 'kotlin. Unit', but 'it(kotlin.Comparable<*> & java.io.Serializable)' was expected.
instead.
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744949177a4602810.html
评论列表(0条)