expression - Since Kotlin is a statically typed language, why can I implement an ifelse returning different types to a variable?

This question arose while I was experimenting with Kotlin's static typing feature. The scenario is

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
Add a comment  | 

1 Answer 1

Reset to default 1

This 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 Any1.

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条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信