メインコンテンツまでスキップ

Kotlin の基本: 型・null 安全・式

この章では、Kotlin のコードを書くときに毎回使う言語の基本を扱います。パッケージと import から始めて、変数と型、null 安全、値を返す if や when を経て、範囲とループまで順に進みます。読み終えると、文字列で届いた数量を数値に直す処理や、金額で送料を分ける処理を書けるようになります。数量が届かなかったときのように値が無い場合も、コンパイラの検査を受けながら扱えます。

コード例の多くは main を省いて文だけを載せています。はじめに のとおり、fun main() { } の中に書けば動きます。

パッケージと import​

Kotlin のファイルは、先頭の package 行で自分の属するパッケージを宣言します。パッケージは宣言をまとめる名前の空間で、同じ名前の関数やクラスでも、パッケージが違えば別のものとして扱われます。次のファイルは、税込みの価格を計算する関数を com.example.shop.catalog に置きます。

package com.example.shop.catalog

fun priceWithTax(price: Int): Int {
return price + price / 10
}

別のパッケージにある宣言を使うときは、import 行で完全な名前を書きます。

package com.example.shop.order

import com.example.shop.catalog.priceWithTax

fun main() {
println(priceWithTax(1200))
}
実行結果
1320

import を書かずに com.example.shop.catalog.priceWithTax(1200) と完全な名前で呼んでも動きます。同じ名前の宣言を 2 つ使うときは、import com.example.shop.catalog.priceWithTax as taxIncluded のように as で別名を付けられます。package 行を省いたファイルは、名前の無いパッケージに属します。

println や、この章で使う listOf と mapOf は import なしで使えます。kotlin や kotlin.collections や kotlin.text などのパッケージは、すべてのファイルに自動で import されるためです。自動で import されないパッケージもあり、たとえば kotlin.math の max を使うには import kotlin.math.max が要ります。

パッケージの名前とディレクトリの構成は、一致させなくてもコンパイルできます。ただし、一致させておくとファイルを探しやすいので、第 2 部のアプリケーションではそろえます。

val と var​

変数は val か var で宣言します。val は一度だけ代入できる変数で、var は何度でも代入し直せる変数です。

val orderNumber = 1001
var quantity = 1
quantity = quantity + 2
quantity += 1
println(orderNumber)
println(quantity)
実行結果
1001
4

+= は「右辺を足して代入し直す」の短い書き方です。-= や *= も同じ形で使えます。val に代入し直そうとすると、コンパイルが通りません。

val orderNumber = 1001
orderNumber = 1002
コンパイルエラー
'val' cannot be reassigned.

迷ったら val で宣言し、代入し直す必要が出たときだけ var に変えてください。値が途中で変わらないと分かっている変数は、読むときに追う範囲が狭くなります。

val が固定するのは「変数がどの値を指すか」で、指している値の中身ではありません。val productCodes = mutableListOf("P-001") のように中身を変えられるリストを val に入れると、productCodes.add("P-002") で要素を足せます。中身を変えられるリストと変えられないリストの違いは、コレクションとジェネリクス で扱います。

const val​

値がコンパイルの時点で決まる定数は、const val で宣言できます。const val はファイルの最上位か object の中にだけ書け、型は数値や String などの基本型に限られます。

const val MAX_LINES_PER_ORDER = 100

fun main() {
val lineCount = 120
println(MAX_LINES_PER_ORDER)
println(lineCount > MAX_LINES_PER_ORDER)
}
実行結果
100
true

const val の名前は、大文字と _ で書く慣習があります。関数の呼び出しの結果のように、実行してみないと決まらない値には const を付けられません。その場合は普通の val を使います。

基本型と型推論​

Kotlin の変数には必ず型があります。ただし、初期値から型が決まるときは、型を書かなくてもコンパイラが決めます。これを型推論と呼びます。型を明示するときは、変数名の後ろに : String のように書きます。

val quantity = 3
val orderNumber = 10_000_000_000
val weightKg = 2.5
val discountRate = 0.1f
val grade = 'A'
val isAvailable = true
val productName: String = "ノート"
println(quantity::class.simpleName)
println(orderNumber::class.simpleName)
println(weightKg::class.simpleName)
println(discountRate::class.simpleName)
println(grade::class.simpleName)
println(isAvailable::class.simpleName)
println(productName::class.simpleName)
実行結果
Int
Long
Double
Float
Char
Boolean
String

