Androidなんも分からん (Kotlin編)
初めに
Androidなんも分からんから今年こそは分かるようになりたい
正月なので新しいことを始めるやつ2019年版は、KotlinおよびAndroidです。よろしくおねがいします
— kageです。 (@kage_miku) January 1, 2019
この記事は分かるようになるまでの過程の自分用メモ。決して初学者用の便利まとめなんかではない。すまん。体裁を整えていないのでクソ長いです。ほんますまん。
とりあえずKotlinがなんも分からん。
環境準備
Kotlin公式のTutorialsに沿って、Kotlinの開発環境のセットアップをする。
方法はいろいろあるっぽいけど、一旦はVimでKotlin書きたいんで、brewでぶちこむ。
1 |
$ brew install kotlin |
vimのkotlin用プラグインをぶちこむ。パッとググって出てきたのはこれ
初めなんかハイライトしてくれなかったけど、kotlinのfiletypeの設定とか書いてガチャガチャしてたらできた。
いつものやつやる。(使ってるHighlighterがKotlinに対応してないっぽいけど、大丈夫なのかな)
1 2 3 |
fun main(args: Array<String>) { println("Hello, World!") } |
1 2 3 |
$ kotlinc hello.kt -include-runtime -d hello.jar $ java -jar hello.jar Hello, World! |
Kotlin完全に理解しました。
-include-runtime
ってオプションは、Kotlinのランタイムを.jarに埋め込むことで、それ単体で実行可能にしてくれるオプションらしい。他のKotlinプログラムから実行されるライブラリなどを作る際は、このオプションは外してしまって良いらしい。
kotlinc
とjava
の2種類叩くのややこしいな、と思ったらkotlin
コマンドというのもあるらしい
1 |
$ kotlin -classpath hello.jar HelloKt |
-classpath
でコンパイル対象にしたいjarファイル群を指定するっぽい。HelloKtは、もとのファイル名がhello.ktだった際に、Kotlinのコンパイラによって生成されるメインクラス名とのこと。なるほどね。分からん。
この辺のtermは、やっぱJavaに対する経験がほどよくあったほうが理解がはやそう。ちなみに僕にはない。
真面目にやる
とりあえず、公式のReferenceを読み進める
Getting Started
とりあえずこれからやろう
Basic Syntax
関数定義はScalaに近いっぽい。defではなくてfunだけど。
1 2 3 4 5 6 7 8 9 |
fun sum(a: Int, b: Int): Int { return a + b } fun sum(a: Int, b: Int) = a + b fun printSum(a: Int, b: Int): Unit { println("sum of $a and $b is ${a + b}") } |
2つめの宣言方法とか、いわゆるVoid型を返値とすることを表すUnitとか超Scalaっぽい。ScalaもJVM言語だもんね。というか、KotlinはScalaの影響も多大に受けた言語っぽいのでそれはそう。${a + b}で文字列内での評価結果展開ができるのもいっしょ。
valがimmutable変数宣言で、varがmutable変数宣言なのも同じ。
1 2 3 |
val a: Int = 1 var b: 2 b += 1 |
Optional型というか、nullable変数はInt?のように表す。このSyntaxはモダンなものに近い。
1 2 3 |
fun parseInt(str: String): Int? { // ... } |
1 2 3 4 5 6 7 8 |
fun getStringLength(obj: Any): Int? { // `obj` is automatically cast to `String` on the right-hand side of `&&` if (obj is String && obj.length > 0) { return obj.length } return null } |
これ驚いた。Swiftとかだとif-let内でas? String
とかしてString型として束縛するところを、Kotlinだとis使った分岐内では自動でその型にキャストしてくれるらしい。isの否定は!isなのはしょうがないけど微妙感ある。
同じことをするifブロックをSwiftで書くとこう。
1 2 3 |
if let str = obj as? String, str.count > 0 { return str.count } |
Kotlinの方が若干コンパクト。
when式はScalaでいうmatch式。switch文ぽいものが式になってるのもScalaに同じ。
1 2 3 4 5 6 7 8 |
fun describe(obj: Any): String = when (obj) { 1 -> "One" "Hello" -> "Greeting" is Long -> "Long" !is String -> "Not a string" else -> "Unknown" } |
Idioms
急にCreating DTOs (POJOs/POCOs)とか出てきてDTOってなんだ🤔🤔 と首をかしげていたけど、Data Transfer Objectのことらしい。なるほどね。
Scalaでいうcase classはKotlinではdata classと表記・宣言するっぽい。
1 |
data class Customer(val name: String, val email: String) |
lazyに初期化を行う変数は、型宣言のあとにby lazy…と続ける。
1 2 3 |
val p: String by lazy { // compute the string } |
SwiftやC#でいうところのnil合体演算子(nil coalescing operator) ??はKotlinでは?:のよう。3項演算子を短縮したように?と:が使われていて、こっちのほうが個人的に好み。
1 2 3 |
val files = File("Test").listFiles() println(files?.size ?: "empty") |
Coding Conventions
ふと思ったけど、KotlinのGetting StartedはBasic Syntax -> Idioms -> Coding Conventionsって流れになっててすごくスムーズ。Getting StartedにCoding Conventionsまで入れちゃうのとかかなり良さを感じる。新しいプログラミング言語始めるときって、文法とかはもちろんだけど、文法を超えた、慣例的なIdiomsやCoding Conventionsを本当は知りたい、ってときが多々あるしね。Goに入ってはGoに従え、じゃないけど(ほんますまん)。
クラスを定義する際のクラス内レイアウトについて言及があった。
- Property declarations and initializer blocks
- Secondary constructors
- Method declarations
- Companion object
こういうのを公式で書いてくれるのはありがたい。
Basics
続いてBasicsを読む。サクッとKotlin書き始める分にはこの章まででよさそう。
Basic Types
Kotlinの数値型では暗黙的なキャストはできない。明示的にメソッドを呼び出してキャストを行う。
1 2 3 4 |
val b: Byte = 1 // OK, literals are checked statically val i: Int = b // ERROR val i: Int = b.toInt() // OK: explicitly widened |
初期値を伴ったArrayの宣言にはarrayOf
を使うことができる。
1 2 3 |
val arr = arrayOf(1, 2, 3) val arr2: IntArray = intArrayOf(1, 2, 3) |
arr2では、primitive型(ここではint)用の特殊なarrayを宣言している。こちらの場合、boxingによるオーバヘッドは発生しない。
文字列は特に変わりなし
1 |
val s = "Hello, world!\n" |
immutableであり、s[i]
によって各要素にアクセスできる。
Packages and Imports
Kotlinファイルにおいて、いくつかのpackageはデフォルトでimportされている。
- kotlin.*
- kotlin.annotation.*
- kotlin.collections.*
- kotlin.comparisons.* (since 1.1)
- kotlin.io.*
- kotlin.ranges.*
- kotlin.sequences.*
- kotlin.text.*
Scalaと同様に、asを用いて別名でimportできる。
1 2 |
import foo.Bar // Bar is accessible import bar.Bar as bBar // bBar stands for 'bar.Bar' |
Control Flow
Scalaと同様に、ifはexpressionである。各ブロック内の最後に記述された式がif式の値となる。
1 2 3 4 5 6 7 |
val max = if (a > b) { print("Choose a") a } else { print("Choose b") b } |
whenも同様にexpressionである。else
が、Cライクなswitch文でいうところのdefault
となる。
1 2 3 4 5 6 7 |
when (x) { 1 -> print("x == 1") 2 -> print("x == 2") else -> { // Note the block print("x is neither 1 nor 2") } } |
各分岐条件ではin
やis
を使用できる。
1 2 3 4 5 6 7 8 9 10 11 |
when (x) { in 1..10 -> print("x is in the range") in validNumbers -> print("x is valid") !in 10..20 -> print("x is outside the range") else -> print("none of the above") } fun hasPrefix(x: Any) = when(x) { is String -> x.startsWith("prefix") else -> false } |
forループはよくある構文。whileループやdo-whileループもある。
1 2 3 4 5 6 7 8 9 10 11 |
for (item: Int in ints) { // ... } while (x > 0) { x-- } do { val y = retrieveData() } while (y != null) // y is visible here! |
Returns and Jumps
Kotlinではfor文などにlabelをつけることができる。break
およびcontinue
でそのラベルを指定したジャンプを行える。
1 2 3 4 5 |
loop@ for (i in 1..100) { for (j in 1..100) { if (...) break@loop } } |
Kotlinでかなり驚いた点。ラムダ式内でのreturn
は、もっとも内側のfunctionからのreturnになる。つまりbreakのような振る舞いをする(ラムダ式後続の文が実行されないという点で厳密には異なる)。continueのような振る舞いをさせたい場合、ラムダ式にラベルを付け、returnではそのラベルを明示する必要がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fun foo() { listOf(1, 2, 3, 4, 5).forEach lit@{ if (it == 3) return@lit // local return to the caller of the lambda, i.e. the forEach loop print(it) } print(" done with explicit label") } fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@forEach // local return to the caller of the lambda, i.e. the forEach loop print(it) } print(" done with implicit label") } |
2つめの例のように、関数名と同名のラベルをreturn時に指定することで、同様の振る舞いになる。ちなみにラムダ式ではなく無名関数の場合は、最も内側のfunctionがその無名関数であるため、continueと同様な振る舞いになる。
Classes and Objects
クラス分からんとなんも分からんよな。
Classes and Inheritance
クラス宣言時にはprimary constructorおよびsecondary constructorを宣言できる。クラス宣言時に、primary constructor内にvalないしはvar付きで引数を書くと、それがそのままpropertyとして生える。この辺のpropertyの話はScalaといっしょ。
1 2 3 4 5 |
class Hoge constructor(val id: Int) { init { println("id = ${id}") } } |
なお、特に修飾子がない場合constructr
は省略でき、class Hoge(val id: Int)
のように書ける。クラスの初期化は書いた順に上から行われる。
secondary constructorはクラス定義内に書くことができる
1 2 3 4 5 |
class Person(val name: String) { constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } } |
このとき、primary constructorがある場合は、上記のthis(name)
のように、propertyの初期化をdelegateしてあげる必要がある。
なお、クラスのインスタンスは次のようにして生成する。その際、new keywordはいらない。
1 |
val customer = Customer("kagemiku") |
クラスの継承、およびメソッドのオーバロードの際は、元となるクラスおよびメソッドにopen修飾子が必須となる。
1 2 3 4 5 6 7 |
open class Base { open fun v() { ... } fun nv() { ... } } class Derived() : Base() { override fun v() { ... } } |
なお、同名のメソッドをもつ複数の基底クラスを継承したクラスでは、該当のメソッドを必ずoverrideする必要がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
open class A { open fun f() { print("A") } fun a() { print("a") } } interface B { fun f() { print("B") } // interface members are 'open' by default fun b() { print("b") } } class C() : A(), B { // The compiler requires f() to be overridden: override fun f() { super<A>.f() // call to A.f() super<B>.f() // call to B.f() } } |
Properties and Fields
mutableなクラスプロパティはカスタムされたgetter/setterを持てる。
1 2 3 4 5 |
var stringRepresentation: String get() = this.toString() set(value) { setDataFromString(value) // parses the string and assigns values to other properties } |
Swiftみたいに中括弧{ }がないからちょっと不安になるな…
Kotlinではfieldの直接的な宣言はできない(つまり、propertyはsetter/getterメソッドを提供しているに過ぎない)。しかし、field
というキーワードをアクセッサ内で用いることで、backing fieldを自動で生成する。
1 2 3 4 |
var counter = 0 // Note: the initializer assigns the backing field directly set(value) { if (value >= 0) field = value } |
Interfaces
interfaceの宣言方法はごく普通。interfaceの継承も当然できる。interfaceを実装するclassでは、primary constructorの中でoverride
を付与することで、interfaceのpropertyを実装できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface Named { val name: String } interface Person : Named { val firstName: String val lastName: String override val name: String get() = "$firstName $lastName" } data class Employee( // implementing 'name' is not required override val firstName: String, override val lastName: String, val position: Position ) : Person |
Visibility Modifiers
classおよびinterfaceのメンバには次の4つの可視性に関する修飾子をつけることができる。
- private: 同じclassからのみ参照できる
- protected: 同じclassおよびそのクラスを継承したクラスからのみ参照できる
- internal: そのclassが宣言されたmoduleと同じmodule内からのみ参照できる
- public: そのclassを参照することのできるすべての場所から参照できる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
open class Outer { private val a = 1 protected open val b = 2 // open is required if the property may be overridden internal val c = 3 val d = 4 // public by default protected class Nested { public val e: Int = 5 } } class Subclass : Outer() { // a is not visible // b, c and d are visible // Nested and e are visible override val b = 5 // 'b' is protected } class Unrelated(o: Outer) { // o.a, o.b are not visible // o.c and o.d are visible (same module) // Outer.Nested is not visible, and Nested::e is not visible either } |
Extensions
KotlinにおけるExtension Functionは次のようにして生やす。Swiftみたいにextension
ブロックで囲む感じではない。ドキュメント読んだ感じ、これはC#ライクな方法なのかな。
1 2 3 4 5 |
fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp } |
なお、KotlinにおけるExtension Functionは静的に解決される。ランタイムではなく、コンパイル時の型で呼び出されるfunctionが決まる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
open class C class D: C() fun C.foo() = "c" fun D.foo() = "d" fun printFoo(c: C) { println(c.foo()) } printFoo(D()) |
この場合、出力されるのはcである。また、メンバであるmethodと同じsignatureのfunctionをextensionで定義した場合、呼び出されるのはメンバの方らしい。というか定義できちゃうんね。
もちろん、propertyもextensionで追加できる
1 2 |
val <T> List<T>.lastIndex: Int get() = size - 1 |
Swiftの場合、computed property、乱暴に言い換えればgetterのみをもつpropertyだけをextensionで追加できたが、Kontlinの場合はsetterも持たせることができるらしい。その際、明示的な初期化はできないようなので、getter/setterの双方の明示的な実装が必須となる。
Data Classes
data classはScalaにおけるcase class
1 |
data class User(val name: String, val age: Int) |
data classの場合、次のメンバがコンパイラによって自動で生える。初めの2つは明示的に宣言することもできる。
- equals()/hashCode()
- toString()
- componentN()
- copy()
基本的に、primary constructor内の引数それぞれを考慮したtoString()
、equals()
、hashCode()
およびcopy()
が生える。これらのメソッドに考慮に入れたくないが、propertyとして持ちたいものがある場合、primary constructorからははずしてクラス内に書いてしまえば良い。
1 2 3 |
data class Person(val name: String) { var age: Int = 0 } |
copy()
の際、特定のpropertyだけを変更してほかはそのままでcopyするといったことができる。
1 2 |
val jack = User(name = "Jack", age = 1) val olderJack = jack.copy(age = 2) |
ぶっちゃけstructが欲しいけど、そんなこと言ったらきっとぶん殴られるんだと思う。
Sealed Classes
Scalaのsealed classと同じ。
1 2 3 4 |
sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() |
sealed
修飾子がついたclassは自動でabstract
として扱われる。すなわちそのsealed classそのもののインスタンス化はできない。sealed classの利点はScalaのmatch式使用時とほぼ同様である(多分)。Kotlinにおいては、when式使用時にelse
節を省略できるようになる。
Generics
Genericなclassの宣言方法は普通。
1 2 3 |
class Box<T>(t: T) { var value = t } |
covariantな(共変な)型パラメータはout
と共に宣言する。
1 2 3 4 5 6 7 8 |
interface Source<out T> { fun nextT(): T } fun demo(strs: Source<String>) { val objects: Source<Any> = strs // This is OK, since T is an out-parameter // ... } |
他方、contravariantな(反変な)型パラメータはin
と共に宣言する
1 2 3 4 5 6 7 8 9 |
interface Comparable<in T> { operator fun compareTo(other: T): Int } fun demo(x: Comparable<Number>) { x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number // Thus, we can assign x to a variable of type Comparable<Double> val y: Comparable<Double> = x // OK! } |
これらのように、Javaのときとは対象的に、ジェネリクス宣言時に変性を指定するので、declaration-site varianceと呼ぶ。Javaの場合はuse-site varianceと呼ぶらしい。
Scalaの+A
とか-A
とかもややこしかったけど、KotlinはKotlinでややこしいな… 慣れだとは思うけど。
さらに、Javaのようなuse-site varianceもできるらしい。
1 2 3 |
fun copy(from: Array<out Any>, to: Array<Any>) { ... } fun fill(dest: Array<in String>, value: String) { ... } |
1つめの場合、fromにはArray<String>も渡すことができる(covariant)。2つめの場合、destにはArray<Any>も渡すことができる(contravariant)。
もちろん、Genericなfunctionも宣言できる。その場合、型パラメータはfunction名よりも前につけ、呼び出す際はfunction名の後につける(ややこしい)。
1 2 3 4 5 |
fun <T> singletonList(item: T): List<T> { // ... } val l = singletonList<Int>(1) |
Nested Classes
inner
をネストされたクラスの定義につけると、外側のメンバにアクセスできる。
1 2 3 4 5 6 7 8 |
class Outer { private val bar: Int = 1 inner class Inner { fun foo() = bar } } val demo = Outer().Inner().foo() // == 1 |
Enum Classes
enumはclassとして提供される(マジか)。
1 2 3 4 5 6 7 8 9 |
enum class Direction { NORTH, SOUTH, WEST, EAST } enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) } |
2つめの例は、enumのそれぞれのconstant(case的な)はenum classのinstanceであることを示している。マジか。
こういうこともできる(マジか)。
1 2 3 4 5 6 7 8 9 10 11 |
enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState } |
ProtocolState.signal()はabstractなメソッドである。それを、それぞれのcaseに付随したanonymous class内でoverrideして実装している(マジか)。
なお、それぞれのenum constantは次のmethodを持っている。
1 2 |
val name: String val ordinal: Int |
Objects
anonymous classのobjectは次のようにして宣言できる。これはobject expressionと呼ばれる。object expressionは、わざわざclassを宣言したくはないが、anonymous classっぽいものを宣言したいときに便利である。
1 2 3 4 5 |
window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { ... } override fun mouseEntered(e: MouseEvent) { ... } }) |
また、Scalaと同様に、Kotlinもobjectという形でシングルトンなclassを宣言できる。これはobject declarationsと呼ばれる。
1 2 3 4 5 6 7 8 |
object DataProviderManager { fun registerDataProvider(provider: DataProvider) { // ... } val allDataProviders: Collection<DataProvider> get() = // ... } |
Scalaでいうcompanion objectを宣言するには、class内でcompanion
を伴ったobjectを宣言する。
1 2 3 4 5 6 7 |
class MyClass { companion object Factory { fun create(): MyClass = MyClass() } } val instance = MyClass.create() |
Inline Classes
primitive型とかをpropertyとして持っているけど、classのインスタンス化に伴うオーバヘッドがきついよ〜〜〜〜ってときのために、Kotlinではinline classというものがある。(Kotlin 1.3時点でexperimentalな機能らしい)
1 2 3 4 5 |
inline class Password(val value: String) // No actual instantiation of class 'Password' happens // At runtime 'securePassword' contains just 'String' val securePassword = Password("Don't try this in production") |
methodやaccessorも持てるが、それらはstatic methodとして呼び出される(なるほど)。
Delegation
Kotlinでは簡単に、構文レベルでDelegation patternを実現できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
interface Base { fun print() } class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } class Derived(b: Base) : Base by b fun main() { val b = BaseImpl(10) Derived(b).print() } |
by
によって、Baseの各memberへforwardするmemberがDerivedに生える。methodなどをoverrideした際は、そちらが優先されて呼び出されるっぽい。
Delegated Properties
Delegated Propertiesというものもあるらしい。 setter/getterの処理をwrapできる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Example { var p: String by Delegate() } class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name}' in $thisRef.") } } |
そんで、いくつかのDelegateが標準ライブラリには備わっている。
まずはLazy。多分一番使うのかな?
1 2 3 4 5 6 7 8 9 |
val lazyValue: String by lazy { println("computed!") "Hello" } fun main() { println(lazyValue) println(lazyValue) } |
次にObservable。直前の値と新しい値を、値が更新されるときにhandler内で参照できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import kotlin.properties.Delegates class User { var name: String by Delegates.observable("<no name>") { prop, old, new -> println("$old -> $new") } } fun main() { val user = User() user.name = "first" user.name = "second" } |
次にmap。primary constructorなどで渡したmapに実際の値を保存するっぽい。
1 2 3 4 5 6 7 8 9 10 11 12 |
class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map } val user = User(mapOf( "name" to "John Doe", "age" to 25 )) println(user.name) // Prints "John Doe" println(user.age) // Prints 25 |
終わりに
長くなってきたので、続き(functionとか)は別の記事で。
なお、この記事におけるコード例は、そのほとんどがKotlinの公式からコピペしたものです。
最近のコメント