アプリボットで開発しているアプリ「Qunme」では、プログラミング言語としてiOSにSwiftを、AndroidにKotlinを採用しています。
Kotlin導入の狙いの一つに、Swiftと文法が似ているためロジックを共有して工数を削減できるのではないか、ということがありました。
実際に導入してみて、共有できる部分もありましたが共通化が難しい部分もありましたので、今回はSwiftとKotlinで違いが大きい部分を知見として紹介します。
似てない点1: extension + protocol
さて、似てない点です。
まずSwiftはクラスを拡張することで、メソッドや計算型プロパティを拡張することができます。
// Stringクラスを拡張
extension String{
// プロパティ(計算型のみ)
static var delimiter: String{
return ","
}
// メソッド
func getRepeated() -> String{
return self + self
}
}
// 123123,456456
print("123".getRepeated() + String.delimiter + "456".getRepeated())
これに対しKotlinは、メソッドやプロパティ単位で拡張します。
// Stringクラスのプロパティを拡張(バッキングフィールドをもたないもののみ)
val String.Companion.delimiter: String
get() = ","
// Stringクラスのメソッドを拡張
fun String.getRepeated(): String{
return this + this
}
// 123123,456456
println("123".getRepeated() + String.delimiter + "456".getRepeated())
このあたりはSwiftとKotlinを同じ設計方針で置き換え可能なのですが、置き換えられないのはSwiftでよく見る、拡張したクラスをprotocolに適合させる、というケースです。
// 拡張元クラス
class SomeViewController: UIViewController{
...
}
// 拡張してprotocolを適合
extension SomeViewController: UITableViewDelegate {
// UITableViewDelegateで定義されたメソッド群の実装(省略)
...
}
このような拡張したクラスに対してprotocol(Kotlinでいうinterface)をimplementする設計はKotlinではできません。
似てない点2: enum
enumは似ているようで似ていません。
違いの本質は、Swiftではenumの実体は(基本的に)Intであるのに対して、Kotlinではenumの実体はクラスだという点です。
例えば、Kotlinでは次のようにenumを初期化することができます。
// 各要素はEnumクラスのインスタンスなので初期値(lastNameとage)を持てる
enum class Character(val lastName: String, val age: Int){
TAKI("立花", 14),
MITSUHA("宮水", 17)
}
// 立花
println(Character.TAKI.lastName)
このように各要素が複数の初期値を持つ書き方はSwiftではできません。
上記をSwiftで表現するには次のようにします。
enum Character{
case Taki
case Mitsuha
// 計算プロパティ
var lastName: String{
switch self {
case .Taki: return "立花"
case .Mitsuha: return "宮水"
}
}
// メソッド
func getAge() -> Int{
switch self {
case .Taki: return 14
case .Mitsuha: return 17
}
}
}
// 立花
print(Character.Taki.lastName)
このような挙動から、SwiftとKotlinではenumの実体が異なることがわかります。
これがenumに関する1つ目の違いです。
また上記の用法とは別に、SwiftのenumにはAssociated valuesという、Kotlinにはないenumの用法があります。
// 定義
enum User{
case ID(Int)
case FullName(String, String)
}
// 初期化時に値を渡して保持させられる
let user = User.FullName("Taro", "Yamada")
これにはジェネリクスも適用できて、次のように使われたりします。
// Associated values + Generics
enum Result<T> {
case Success(T)
case Error(NSError)
}
// APIのコールバックなどでこんな感じで使う
// resultの型がResult<SomeResponse>
SomeAPI().fetch{ result in
switch result {
case .Success(let res):
// 成功時の処理
// resはSomeResponse型
case .Error(let error):
// 失敗時の処理
// errorはNSError型
}
}
このAssociated valuesはKotlinのenumでは実現できないので、Swiftと異なる点となります。
これがenumに関する2つ目の違いです。
ちなみにKotlinで上記のResultパターンを実現する場合、enum classではなくsealed classを使います。
// sealed class -> 内部クラスのみがこのクラスを継承できる かつ このクラス自体のインスタンス化はできない
sealed class Result<T>{
class Success<T>(val response: T): Result<T>()
class Error<T>(val error: Exception): Result<T>()
}
似てない点3: property
最後にプロパティです。
まず、Swiftの計算型プロパティは値を保持しません。
C#などでいうプロパティと同様です。
// 保持型プロパティ -> 値を保持する
private var _token: String? = nil
// 計算型プロパティ -> 値を保持しない(ここでは保持型プロパティにアクセス)
var token: String?{
get{
return _token
}
set{
_token = newValue
// ストレージに保存する
saveToStorage(newValue)
}
}
一方、Kotlinには値を保持しない計算型プロパティという考え方はありません。
Kotlinでは基本的に、プロパティを定義すると上記の例でいう保持型プロパティのような役割をするフィールド(バッキングフィールド)が暗黙的に定義されます。
上記のコードはKotlinでは次のように書けます。
// field -> 暗黙的に生成されるフィールド(バッキングフィールド)
// これがSwiftでいう保持型プロパティのように振る舞う
var token: String? = null
get() = field
set(value) {
field = value
saveToStorage(value)
}
そして、上記コード内のバッキングフィールドは生成する必要性がない場合には生成されず、その場合にはプロパティの初期化は必要ありません(というか不可能)。
したがってこの条件を満たしている場合のみ、KotlinのプロパティがSwiftの計算型プロパティと同等の挙動をします。
// バッキングフィールドに代わり値を保持するためのプロパティ
private var _token: String? = null
// 以下のプロパティはバッキングフィールドに一切アクセスしていない ∴生成されない
var token: String?
get() = _token
set(value) {
_token = value
saveToStorage(value)
}
SwiftとKotlinのプロパティにはこのような考え方の違いがあります。
ちなみに最初のSwiftのコードは保持型プロパティとプロパティ監視を使って端的に書くこともできます(こう書きましょう)。
// 保持型プロパティ + プロパティ監視
var token: String? = nil{
didSet{
saveToStorage(token)
}
}
まとめ
SwiftとKotlinは確かに似ていますが、それぞれが作られた背景もあって異なる部分も少なくありません。
Swift・Kotlin間の移植の際など、この記事が何らかの参考になれば幸いです。
この記事へのコメントはありません。