::class.simpleName は、値の実際の型の名前を返します。実行結果は上から順に、7 つの変数の型です。

  • 整数の文字は Int になり、Int に収まらない大きさなら Long になります。1200L のように L を付けると、小さな値でも Long になります。より小さな整数の型として Byte と Short もあります
  • 小数点を含む文字は Double になり、f を付けると Float になります
  • 数字の途中の _ は読みやすくするための区切りで、値には影響しません
  • Char は 1 文字で、シングルクォートで囲みます。String はダブルクォートで囲みます

数値の変換​

数値の型は、代入のときに自動では変わりません。Int の値を Long の変数に入れるには、toLong() で変換します。

val unitPrice = 1_200
val quantity = 3
val total: Long = (unitPrice * quantity).toLong()
println(total)
println(total.toDouble() / quantity)
println(7 / 2)
println(7 % 2)
println(7 / 2.0)
実行結果
3600
1200.0
3
1
3.5

整数どうしの / は、小数点以下を切り捨てた整数を返します (実行結果の 3 行目)。% は割り算の余りです。片方が Double なら、計算は Double で行われます (実行結果の 2 行目と 5 行目)。変換の関数は toInt()、toLong()、toDouble() などがあり、どの数値の型にもそろっています。

変換を書かずに Int の計算結果を Long の変数に入れると、コンパイルが通りません。

val unitPrice = 1_200
val quantity = 3
val total: Long = unitPrice * quantity
コンパイルエラー
Initializer type mismatch: expected 'Long', actual 'Int'.

Int の値は Long の範囲にすべて収まりますが、Kotlin は型が違えば変換を求めます。どこで型が変わるかが、コードに必ず書かれることになります。

文字列​

文字列テンプレート​

文字列の中に $ と変数名を書くと、その変数の値が埋め込まれます。これを文字列テンプレートと呼びます。式を埋め込むときや、変数名のすぐ後ろに文字が続くときは ${ } で囲みます。

val productName = "ノート"
val unitPrice = 180
val quantity = 3
println("商品: $productName")
println("${productName}を${quantity}冊")
println("合計: ${unitPrice * quantity} 円")
println("商品名は \"$productName\"")
println("\$productName は変数の名前")
実行結果
商品: ノート
ノートを3冊
合計: 540 円
商品名は "ノート"
$productName は変数の名前

2 行目の ${productName}を は、波括弧を外すと $productNameを が 1 つの変数名として読まれ、コンパイルが通りません。文字列の中でダブルクォートや $ そのものを書くときは、前に \ を付けます。\n (改行) や \t (タブ) も同じ書き方です。

raw 文字列​

ダブルクォート 3 つで囲んだ文字列を raw 文字列と呼びます。raw 文字列の中では \ が特別な意味を持たず、改行もそのまま文字列に入ります。

val orderNumber = 1001
val message = """
注文番号: $orderNumber
保存先: C:\shop\orders
""".trimIndent()
println(message)
実行結果
注文番号: 1001
保存先: C:\shop\orders

trimIndent() は、各行に共通する字下げと、先頭と末尾の空行を取り除きます。コードの字下げに合わせて書いた複数行の文字列を、字下げなしの文字列にするときに使います。raw 文字列の中でもテンプレートは使えます。

ドル記号を文字のまま含む文字列​

raw 文字列の中では \$ が使えません。JSON のように $ を多く含む文字列を書くときは、文字列の前に $$ を付けます。すると、$ が 2 つ続いたときだけテンプレートになり、1 つの $ は文字のまま残ります。

val minPrice = 1000
val filter = $$"""{"price": {"$gte": $$minPrice}}"""
println(filter)
実行結果
{"price": {"$gte": 1000}}

この書き方は Kotlin 2.2 で正式な機能になりました。前に付ける $ の数が、テンプレートとして読む $ の数になります。

等価性​

Kotlin には 2 種類の「等しい」があります。== は値が等しいかを比べ、=== は 2 つが同じオブジェクトかを比べます。否定はそれぞれ != と !== です。

val code = "P-001"
val built = StringBuilder("P-").append("001").toString()
println(code == built)
println(code === built)
println(code != built)
実行結果
true
false
false

built は文字を組み立てて作った別の文字列のオブジェクトです。中身の文字が同じなので == では等しく、オブジェクトが別なので === では等しくありません。値を比べたい場面のほとんどでは == を使います。

