如何告诉Kotlin,如果参数不为空,则函数不返回null?

10

我想编写一个方便的扩展程序,可以在解析Map时同时提取其值。 如果解析失败,该函数应返回默认值。 所有这些都能正常工作,但我想告诉Kotlin编译器,当默认值不为null时,结果也不会为null。 我可以通过Java中的@Contract注释来实现这一点,但似乎在Kotlin中无法使用。 这可以完成吗? 扩展功能的合同不起作用吗?以下是Kotlin尝试:

import org.jetbrains.annotations.Contract

private const val TAG = "ParseExtensions"

@Contract("_, !null -> !null")
fun Map<String, String>.optLong(key: String, default: Long?): Long? {
    val value = get(key)
    value ?: return default

    return try {
        java.lang.Long.valueOf(value)
    } catch (e: NumberFormatException) {
        Log.e(TAG, e)
        Log.d(TAG, "Couldn't convert $value to long for key $key")

        default
    }
}

fun test() {
    val a = HashMap<String, String>()

    val something: Long = a.optLong("somekey", 1)
}

在上面的代码中,即使使用非空默认值1调用optLong,IDE也会在分配给something时突出显示错误。为了比较,在Java中测试可为空性的类似代码如下所示,该代码使用注释和合同:
public class StackoverflowQuestion
{
    @Contract("_, !null -> !null")
    static @Nullable Long getLong(@NonNull String key, @Nullable Long def)
    {
        // Just for testing, no real code here.
        return 0L;
    }

    static void testNull(@NonNull Long value) {
    }

    static void test()
    {
        final Long something = getLong("somekey", 1L);
        testNull(something);
    }
}

上面的代码没有显示任何错误。只有当删除@Contract注释时,IDE才会警告使用可能为null值调用testNull()

为什么要设置默认值?让方法调用者像你一样处理它。如果“value”为空,则返回null。“map.optLong(“key”)?:6L” - killjoy
另外,考虑使用 string.toLongOrNull() 代替 Long.valueOf(string),它可以自动处理错误格式。 - killjoy
@killjoy 使用 null 作为默认值可能意味着多种情况,不仅仅是没有找到值。如果他决定在这种情况下使用 null 默认值是好的,那么可以将 default 参数默认设置为 null - Aro
3个回答

3
您可以通过将该函数泛化来实现此目的。
fun <T: Long?> Map<String, String>.optLong(key: String, default: T): T 
{
    // do something.
    return default
}

这可以像这样使用:

fun main(args: Array<String>) {
    val nullable: Long? = 0L
    val notNullable: Long = 0L

    someMap.optLong(nullable) // Returns type `Long?`
    someMap.optLong(notNullable) // Returns type `Long`
}

这是因为Long?Long的超类型。通常会推断出类型,以便根据参数返回可空或非可空类型。
这将“告诉Kotlin编译器,当默认值不为null时,结果也不会为null。”

1
返回分支强制我将 Long! 无检查转换为 T,但由于这是在辅助函数内部,所以我不介意。 - Grzegorz Adam Hankiewicz
@GrzegorzAdamHankiewicz 你最终的方法签名是什么?我认为 Long! 不需要转换为 T,因为它是 T 的子类型(有点像)。 - Aro
我复制了你的示例,并用上面的代码替换了“做某事”的部分。 - Grzegorz Adam Hankiewicz
这适用于 Kotlin 调用者,其中返回值取决于传递参数的 声明 类型。但是 @Contract 还考虑其他因素,例如您是否已经检查过 Long? 实际上是否为 null 或者能否强制转换为 Long!。请参见我的答案,了解如何使 @Contract 与 Kotlin 扩展函数配合使用的解释(它还可以使其从 Java 调用者中起作用)。 - Joe

2
很遗憾,在Kotlin 1.2或以下版本中无法做到这一点。
然而,Kotlin正在研发未公布的“contract dsl”,目前还不可用(因为它们在stdlib中被声明为“internal”),但您可以使用一些技巧在代码中使用它们(通过自己编译stdlib,将所有内容设为public)。
您可以在stdlib中查看它们。
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

也许会有类似以下的内容:
contract {
   when(null != default) implies (returnValue != null)
}

在未来,可以解决您的问题。

解决方法

个人建议将 default 的类型替换为 NotNull 的 Long 并按以下方式调用:

val nullableLong = blabla
val result = nullableLong?.let { oraora.optLong(mudamuda, it) }

resultLong? 类型,只有当 nullableLong 为空时才会为 null。


很酷,我不知道那是他们正在开发的东西。 - Todd

1

@Contract 适用于 Kotlin 扩展函数,只需要将其更改以与已编译的字节码配合使用即可。扩展函数被编译为静态方法的字节码:

fun ClassA?.someMethod(arg: ClassB): ClassC? {
    return this?.let { arg.someMethod(it)!! }
}

Java会将其视为可为空,因此需要您对结果进行空值检查。但实际契约是:“如果ClassA为空,则返回null;否则,如果ClassA不为空,则返回非null”。但IntelliJ无法理解这一点(至少从Java源代码中看来如此)。

当该方法被编译为Java字节码时,实际上是:

@Nullable static ClassC someMethod(@Nullable ClassA argA, @NonNull ClassB argB) {}

因此,在编写您的@Contract时,需要考虑到合成的第一个参数:

@Contract("null, _ -> null; !null, _ -> !null")
fun ClassA?.someMethod(arg: ClassB): ClassC? {...}

接下来,IntelliJ将理解静态方法的契约,并且将理解返回值的可空性取决于第一个参数的非空性。

因此,与此问题相关的简短版本是,您只需要向合同添加额外的_参数,以表示“this”参数:

@Contract("_, _, !null -> !null") // args are: ($this: Map, key: String, default: Long?)
fun Map<String, String>.optLong(key: String, default: Long?): Long? {

我刚试了一下,但不幸的是它似乎没有起作用。我仍然得到“只允许在可空接收器上进行安全(?.)或非空断言(!!.)调用”的错误提示。 - Mygod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接