Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 5 years have passed since last update.
Android開発を受注したからKotlinをガッツリ使ってみたら最高だった
関連記事
この記事も古くなりましたね。執筆時の実装バージョン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スクリプトが、下記のように変更されます。
applyplugin:'com.android.application'android{compileSdkVersion22buildToolsVersion"22.0.1"defaultConfig{applicationId"jp.co.qoncept.apptest"minSdkVersion18targetSdkVersion22versionCode1versionName"1.0"}buildTypes{release{minifyEnabledfalseproguardFilesgetDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}}}dependencies{compilefileTree(dir:'libs',include:['*.jar'])compile'com.android.support:appcompat-v7:22.2.0'}applyplugin:'com.android.application'applyplugin:'kotlin-android'android{compileSdkVersion22buildToolsVersion"22.0.1"defaultConfig{applicationId"jp.co.qoncept.apptest"minSdkVersion18targetSdkVersion22versionCode1versionName"1.0"}buildTypes{release{minifyEnabledfalseproguardFilesgetDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}}sourceSets{main.java.srcDirs+='src/main/kotlin'}}dependencies{compilefileTree(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は違います。
型推論は基本ですよね。
見た目
funmain(args:Array<String>){println("Hello, world!")}セミコロンレススタイル、コードブロックはブレーススタイル、型表記はパスカルスタイル(変数、コロン、型の並び)
varsum=0listOf(1,2,3).filter{it>0}.forEach{sum+=it}print(sum)クロージャはブレース{}だけで書く、末尾引数のクロージャを関数呼び出しの後ろに書け、その時引数が他に無ければパーレン()を省略できる
この辺りの構文仕様はSwiftと同じなので、移植作業が楽になります。
Optional (Nullable)
型として、nullを持つ型と持たない型が区別されます。
型検査をして中身がnullで無いことを確認すると、その時点で中身の型にキャストされます。
一般的にこの機能を提供するものをOptionalといいますが、KotlinではNullableと言います。
fungetLengthOfString(str:String):Int{returnstr.length()}fungetLengthOfStringOpt(str:String?):Int{if(str!=null){returngetLengthOfString(str)}else{return0}}funmain(args:Array<String>){vala=getLengthOfString("hello")valb=getLengthOfStringOpt("world")valc=getLengthOfStringOpt(null)println("$a, $b, $c")}Nullable型は、中身の型の右にハテナ?をつけて表記します。
SwiftのOptional型と同じ書き方なのが嬉しいです。
Javaには言語機能としてのOptionalはありません。ヌルポで死にます。
(標準ライブラリは言語機能に含めない、と考えています。なお、Javaの標準ライブラリのOptionalは、null安全性を提供する機能としては不十分だと思います。)
ちょっと変なところ
NullableのNullableが作れません。Nullableになってしまいます。
SwiftでOptionalのOptionalが出てくるコードの移植では工夫が必要です。
funwrap(a:Int?):Int??{returna}fundesc(a:Int??){if(a==null){println("None")}else{if(a==null){println("Some(None)")}else{println("Some(Some($a))")}}}funmain(args:Array<String>){vala:Int??=wrap(null)desc(a)// Some(None)が期待されるが、Noneとなる。}フローベースの型キャスト(smart casts)
if文でnullチェックしたりis演算子で型チェックをすると、それを考慮して型が自動的にキャストされます。
openclassAnimal{}classCat:Animal(){funnyaa(){println("nyaa")}}classDog:Animal(){funwan(){println("wan")}}funspeak(animal:Animal){if(animalisCat){animal.nyaa()}if(animalisDog){animal.wan()}}funspeak2(animal:Animal?){if(animal==null){println("null")return}speak(animal)}funmain(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版は下記になります。
classAnimal{}classCat:Animal{funcnyaa(){print("nyaa")}}classDog:Animal{funcwan(){print("wan")}}funcspeak(animal:Animal){ifletanimal=animalas?Cat{animal.nyaa()}ifletanimal=animalas?Dog{animal.wan()}}funcspeak2(animal:Animal?){guardletanimal=animalelse{print("null")return}speak(animal)}funcmain(){speak2(Cat())speak2(Dog())speak2(nil)}main()speak2ではこのためにわざわざguard文とやらを使わないといけません。speak, speak2共に、let animal =を書くのが冗長です。
ifの丸括弧が省略できるのはいいですね。
Javaは下記のようになるでしょうか。
importjava.util.*;importjava.lang.*;importjava.io.*;classAnimal{}classCatextendsAnimal{voidnyaa(){Ideone.print("nyaa");}}classDogextendsAnimal{voidwan(){Ideone.print("wan");}}classIdeone{publicstaticvoidprint(Stringstr){System.out.println(str);}staticvoidspeak(Animalanimal){if(animalinstanceofCat){Catcat=(Cat)animal;cat.nyaa();}if(animalinstanceofDog){Dogdog=(Dog)animal;dog.wan();}}staticvoidspeak2(Animalanimal){if(animal==null){print("null");return;}speak(animal);}publicstaticvoidmain(String[]args)throwsjava.lang.Exception{speak2(newCat());speak2(newDog());speak2(null);}}nullチェックに関してはコードが正しいことを祈って実行するしかありません。そしてspeakについては、CatとDogをそれぞれ3回書かされます。(キャスト用の関数を用意すれば、2回 + nullチェックにはできますが)
Unsafe cast
Nullableがnullだった場合にクラッシュする中身の取り出しと、型が違った場合にクラッシュするキャストがあります。
funhoge(a:Int?,b:Animal?){valc:Int=a!!// nullだったら例外vald:Cat?=bas?Cat// Catで無ければnullvale:Cat=basCat// Catで無ければ例外}Swiftは下記のようになります。
funchoge(a:Int?,b:Animal?){letc:Int=a!// nilだったら例外letd:Cat?=bas?Cat// Catで無ければnillete:Cat=bas!Cat// Catで無ければ例外}Kotlinは2つのビックリ!で剥がせます。Swiftは1つです。
危険なasは、Swiftにはビックリがついています。
オプショナルのメソッド呼び出し
Kotlinでオプショナルに包まれた値のメソッドを呼び出す際、
値があればメソッド呼び出し、nullであればnullが欲しい場合、
ifでの型チェックをせずとも下記のように記述できます。
funhoge(user:User?){valname:String?=user?.nameprintln("name=$name")}elvis演算子を使えば、nullだった場合のデフォルトを指定できます。
funhoge(user:User?){valname:String=user?.name?:"no name"println("name=$name")}Swiftにもハテナドット?.でのメソッド呼び出しがあります。
また、elvisについてはSwiftではハテナハテナ??です。
よく似ている2つの言語ですが、ハテナドット?.を立て続けた場合の構文木が異なっています。
ユーザーの名前の文字数を取得するケースを考えてみます。
classUser{varname:String="tanaka"}funhoge(user:User?){println(user?.name?.length())}Kotlinでは?.が2回出てきます。これは、次のように解釈されているからです。
(user?.name)?.length()?.を使わずに書くと次のようになります。
valname:String?=if(user!=null){user.name}else{null}vallength:Int?=if(name!=null){name.length}else{null}一方、Swiftの場合は次のようになります。
classUser{varname:String="tanaka"}funchoge(user:User?){print(user?.name.characters.count)}main()nameの後の?.がSwiftでは.になっています。これは、次のように解釈されているからです。
user?.(name.characters.count)ただし、このカッコは概念の説明のためであり、Swiftとしては不正になってしまいました。
書き下すと次のようになります。
ifletuser=user{returnuser.name.characters.count}else{returnnil}整理すると下記のようになっています。
Kotlinの場合は、?.は一つ次のキーワードだけを処理し、その結果をさらにその右で使用します。
Swiftの場合は、?.はそれより右全てを括っており、Noneだった場合は右全てをスキップします。
この違いは全く同じ見た目のコードが、全く違う意味を持つことになるので、
移植する際は注意が必要です。
個人的にはKotlinの仕様の方が直感的で好きです。
始めてSwiftを書いた時、Kotlin仕様を想像していたので、
エラーが出て混乱した事があります。
Javaの場合は、第一引数にレシーバ、第二引数にオペレータを取る高階関数を作って、?.の挙動をエミュレートするのが良いでしょう。
if文で書くと、レシーバの式を2回書かないといけないからです。
メソッド呼び出しじゃないけどオプショナルにチェーンするやつ
上記の?.を使えばオプショナルでもめんどくさくならずにコードが書けますが、
次のように、?.では書けないけれど、nullじゃない場合に処理を続けたいケースが有ります。
valresult:Booleanif(user!=null){result=write(user)}else{result=false}こういうケースでは、kotlinでは次のような記法が使えます。
valresult:Boolean=user?.let{write(it)}?:falseletの定義、実装は次のようになっています。
publicinlinefun<T,R>T.let(f:(T)->R):R=f(this)これは、全ての型Tに対して定義された拡張メソッドで、
引数としてクロージャを一つ取ります。
そしてそのクロージャにメソッドのレシーバが渡され、すぐに呼びだされ、
その結果がlet自体の評価値となるものです。
上記の例では?.があるので、letが実行されるのはUser?がnullで無い場合です。
itはクロージャの暗黙の引数なので、it: Userとなっています。
そして、writeの返り値がletの返り値となるので、期待した挙動になるわけです。
Swiftの場合は、Optional自体に定義されたflatMapメソッドが使えます。
letresult:Bool=user.flatMap{write($0)}??falseこの場合は、オプショナル自体のメソッドなので?.ではなく.になります。
基本的な高階関数
基本的な高階関数が使えます。クロージャが{}なのでシンプルに書けます。
funmain(args:Array<String>){vala=(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も似たような感じでかけます。
leta=(0...10).filter{$0%2==0}.map{$0*$0}.reduce(""){lets=$0!=""?$0+"_":""returns+String($1)}print(a)// 0_4_16_36_64_100 が出力されるJavaだとこうでしょうか。
Stringa=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は文字列中に$で変数が、${}で式が書けます。
funhoge(i:Int,user:User){println("i is $i, user name is ${user.name}")}$iのところが変数展開、${user.name}のところが式展開です。
Swiftは\()です。
funchoge(i:Int,user:User){print("i is\(i), user name is\(user.name)")}Javaは文法が無いので、下記のようになるでしょう。
voidhoge(inti,Useruser){print("i is "+i+", user name is "+user.name);}JavaのSAM変換
JavaではJava8が出た時に、ラムダ式とSAM変換という大きな機能の追加がありました。
これは、メソッドを1つだけ持つインターフェースを引数にとる関数の呼びだし箇所において、
ラムダ式が書けるようになるというものです。
例えば下記がJava7のコードです。
Androidでよくある、ボタンのクリックハンドラを設定するものです。
button.setOnClickListener(newView.OnClickListener{@OverridevoidonClick(Viewview){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は既存のクラスに対して、あとからメソッドを付け足す事ができます。
funInt.square():Int=this*thisfun<T>List<T>.evens():List<T>=withIndex().filter{it.index%2==0}.map{it.value}funList<Int>.squareEvens():List<Int>=evens().map{it.square()}funmain(args:Array<String>){vala=3println(a.square())// 9と出力valb=listOf("a","b","c","d","e")println(b.evens())// [a, c, e]と出力valc=listOf(10,20,30,40,50)println(c.squareEvens())// [100, 900, 2500]と出力}ジェネリクス型の拡張メソッドについては、T全てについてと、特定のTについての定義ができます。
関数の本文は=スタイルで書いてみました。
Swiftでは下記となります。
extensionIntegerType{funcsquare()->Self{returnself*self}}extensionArray{funcevens()->Array<Element>{returnenumerate().filter{$0.index%2==0}.map{$0.element}}}extensionArraywhereElement:IntegerType{funcsquareEvens()->Array<Element>{returnevens().map{$0.square()}}}funcmain(){leta=3print(a.square())// 9と出力letb=["a","b","c","d","e"]print(b.evens())// ["a", "c", "e"]と出力letc=[10,20,30,40,50]print(c.squareEvens())// [100, 900, 2500]と出力}main()Elementに対する制約はプロトコルの必要があるようで、Intでは書けなかったのでIntegerTypeになっています。理由がよくわかりませんでした。
KotlinもSwiftも、同様にしてプロパティを追加することができます。
移植ではwithIndexとenumerateの対応も嬉しいです。
Javaでは拡張メソッドが無いので、第一引数にthisを取るスタティックメソッドとして実装するでしょう。
publicclassMain{publicstaticvoidprint(Stringstr){System.out.println(str);}publicstaticintintSquare(intx){returnx*x;}publicstatic<T>List<T>listEvens(List<T>list){returnIntStream.range(0,list.size()).filter(i->i%2==0).mapToObj(i->list.get(i)).collect(Collectors.toList());}publicstaticList<Integer>intListSquareEvens(List<Integer>list){returnlistEvens(list).stream().map(i->intSquare(i)).collect(Collectors.toList());}publicstaticvoidmain(String[]args)throwsjava.lang.Exception{inta=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 classVector2(valx:Double,valy:Double){operatorfunplus(o:Vector2):Vector2=Vector2(x+o.x,y+o.y)infixfundot(o:Vector2):Double=x*o.x+y*o.yoverridefuntoString():String="($x, $y)"}operatorfunDouble.times(o:Vector2):Vector2=Vector2(this*o.x,this*o.y)funmain(args:Array<String>){vala=Vector2(1.0,2.0)+Vector2(3.0,4.0)println(a)// (4.0, 6.0)と出力valb=3.0*Vector2(0.0,1.0)println(b)// (0.0, 3.0)と出力valc=Vector2(2.0,0.0)dotVector2(2.0,3.0)println(c)// 4.0と出力}足し算+はメソッド、掛け算*はDoubleの拡張メソッドとして書きました。
dotを中置で呼び出しています。
データクラスとプライマリコンストラクタの機能も使用しています。
Swiftでも書いてみます。
classVector2:CustomStringConvertible{letx:Doublelety:Doubleinit(_x:Double,_y:Double){self.x=xself.y=y}vardescription:String{return"(\(x),\(y))"}}func+(l:Vector2,r:Vector2)->Vector2{returnVector2(l.x+r.x,l.y+r.y)}func*(l:Double,r:Vector2)->Vector2{returnVector2(l*r.x,l*r.y)}infixoperator●{associativityleftprecedence140}func●(l:Vector2,r:Vector2)->Double{returnl.x*r.x+l.y*r.y}funcmain(){leta=Vector2(1.0,2.0)+Vector2(3.0,4.0)print(a)// (4.0, 6.0)と出力letb=3.0*Vector2(0.0,1.0)print(b)// (0.0, 3.0)と出力letc=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というキーワードでアクセスできます。
classUser{valid:IntvarfamilyName:String="yamada"varfirstName:String="taro"valfullName:Stringget()="$familyName $firstName"vardied:Boolean=falseget(){returnfield}set(value){field=valueif(value){println("${fullName}は死んでしまった")}}constructor(id:Int){this.id=id}}funmain(args:Array<String>){valu=User(3)u.familyName="saito"u.died=true// saito taroは死んでしまった と表示されます}上記の例では、idはvalなのでゲッターが自動定義、familyName、firstNameはvarなのでゲッターとセッターが自動定義されています。fullNameはゲッターを自作して、他のプロパティから動的に値を生成しています。diedはゲッターとセッターを自作しつつ、バッキングフィールドを使用しています。
Swiftでもフィールドのようなものはプロパティです。
ゲッターセッターだけでなく、willSetやdidSetといったものも定義できます。
バッキングフィールドは自動定義されません。
KotlinにはdidSetなどの言語機能は無いため、移植の場合はセッター上でエミュレートします。
classUser{letid:IntvarfamilyName:String="yamada"varfirstName:String="taro"varfullName:String{get{return"\(familyName)\(firstName)"}}vardied:Bool=false{didSet{ifdied{print("\(fullName)は死んでしまった")}}}init(_id:Int){self.id=id}}funcmain(){letu=User(3)u.familyName="saito"u.died=true// saito taroは死んでしまった と表示されます}main()Javaではフィールドとプロパティは明確に区別されていて、
メソッドとしてゲッターとセッターを自力で定義したものをプロパティと呼びます。
これが移植の際に面倒な事になります。
Swiftで書かれた次のクラスがあったとします。
classUser{vardied:Bool=false}funchoge(u:User){u.died=true}これをJavaでフィールドに移植したとします。
classUser{booleandied=false;}voidhoge(Useru){u.died=true}そのあと、Swift版が次のように変更されたとします。
classUser{vardied:Bool=falsedidSet{println("死んでしまった!")}}この際、Javaは次のように修正が必要です。
classUser{booleandied=false;booleangetDied(){returndied;}voidsetDied(booleanvalue){died=value;println("死んでしまった!");}}voidhoge(Useru){u.setDied(true);}ゲッターとセッターを実装するのは良いとして、
フィールドへの代入をしている部分をセッターの呼び出しに変更する
必要があります。
これは、複数箇所あるうえに、移植元ではdiffが生じないため、
見落としてしまうリスクが高いです。
見落としてしまえばバグになります。
しかもコンパイル時にはわかりません。
10箇所ある代入のつい1箇所だけ対応忘れがあったりすれば、
やっかいなバグとなるでしょう。
なので、プロパティがある言語から移植するなら、プロパティがある言語が望ましいのです。
Javaプロパティアクセサのプロパティ化
Javaにおいて、フィールド名nameに対して、nameのプロパティを作る際は、
ゲッターString getName()とセッターsetName(String name)を定義します。
そして、呼び出しの際は下記のように関数呼び出しの形を取ります。
// 読み込みStringname=user.getName();// 書き込みuser.setName(newName);しかし、Kotlinの場合は、プロパティnameに対しては、
呼び出しの際は関数呼び出しの形を取りません。
// 読み込みvalname=user.name// 書き込みuser.name=newName関数呼び出しの形ではありませんが、nameのゲッターやセッターが呼び出されます。
さて、KotlinがJavaのメソッドを呼び出す際、
このようなgetXxxx()とsetXxxx(value)を、
Kotlinにおけるプロパティxxxxの扱いでアクセスできます。
例えば下記はAndroidでボタンを非表示にする例です。
button.visibility=View.INVISIBLEAndroid SDKはJavaで定義されており、
本来のJavaではsetVisibility()を呼び出すところですが、
Kotlinからはvisibilityプロパティのように取り扱えるのです。
Delegated Property
Delegated PropertyはKotlinのおもしろい機能です。
プロパティのゲッターやセッターの実装を、別のオブジェクトに移譲する事ができます。
Lazy
例としてLazyを取り上げます。
valfullName:Stringbylazy{familyName+" "+firstName}fullNameはvalなので定数ですが、初めてゲッターが呼ばれた時に、
lazyに渡しているクロージャが実行され、その評価結果が返ります。
2回目以降のゲッター呼び出しては、初回の結果が返されます。
もしこれをJavaなどで実装しようとした場合、
ゲッターの中でif文を書いたりすることになります。
そうした定型で冗長な部分を記述する必要がありません。
Swiftにもlazyというキーワードがあり、
同じ機能を提供する言語の機能があります。
しかしこれと比べてKotlinが興味深いのは、lazyは特別な言語機能ではなく、byのみが言語機能で、
lazyはクロージャを引数に取る標準ライブラリ関数という事です。
この関数が返すオブジェクトが、実際のプロパティのゲッター、セッターを処理します。
notNull
この節の内容は古くなりました。M13からはlateinitを使ったほうが良いと思います。
もう一つ興味深いデリゲートを紹介します。
varname:StringbyDelegates.notNull()これは、1度もセットされていない状態でゲッターが呼ばれると例外が飛んでクラッシュし、
1度セットされたあとなら、ゲッターはその値が普通に読み取れます。
Swiftにおいてこれと近い意味をもつのは、ビックリ!型です。
正確にはImplicitly Unwrapped Optionalと言います。
varname:String!こいつは初期状態がnilで、nilの状態のときに読むとクラッシュしますが、
値が入っていれば普通に読めて、普段は値の型として振る舞うものです。
Kotlinとの微妙な違いは、KotlinのnotNullにnullを入れる事はできないけれど、
Swiftのビックリにはnilを入れる事ができる点です。
SwiftのものはあくまでOptionalということですね。
しかし、大体の場合でわざわざnilを入れることはしないので、
(そういう場合は普通のOptionalが望ましいからです)
移植はだいたいこれでいけます。
このケースも、Swiftの!は言語機能なのに対して、
Kotlinのこれはやはり標準ライブラリで提供される実装です。
おもしろいです。
Kotterknife
Android開発といえばビューのバインディングですが、
butterknifeの作者さんがkotterknifeというKotlin版のバターナイフを提供しています。
これは、このbyを使ってバインディングするものです。
サンプルコードを引用します。
publicclassPersonView(context:Context,attrs:AttributeSet?):LinearLayout(context,attrs){valfirstName:TextViewbybindView(R.id.first_name)vallastName:TextViewbybindView(R.id.last_name)// Optional binding.valdetails:TextView?bybindOptionalView(R.id.details)// List binding.valnameViews:List<TextView>bybindViews(R.id.first_name,R.id.last_name)// List binding with optional items being omitted.valnameViews:List<TextView>bybindOptionalViews(R.id.first_name,R.id.middle_name,R.id.last_name)}@IBOutletと!を使うiOS開発や、アノテーションとリフレクションを駆使したJavaのButterknifeより、
この方式が綺麗で好ましいと思います。
なお、ビルドに介入することでエクステンションメソッドを自動実装してくれて、
プロパティ定義すら不要なプラグインが有ります。
僕は言語機能での実装が好ましいと思います。
lateinit
プロパティに対する修飾子としてlateinitを使うと、
初期値が不要な非オプショナル型の変数が定義できます。
classUser{lateinitvarname: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になっています。
openclassAnimalclassCat:Animal()classBox<outT>(valvalue:T){overridefuntoString():String="Box($value)"}funmain(args:Array<String>){vala:Box<Animal>valb:Box<Cat>=Box(Cat())a=bprintln(a)// Box(Cat@xxxxxxxx)と表示}バリアンスが機能しているため、Boxの値をBoxの変数に代入できています。
Declaration Siteというのは、宣言時指定ということで、Boxの型パラメータTを書くその場で、out Tと記述することで、BoxがTに関してcovarianceであると宣言しています。
このoutを消すとコンパイルエラーとなります。
SwiftもDeclaration Siteであるのに対して、JavaがUse Siteです。
Javaで上記の例を書くと以下のようになります。
classAnimal{}classCatextendsAnimal{}classBox<T>{finalTvalue;Box(Tvalue){this.value=value;}publicStringtoString(){return"Box("+value.toString()+")";}}publicclassMain{publicstaticvoidprint(Stringstr){System.out.println(str);}publicstaticvoidmain(String[]args)throwsjava.lang.Exception{finalBox<?extendsAnimal>a;finalBox<Cat>b=newBox<Cat>(newCat());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つずつに対してクロージャを呼び出すものです。
funhasZeros(ints:List<Int>):Boolean{ints.forEach{if(it==0){returntrue}}returnfalse}実はこのコードでは、forEachの中に書かれたreturn trueが、
そのクロージャ自身ではなく、fun hasZeros()を脱出するのです。
そもそもKotlinでは、クロージャ{}の中にはreturnが書けません。
クロージャの評価結果は、クロージャのコードの最後に書かれた式の値になります。
ただし例外として、インライン化された高階関数に渡されるクロージャの中だけは、returnを書くことができて、その場合は、returnから最も近いfunを脱出します
forEachの実装は以下のようになっています。
publicinlinefun<T>Iterable<T>.forEach(operation:(T)->Unit):Unit{for(elementinthis)operation(element)}この、funの前についているinlineがポイントです。
これがついていると関数がインライン化され、
この関数を呼び出しているところに、この関数の中身がベタ書きされます。
つまり、上記例は下記のように解釈されます。
funhasZeros(ints:List<Int>):Boolean{for(iinints){if(i==0){returntrue}}returnfalse}これで、なぜ大域脱出ができてしまうのかがわかったと思います。
なお、inlineの指定は闇雲にできるものではなく、
インライン化できないような関数に付いている場合は、コンパイルエラーになります。
だから、この大域脱出の機能は危険な香りがするようでいて、
正しく使えるときだけ実装、利用することができ、
そうでないケースではコンパイルエラーとなるので安全です。
これができると、forEachもそうですが、
高階関数を定義することで、制御構文を自作できるような効果があります。
例えば、runという下記の標準関数が有ります。
publicinlinefun<R>run(f:()->R):R=f()引数として与えられたクロージャを実行するだけの関数ですが、
これが、ローカルなスコープを作るのに使えます。
funsetup(){run{valx=3if(!createPoint(x)){return}}run{valx="taro"if(!createUser(x)){return}}println("ok!")}上記の例では、2つのxはそれぞれ異なるクロージャのローカル変数なので衝突しません。
そして、createPointなどが失敗した際には、setup自体を中断しています。
Swiftで同じように、ローカルスコープのために高階関数を利用しようとすると、
その中ではreturnが使えなくなってしまうために、for inやif true { }を使わざるを得ません。
逆に言うと、これらを使えば構文のようなものが作れるといえます。
runには実はもう一つ定義があって、それを使うとこんなコードが書けます。
classUser{varname:String=""varage:Int=0}funhoge(user:User){user.run{name=makeUserName()?:returnage=3}}runの中でアクセスしているnameやageは、userのプロパティです。
このクロージャーの中が、Userのメソッドの実装時のようなthisスコープになっているのです。
そしてもちろん、その途中で大域脱出ができます。
これは下記のように実装されています。
publicinlinefun<T,R>T.run(f:T.()->R):R=f()全ての型Tに対する拡張メソッドrunとして定義されており、
引数のクロージャの型は、Tのメソッド、つまりレシーバとしてT型のインスタンスを受けるようになっています。
本体のf()は、拡張メソッドの定義中なので、this.f()の省略形です。
クロージャの型が、runの引数の型に基いて、
T型のメソッドの型として解決しているのです。
メソッドだからnameやageがthis.無しでアクセスできるわけです。
この、クロージャのメソッドの型としての解決が本当に強力で、
複雑な応用例では下記があります。
funresult(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 contenta(href="http://kotlinlang.org"){+"Kotlin"}// mixed contentp{+"This is some"b{+"mixed"}+"text. For more see the"a(href="http://kotlinlang.org"){+"Kotlin"}+"project"}p{+"some text"}// content generated byp{for(arginargs)+arg}}}一見HTMLを簡単な記法で書いているかのようですが、
これは正当なKotlinコードです。
しかも、bodyタグはhtmlタグの中に書く、といった事が、
静的に型検査されています。
ところで、大域ではなく、クロージャを中断するだけのローカルなreturnがしたい場合があります。
そのようなケースでは、もう一つのクロージャ記法が使えます。
listOf(1,2,3,4).forEach(fun(i){if(i%2==0)returnprint(i)})// 13が出力されますfun記法であれば、インライン化とは関係なくクロージャの中で常にreturnが使えます。
そしてローカルなreturnになります。
先程述べたreturnから最も近いfunを脱出するというルールにも合致しています。
プライマリコンストラクタ
Kotlinではコンストラクタを複数定義できます。
そして、特別なプライマリコンストラクタというコンストラクタを1つだけ作る事ができます。
これがある場合は、他のコンストラクタは最終的にプライマリを呼び出す必要があります。
そして、プライマリコンストラクタでは、引数定義と同時にプロパティ定義を行うことができ、
これがなかなか便利です。キーワードを1回書くだけで良いからです。
classPerson(valname:String,valage:Int,valheight:Double){init{// プライマリコンストラクタの本文ですprint("1")}constructor(name:String):this(name,20,170.0){// セカンダリコンストラクタの本文ですprint("2")}constructor():this("saito"){// セカンダリコンストラクタその2です。他のセカンダリを呼び出します。print("3")}}funmain(args:Array<String>){Person("yamada",19,160.0)// 1が表示されます。println()Person("tanaka")// 12が表示されます。println()Person()// 123が表示されます。println()}プライマリコンストラクタの引数についているvalが、プロパティ定義の指定です。
プライマリは定義しないこともできます。
Swiftの場合は、無印イニシャライザとコンビニエンスイニシャライザがあります。
Kotlinと同様、コンビニエンスは無印を呼び出す必要があります。
kotlinと異なり、無印版を複数定義できます。
コンストラクタでのプロパティ定義構文が無いので、
プロパティ、コンストラクタの引数、コンストラクタの本文での左辺値と右辺値で、
合計4回も同じキーワードを書かねばなりません。
classPerson{letname:Stringletage:Intletheight:Doubleinit(_name:String,_age:Int,_height:Double){// プライマリ1self.name=nameself.age=ageself.height=height}init(_name:String,_age:Int,_height:Int){// プライマリ2self.name=nameself.age=ageself.height=Double(height)}convenienceinit(_name:String){// セカンダリ1self.init(name,20,170.0)}convenienceinit(){// セカンダリ2self.init("saito")}}移植の観点では、Swiftで無印が複数あっても、
プロパティ全てを埋めるプライマリを新たに作って、
無印とコンビニエンスを全てセカンダリとして書けば、
だいたい大丈夫だと思います。
Javaの場合はSwiftと大体同じルールですが、convenienceキーワードのようなものは無いですね。
特別な型
Kotlinが定義している特別な型について紹介します。
Any
Anyは全ての型を代入可能な型です。
ただし、オプショナル型は含まれません。
ジェネリクスの型パラメータを定義するとき、
nullを排除するときに使ったりします。
classNonNullBox<T:Any>classNullableBox<T>NonNullBoxにはオプショナル型は入れられませんが、NullableBoxには入れられます。
Unit
Unitは値が一つしか無く、他の型と独立な型です。
C言語のvoidやSwiftのVoidなどに対応し、関数返り値の型を省略した時はUnitになっています。Unit型の値はUnitです。
funa():Unit{returnUnit}funb():Unit{return}func(){}a,b,cはどれも同じ意味です。
逆にvoid的なものはKotlinには存在しません。
Nothing
Nothingは値が存在せず、他の全ての型に 代入可能な型です。
Anyは全ての型を 代入可能ですが、それと逆になっています。
値が存在しないため、関数の返り値に指定すると、
入ったら絶対に脱出しない関数になります。
値が存在しないので返り値をreturnできないからです。
下記のようなコードがコンパイルできます。
funcrash():Nothing{throwException()}funmainLoop(proc:()->Unit):Nothing{while(true){proc()}}他にも、Nothingの値が存在しないことを利用して、nullにだけマッチする変数の型を作ることができます。
下記に例を示します。
classJson{constructor(aNull:Nothing?){}constructor(aString:String){}}このようにすると、Json(null)は1つ目のコンストラクタ、Json("aaa")は2つ目のコンストラクタというふうにオーバロードを区別できます。
Kotlinにはnull自体には型が無いので、
このようにNothingが使えます。
さて、値が存在しないのに代入可能というのはどういうことかというと、
ジェネリクスのバリアンスでこれが効いてきます。
下記に例を示します。
classResult<outT:Any,outE:Any>privateconstructor(valvalue:T?,valerror:E?){companionobject{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)}}funproc1():Result<Int,Nothing>{returnResult.Ok(3)}funmain(args:Array<String>){valret: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 classVector3(valx:Double,valy:Double,valz:Double)funmain(args:Array<String>){vala=Vector3(1.0,2.0,3.0)println(a)// Vector3(x=1.0, y=2.0, z=3.0) と出力されます。val(x,y,z)=avalb=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つのパッケージで同じクラス名があるとき、
それぞれを別名をつけてインポートすることで、
実装部で長いフルネームを書く必要がありません。
importcom.omochimetaru.BitmapasMyBitmapimportandroid.graphics.BitmapasABitmapfunhoge(a:MyBitmap){}funfuga(a:ABitmap){}Swiftも同じことができます。
Javaはこれがつらいですね。
importcom.omochimetaru.Bitmap;importandroid.graphics.Bitmap;voidhoge(com.omochimetaru.Bitmapa){}voidfuga(android.graphics.Bitmapa){}Enum, 値付きEnum, sealed class(Tagged Enum)
Kotlinにはenumがあります。
enumclassDirection{NORTH,SOUTH,WEST,EAST}enumclassColor(valrgb:Int){RED(0xFF0000),GREEN(0x00FF00),BLUE(0x0000FF)}2つ目の例のように、値つきenumも作れます。
しかし、Swiftでできるような、enumの値ごとに異なったプロパティを持たせる、
Tagged Enumというやつは、enumでは作れません。
Swiftの例を示します。
enumEither<T,U>{caseLeft(T)caseRight(U)}LeftとRightでプロパティの型が違っています。
その他、OptionalではSomeにはプロパティがあるがNoneには無い、
といったパターンもあります。
Kotlinではsealed classを使って同じ事ができます。
sealed classは、継承を禁止したクラスです。
しかし、そのクラスの内部では継承する事ができます。
これによって、事前に用意したサブクラスのみが存在するクラスとなります。
そうすると、when文(C言語のswitchのようなもの)において、
型判定の網羅チェックが働くようになり、分岐の取りこぼしが無い事がコンパイラによって保証されます。
sealedclassExpr{classConst(valnumber:Double):Expr()classSum(e1:Expr,e2:Expr):Expr()objectNotANumber:Expr()}funeval(expr:Expr):Double=when(expr){isConst->expr.numberisSum->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が使いたくなってきたんじゃないでしょうか!
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme
