浮動小数点数(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 関数実装完全版
/**
* 与えられた 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?
}
この記事を書いた理由
次の記事を偶然見かけたのですが、直観的でないように感じたためこの記事を書きました。
/以上