Why do generic functions can't have default arguments?


#1

I wrote a helper function to work with JUnit4:

fun <T : Exception> expectException(kClass: KClass<T>, assert: () -> Unit) {
    val exceptionName = kClass.simpleName
    try {
        assert()
        Assert.fail("Expect to throw [$exceptionName] exception.")
    } catch (e: Exception) {
        if (!kClass.java.isInstance(e)) {
            if (e is AssertionError) {
                throw e
            } else {
                Assert.fail("Expect to throw [$exceptionName] exception but was [${e.javaClass.simpleName}].")
            }
        }
    }
}

It works, but I want to make it better which can be called like this:

expectThrows{
  doSomething()
}

So I tried

fun <T : Exception> expectException(kClass: KClass<T> =Exception::class, assert: () -> Unit){
   //sameCode
}

But got a Kotlin: Type mismatch: inferred type is KClass<Exception> but KClass<T> was expected

I must do this like I used to do in JAVA:
fun expectThrows(assert: () -> Unit) = expectThrows(Exception::class, assert)

So why do generic functions can’t have default arguments?


#2

Generic functions can definitely have default arguments, but not in the way you’re using them. You’re saying "I want to have a function that can have any exception type as a parameter, and if I don’t specify the parameter value, it will be Exception::class". Now suppose that you’re calling the function in this way:

expectThrows<IllegalArgumentException> { ... }

In this call, the type of the kClass parameter will be KClass<IllegalArgumentException>, and the value will be Exception::class. Exception is not a subclass of IllegalArgumentException, so the code will not be valid, and the compiler rejects it.

To fix the problem, note that you don’t actually need the function to be generic; you’re not using T in the function body at all. Therefore, you can change the function to:

fun expectException(kClass: KClass<out Exception> = Exception::class, assert: () -> Unit) { ... }

#3

@zhangdatou You might be interested in how we’ve implemented the similar function in our kotlin-test library: https://github.com/JetBrains/kotlin/blob/master/libraries/kotlin.test/shared/src/main/kotlin.jvm/kotlin/test/TestAssertionsJVM.kt#L8


#4

Thanks, I fixed my code, then added a “catch” for fun.

Now I can write tests like this:

expectThrows(ArrayIndexOutOfBoundsException::class) {
    intArrayOf(0)[1]
} catch { e ->
    Assert.assertEquals("1", e.message)
}

:sunglasses:

Here’s my code:

fun <T : Throwable> expectThrows(kClass: KClass<T>, block: () -> Unit): ExpectThrows<T> {
        val exceptionName = kClass.simpleName
        try {
            block()
        } catch(e: Throwable) {
            if (!kClass.java.isInstance(e)) {
                Assert.fail("Expected an exception of type $exceptionName to be thrown, but was ${e.javaClass.simpleName}")
                error("")
            } else {
                @Suppress("UNCHECKED_CAST")
                return ExpectThrows(e as T)
            }
        }
        Assert.fail("Expected an exception of type $exceptionName to be thrown, but was completed successfully.")
        error("")
    }

    class ExpectThrows<out T : Throwable> internal constructor(val exception: T) {
        infix fun catch(assertion: (e: T) -> Unit) {
            assertion(exception)
        }
    }

Many thanks.