関連記事
この記事も古くなりましたね。執筆時の実装バージョンKotlin 0.12から1.0.2へのアップグレード対応をした際の知見を記事にしました。
Kotlinを実案件で使いました
先日、僕の勤め先のQonceptは『リアル鬼ごっこ』×富士急ハイランド 巨大遊園地からの逃走を開発、リリースしました。
富士急ハイランドで実際に鬼ごっこをする企画で、一般のお客さんがスマホで専用アプリを使いながらクリアを目指します。園内には鬼役のスタッフや、ゲーム進行に関わる設備などがあり、これらとスマホがiBeacon(BluetoothLE)を用いて連動することで、ダメージを受けたり、アイテムを使用したり、クイズを解いたりなどします。
Qonceptの開発範囲は、iOSアプリ(とAppleWatchアプリ)、Androidアプリ、サーバサイドでした。
受注確定となった時点で、残り日数と開発者リソースに対して、全体の実装ボリュームがかなり大きかったので、どうやって間に合わせるか検討しました。特にこの頃、iOSはSwiftの採用でObjective-Cよりも快適な開発ができるようになっていた中、AndroidのJava開発はいろいろとプレッシャーとなっていました。
そこで思い当たったのがKotlinでした。以前からちょくちょくと耳に挟んでおり、なんとなく良いものらしいと認識していました。
Kotlinを採用するなら今しかない、と公式サイトのドキュメントを一気読みしました。これなら行けると判断、 iOS版をSwiftで実装しながら、平行してこれをKotlinで移植しながらAndroid版を実装する方針にしました。
最終的には、バッチリオンスケジュール、アプリの品質も安定、お客さんからの評判も良かったということで、めでたしめでたしとなりました。
Kotlinマジ最高
導入が長くなってしまいましたが、上述のとおりKotlinでガッツリ開発したところ、Kotlinマジ最高だという高まりが得られました。(iOS版は他のスタッフが開発、Android版への移植は僕が行いました)
これはもっと広まってもらって、Kotlin開発者が増えて世の中に普及することで、今後も進化、保守されていってほしいので、Kotlinを布教するべく本記事を書くことにしました。
以下では、Kotlinを主に Android開発、 Swiftからの移植、 実案件で使う、という観点を軸にして紹介していきます。
バージョン
案件実装時は(確か)Kotlin M11でした。
記事を書いた時点でM14がリリースされており、
気がつく範囲でM14に即した内容で書いています。
言語周辺
言語仕様そのものに触れる前に、言語周辺について書きます。
後ろ立ては某企業
趣味開発とは異なり実案件の場合、あまり有名でない言語は、開発が中断されたり将来消滅してしまうものはリスクとなります。
この点Kotlinは普及こそまだまだであるものの、オープンソースです。いきなりコンパイラ等が入手不能となって完全に詰む、という事は考えにくいでしょう。
また、開発しているのはJetbrainsです。JetbrainsはIntelliJ IDEAというJava IDEを開発、販売していることで有名です。JavaのIDEを開発しているぐらいですから、コンパイラ関連の技術力の高さやプログラミング言語への理解の深さはかなりのものだと思います。Androidの開発環境がEclipse + ADT PluginからAndroid Studioに切り替わって久しいですが、このAndroid Studio自体、IntelliJにAndroid開発のための改造を施したものです。Googleがこの舵取りをしたことも、Jetbrainsの頼もしさを説得する一面です。
導入が簡単
新しい言語を採用する場合、開発環境の構築でトラブルが多発して時間を消耗したり、充実した環境が整わない結果、言語自体の生産性を開発環境が相殺してしまう恐れがあります。
Kotlinはここがかなり楽ちんです。まず、IDE連携用にはAndroid Studio(IntelliJ)用のプラグインがJetbrainsから提供されています。
IDE本体も言語も同じところが出しているので、各種連携はバッチリです。Swift + XcodeはいまだにできないRefactor renameなどもちゃんとできます。
プラグインは Android Studio > Preferences > Plugins > Install Jetbrains Plugin > Kotlin
からインストールできます。
新しいバージョンのプラグインが出た時は、Android Studioが検知、通知をしてくれて簡単にアップデートできます。
プロジェクトのビルドへの導入も簡単です。
Android Studio > Tools > Kotlin > Configure Kotlin in Project
をクリックすればダイアログが出て、OKを押すとセットアップしてくれます。
そうすると、アプリケーションモジュールのgradleスクリプトが、下記のように変更されます。
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "jp.co.qoncept.apptest"
minSdkVersion 18
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "jp.co.qoncept.apptest"
minSdkVersion 18
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
buildscript {
ext.kotlin_version = '0.13.1514'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
repositories {
mavenCentral()
}
あとは普通にビルドしてやれば、gradleスクリプトがKotlinコンパイラの取得から全部やってくれます。新しいバージョンのKotlinが出た時は、ext.kotlin_version
のところを書き換えてやればよいです。
Javaとの連携能力が高い
開発言語を変更する場合、これまでの言語との同時使用が困難だったり、あまりシームレスではない場合、既存のプロジェクトに追加で導入する事ができませんし、過去のコード資産が無駄になりますし、万が一できない事等にぶつかった場合に回避できません。
その点KotlinはJavaとの連携能力がとても高いです。
ScalaやGroovyなどの言語と同様に、Javaバイトコードにコンパイルされて、JVMの上で動かすことができます。
言語仕様としてのJava連携がかなり重視されており、
既存のJavaソースのプロジェクトにKotlinソースを追加で混ぜていくようにして導入できます。
また、Kotlinから自然な記述でJavaのクラスやメソッドを呼び出せます。
公式サイトにも100% interoperable with Javaなんて書いてあります。
もしKotlinでうまく書けない事があっても、その部分だけ従来通りJavaで書くことができます。
このあたりはSwiftとObjective-Cの関係によく似ています。
これがあったので、何かあっても大丈夫だろうと考えていました。
しかし最終的には、Kotlinが気に入ってしまったので、既存のJava実装を持っている部分も、新たにKotlinで書きなおしました。
言語の紹介
型推論付き静的型付け
Kotlinは型推論のある静的型付け言語です。Swiftもそうです。Javaは違います。
型推論は基本ですよね。
見た目
fun main(args: Array<String>) {
println("Hello, world!")
}
セミコロンレススタイル、コードブロックはブレーススタイル、型表記はパスカルスタイル(変数、コロン、型の並び)
var sum = 0
listOf(1,2,3).filter { it > 0 }.forEach {
sum += it
}
print(sum)
クロージャはブレース{}
だけで書く、末尾引数のクロージャを関数呼び出しの後ろに書け、その時引数が他に無ければパーレン()
を省略できる
この辺りの構文仕様はSwiftと同じなので、移植作業が楽になります。
Optional (Nullable)
型として、nullを持つ型と持たない型が区別されます。
型検査をして中身がnullで無いことを確認すると、その時点で中身の型にキャストされます。
一般的にこの機能を提供するものをOptionalといいますが、KotlinではNullableと言います。
fun getLengthOfString(str: String): Int {
return str.length()
}
fun getLengthOfStringOpt(str: String?): Int {
if (str != null) {
return getLengthOfString(str)
} else {
return 0
}
}
fun main(args: Array<String>) {
val a = getLengthOfString("hello")
val b = getLengthOfStringOpt("world")
val c = getLengthOfStringOpt(null)
println("$a, $b, $c")
}
Nullable型は、中身の型の右にハテナ?
をつけて表記します。
SwiftのOptional型と同じ書き方なのが嬉しいです。
Javaには言語機能としてのOptionalはありません。ヌルポで死にます。
(標準ライブラリは言語機能に含めない、と考えています。なお、Javaの標準ライブラリのOptionalは、null安全性を提供する機能としては不十分だと思います。)
ちょっと変なところ
NullableのNullableが作れません。Nullableになってしまいます。
SwiftでOptionalのOptionalが出てくるコードの移植では工夫が必要です。
fun wrap(a: Int?): Int?? {
return a
}
fun desc(a: Int??) {
if (a == null) {
println("None")
} else {
if (a == null) {
println("Some(None)")
} else {
println("Some(Some($a))")
}
}
}
fun main(args: Array<String>) {
val a: Int?? = wrap(null)
desc(a) // Some(None)が期待されるが、Noneとなる。
}
フローベースの型キャスト(smart casts)
if文でnullチェックしたりis演算子で型チェックをすると、それを考慮して型が自動的にキャストされます。
open class Animal {}
class Cat: Animal() {
fun nyaa() { println("nyaa") }
}
class Dog: Animal() {
fun wan() { println("wan") }
}
fun speak(animal: Animal) {
if (animal is Cat) { animal.nyaa() }
if (animal is Dog) { animal.wan() }
}
fun speak2(animal: Animal?) {
if (animal == null) {
println("null")
return
}
speak(animal)
}
fun main(args: Array<String>) {
speak2(Cat()) // nyaaと出る
speak2(Dog()) // wanと出る
speak2(null) // nullと出る
}
speak2の頭でnullチェックをしてreturnしているので、if以降はAnimal?
ではなくAnimal
になっており、speakが呼び出せます。
speakのifのthen句の部分では、isによるチェックが効いているので、サブクラスのCatやDogにキャストされており、それら専用のメソッドが呼び出せます。
等価なコードのSwift版は下記になります。
class Animal {}
class Cat: Animal {
func nyaa() { print("nyaa") }
}
class Dog: Animal {
func wan() { print("wan") }
}
func speak(animal: Animal) {
if let animal = animal as? Cat { animal.nyaa() }
if let animal = animal as? Dog { animal.wan() }
}
func speak2(animal: Animal?) {
guard let animal = animal else {
print("null")
return
}
speak(animal)
}
func main() {
speak2(Cat())
speak2(Dog())
speak2(nil)
}
main()
speak2ではこのためにわざわざguard文とやらを使わないといけません。speak, speak2共に、let animal =
を書くのが冗長です。
ifの丸括弧が省略できるのはいいですね。
Javaは下記のようになるでしょうか。
import java.util.*;
import java.lang.*;
import java.io.*;
class Animal {}
class Cat extends Animal {
void nyaa() { Ideone.print("nyaa"); }
}
class Dog extends Animal {
void wan() { Ideone.print("wan"); }
}
class Ideone
{
public static void print(String str) { System.out.println(str); }
static void speak(Animal animal) {
if (animal instanceof Cat){
Cat cat = (Cat)animal;
cat.nyaa();
}
if (animal instanceof Dog) {
Dog dog = (Dog)animal;
dog.wan();
}
}
static void speak2(Animal animal) {
if (animal == null) {
print("null");
return;
}
speak(animal);
}
public static void main (String[] args) throws java.lang.Exception
{
speak2(new Cat());
speak2(new Dog());
speak2(null);
}
}
nullチェックに関してはコードが正しいことを祈って実行するしかありません。そしてspeakについては、Cat
とDog
をそれぞれ3回書かされます。(キャスト用の関数を用意すれば、2回 + nullチェックにはできますが)
Unsafe cast
Nullableがnullだった場合にクラッシュする中身の取り出しと、型が違った場合にクラッシュするキャストがあります。
fun hoge(a: Int?, b: Animal?) {
val c: Int = a!! // nullだったら例外
val d: Cat? = b as? Cat // Catで無ければnull
val e: Cat = b as Cat // Catで無ければ例外
}
Swiftは下記のようになります。
func hoge(a: Int?, b: Animal?) {
let c: Int = a! // nilだったら例外
let d: Cat? = b as? Cat // Catで無ければnil
let e: Cat = b as! Cat // Catで無ければ例外
}
Kotlinは2つのビックリ!
で剥がせます。Swiftは1つです。
危険なas
は、Swiftにはビックリがついています。
オプショナルのメソッド呼び出し
Kotlinでオプショナルに包まれた値のメソッドを呼び出す際、
値があればメソッド呼び出し、nullであればnullが欲しい場合、
ifでの型チェックをせずとも下記のように記述できます。
fun hoge(user: User?) {
val name: String? = user?.name
println("name=$name")
}
elvis演算子を使えば、nullだった場合のデフォルトを指定できます。
fun hoge(user: User?) {
val name: String = user?.name ?: "no name"
println("name=$name")
}
Swiftにもハテナドット?.
でのメソッド呼び出しがあります。
また、elvisについてはSwiftではハテナハテナ??
です。
よく似ている2つの言語ですが、ハテナドット?.
を立て続けた場合の構文木が異なっています。
ユーザーの名前の文字数を取得するケースを考えてみます。
class User {
var name: String = "tanaka"
}
fun hoge(user: User?) {
println(user?.name?.length())
}
Kotlinでは?.
が2回出てきます。これは、次のように解釈されているからです。
( user?.name )?.length()
?.
を使わずに書くと次のようになります。
val name: String? = if (user != null) { user.name } else { null }
val length: Int? = if (name != null) { name.length } else { null }
一方、Swiftの場合は次のようになります。
class User {
var name: String = "tanaka"
}
func hoge(user: User?) {
print(user?.name.characters.count)
}
main()
nameの後の?.
がSwiftでは.
になっています。これは、次のように解釈されているからです。
user?.( name.characters.count )
ただし、このカッコは概念の説明のためであり、Swiftとしては不正になってしまいました。
書き下すと次のようになります。
if let user = user {
return user.name.characters.count
} else {
return nil
}
整理すると下記のようになっています。
Kotlinの場合は、?.
は一つ次のキーワードだけを処理し、その結果をさらにその右で使用します。
Swiftの場合は、?.
はそれより右全てを括っており、Noneだった場合は右全てをスキップします。
この違いは全く同じ見た目のコードが、全く違う意味を持つことになるので、
移植する際は注意が必要です。
個人的にはKotlinの仕様の方が直感的で好きです。
始めてSwiftを書いた時、Kotlin仕様を想像していたので、
エラーが出て混乱した事があります。
Javaの場合は、第一引数にレシーバ、第二引数にオペレータを取る高階関数を作って、
?.
の挙動をエミュレートするのが良いでしょう。
if文で書くと、レシーバの式を2回書かないといけないからです。
メソッド呼び出しじゃないけどオプショナルにチェーンするやつ
上記の?.
を使えばオプショナルでもめんどくさくならずにコードが書けますが、
次のように、?.
では書けないけれど、nullじゃない場合に処理を続けたいケースが有ります。
val result: Boolean
if (user != null) {
result = write(user)
} else {
result = false
}
こういうケースでは、kotlinでは次のような記法が使えます。
val result: Boolean = user?.let { write(it) } ?: false
letの定義、実装は次のようになっています。
public inline fun <T, R> T.let(f: (T) -> R): R = f(this)
これは、全ての型Tに対して定義された拡張メソッドで、
引数としてクロージャを一つ取ります。
そしてそのクロージャにメソッドのレシーバが渡され、すぐに呼びだされ、
その結果がlet自体の評価値となるものです。
上記の例では?.
があるので、letが実行されるのはUser?
がnullで無い場合です。
itはクロージャの暗黙の引数なので、it: User
となっています。
そして、writeの返り値がletの返り値となるので、期待した挙動になるわけです。
Swiftの場合は、Optional自体に定義されたflatMapメソッドが使えます。
let result: Bool = user.flatMap { write($0) } ?? false
この場合は、オプショナル自体のメソッドなので?.
ではなく.
になります。
基本的な高階関数
基本的な高階関数が使えます。クロージャが{}
なのでシンプルに書けます。
fun main(args: Array<String>) {
val a = (0..10)
.filter { it % 2 == 0 }
.map { it * it }
.fold("") { s, i ->
(if (s != "") s + "_" else "") + i.toString()
}
println(a) // 0_4_16_36_64_100 が出力される
}
swiftも似たような感じでかけます。
let a = (0...10)
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.reduce("") {
let s = $0 != "" ? $0 + "_" : ""
return s + String($1)
}
print(a) // 0_4_16_36_64_100 が出力される
Javaだとこうでしょうか。
String a = IntStream.rangeClosed(0, 10)
.mapToObj(i -> Integer.valueOf(i))
.filter(i -> i % 2 == 0)
.map(i -> i * i)
.reduce("", (s, i) ->
(!s.equals("") ? s + "_" : "") + i
, (s1, s2) -> s1 + s2);
print(a);
KotlinとSwiftはクロージャリテラルと関数呼び出しの表記がよく似ています。
Swift版のreduceの中身は、ひとまとめで書こうとしたところ、型推論がタイムアウトしてコンパイルできなかったので、一度letに入れました。
Kotlinでは、クロージャの暗黙の引数は、引数が1つの時に限りit
が使用できます。複数の時は引数名が必要です。
Swiftは$0
,$1
,$2
,...と使えます。
Kotlinには三項演算子はありませんが、if文が式として扱えます。
文字列中の変数展開
Kotlinは文字列中に$
で変数が、${}
で式が書けます。
fun hoge(i: Int, user: User) {
println("i is $i, user name is ${user.name}")
}
$i
のところが変数展開、${user.name}
のところが式展開です。
Swiftは\()
です。
func hoge(i: Int, user: User) {
print("i is \(i), user name is \(user.name)")
}
Javaは文法が無いので、下記のようになるでしょう。
void hoge(int i, User user) {
print("i is " + i + ", user name is " + user.name);
}
JavaのSAM変換
JavaではJava8が出た時に、ラムダ式とSAM変換という大きな機能の追加がありました。
これは、メソッドを1つだけ持つインターフェースを引数にとる関数の呼びだし箇所において、
ラムダ式が書けるようになるというものです。
例えば下記がJava7のコードです。
Androidでよくある、ボタンのクリックハンドラを設定するものです。
button.setOnClickListener(new View.OnClickListener {
@Override
void onClick(View view) {
println("clicked");
}
});
これがJava8ではこのように書けるのでした。
button.setOnClickListener(view -> {
println("clicked");
});
これによりJava8ではラムダ式を導入するにあたって、
Java7以前からあるコードを無駄にしたり修正したりすることなく、
ラムダ式を使ってより快適に書けるようになりました。
この、ラムダ式から自動変換の対象になるインターフェースは、
メソッドが1つだけである必要があります。
これを、Single Abstract Method、略してSAMと呼ぶため、
この変換をSAM変換と呼びます。
さて、KotlinもJava8と同様にSAM変換を搭載しています。
これはJavaと連携してJavaのライブラリを使う上で、
無かったらやってられないほど重要かつ基本的な機能です。
上記の例はKotlinで次のように書けます。
button.setOnClickListener { view ->
println("clicked")
}
書きやすくて良いですね。
上記の例では、引数がクロージャ1つ渡すだけなので、
関数呼び出しカッコ()
の省略をしています。
拡張メソッド
Kotlinは既存のクラスに対して、あとからメソッドを付け足す事ができます。
fun Int.square(): Int = this * this
fun <T> List<T>.evens(): List<T> = withIndex().filter { it.index % 2 == 0 }.map { it.value }
fun List<Int>.squareEvens(): List<Int> = evens().map { it.square() }
fun main(args: Array<String>) {
val a = 3
println(a.square()) // 9と出力
val b = listOf("a", "b", "c", "d", "e")
println(b.evens()) // [a, c, e]と出力
val c = listOf(10, 20, 30, 40, 50)
println(c.squareEvens()) // [100, 900, 2500]と出力
}
ジェネリクス型の拡張メソッドについては、T全てについてと、特定のTについての定義ができます。
関数の本文は=
スタイルで書いてみました。
Swiftでは下記となります。
extension IntegerType {
func square()-> Self {
return self * self
}
}
extension Array {
func evens()-> Array<Element> {
return enumerate().filter { $0.index % 2 == 0 }.map { $0.element }
}
}
extension Array where Element: IntegerType {
func squareEvens()-> Array<Element> {
return evens().map { $0.square() }
}
}
func main() {
let a = 3
print(a.square()) // 9と出力
let b = ["a", "b", "c", "d", "e"]
print(b.evens()) // ["a", "c", "e"]と出力
let c = [10, 20, 30, 40, 50]
print(c.squareEvens()) // [100, 900, 2500]と出力
}
main()
Elementに対する制約はプロトコルの必要があるようで、Int
では書けなかったのでIntegerType
になっています。理由がよくわかりませんでした。
KotlinもSwiftも、同様にしてプロパティを追加することができます。
移植ではwithIndex
とenumerate
の対応も嬉しいです。
Javaでは拡張メソッドが無いので、第一引数にthisを取るスタティックメソッドとして実装するでしょう。
public class Main
{
public static void print(String str) {
System.out.println(str);
}
public static int intSquare(int x) { return x * x; }
public static <T> List<T> listEvens(List<T> list) {
return IntStream.range(0, list.size())
.filter(i -> i % 2 == 0)
.mapToObj(i -> list.get(i))
.collect(Collectors.toList());
}
public static List<Integer> intListSquareEvens(List<Integer> list) {
return listEvens(list).stream()
.map(i -> intSquare(i))
.collect(Collectors.toList());
}
public static void main (String[] args) throws java.lang.Exception
{
int a = 3;
print("" + intSquare(a)); // 9と出力
List<String> b = listEvens(Arrays.asList("a", "b", "c", "d", "e"));
print("" + b); // [a, c, e]と出力
List<Integer> c = intListSquareEvens(Arrays.asList(10, 20, 30, 40, 50));
print("" + c); // [100, 900, 2500]と出力
}
}
この方式の辛いところは、衝突を避けるためにメソッド名にプレフィックスが必要になる事、呼び出しの時に、f(g(h(x)))
という形になるため、後に適用するものほど前に来る事などがあります。特に移植においては、元がメソッドチェーンの形になっている場合は、記述順がひっくり返すことになるので煩雑な作業です。個人的には可読性も下がっていると思います。
なお、withIndex
相当の方法がわからなかったので、ごまかした書き方になっています。
オペレーターオーバーロード
Kotlinではオペレーターオーバーロードがあります。
ですが、直接演算子の表記をメソッド名にするC++やSwiftのものとは違って、
予め決められた演算子に対応する名前のメソッドを、operator
キーワードと共に実装します。
自分で演算子を追加することはできませんが、引数が一つのメソッドについては infix
指定子をつけることで中置を可能にできます。これによってキーワードとしての演算子追加のような事はできます。
data class Vector2(val x: Double, val y: Double) {
operator fun plus(o: Vector2): Vector2 = Vector2(x + o.x, y + o.y)
infix fun dot(o: Vector2): Double = x * o.x + y * o.y
override fun toString(): String = "($x, $y)"
}
operator fun Double.times(o: Vector2): Vector2 = Vector2(this * o.x, this * o.y)
fun main(args: Array<String>) {
val a = Vector2(1.0, 2.0) + Vector2(3.0, 4.0)
println(a) // (4.0, 6.0)と出力
val b = 3.0 * Vector2(0.0, 1.0)
println(b) // (0.0, 3.0)と出力
val c = Vector2(2.0, 0.0) dot Vector2(2.0, 3.0)
println(c) // 4.0と出力
}
足し算+
はメソッド、掛け算*
はDouble
の拡張メソッドとして書きました。
dotを中置で呼び出しています。
データクラスと プライマリコンストラクタの機能も使用しています。
Swiftでも書いてみます。
class Vector2: CustomStringConvertible {
let x: Double
let y: Double
init(_ x: Double, _ y: Double) {
self.x = x
self.y = y
}
var description: String {
return "(\(x), \(y))"
}
}
func +(l: Vector2, r: Vector2)-> Vector2 {
return Vector2(l.x + r.x, l.y + r.y)
}
func *(l: Double, r: Vector2)-> Vector2 {
return Vector2(l * r.x, l * r.y)
}
infix operator ● {
associativity left
precedence 140
}
func ●(l: Vector2, r: Vector2)-> Double {
return l.x * r.x + l.y * r.y
}
func main() {
let a = Vector2(1.0, 2.0) + Vector2(3.0, 4.0)
print(a) // (4.0, 6.0)と出力
let b = 3.0 * Vector2(0.0, 1.0)
print(b) // (0.0, 3.0)と出力
let c = Vector2(2.0, 0.0) ● Vector2(2.0, 3.0)
print(c) // 4.0と出力
}
main()
●
は、「まる」で変換すると出てくるユニコード文字です。
この例ではSwiftの機能を使ってこの記号を演算子として定義しました。
Kotlinは演算子を作ることはできないので、Swiftで定義された独自演算子の移植の際はメソッドにします。
ですが、演算子優先度までは移植できないので、カッコ()
をつけていく必要があるでしょう。
Javaはこの辺りはできないので、移植の際はいろいろと大変です。
拡張メソッドの場合と同様なつらみがあります。
プロパティ
Kotlinのフィールドのようなものは全てプロパティです。
定数はval, 変数はvarで定義し、
valにはゲッター、varにはゲッターとセッターを定義することもできます。
ゲッターセッターの実装ためのバッキングフィールドが自動定義されていて、
field
というキーワードでアクセスできます。
class User {
val id: Int
var familyName: String = "yamada"
var firstName: String = "taro"
val fullName: String
get() = "$familyName $firstName"
var died: Boolean = false
get() { return field }
set(value) {
field = value
if (value) {
println("${fullName}は死んでしまった")
}
}
constructor(id: Int) {
this.id = id
}
}
fun main(args: Array<String>) {
val u = User(3)
u.familyName = "saito"
u.died = true // saito taroは死んでしまった と表示されます
}
上記の例では、id
はval
なのでゲッターが自動定義、
familyName
、firstName
はvar
なのでゲッターとセッターが自動定義されています。
fullName
はゲッターを自作して、他のプロパティから動的に値を生成しています。
died
はゲッターとセッターを自作しつつ、バッキングフィールドを使用しています。
Swiftでもフィールドのようなものはプロパティです。
ゲッターセッターだけでなく、willSetやdidSetといったものも定義できます。
バッキングフィールドは自動定義されません。
KotlinにはdidSetなどの言語機能は無いため、移植の場合はセッター上でエミュレートします。
class User {
let id: Int
var familyName: String = "yamada"
var firstName: String = "taro"
var fullName: String {
get { return "\(familyName) \(firstName)" }
}
var died: Bool = false {
didSet {
if died {
print("\(fullName)は死んでしまった")
}
}
}
init(_ id: Int) {
self.id = id
}
}
func main() {
let u = User(3)
u.familyName = "saito"
u.died = true // saito taroは死んでしまった と表示されます
}
main()
Javaではフィールドとプロパティは明確に区別されていて、
メソッドとしてゲッターとセッターを自力で定義したものをプロパティと呼びます。
これが移植の際に面倒な事になります。
Swiftで書かれた次のクラスがあったとします。
class User {
var died: Bool = false
}
func hoge(u: User) {
u.died = true
}
これをJavaでフィールドに移植したとします。
class User {
boolean died = false;
}
void hoge(User u) {
u.died = true
}
そのあと、Swift版が次のように変更されたとします。
class User {
var died: Bool = false
didSet {
println("死んでしまった!")
}
}
この際、Javaは次のように修正が必要です。
class User {
boolean died = false;
boolean getDied() { return died; }
void setDied(boolean value) {
died = value;
println("死んでしまった!");
}
}
void hoge(User u) {
u.setDied(true);
}
ゲッターとセッターを実装するのは良いとして、
フィールドへの代入をしている部分をセッターの呼び出しに変更する
必要があります。
これは、複数箇所あるうえに、移植元ではdiffが生じないため、
見落としてしまうリスクが高いです。
見落としてしまえばバグになります。
しかもコンパイル時にはわかりません。
10箇所ある代入のつい1箇所だけ対応忘れがあったりすれば、
やっかいなバグとなるでしょう。
なので、プロパティがある言語から移植するなら、プロパティがある言語が望ましいのです。
Javaプロパティアクセサのプロパティ化
Javaにおいて、フィールド名name
に対して、
name
のプロパティを作る際は、
ゲッターString getName()
とセッターsetName(String name)
を定義します。
そして、呼び出しの際は下記のように関数呼び出しの形を取ります。
// 読み込み
String name = user.getName();
// 書き込み
user.setName(newName);
しかし、Kotlinの場合は、プロパティname
に対しては、
呼び出しの際は関数呼び出しの形を取りません。
// 読み込み
val name = user.name
// 書き込み
user.name = newName
関数呼び出しの形ではありませんが、
name
のゲッターやセッターが呼び出されます。
さて、KotlinがJavaのメソッドを呼び出す際、
このようなgetXxxx()
とsetXxxx(value)
を、
Kotlinにおけるプロパティxxxx
の扱いでアクセスできます。
例えば下記はAndroidでボタンを非表示にする例です。
button.visibility = View.INVISIBLE
Android SDKはJavaで定義されており、
本来のJavaではsetVisibility()
を呼び出すところですが、
Kotlinからはvisibility
プロパティのように取り扱えるのです。
Delegated Property
Delegated PropertyはKotlinのおもしろい機能です。
プロパティのゲッターやセッターの実装を、別のオブジェクトに移譲する事ができます。
Lazy
例としてLazyを取り上げます。
val fullName: String by lazy {
familyName + " " + firstName
}
fullName
はvalなので定数ですが、初めてゲッターが呼ばれた時に、
lazyに渡しているクロージャが実行され、その評価結果が返ります。
2回目以降のゲッター呼び出しては、初回の結果が返されます。
もしこれをJavaなどで実装しようとした場合、
ゲッターの中でif文を書いたりすることになります。
そうした定型で冗長な部分を記述する必要がありません。
Swiftにもlazyというキーワードがあり、
同じ機能を提供する言語の機能があります。
しかしこれと比べてKotlinが興味深いのは、lazyは特別な言語機能ではなく、
by
のみが言語機能で、
lazyはクロージャを引数に取る標準ライブラリ関数という事です。
この関数が返すオブジェクトが、実際のプロパティのゲッター、セッターを処理します。
notNull
この節の内容は古くなりました。M13からはlateinit
を使ったほうが良いと思います。
もう一つ興味深いデリゲートを紹介します。
var name: String by Delegates.notNull()
これは、1度もセットされていない状態でゲッターが呼ばれると例外が飛んでクラッシュし、
1度セットされたあとなら、ゲッターはその値が普通に読み取れます。
Swiftにおいてこれと近い意味をもつのは、ビックリ!
型です。
正確にはImplicitly Unwrapped Optional
と言います。
var name: String!
こいつは初期状態がnilで、nilの状態のときに読むとクラッシュしますが、
値が入っていれば普通に読めて、普段は値の型として振る舞うものです。
Kotlinとの微妙な違いは、KotlinのnotNullにnullを入れる事はできないけれど、
Swiftのビックリにはnilを入れる事ができる点です。
SwiftのものはあくまでOptionalということですね。
しかし、大体の場合でわざわざnilを入れることはしないので、
(そういう場合は普通のOptionalが望ましいからです)
移植はだいたいこれでいけます。
このケースも、Swiftの!
は言語機能なのに対して、
Kotlinのこれはやはり標準ライブラリで提供される実装です。
おもしろいです。
Kotterknife
Android開発といえばビューのバインディングですが、
butterknifeの作者さんがkotterknifeというKotlin版のバターナイフを提供しています。
これは、このby
を使ってバインディングするものです。
サンプルコードを引用します。
public class PersonView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
val firstName: TextView by bindView(R.id.first_name)
val lastName: TextView by bindView(R.id.last_name)
// Optional binding.
val details: TextView? by bindOptionalView(R.id.details)
// List binding.
val nameViews: List<TextView> by bindViews(R.id.first_name, R.id.last_name)
// List binding with optional items being omitted.
val nameViews: List<TextView> by bindOptionalViews(R.id.first_name, R.id.middle_name, R.id.last_name)
}
@IBOutlet
と!
を使うiOS開発や、アノテーションとリフレクションを駆使したJavaのButterknifeより、
この方式が綺麗で好ましいと思います。
なお、ビルドに介入することでエクステンションメソッドを自動実装してくれて、
プロパティ定義すら不要なプラグインが有ります。
僕は言語機能での実装が好ましいと思います。
lateinit
プロパティに対する修飾子としてlateinit
を使うと、
初期値が不要な非オプショナル型の変数が定義できます。
class User {
lateinit var name: String
}
lateinitになっている型は、書き込む前に読み込むと例外が飛んでクラッシュします。
Swiftのビックリ!
型と同じように使えます。
Delegates.notNullとの違い
Delegates.notNullとの違いはよくわかりません。
ドキュメントによると、lateinitは自然なフィールド名を作るので、
DIツールとの相性(自動生成バイトコードやリフレクションの事でしょうか)が良い
と書いてあります。
しかし、Kotlinコードだけの世界でみるとその違いは関係ありません。
唯一見つけた違いlateinitはvalには使えずvarのみに使えます。
notNullはvalにも使えます。
しかし、notNullがvalで使うのはクラッシュする可能性だけがあり、メリットは全く無いので、
valが禁止されたlateinitの方が、安全であり、わずかに優れていると思います。
上述の説でnotNullが言語機能によらない魅力を語っていますが、
lateinitを使うほうが良さそうです。
ジェネリクスとDeclaration Site Variance
Kotlinはジェネリクスをサポートしています。
ジェネリクスの型パラメータのバリアンスについては、
Declaration Site Varianceになっています。
open class Animal
class Cat: Animal()
class Box<out T>(val value: T) {
override fun toString(): String = "Box($value)"
}
fun main(args: Array<String>) {
val a: Box<Animal>
val b: Box<Cat> = Box(Cat())
a = b
println(a) // Box(Cat@xxxxxxxx)と表示
}
バリアンスが機能しているため、Boxの値をBoxの変数に代入できています。
Declaration Siteというのは、宣言時指定ということで、Boxの型パラメータTを書くその場で、
out T
と記述することで、BoxがTに関してcovarianceであると宣言しています。
このout
を消すとコンパイルエラーとなります。
SwiftもDeclaration Siteであるのに対して、JavaがUse Siteです。
Javaで上記の例を書くと以下のようになります。
class Animal {}
class Cat extends Animal {}
class Box<T> {
final T value;
Box(T value) {
this.value = value;
}
public String toString() { return "Box(" + value.toString() + ")"; }
}
public class Main
{
public static void print(String str) {
System.out.println(str);
}
public static void main (String[] args) throws java.lang.Exception
{
final Box<? extends Animal> a;
final Box<Cat> b = new Box<Cat>(new Cat());
a = b;
print(a.toString()); // Box(Cat@xxxxxxxx)と出力される。
}
}
Box自体の定義はバリアンスについて書かれず、ローカル変数a
を定義するときの型として、
境界型として記述しています。その他、関数引数の定義で境界型が出てきます。
Declaration SiteとUse Siteの良し悪しはここでは省略しますが、
僕は前者が好きなのでKotlinが好きです。
その他、Swift, C#, GoなどもDeclaration Siteです。
Swiftと同じなので、Swiftからの移植はやりやすいです。
SwiftからJavaへの移植となると、けっこう大変です。
宣言は一箇所なのに対して、使用箇所(関数引数、ローカル変数)はたくさんあるので、
理論的にはそれを全て? extends T
や? super T
で書かないと正しい移植になりません。
実際には諦めてしまってバリアンスを捨て、
コンパイルエラーが出たところだけ直す、
ということになってしまうかもしれません。
クロージャと大域脱出
Kotlinのクロージャは思わぬ機能を持っています。
次のコードは、他の言語に慣れていると意味不明に見えます。
なお、forEach
はクロージャを一つとり、
レシーバのリストの要素1つずつに対してクロージャを呼び出すものです。
fun hasZeros(ints: List<Int>): Boolean {
ints.forEach {
if (it == 0) { return true }
}
return false
}
実はこのコードでは、forEach
の中に書かれたreturn true
が、
そのクロージャ自身ではなく、fun hasZeros()
を脱出するのです。
そもそもKotlinでは、クロージャ{}
の中にはreturnが書けません。
クロージャの評価結果は、クロージャのコードの最後に書かれた式の値になります。
ただし例外として、インライン化された高階関数に渡されるクロージャの中だけは、
return
を書くことができて、その場合は、 return
から最も近いfun
を脱出します
forEachの実装は以下のようになっています。
public inline fun <T> Iterable<T>.forEach(operation: (T) -> Unit): Unit {
for (element in this) operation(element)
}
この、fun
の前についているinline
がポイントです。
これがついていると関数がインライン化され、
この関数を呼び出しているところに、この関数の中身がベタ書きされます。
つまり、上記例は下記のように解釈されます。
fun hasZeros(ints: List<Int>): Boolean {
for (i in ints) {
if (i == 0) { return true }
}
return false
}
これで、なぜ大域脱出ができてしまうのかがわかったと思います。
なお、inline
の指定は闇雲にできるものではなく、
インライン化できないような関数に付いている場合は、コンパイルエラーになります。
だから、この大域脱出の機能は危険な香りがするようでいて、
正しく使えるときだけ実装、利用することができ、
そうでないケースではコンパイルエラーとなるので安全です。
これができると、forEach
もそうですが、
高階関数を定義することで、制御構文を自作できるような効果があります。
例えば、run
という下記の標準関数が有ります。
public inline fun <R> run(f: () -> R): R = f()
引数として与えられたクロージャを実行するだけの関数ですが、
これが、ローカルなスコープを作るのに使えます。
fun setup() {
run {
val x = 3
if (!createPoint(x)) { return }
}
run {
val x = "taro"
if (!createUser(x)) { return }
}
println("ok!")
}
上記の例では、2つのx
はそれぞれ異なるクロージャのローカル変数なので衝突しません。
そして、createPoint
などが失敗した際には、setup
自体を中断しています。
Swiftで同じように、ローカルスコープのために高階関数を利用しようとすると、
その中ではreturnが使えなくなってしまうために、
for in
やif true { }
を使わざるを得ません。
逆に言うと、これらを使えば構文のようなものが作れるといえます。
runには実はもう一つ定義があって、それを使うとこんなコードが書けます。
class User {
var name: String = ""
var age: Int = 0
}
fun hoge(user: User) {
user.run {
name = makeUserName() ?: return
age = 3
}
}
run
の中でアクセスしているname
やage
は、user
のプロパティです。
このクロージャーの中が、Userのメソッドの実装時のようなthisスコープになっているのです。
そしてもちろん、その途中で大域脱出ができます。
これは下記のように実装されています。
public inline fun <T, R> T.run(f: T.() -> R): R = f()
全ての型Tに対する拡張メソッドrunとして定義されており、
引数のクロージャの型は、Tのメソッド、つまりレシーバとしてT型のインスタンスを受けるようになっています。
本体のf()
は、拡張メソッドの定義中なので、this.f()
の省略形です。
クロージャの型が、runの引数の型に基いて、
T型のメソッドの型として解決しているのです。
メソッドだからnameやageがthis.
無しでアクセスできるわけです。
この、クロージャのメソッドの型としての解決が本当に強力で、
複雑な応用例では下記があります。
fun result(args: Array<String>) =
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
// an element with attributes and text content
a(href = "http://kotlinlang.org") {+"Kotlin"}
// mixed content
p {
+"This is some"
b {+"mixed"}
+"text. For more see the"
a(href = "http://kotlinlang.org") {+"Kotlin"}
+"project"
}
p {+"some text"}
// content generated by
p {
for (arg in args)
+arg
}
}
}
一見HTMLを簡単な記法で書いているかのようですが、
これは正当なKotlinコードです。
しかも、bodyタグはhtmlタグの中に書く、といった事が、
静的に型検査されています。
ところで、大域ではなく、クロージャを中断するだけのローカルなreturnがしたい場合があります。
そのようなケースでは、もう一つのクロージャ記法が使えます。
listOf(1,2,3,4).forEach(fun(i) {
if (i % 2 == 0) return
print(i)
})
// 13が出力されます
fun
記法であれば、インライン化とは関係なくクロージャの中で常にreturnが使えます。
そしてローカルなreturnになります。
先程述べた return
から最も近いfun
を脱出するというルールにも合致しています。
プライマリコンストラクタ
Kotlinではコンストラクタを複数定義できます。
そして、特別なプライマリコンストラクタというコンストラクタを1つだけ作る事ができます。
これがある場合は、他のコンストラクタは最終的にプライマリを呼び出す必要があります。
そして、プライマリコンストラクタでは、引数定義と同時にプロパティ定義を行うことができ、
これがなかなか便利です。キーワードを1回書くだけで良いからです。
class Person(val name: String, val age: Int, val height: Double) {
init {
// プライマリコンストラクタの本文です
print("1")
}
constructor(name: String): this(name, 20, 170.0) {
// セカンダリコンストラクタの本文です
print("2")
}
constructor(): this("saito") {
// セカンダリコンストラクタその2です。他のセカンダリを呼び出します。
print("3")
}
}
fun main(args: Array<String>) {
Person("yamada", 19, 160.0) // 1が表示されます。
println()
Person("tanaka") // 12が表示されます。
println()
Person() // 123が表示されます。
println()
}
プライマリコンストラクタの引数についているval
が、プロパティ定義の指定です。
プライマリは定義しないこともできます。
Swiftの場合は、無印イニシャライザとコンビニエンスイニシャライザがあります。
Kotlinと同様、コンビニエンスは無印を呼び出す必要があります。
kotlinと異なり、無印版を複数定義できます。
コンストラクタでのプロパティ定義構文が無いので、
プロパティ、コンストラクタの引数、コンストラクタの本文での左辺値と右辺値で、
合計4回も同じキーワードを書かねばなりません。
class Person {
let name: String
let age: Int
let height: Double
init(_ name: String, _ age: Int, _ height: Double) {
// プライマリ1
self.name = name
self.age = age
self.height = height
}
init(_ name: String, _ age: Int, _ height: Int) {
// プライマリ2
self.name = name
self.age = age
self.height = Double(height)
}
convenience init(_ name: String) {
// セカンダリ1
self.init(name, 20, 170.0)
}
convenience init() {
// セカンダリ2
self.init("saito")
}
}
移植の観点では、Swiftで無印が複数あっても、
プロパティ全てを埋めるプライマリを新たに作って、
無印とコンビニエンスを全てセカンダリとして書けば、
だいたい大丈夫だと思います。
Javaの場合はSwiftと大体同じルールですが、
convenience
キーワードのようなものは無いですね。
特別な型
Kotlinが定義している特別な型について紹介します。
Any
Anyは全ての型を代入可能な型です。
ただし、オプショナル型は含まれません。
ジェネリクスの型パラメータを定義するとき、
nullを排除するときに使ったりします。
class NonNullBox<T: Any>
class NullableBox<T>
NonNullBox
にはオプショナル型は入れられませんが、
NullableBox
には入れられます。
Unit
Unitは値が一つしか無く、他の型と独立な型です。
C言語のvoidやSwiftのVoid
などに対応し、関数返り値の型を省略した時はUnitになっています。
Unit
型の値はUnit
です。
fun a(): Unit {
return Unit
}
fun b(): Unit {
return
}
fun c() { }
a
, b
, c
はどれも同じ意味です。
逆にvoid的なものはKotlinには存在しません。
Nothing
Nothingは値が存在せず、他の全ての型 に 代入可能な型です。
Anyは全ての型 を 代入可能ですが、それと逆になっています。
値が存在しないため、関数の返り値に指定すると、
入ったら絶対に脱出しない関数になります。
値が存在しないので返り値をreturnできないからです。
下記のようなコードがコンパイルできます。
fun crash(): Nothing {
throw Exception()
}
fun mainLoop(proc: ()-> Unit): Nothing {
while (true) {
proc()
}
}
他にも、Nothingの値が存在しないことを利用して、
null
にだけマッチする変数の型を作ることができます。
下記に例を示します。
class Json {
constructor(aNull: Nothing?) {}
constructor(aString: String) {}
}
このようにすると、Json(null)
は1つ目のコンストラクタ、
Json("aaa")
は2つ目のコンストラクタというふうにオーバロードを区別できます。
Kotlinにはnull
自体には型が無いので、
このようにNothingが使えます。
さて、値が存在しないのに代入可能というのはどういうことかというと、
ジェネリクスのバリアンスでこれが効いてきます。
下記に例を示します。
class Result<out T: Any, out E: Any>
private constructor(
val value: T?,
val error: E?)
{
companion object {
fun <T: Any, E: Any> Ok(value: T): Result<T, E> = Result(value, null)
fun <T: Any, E: Any> Error(error: E): Result<T, E> = Result(null, error)
}
}
fun proc1(): Result<Int, Nothing> {
return Result.Ok(3)
}
fun main(args: Array<String>) {
val ret: Result<Int, Exception> = proc1()
}
Resultは値とエラーの2つの型をcovarianceで持つジェネリクス型です。
ここで、proc1
は絶対に失敗しないメソッドなので、
エラーの型をNothing
にして定義しています。
そしてその結果を、Result<Int, Exception>
に代入しています。
つまり、一般のエラーがありうる場合の処理に対して、
エラーが無かった場合の型を、キャスト無しで安全に代入できています。
これはNothing is Exception
だからですが、isの右側にはどんな型でも取れます。
ExceptionではなくエラーメッセージとしてStringでエラーハンドリングしているケースでも、
Result<Int, String>
に対してResult<Int, Nothing>
を代入することができるのです。
値が存在しないからこそ何にでも成れるというのはおもしろいです。
データクラスとタプル
Kotlinにはデータクラスという機能があります。
data class Vector3(val x: Double, val y: Double, val z: Double)
fun main(args: Array<String>) {
val a = Vector3(1.0, 2.0, 3.0)
println(a) // Vector3(x=1.0, y=2.0, z=3.0) と出力されます。
val (x, y, z) = a
val b = a.copy(x=0.0, z=4.0)
println(b) // Vector3(x=0.0, y=2.0, z=4.0) と出力されます。
}
データクラスにすると、いくつかのメソッドが自動定義されます。
equals
とhashCode
が定義されます。
これによって、値比較ができるようになり、マップのキーとして使えるようになります。
toString
が定義されます。
プロパティの中身が表示されるのでデバッグが楽です。
componentN
が定義されます。
Vector3の場合は、component1()
, component2()
, component3()
が定義されます。
これはそれぞれのプロパティのゲッターです。
そして、これが定義されているクラスは、
それらのプロパティを変数にバラバラに代入することができます。
val (x, y, z) = a
となっているところです。
copy
が定義されます。
これは、プロパティと同名の引数を取るメソッドで、
デフォルト引数として自身のプロパティ値が設定されています。
そして、引数で指定されたプロパティを指定した新たらインスタンスを返します。
そのため、特定のプロパティだけを書き換えたコピーを作るメソッドになります。
イミュータブルプログラミングをしようとすると、
特定のプロパティだけ変更したコピーを作るのが面倒です。
withName(newName) // nameだけ変更したコピーを返す
のような、
一つだけ変更するものを全てのプロパティについて用意しても、
複数変更する場合はメソッドチェーンをそれだけ書かなければなりません。
一方、全てのプロパティを取るものとしてコンストラクタはありますが、
全ては変更しない場合には、同じ値を再指定するのが面倒です。
このcopy
はこの面倒臭さからプログラマを解放して、
イミュータブル主義をもっと簡単に使えるようにしてくれます。
Kotlinにはタプルがありません。
しかし、データクラスを使えば同じ要求が満たせます。
上記に例を示したとおり、
クラス定義と言っても最小限のタイピング量で済んでおり、
あまりめんどくさくありません。
別名インポート
Kotlinには別名インポートがあります。
異なる2つのパッケージで同じクラス名があるとき、
それぞれを別名をつけてインポートすることで、
実装部で長いフルネームを書く必要がありません。
import com.omochimetaru.Bitmap as MyBitmap
import android.graphics.Bitmap as ABitmap
fun hoge(a: MyBitmap) {
}
fun fuga(a: ABitmap) {
}
Swiftも同じことができます。
Javaはこれがつらいですね。
import com.omochimetaru.Bitmap;
import android.graphics.Bitmap;
void hoge(com.omochimetaru.Bitmap a) {
}
void fuga(android.graphics.Bitmap a) {
}
Enum, 値付きEnum, sealed class(Tagged Enum)
Kotlinにはenumがあります。
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
2つ目の例のように、値つきenumも作れます。
しかし、Swiftでできるような、enumの値ごとに異なったプロパティを持たせる、
Tagged Enumというやつは、enumでは作れません。
Swiftの例を示します。
enum Either<T, U> {
case Left(T)
case Right(U)
}
LeftとRightでプロパティの型が違っています。
その他、OptionalではSomeにはプロパティがあるがNoneには無い、
といったパターンもあります。
Kotlinではsealed class
を使って同じ事ができます。
sealed classは、継承を禁止したクラスです。
しかし、そのクラスの内部では継承する事ができます。
これによって、事前に用意したサブクラスのみが存在するクラスとなります。
そうすると、when文(C言語のswitchのようなもの)において、
型判定の網羅チェックが働くようになり、分岐の取りこぼしが無い事がコンパイラによって保証されます。
sealed class Expr {
class Const(val number: Double) : Expr()
class Sum(e1: Expr, e2: Expr) : Expr()
object NotANumber : Expr()
}
fun eval(expr: Expr): Double = when(expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
NotANumber -> Double.NaN
// the `else` clause is not required because we've covered all the cases
}
上記の例の通り、smart castがあるので、whenの各節では、
同じ変数名がすでにキャスト済みになっています。
Type Aliasが無い
Kotlinにはタイプエイリアスがありません。
Swiftではややこしいクロージャの型とかに名前をつけたりするのですが、
移植するときに全部ベタ書きの定義になってしまいます。
おわりに
ここには書ききれていないこともありますが、
ここまで読んだ人は結構Kotlinが使いたくなってきたんじゃないでしょうか!