== は、内部で equals という関数を呼びます。自分で定義したクラスは、equals を書かない限り、同じオブジェクトどうしのときだけ等しくなります。

class Product(val code: String)

fun main() {
val first = Product("P-001")
val second = Product("P-001")
val same = first
println(first == second)
println(first === same)
}
実行結果
false
true

first と second は同じ商品コードを持っていますが、== で等しくなりません (実行結果の 1 行目)。プロパティの値が同じなら等しいとするクラスは、クラスとオブジェクト で扱う data class で書けます。

null 安全​

null を許す型​

Kotlin の型は、そのままでは null を入れられません。null を入れられる型にするには、型名の後ろに ? を付けます。Int? は「Int の値か、null」を表す型です。たとえば文字列の toIntOrNull() は、整数として読めればその値を、読めなければ null を返す関数で、戻り値の型は Int? です。呼んだ側は、null が返りうることを型から知れます。

null を許す型の値に対しては、メンバーを直接呼べません。

val productNames = mapOf("P-001" to "ボールペン", "P-002" to "ノート")
val name: String? = productNames["P-001"]
println(name.length)
コンパイルエラー
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.

mapOf はキーと値の組を並べて作る読み取り専用の対応表で、productNames["P-001"] はキーに対応する値を返します。キーが無ければ null を返すので、戻り値の型は String? です。null に対して length を呼ぶ誤りを、Kotlin はコンパイルの時点で止めます。止められた呼び出しを書き直す方法は、次の 4 つです。

安全呼び出し​

?. は安全呼び出しと呼ばれ、左辺が null でなければメンバーを呼び、null なら呼ばずに式全体を null にします。

val productNames = mapOf("P-001" to "ボールペン", "P-002" to "ノート")
val found: String? = productNames["P-002"]
val missing: String? = productNames["P-999"]
println(found?.length)
println(missing?.length)
println(missing?.trim()?.length)
実行結果
3
null
null

安全呼び出しの結果は、呼んだメンバーの型に ? が付いた型になります。found?.length の型は Int? です。3 行目のように ?. をつなげると、途中で null になった時点で残りの呼び出しは行われず、全体が null になります。

エルビス演算子​

?: はエルビス演算子と呼ばれ、左辺が null のときに右辺の値を使います。null だったときの値を決めて、null を許さない型に戻すときに使います。

val productNames = mapOf("P-001" to "ボールペン", "P-002" to "ノート")
val name = productNames["P-999"] ?: "不明な商品"
val quantity = "三".toIntOrNull() ?: 1
println(name)
println(quantity)
実行結果
不明な商品
1

name の型は String? でなく String になります。右辺には値のほか、return や throw も書けます。この使い方は後の「Nothing」の節で扱います。

非 null 表明​

!! は、値が null でないと書き手が表明して、null を許さない型として扱う演算子です。

val unitPrice: Int? = "1200".toIntOrNull()
val total = unitPrice!! * 2
println(total)
実行結果
2400

値が実際に null だったときは、!! の位置で NullPointerException という例外が出て、処理が止まります。!! はコンパイラの検査を外す書き方なので、null にならない理由を説明できるときにだけ使ってください。多くの場合は、?. か ?: か、次のスマートキャストで書き直せます。

スマートキャスト​

if で null でないことを確かめると、その中では変数を null を許さない型として扱えます。コンパイラが条件から型を絞り込むこの仕組みを、スマートキャストと呼びます。

val quantity: Int? = "5".toIntOrNull()
if (quantity != null) {
println(quantity * 2)
}
if (quantity != null && quantity >= 5) {
println("5 個以上の注文")
}
実行結果
10
5 個以上の注文

1 つ目の if の中では、quantity の型が Int として扱われるので * 2 が書けます。2 つ目のように && の右側でも、左側で確かめた結果が使われます。スマートキャストが働くのは、確かめてから使うまでのあいだに値が変わらないとコンパイラが判断できる場合です。委譲 (クラスとオブジェクト の by lazy など) を使わないローカルの val は、この条件を満たします。

Any と Unit と Nothing​

Any​

Any は、null を除くすべての型の親になる型です。Int も String も自分で定義したクラスも Any の一種なので、Any 型の変数にはどの値も入ります。null まで含めるときは Any? と書きます。Any が持つメンバーは equals、hashCode、toString の 3 つです。

