3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】nullable な数値の四則演算

3
Last updated at Posted at 2025-12-13

浮動小数点数(Float や Double)の四則演算では、被演算子の少なくとも一方が NaN であれば、演算結果も NaN になります。

fun main() {
    val a = 1.0
    val b = 2.0
    val c = Double.NaN
    println(a + b) // > 3.0
    println(a + c) // > NaN
}

しかし被演算子が nullable な数値(整数、浮動小数点数)の四則演算はコンパイルエラーになります。

fun main() {
    val a = 1
    val b = 2
    val c = null as Int?
    println(a + b) // > 3
    println(a + c) // コンパイルエラー!
}

NaN の場合と同様に、少なくとも一方が null であれば演算結果を null にするにはどうすればよいでしょうか。

演算子のオーバーロードによる解決案(⚠️不完全な方法)

Kotlin では演算子をオーバーロードすることができます。
被演算子が共に nullable の演算子を定義することで、「少なくとも一方が null であれば演算結果を null にする」を実現できます。

⚠️不完全な方法
/**
 * this と other の和を返す。
 * 少なくとも一方が null の場合には null を返す。
 */
operator fun Int?.plus(other: Int?): Int? = when {
    this == null -> null
    other == null -> null
    else -> this + other
}
// 減算・乗算・除算は省略

fun main() {
    val a = 1
    val b = 2
    val c = null as Int?
    println(a + b) // > 3
    println(a + c) // > null
}

しかしこれだと意図せず nullable が入り込んでしまったときに、コンパイルエラーにならず意図しない結果になってしまいます。(浮動小数点数の演算で意図せず NaN になってしまったことはありませんか?)

nullable 同士の四則演算ができるスコープを限定する改善案

nullable 同士の四則演算ができるスコープを明示して、その外では演算できない(これまで通りコンパイルエラーになる)ようにすることで、意図せず nullable が入り込んでしまう問題を解消できます。

/**
 * nullable な数値同士の四則演算ができるスコープ。
 * 数値の少なくとも一方が null の場合は演算結果も null になる。
 */
class NullableOperationScope {
    /**
     * this と other の和を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Int?.plus(other: Int?): Int? = when {
        this == null -> null
        other == null -> null
        else -> this + other
    }
    // 減算・乗算・除算は省略
}

/**
 * 与えられた block 関数の中で nullable な数値同士の四則演算ができるようにする。
 * 数値の少なくとも一方が null の場合は演算結果も null になる。
 */
inline fun nullableOperation(block: NullableOperationScope.() -> Unit): Unit =
    NullableOperationScope().block()

fun main() {
    val a = 1
    val b = 2
    val c = null as Int?

    // このブロック内でのみ nullable な数値同士の四則演算ができる。
    nullableOperation {
        println(a + b) // > 3
        println(a + c) // > null
    }

    // ブロック外では nullable な数値同士の四則演算ができない。
    println(a + c) // コンパイルエラー!
}
nullableOperation 関数実装完全版
nullableOperation.kt
/**
 * 与えられた block 関数の中で nullable な数値同士の四則演算ができるようにする。
 * 数値の少なくとも一方が null の場合は演算結果も null になる。
 */
inline fun nullableOperation(block: NullableOperationScope.() -> Unit): Unit =
    NullableOperationScope().block()

/**
 * nullable な数値同士の四則演算ができるスコープ。
 * 数値の少なくとも一方が null の場合は演算結果も null になる。
 */
class NullableOperationScope {
    // region Int?

    /**
     * this と other の和を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Int?.plus(other: Int?): Int? = when {
        this == null -> null
        other == null -> null
        else -> this + other
    }

    /**
     * this と other の差を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Int?.minus(other: Int?): Int? = when {
        this == null -> null
        other == null -> null
        else -> this - other
    }

    /**
     * this と other の積を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Int?.times(other: Int?): Int? = when {
        this == null -> null
        other == null -> null
        else -> this * other
    }

    /**
     * this と other の商を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Int?.div(other: Int?): Int? = when {
        this == null -> null
        other == null -> null
        else -> this / other
    }

    /**
     * this と other の剰余を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Int?.rem(other: Int?): Int? = when {
        this == null -> null
        other == null -> null
        else -> this % other
    }

    // endregion Int?

    // region Long?

    /**
     * this と other の和を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Long?.plus(other: Long?): Long? = when {
        this == null -> null
        other == null -> null
        else -> this + other
    }

    /**
     * this と other の差を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Long?.minus(other: Long?): Long? = when {
        this == null -> null
        other == null -> null
        else -> this - other
    }

    /**
     * this と other の積を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Long?.times(other: Long?): Long? = when {
        this == null -> null
        other == null -> null
        else -> this * other
    }

    /**
     * this と other の商を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Long?.div(other: Long?): Long? = when {
        this == null -> null
        other == null -> null
        else -> this / other
    }

    /**
     * this と other の剰余を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Long?.rem(other: Long?): Long? = when {
        this == null -> null
        other == null -> null
        else -> this % other
    }

    // endregion Long?

    // region Float?

    /**
     * this と other の和を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Float?.plus(other: Float?): Float? = when {
        this == null -> null
        other == null -> null
        else -> this + other
    }

    /**
     * this と other の差を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Float?.minus(other: Float?): Float? = when {
        this == null -> null
        other == null -> null
        else -> this - other
    }

    /**
     * this と other の積を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Float?.times(other: Float?): Float? = when {
        this == null -> null
        other == null -> null
        else -> this * other
    }

    /**
     * this と other の商を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Float?.div(other: Float?): Float? = when {
        this == null -> null
        other == null -> null
        else -> this / other
    }

    /**
     * this と other の剰余を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Float?.rem(other: Float?): Float? = when {
        this == null -> null
        other == null -> null
        else -> this % other
    }

    // endregion Float?

    // region Double?

    /**
     * this と other の和を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Double?.plus(other: Double?): Double? = when {
        this == null -> null
        other == null -> null
        else -> this + other
    }

    /**
     * this と other の差を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Double?.minus(other: Double?): Double? = when {
        this == null -> null
        other == null -> null
        else -> this - other
    }

    /**
     * this と other の積を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Double?.times(other: Double?): Double? = when {
        this == null -> null
        other == null -> null
        else -> this * other
    }

    /**
     * this と other の商を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Double?.div(other: Double?): Double? = when {
        this == null -> null
        other == null -> null
        else -> this / other
    }

    /**
     * this と other の剰余を返す。
     * 少なくとも一方が null の場合には null を返す。
     */
    operator fun Double?.rem(other: Double?): Double? = when {
        this == null -> null
        other == null -> null
        else -> this % other
    }

    // endregion Double?
}

この記事を書いた理由

次の記事を偶然見かけたのですが、直観的でないように感じたためこの記事を書きました。

/以上

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?