変数・型・null 安全
基礎val は再代入不可(Java の final に相当)、var は再代入可。型は右辺から推論されるため省略できる。
val name: String = "Kotlin" // 明示的型指定
val version = 2 // 推論 → Int
var count = 0
count = 1 // OK
// name = "Java" // コンパイルエラー(val は再代入不可)型に ? を付けると nullable になる。?.(safe call)、?:(Elvis 演算子)、!!(non-null assertion)を使い null を安全に扱う。
var s: String? = null // nullable
val len = s?.length // null → null(NPE 発生せず)
val len2 = s?.length ?: 0 // null → 0
val len3 = s!!.length // null なら NullPointerException!! は null でないことが確実な場合のみ使用すること。乱用すると NPE を招く。is で型チェックした後、コンパイラが自動的にその型へキャストする。明示的な as キャストが不要になる。
fun describe(obj: Any) {
if (obj is String) {
println(obj.length) // obj は自動的に String にキャスト
}
if (obj !is Int) return
println(obj + 1) // obj は Int にキャスト済み
}Kotlin に Java のような primitive 型はなく、すべてクラス(Int・Long・Double・Boolean 等)として扱う。JVM バイトコードでは可能な限りプリミティブに最適化される。
val i: Int = 42
val l: Long = 100L
val d: Double = 3.14
val b: Boolean = true
val c: Char = 'A'
// 数値リテラルの区切り
val million = 1_000_000制御フロー
基礎Kotlin の if は**式**(値を返す)。三項演算子 ? : の代わりに使える。
val max = if (a > b) a else b
val grade = if (score >= 90) "A"
else if (score >= 70) "B"
else "C"when は Java の switch の強化版。値だけでなく、型チェック・範囲・条件式などを分岐できる。式として値を返せる。
val result = when (x) {
0 -> "zero"
in 1..9 -> "single digit"
is String -> "string"
else -> "other"
}
// 引数なし when
when {
x < 0 -> println("negative")
x == 0 -> println("zero")
else -> println("positive")
}for は in でコレクションや範囲を反復。.. で閉区間、until で半開区間、step でステップ幅を指定できる。
for (i in 1..5) print(i) // 1 2 3 4 5
for (i in 1 until 5) print(i) // 1 2 3 4
for (i in 5 downTo 1 step 2) print(i) // 5 3 1
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
println("${index}: ${value}")
}
var n = 3
while (n > 0) n--ラムダ内から外側の関数へ return するには return@ラベル名 を使う。break・continue も同様にラベルで対象ループを指定できる。
fun findFirst(list: List<Int>, target: Int) {
list.forEach { n ->
if (n == target) return@forEach // ラムダから抜ける(次要素へ)
println(n)
}
}
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) break@outer // 外側 for を break
}
}関数
基礎引数にデフォルト値を設定でき、Java のようなオーバーロードを減らせる。呼び出し時は引数名を指定して渡せる(順序不問)。
fun greet(name: String, greeting: String = "Hello") =
"${greeting}, ${name}!"
println(greet("Alice")) // Hello, Alice!
println(greet("Bob", "Hi")) // Hi, Bob!
println(greet(greeting = "Hey", name = "Carol")) // Hey, Carol!関数本体が一つの式の場合、= 式 の形で書ける。戻り値型も推論される。
fun double(x: Int): Int = x * 2
fun max(a: Int, b: Int) = if (a > b) a else b // 戻り値型を推論
fun isEven(n: Int) = n % 2 == 0infix を付けた単一引数のメンバー関数または拡張関数は、中置記法(スペース区切り)で呼べる。mapOf の to などが好例。
infix fun Int.pow(exp: Int): Int =
Math.pow(toDouble(), exp.toDouble()).toInt()
val result = 2 pow 10 // 1024(2.pow(10) と同等)
// 標準ライブラリの infix 例
val pair = "key" to "value" // Pair<String, String>tailrec を付けると末尾再帰をループに最適化してくれる。スタックオーバーフローを防ぎつつ再帰的な記述ができる。
tailrec fun factorial(n: Long, acc: Long = 1): Long =
if (n <= 1) acc else factorial(n - 1, acc * n)
println(factorial(100_000)) // スタックオーバーフローなしクラスとオブジェクト
基礎data class は equals・hashCode・toString・copy・componentN を自動生成。イミュータブルな値オブジェクトに最適。
data class Point(val x: Int, val y: Int)
val p1 = Point(1, 2)
val p2 = p1.copy(y = 3) // Point(1, 3)
println(p1 == p2) // false(値比較)
val (x, y) = p1 // 構造分解宣言object 宣言でシングルトンを作成。companion object はクラス内に定義し、Java の static メンバーに相当する。
object AppConfig {
val version = "1.0"
fun reset() { /* ... */ }
}
class MyClass {
companion object {
const val TAG = "MyClass"
fun create() = MyClass()
}
}
AppConfig.version
MyClass.TAG
MyClass.create()sealed class はサブクラスを同一ファイル(または同一パッケージ)に限定する。when と組み合わせると網羅チェックが効いて安全な分岐が書ける。
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun handle(r: Result<String>) = when (r) {
is Result.Success -> println(r.data)
is Result.Failure -> println(r.error)
Result.Loading -> println("loading...")
// else 不要(網羅済み)
}クラスヘッダーに書く primary コンストラクタと、本体内に定義する secondary コンストラクタがある。init ブロックで初期化ロジックを記述できる。
class Person(val name: String, var age: Int) { // primary
init {
require(age >= 0) { "age must be >= 0" }
}
constructor(name: String) : this(name, 0) // secondary
}拡張関数・プロパティ
基礎既存クラスのソースを変更せずにメソッドを追加できる。receiver型.関数名 で定義し、this でレシーバーを参照する。
fun String.isPalindrome(): Boolean = this == this.reversed()
println("racecar".isPalindrome()) // true
println("kotlin".isPalindrome()) // false
fun Int.isEven() = this % 2 == 0
println(4.isEven()) // true既存クラスにプロパティを追加できる。バッキングフィールドを持てないため get() と必要なら set() を実装する。
val String.wordCount: Int
get() = trim().split("\s+".toRegex()).size
println("hello world foo".wordCount) // 3let・run・apply・also・with はラムダをレシーバーや引数として渡す高階関数。オブジェクトの設定・変換・null チェックを簡潔に書ける。
// let: null チェック+変換(it でアクセス、返り値=ラムダ結果)
val len = str?.let { it.length }
// apply: レシーバー返却、初期化に便利(this でアクセス)
val sb = StringBuilder().apply {
append("Hello")
append(", World")
}
// also: 副作用(ログ等)、オブジェクト自体を返す
val nums = mutableListOf(1, 2, 3).also { println("list: $it") }
// run: 結果を返す(this でアクセス)
val result = " kotlin ".run { trim().uppercase() }
// with: 拡張関数ではない(引数でオブジェクトを渡す)
val s = with(StringBuilder()) { append("a"); append("b"); toString() }拡張関数はコンパイル時に静的ディスパッチされる。サブクラスで override はできず、メンバー関数と同名の場合はメンバー関数が優先される。
open class Animal
class Dog : Animal()
fun Animal.speak() = "..."
fun Dog.speak() = "Woof"
val d: Animal = Dog()
println(d.speak()) // "..."(静的ディスパッチ:宣言型 Animal で解決)インターフェース・継承
基礎Kotlin の interface はデフォルト実装(メソッド本体)を持てる。プロパティも宣言でき、実装クラスで override する。
interface Greeter {
val greeting: String
fun greet(name: String) = "${greeting}, ${name}!" // デフォルト実装
}
class FormalGreeter : Greeter {
override val greeting = "Good day"
}
println(FormalGreeter().greet("Alice")) // Good day, Alice!Kotlin のクラスはデフォルト final。継承を許可するには open を付ける。メソッドも同様に open が必要。override したメンバーも暗黙的に open になる。
open class Shape {
open fun area(): Double = 0.0
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
}
// さらに継承させたくない場合
// final override fun area() = ...abstract クラスはインスタンス化不可。抽象メンバーは実装クラスで必ず override する。インターフェースと異なりフィールドを持てる。
abstract class Animal(val name: String) {
abstract fun sound(): String
fun describe() = "${name} says ${sound()}"
}
class Cat(name: String) : Animal(name) {
override fun sound() = "Meow"
}
println(Cat("Whiskers").describe()) // Whiskers says Meowby キーワードでインターフェース実装を別のオブジェクトに委譲できる。コンポジションをボイラープレートなしに記述できる。
interface Logger {
fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) = println("[LOG] ${msg}")
}
class Service(logger: Logger) : Logger by logger {
fun run() { log("service started") }
}
Service(ConsoleLogger()).run() // [LOG] service startedジェネリクス
基礎 で型パラメータに上限制約を設ける。複数制約は where 節で記述する。
fun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b
// 複数制約
fun <T> process(item: T) where T : Serializable, T : Comparable<T> {
// ...
}
println(max(3, 7)) // 7
println(max("apple", "banana")) // bananainline fun の型パラメータに reified を付けると、実行時に型情報を保持できる。is T チェックや T::class が使えるようになる。
inline fun <reified T> filterByType(list: List<Any>): List<T> =
list.filterIsInstance<T>()
inline fun <reified T> Any.isType() = this is T
val mixed = listOf(1, "a", 2, "b")
println(filterByType<String>(mixed)) // [a, b]
println(42.isType<Int>()) // trueKotlin は宣言サイト変性をサポート。out T(共変)は読み取り専用、in T(反変)は書き込み専用を表す。
// out: covariant — Producer<Dog> は Producer<Animal> として使える
interface Producer<out T> { fun produce(): T }
// in: contravariant — Consumer<Animal> は Consumer<Dog> として使える
interface Consumer<in T> { fun consume(item: T) }
// 使用サイト変性(プロジェクション)
fun copy(from: Array<out Any>, to: Array<Any>) {
from.forEachIndexed { i, v -> to[i] = v }
}ラムダ・高階関数
基礎ラムダは { 引数 -> 本体 } で記述。外側スコープの変数をキャプチャ(クロージャ)できる。最後の引数がラムダなら括弧の外に出せる(trailing lambda)。
val square: (Int) -> Int = { x -> x * x }
println(square(5)) // 25
// trailing lambda
listOf(1, 2, 3).forEach { println(it) }
// クロージャ
var counter = 0
val increment = { counter++ }
increment(); increment()
println(counter) // 2関数を引数や戻り値として扱う高階関数。filter・map・reduce・fold などが標準ライブラリに豊富に揃っている。
val nums = listOf(1, 2, 3, 4, 5)
val evens = nums.filter { it % 2 == 0 } // [2, 4]
val doubled = nums.map { it * 2 } // [2, 4, 6, 8, 10]
val sum = nums.reduce { acc, v -> acc + v } // 15
val product = nums.fold(1) { acc, v -> acc * v } // 120引数が一つのラムダは it で参照できる。ネストする場合は名前をつけて明示した方が読みやすい。
listOf("a", "bb", "ccc").filter { it.length > 1 } // [bb, ccc]
// ネスト時は it を使わず名前をつける
listOf(listOf(1, 2), listOf(3, 4)).flatMap { inner ->
inner.map { num -> num * 2 }
}inline fun はコンパイル時にラムダを呼び出し元にインライン展開し、関数オブジェクトの生成コストを削減する。
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
val ns = measure { repeat(1_000_000) { /* work */ } }
println("${ns}ns") // ラムダオブジェクト生成なしコレクション
基礎Kotlin はコレクションを読み取り専用(listOf)と変更可能(mutableListOf)に分けて扱う。インターフェースレベルで区別されている。
val immutable = listOf(1, 2, 3)
// immutable.add(4) // コンパイルエラー
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
val map = mapOf("a" to 1, "b" to 2)
val mutableMap = mutableMapOf("a" to 1)
mutableMap["c"] = 3Sequence は中間処理を遅延評価するため、大量データや無限列に効率的。asSequence() で変換できる。
// List: filter と map がそれぞれ全要素を処理(中間コレクション生成)
(1..1_000_000).toList().filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Sequence: 要素ごとに filter→map→take を通過(中間コレクション不要)
(1..1_000_000).asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(5)
.toList() // terminal: ここで初めて実行コレクションをグループ化・マップ変換・分割・結合する標準関数。
val words = listOf("apple", "banana", "apricot", "blueberry")
val byFirst = words.groupBy { it[0] }
// {a=[apple, apricot], b=[banana, blueberry]}
val lengths = words.associate { it to it.length }
// {apple=5, banana=6, ...}
val (aWords, bWords) = words.partition { it.startsWith("a") }
val zipped = listOf(1, 2, 3).zip(listOf("a", "b", "c"))
// [(1, a), (2, b), (3, c)]data class や Pair・Map.Entry は componentN() 関数により変数の構造分解ができる。
val (x, y) = Pair(10, 20)
println("${x}, ${y}") // 10, 20
data class Color(val r: Int, val g: Int, val b: Int)
val (r, g, b) = Color(255, 128, 0)
// Map エントリーを分解
for ((key, value) in mapOf("a" to 1, "b" to 2)) {
println("${key}=${value}")
}標準ライブラリ・文字列
基礎バックティック文字列ではなく通常の文字列内で ${式} を使って値を埋め込める。単純な変数は $変数名 でも書ける。
val name = "Kotlin"
val version = 2
println("Hello, ${name} ${version}!") // Hello, Kotlin 2!
println("Length: ${name.length}") // Length: 6
println("Is $name great? ${true}") // Is Kotlin great? true"""...""" で raw 文字列(エスケープ不要)を書ける。trimIndent() や trimMargin() で共通インデントを除去できる。
val json = """
{
"name": "Kotlin",
"version": 2
}
""".trimIndent()
val text = """
|Line 1
|Line 2
""".trimMargin() // | をマージンプレフィックスとして除去buildString { } は StringBuilder を用いた文字列構築の DSL。可読性が高く、+ による文字列連結より効率的。
val result = buildString {
append("Hello")
append(", ")
append("World")
appendLine("!")
repeat(3) { append("*") }
}
println(result)
// Hello, World!
// ***Kotlin は文字列を操作する拡張関数を豊富に提供している。
val s = " Hello, Kotlin! "
println(s.trim()) // "Hello, Kotlin!"
println(s.trim().lowercase()) // "hello, kotlin!"
println("abc".padStart(5, '0')) // "00abc"
println("a,b,c".split(",")) // [a, b, c]
println("Hello".repeat(3)) // HelloHelloHello
println("kotlin".substringBefore("lin")) // "kot"ファイル・IO
基礎java.io.File に対する Kotlin 拡張関数で手軽にファイル I/O ができる。
import java.io.File
val content = File("input.txt").readText(Charsets.UTF_8)
File("output.txt").writeText("Hello, Kotlin!")
File("data.txt").forEachLine { line ->
println(line)
}
val lines: List<String> = File("data.txt").readLines()use { } は Java の try-with-resources に相当。ブロック終了時(例外発生時も)に close() が自動で呼ばれる。
import java.io.BufferedReader
import java.io.FileReader
BufferedReader(FileReader("input.txt")).use { reader ->
reader.lineSequence().forEach { println(it) }
} // ここで reader.close() が自動呼び出しkotlin.io パッケージはファイル操作の拡張関数を多数提供する。
import java.io.File
// ファイルコピー
File("src.txt").copyTo(File("dst.txt"), overwrite = true)
// ディレクトリ再帰削除
File("tmp_dir").deleteRecursively()
// ディレクトリウォーク
File("src").walk()
.filter { it.extension == "kt" }
.forEach { println(it.path) }型システム・キャスト
基礎as は unsafe キャスト(失敗で ClassCastException)、as? は safe キャスト(失敗で null)。
val obj: Any = "Hello"
val s1: String = obj as String // OK
val n1: Int? = obj as? Int // null(ClassCastException なし)
val s2: String = (obj as? String) ?: "fallback"Any はすべての型のスーパー型(Java の Object)。Unit は値を返さない関数の戻り値型(Java の void)。Nothing は「決して値を返さない」を表す(例外を投げる関数など)。
fun log(msg: Any) = println(msg) // Any は何でも受け取れる
fun fail(msg: String): Nothing =
throw IllegalStateException(msg) // Nothing: 値が返らない
fun doWork(): Unit { // Unit = void、省略可
println("working")
// return Unit は暗黙
}typealias で既存の型に別名を付けられる。複雑な関数型やジェネリクスを簡潔に表現するのに便利。
typealias StringList = List<String>
typealias ClickHandler = (x: Int, y: Int) -> Unit
fun processNames(names: StringList) { /* ... */ }
val onClick: ClickHandler = { x, y -> println("clicked $x, $y") }@JvmInline value class は単一プロパティをラップするが、JVM ではボクシングを排除して効率的に動作する。型安全なラッパーに使う。
@JvmInline
value class UserId(val id: Long)
@JvmInline
value class Email(val address: String)
fun sendEmail(to: Email, from: Email) { /* ... */ }
// コンパイル後は Long として展開され、オブジェクト生成なし
sendEmail(Email("a@example.com"), Email("b@example.com"))コルーチン基礎
応用suspend 関数はコルーチン内でのみ呼び出せる中断可能な関数。スレッドをブロックせず、他のコルーチンに処理を譲れる。
import kotlinx.coroutines.*
suspend fun fetchData(): String {
delay(1000) // スレッドをブロックせず 1 秒待機
return "data"
}
fun main() = runBlocking {
val result = fetchData()
println(result) // data
}launch は結果を返さない(Job)、async は結果を返す(Deferred)コルーチンビルダー。await() で結果を取得する。
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch: fire-and-forget
val job = launch {
delay(500)
println("launched!")
}
// async: 結果を返す
val deferred = async {
delay(300)
42
}
println("result: ${deferred.await()}") // result: 42
job.join()
}Dispatchers はコルーチンが実行されるスレッドプールを指定する。IO(I/O 処理)・Default(CPU 集中)・Main(UI スレッド)などがある。
import kotlinx.coroutines.*
import java.io.File
fun main() = runBlocking {
launch(Dispatchers.IO) {
val data = File("large.txt").readText() // I/O スレッドで実行
withContext(Dispatchers.Default) {
// CPU 負荷処理(計算用スレッドプール)
processData(data)
}
}
}コルーチンは親スコープの生存期間に縛られる。親がキャンセルされると子もキャンセルされる。これによりリークのない並行処理が書ける。
import kotlinx.coroutines.*
fun main() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
repeat(10) { i ->
delay(100)
println("tick ${i}")
}
}
delay(350)
scope.cancel() // scope 内のすべてのコルーチンをキャンセル
println("cancelled")
}Flow
応用Flow は非同期で複数の値を順次生成するコールドストリーム。flow { } ビルダーで定義し、collect { } で受け取る。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numberFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i) // 値を下流へ流す
}
}
fun main() = runBlocking {
numberFlow()
.map { it * 2 }
.filter { it > 4 }
.collect { value -> println(value) } // 6 8 10
}StateFlow は最新値を保持するホットストリーム(Android ViewModel 状態管理に最適)。SharedFlow は複数コレクターへのブロードキャストに使う。
import kotlinx.coroutines.flow.*
// StateFlow: 常に現在値を保持
class CounterViewModel {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() { _count.value++ }
}
// SharedFlow: バッファ付きブロードキャスト
val sharedFlow = MutableSharedFlow<String>(replay = 1)コールバックベースの API を Flow に変換するビルダー。trySend で値を送出し、awaitClose でクリーンアップを行う。
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
fun locationFlow(): Flow<Location> = callbackFlow {
val listener = LocationListener { location ->
trySend(location)
}
locationManager.register(listener)
awaitClose {
locationManager.unregister(listener) // コレクターが離れたら解除
}
}cold stream(Flow)はコレクターごとに独立して実行される。hot stream(StateFlow/SharedFlow)は独立して動作し、複数コレクターで共有される。
// cold: collect するたびに flow ブロックが再実行
val cold: Flow<Int> = flow { emit(Random.nextInt()) }
cold.collect { println(it) } // 毎回異なる値
// hot: StateFlow は共有された最新値を保持
val hot = MutableStateFlow(0)
// 複数コレクターが同じ値 (0) を受け取るDSL・演算子オーバーロード
応用operator 修飾子を付けた関数で演算子をオーバーロードできる。+→plus、-→minus、[]→get/set などが対応している。
data class Vector(val x: Double, val y: Double) {
operator fun plus(other: Vector) = Vector(x + other.x, y + other.y)
operator fun times(scale: Double) = Vector(x * scale, y * scale)
operator fun unaryMinus() = Vector(-x, -y)
}
val v1 = Vector(1.0, 2.0)
val v2 = Vector(3.0, 4.0)
println(v1 + v2) // Vector(4.0, 6.0)
println(v1 * 2.0) // Vector(2.0, 4.0)@DslMarker アノテーションを使うとネストした DSL スコープで誤った外側スコープの呼び出しをコンパイルエラーにできる。
@DslMarker annotation class HtmlDsl
@HtmlDsl class Div {
private val children = mutableListOf<String>()
fun p(text: String) { children.add("<p>${text}</p>") }
fun build() = "<div>${children.joinToString("")}</div>"
}
fun div(init: Div.() -> Unit): String = Div().apply(init).build()
val html = div {
p("Hello")
p("Kotlin DSL")
}
println(html) // <div><p>Hello</p><p>Kotlin DSL</p></div>buildList { }・buildMap { } はコレクションを DSL スタイルで構築するビルダー。内部は MutableList/MutableMap を使い、完了後にイミュータブルで返す。
val primes = buildList {
add(2)
addAll(listOf(3, 5, 7))
if (true) add(11)
} // List<Int>: [2, 3, 5, 7, 11]
val config = buildMap<String, Any> {
put("host", "localhost")
put("port", 8080)
putAll(mapOf("debug" to true))
}Kotlin Multiplatform・モジュール
応用expect で共通モジュールに API 宣言、actual でプラットフォーム固有の実装を提供する KMP の仕組み。
// commonMain
expect fun platformName(): String
// jvmMain
actual fun platformName(): String =
"JVM ${System.getProperty("java.version")}"
// jsMain
actual fun platformName(): String =
"Browser: ${js("navigator.userAgent")}"Java から Kotlin コードを自然に呼び出せるようにするアノテーション群。
class Config {
companion object {
@JvmStatic
fun create() = Config() // Java: Config.create()
@JvmField
val DEFAULT_TIMEOUT = 30_000 // Java: Config.DEFAULT_TIMEOUT(getter なし)
}
@JvmOverloads
fun connect(host: String, port: Int = 8080, ssl: Boolean = false) { /* ... */ }
// Java 側に connect(host), connect(host,port), connect(host,port,ssl) が生成される
}@Serializable アノテーションで JSON・CBOR などのシリアライズ/デシリアライズを自動生成。kotlinx.serialization ライブラリが必要。
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user) // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name) // AliceKSP はコンパイル時にアノテーションを処理しコードを生成するフレームワーク。kapt より高速で Kotlin を直接サポートしている。
// build.gradle.kts(KSP の設定例)
plugins {
id("com.google.devtools.ksp") version "2.0.0-1.0.21"
}
dependencies {
ksp("com.example:my-processor:1.0.0") // KSP プロセッサを追加
}
// 対象コード(プロセッサがアノテーションを処理してコードを生成)
@MyAnnotation
class Repository { /* ... */ }