Any 型の値は、実際の型が分からないので、そのままでは Int や String としては使えません。実際の型を確かめてから使う方法は、次の「型の判定と変換」で扱います。

Unit​

返す値の無い関数の戻り値の型は Unit です。戻り値の型を書かずに { } の本体で定義した関数は、戻り値の型が Unit になります。

fun printInventory(productCode: String, quantity: Int) {
println("$productCode の在庫: $quantity")
}

fun main() {
val result = printInventory("P-001", 12)
println(result)
}
実行結果
P-001 の在庫: 12
kotlin.Unit

Unit は値を 1 つだけ持つ型で、関数は何も返さないときに、その唯一の値を返します。そのため printInventory の結果も変数に入れられ、出力すると実行結果の 2 行目の文字列になります。関数の書き方は 関数とラムダ で扱います。

Nothing​

Nothing は値を 1 つも持たない型です。戻り値の型が Nothing の関数は、正常に戻ることがありません。必ず例外を投げるか、終わらない関数がこの型になります。

fun rejectOrder(reason: String): Nothing {
throw IllegalArgumentException(reason)
}

fun parseQuantity(input: String): Int {
val quantity = input.toIntOrNull() ?: rejectOrder("数量が数字でない: $input")
return quantity
}

fun main() {
println(parseQuantity("3"))
try {
parseQuantity("三")
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
実行結果
3
数量が数字でない: 三

throw は例外を投げる式で、その型は Nothing です。Nothing はどの型としても扱えるので、エルビス演算子の右辺に置いても、quantity の型は Int のままです。try と catch はこの章の後半で、例外そのものは 例外と失敗の表し方 で扱います。標準ライブラリの TODO() も戻り値の型が Nothing で、まだ書いていない処理の場所に置くと、呼ばれたときに例外を投げます。

型の判定と変換​

is で型を判定する​

is は、値が指定した型かどうかを判定します。!is はその否定です。is で確かめた後は、null の確認と同じようにスマートキャストが働きます。

fun toQuantity(value: Any): Int {
if (value is Int) {
return value
}
if (value !is String) {
return 0
}
return value.toIntOrNull() ?: 0
}

fun main() {
println(toQuantity(3))
println(toQuantity("4"))
println(toQuantity(2.5))
}
実行結果
3
4
0

1 つ目の if の中では、value が Int として扱われるので、そのまま Int の戻り値として返せます。2 つ目の if は「String でなければ戻る」と書いているので、その後ろでは value が String として扱われ、toIntOrNull() を呼べます。

as と as? で変換する​

as は、値を指定した型として扱うキャストの演算子です。値が実際にその型でなければ例外が出ます。as? は、その型でなければ例外の代わりに null を返します。

val attributes: Map<String, Any> = mapOf("code" to "P-001", "inventory" to 12)
val code = attributes["code"] as String
val inventory = attributes["inventory"] as? Int
val weight = attributes["weight"] as? Double
val wrong = attributes["code"] as? Int
println(code.length)
println(inventory)
println(weight)
println(wrong)
実行結果
5
12
null
null

Map<String, Any> は、キーが String で値がどの型でもよい対応表の型です。値の型が Any なので、取り出した値を使うにはキャストが要ります。キーが無いときと値の型が違うときに as? が何を返すかは、実行結果の 3 行目と 4 行目のとおりです。実際の型が分からない値には as? を使い、?: と組み合わせて null だったときの値を決めると、例外を出さずに書けます。

式としての if と when と try​

Kotlin の if、when、try は式で、値を返します。変数の初期値に直接書けるので、「条件によって違う値を入れる」処理を 1 つの val で書けます。

if​

val total = 4_800
val shippingFee = if (total >= 5_000) 0 else 600
println(shippingFee)

val discount = if (total >= 3_000) {
println("3,000 円以上なので割り引く")
total / 10
} else {
0
}
println(discount)
実行結果
600
3,000 円以上なので割り引く
480

波括弧で囲んだ分岐では、最後の式がその分岐の値になります。式として値を使うときは else が必要です。else が無いと、条件を満たさなかったときの値が決まらないためです。値を使わず文として書くときは、else を省けます。

when​

when は、1 つの値を複数の条件と順に比べ、最初に当てはまった分岐の値を返します。

val status = "SHIPPED"
val label = when (status) {
"PENDING" -> "受付済み"
"SHIPPED", "DELIVERED" -> "出荷済み"
else -> "不明"
}
println(label)

val quantity = 25
val size = when (quantity) {
in 1..9 -> "小口"
in 10..99 -> "中口"
else -> "大口"
}
println(size)

val total = 4_800
val shippingFee = when {
total >= 5_000 -> 0
quantity >= 20 -> 300
else -> 600
}
println(shippingFee)
実行結果
出荷済み
中口
300

1 つの分岐に , で複数の値を並べると、どれかに等しいときに当てはまります。in は値が範囲に入っているかを判定します (範囲は後の節で扱います)。3 つ目のように when の後ろの値を省くと、各分岐に条件式を書けます。上から順に判定するので、複数の条件を満たす値は、先に書いた分岐に当てはまります。

when を式として使うときは、すべての場合を網羅する必要があり、多くの場合 else を書きます。else が要らない場合については、クラスとオブジェクト の sealed class で扱います。

when で型を分ける​

when の分岐には is も書けます。分岐の中では、その型へのスマートキャストが働きます。さらに、is の後ろに if で条件を足すと、型と条件の両方を満たすときだけ当てはまる分岐になります。この条件を guard 条件と呼び、Kotlin 2.2 で正式な機能になりました。

fun toQuantity(value: Any): Int {
return when (value) {
is Int if value > 0 -> value
is String -> value.trim().toIntOrNull() ?: 0
else -> 0
}
}

fun main() {
println(toQuantity(4))
println(toQuantity(-2))
println(toQuantity(" 7 "))
println(toQuantity("七"))
}
実行結果
4
0
7
0

負の整数は is Int に当てはまりますが、guard 条件を満たさないので、次の分岐へ進みます。String の分岐にも当てはまらないので、else の値になります (実行結果の 2 行目)。guard 条件の中でも、value は Int として扱われます。

try​

try も式で、例外が出なければ try の本体の最後の式が、例外を捕まえたらその catch の最後の式が、try 全体の値になります。

val input = "12個"
val quantity = try {
input.toInt()
} catch (e: NumberFormatException) {
0
} finally {
println("数量の読み取りを終えた")
}
println(quantity)
実行結果
数量の読み取りを終えた
0

toInt() は、整数として読めない文字列に対して NumberFormatException を投げます。finally の本体は、例外の有無にかかわらず最後に実行されますが、try の値には影響しません。そのため実行結果では、finally の出力が先に出て、quantity の値が後に出ます。

!! が null に当たったときの NullPointerException や、as の型が違うときの ClassCastException も、同じ形で捕まえられます。ただ、それらを try で受け止めるより、?: や as? で null の場合を書くほうが、何が起きうるかをコードから読み取れます。

範囲とループ​

範囲​

1..5 のように 2 つの値を .. でつなぐと、両端を含む範囲になります。

println((1..5).toList())
println((1..<5).toList())
println((5 downTo 1).toList())
println((0..10 step 5).toList())
println(3 in 1..5)
println(7 !in 1..5)
実行結果
[1, 2, 3, 4, 5]
[1, 2, 3, 4]
[5, 4, 3, 2, 1]
[0, 5, 10]
true
true

toList() は範囲の要素をリストにする関数で、ここでは中身を見るために使っています。

  • ..< は終わりの値を含まない範囲です。1 until 5 と書いても同じ範囲になります
  • downTo は大きい値から小さい値へ下る範囲です
  • step は要素の間隔を指定します
  • in と !in は、値が範囲に入っているかを判定します

for​

for は、範囲やリストの要素を 1 つずつ取り出して繰り返します。

val productCodes = listOf("P-001", "P-002", "P-003")
for (code in productCodes) {
println(code)
}
for (index in productCodes.indices) {
println("${index + 1} 行目: ${productCodes[index]}")
}
実行結果
P-001
P-002
P-003
1 行目: P-001
2 行目: P-002
3 行目: P-003

listOf は読み取り専用のリストを作る関数です。indices は、リストの添字 (先頭を 0 とする位置) の範囲を返します。添字が要るときはこの範囲で繰り返し、productCodes[index] で要素を取り出します。

while と do-while​

while は、条件を満たすあいだ本体を繰り返します。do-while は、本体を実行してから条件を判定するので、本体が少なくとも 1 回は実行されます。

var inventory = 10
var shipped = 0
while (inventory >= 3) {
inventory -= 3
shipped++
}
println(shipped)
println(inventory)

var attempts = 5
do {
attempts++
} while (attempts < 3)
println(attempts)
実行結果
3
1
6

shipped++ は shipped に 1 を足す書き方です。do-while の例では、条件が最初から満たされていません。それでも本体が 1 回実行されたことが、実行結果の 3 行目から分かります。

break と continue​

continue はその回の残りを飛ばして次の回へ進み、break は繰り返しを終えます。

for (quantity in 1..10) {
if (quantity % 2 == 0) continue
if (quantity > 5) break
println(quantity)
}
実行結果
1
3
5

ラベルの無い break と continue は、いちばん内側の繰り返しに対して働きます。入れ子の繰り返しで外側を終えたいときは、外側の for に search@ for (...) のようにラベルを付け、内側で break@search と書きます。continue@search と書けば、外側の次の回へ進みます。

まとめ​

  • 宣言は package でパッケージに置き、別のパッケージの宣言は import で使います
  • 変数は val で宣言し、代入し直す必要があるときだけ var にします。コンパイルの時点で決まる定数は const val です
  • 型は初期値から推論されます。数値の型は自動では変わらないので、toLong() などで変換します
  • 文字列には $ と ${ } で値を埋め込めます。raw 文字列は \ と改行をそのまま含みます
  • == は値が等しいか、=== は同じオブジェクトかを比べます
  • null を入れられるのは ? を付けた型だけです。?.、?:、スマートキャストで null の場合を書き、!! は理由を説明できるときに限ります
  • Any はすべての型の親、Unit は返す値の無い関数の戻り値の型、Nothing は戻らない関数の型です
  • is で型を判定するとスマートキャストが働き、as? は型が違うときに null を返します
  • if、when、try は値を返す式です。when の分岐には guard 条件を付けられます
  • 範囲は ..、..<、downTo、step で作り、for で繰り返します

次に読む​

次章 関数とラムダ では、この章の例で使った fun を詳しく扱います。デフォルト引数と名前付き引数、関数を値として渡すラムダ、既存の型に関数を足す拡張関数を順に見ていきます。

練習問題​

次の関数は、単価が null のときや、数量が数字でないときに例外で止まります。!! と toInt() を使わずに、どちらの場合も 0 を返すように書き直してください
fun lineTotal(unitPrice: Int?, quantityText: String): Int {
return unitPrice!! * quantityText.toInt()
}

解答例

fun lineTotal(unitPrice: Int?, quantityText: String): Int {
val quantity = quantityText.toIntOrNull() ?: return 0
return (unitPrice ?: 0) * quantity
}

fun main() {
println(lineTotal(1200, "3"))
println(lineTotal(null, "3"))
println(lineTotal(1200, "三"))
}
実行結果
3600
0
0

数量は toIntOrNull() で読み、読めなかったときはエルビス演算子の右辺の return 0 で関数を終えます。return も throw と同じく型が Nothing の式なので、quantity の型は Int になります。単価は ?: 0 で null の場合の値を決めます。

元の関数では、null と読めない文字列のどちらでも例外が出て、呼んだ側が捕まえない限り処理が止まります。書き直した関数は、どの入力でも整数を返します。戻り値の型を Int? にして、失敗したときに null を返す書き方もあります。0 と失敗を区別したい場合はそちらを選びます。

注文の数量から割引率 (%) を返す関数 discountPercent を、when で書いてください。数量が 0 以下なら例外を投げ、10 未満は 0、50 未満は 5、それ以上は 10 を返します

解答例

fun discountPercent(quantity: Int): Int {
return when {
quantity <= 0 -> throw IllegalArgumentException("数量は 1 以上にしてください: $quantity")
quantity < 10 -> 0
quantity < 50 -> 5
else -> 10
}
}

fun main() {
for (quantity in listOf(1, 10, 50)) {
println(discountPercent(quantity))
}
try {
discountPercent(0)
} catch (e: IllegalArgumentException) {
println(e.message)
}
}
実行結果
0
5
10
数量は 1 以上にしてください: 0

値を省いた when は上から順に判定するので、quantity < 50 の分岐に来た時点で、数量が 10 以上であることは決まっています。そのため条件を quantity in 10..49 のように両端で書かなくても済みます。

throw の型は Nothing なので、Int を返す分岐と並べても when 全体の型は Int のままです。範囲を境目の値 (1、10、50) で試すと、< と <= の取り違えに気づけます。前提条件を確かめる require は、例外と失敗の表し方 で扱います。