Kotlinでよく使う記法をSwiftで記述する方法

久々にiOSアプリのコードを書く機会があったのですが、「KotlinのあれってSwiftではどう書くんだっけ?」となる場面が結構多かったので、順次まとめていこうと思います。


なお、シンプルな置換でおおむね完結するものについては、以下の表などをご参照ください。

https://willowtreeapps.com/ideas/swift-and-kotlin-the-subtle-differencesより引用


Smart Cast

Kotlinでは、nullableとして宣言された変数であっても、non-nullであると保証されるスコープ内ではnon-nullな変数にキャストされるSmart Castという機能があります。
Swiftではif letを使用することで同様のことが実現できます。

  • Kotlin
val name: String? = "hoge"
print(name)  // hoge

if (name != null) {
  // Smart Castにより、nameはこのスコープ内ではnon-nullな変数として扱えます。
  print("hello, ${name.toUpperCase()}")  // hello, HOGE
}
    
// nameはここではnullableな値として扱われます。
print(name?.toUpperCase()) // HOGE
  • Swift
let name: String? = "hoge"
print(name)  // Optional(hoge)

if let nonOptionalName = name {
  // nameがnon-optionalな値のときにこのスコープ内の処理が実行されます。
  // Optional Bindingにより、nonOptionalNameはこのスコープ内でnon-optionalな変数として扱えます。
  print("hello, \(nonOptionalName.uppercased())")  // hello, HOGE
}

// nameはここではoptionalな値として扱われます。
print(name?.uppercased())  // Optional(HOGE)


ただし、上記のKotlinのサンプルコードでは問題ありませんが、スコープ内の処理中にifでチェックした変数の値が他の箇所から変更される可能性がある場合、その変数に対してSmart Castは行われません。
その場合、スコープ関数のletを使用することでSwiftと同様のことが実現できます。

  • Kotlin
val name: String? = "hoge"
print(name)  // hoge

name?.let { nonNullName ->
  // nameがnon-nullな値のときにこのスコープ内の処理が実行されます。
  // nonNullNameはこのスコープ内でnon-nullな変数です。
  print("hello, ${nonNullName.toUpperCase()}")  // hello, HOGE
}
    
// nameはここではnullableな値として扱われます。
print(name?.toUpperCase()) // HOGE


nullチェックによる早期return

Kotlinではifを使用することで早期returnを行なうことができます。
Swiftではguard letを使用することで同様のことが実現できます。

  • Kotlin
val name: String? = "hoge"

if (name == null) return

// Smart Castにより、nameはここ以降ではnon-nullな変数として扱えます。
...
  • Swift
let name: String? = "hoge"

guard let nonOptionalName = name else { return }

// Optional Bindingにより、nonOptionalNameはここ以降ではnon-optionalな変数として扱えます。
...


他の方法として、Kotlinではelvis演算子(?:)の後にreturnを記述できます。
一方、Swiftではnil-coalescing演算子(??)の後にreturnを記述できません。

  • Kotlin
val name: String? = "hoge"

name ?: return

// Smart Castにより、nameはここ以降ではnon-nullな変数として扱えます。
...
  • Swift
let name: String? = "hoge"

// 🙅このような書き方はできません。
name ?? return


when式

Kotlinのwhenは文ではなく式なので、最後に評価された値を変数に代入できます。
Swiftのswitchは文ですが、クロージャ式内で条件判定を行う処理を記述し、即時実行することで擬似的に同様のことが実現できます。

  • Kotlin
val score = 70
val grade = when (score) {
  in 90..100 -> "SA"
  in 80..89 -> "A"
  in 70..79 -> "B"
  in 60..69 -> "C"
  in 0..59 -> "D"
  else -> throw IllegalStateException()
}

print("grade: $grade") // grade: B
  • Swift
let score = 70
let grade = { () -> String in
  let result: String
  switch score {
  case 90...100:
    result = "SA"
  case 80...89:
    result = "A"
  case 70...79:
    result = "B"
  case 60...69:
    result = "C"
  case 0...59:
    result = "D"
  default:
    fatalError()
  }
  return result
}()
        
print("grade: \(grade)") // grade: B


Swiftのクロージャ式はKotlinのラムダ式のようなものです。
私自身も違いが明確に説明できないので、詳しくは公式ドキュメントなどをご参照ください。

https://kotlinlang.org/docs/reference/lambdas.html#lambda-expressions-and-anonymous-functionskotlinlang.org

docs.swift.org


Kotlinのラムダ式内で条件判定を行う処理を記述し、即時実行することでSwiftと同様のことが実現できるにはできます(ただ、まずやらないと思います😇)。

  • Kotlin
val score = 70
val grade = {
  val result: String
  when (score) {
    in 90..100 -> {
      result = "SA"
    }
    in 80..89 -> {
      result = "A"
    }
    in 70..79 -> {
      result = "B"
    }
    in 60..69 -> {
      result = "C"
    }
    in 0..59 -> {
      result = "D"
    }
    else -> throw IllegalStateException()
  }
  // Kotlinではラムダ式内でreturnが記述できず、最後に評価された値が戻り値となります。
  result
}()

print("grade: $grade") // grade: B


値に対応するenumの取得

例えば、APIからroleIdを受け取り、アプリケーション側ではその値に対応するenum(UserType)を使用して処理を行いたいケースがあるとします。
Kotlinでは以下のような記述で上記のケースを実現できます。

  • Kotlin
// enumの定義
// 規定の値以外のroleIdをプロパティに持つenumオブジェクトを生成しないよう、
// コンストラクタの可視範囲はprivateにしています。
enum class UserType private constructor(val roleId: Int) {
  ADMIN(0),
  PREMIUM(1),
  NORMAL(2),
  ;
    
  companion object {
    fun getUserTypeByRoleId(roleId: Int): UserType? {
      return values().find { it.roleId == roleId }
    }
  }
}

// roleIdから対応するenumを取得
val userType = UserType.getUserTypeByRoleId(1)
print(userType) // PREMIUM
print(userType?.roleId) // 1


一方、SwiftではRaw Valueというものを使用することにより実現できます。
Swiftではenumの型名の後に割り当てたい型を記述することで、その型の値を各列挙子に割り当てることができます。各列挙子に割り当てられている値はrawValueプロパティとして取得できます。
また、rawValueを引数に取り、対応するenumオブジェクト(ない場合はnil)を生成するイニシャライザが自動的に追加されます。

  • Swift
// enumの定義
// 型名の後にIntを記述することで、各列挙子にInt型の値を割り当てることができます。
enum UserType: Int {
  case admin = 0
  case premium = 1
  case normal = 2
}

// roleIdから対応するenumを取得
let userType = UserType(rawValue: 1)
print(userType) // Optional(UserType.premium)
print(userType?.rawValue) // Optional(1)


ただし、上記の用途で割り当てることができるのは整数リテラル(Intなど)、浮動小数点数リテラル(Doubleなど)、文字列リテラル(Stringなど)だけとなります。
上記以外の型の値では、RawRepresentableプロトコルに準拠することで同様のことが実現できます。

なお、enumの型名の後に割り当てたい型を記述したときは、そのenumRawRepresentableに準拠するようSwiftコンパイラが自動的に処理を追加してくれているようです。

// enumの定義
enum UserType: CaseIterable {
  case admin
  case premium
  case normal
}

extension UserType: RawRepresentable {
  typealias RawValue = Int

  init?(rawValue: RawValue) {
    // CaseIterableプロトコルに準拠することでallCasesが使用できるようになります。
    let enumeratorOrNil = Self.allCases.first { $0.rawValue == rawValue }
    
    guard let enumerator = enumeratorOrNil else { return nil }
    self = enumerator
  }
    
  var rawValue: RawValue {
    switch self {
    case .admin: return 0
    case .premium: return 1
    case .normal: return 2
    }
  }
}

// roleIdから対応するenumを取得
let userType = UserType(rawValue: 1)
print(userType) // Optional(UserType.premium)
print(userType?.rawValue) // Optional(1)


Sealed Class

KotlinのSealed Classに相当するものとして、SwiftではAssociated Valueというものがあります。
どちらもenumを拡張したような概念ですが、Kotlinはsealed修飾子をクラスに対して付与して実現するのに対し、Swiftはenumをそのまま使用して実現します。

  • Kotlin
// Sealed Classの定義
sealed class Color {
  object Red : Color()
  object Green : Color()
  object Blue : Color()
  data class RgbColor(val red: Int, val green: Int, val blue: Int): Color()
}

val color = Color.RgbColor(255, 255, 255)
when (color) {
  is Color.Red -> {
    print("red")
  }
  is Color.Green -> {
    print("green")
  }
  is Color.Blue -> {
    print("blue")
  }
  is Color.RgbColor -> {
    // Smart Castにより、colorはこのスコープ内ではRgbColor型の変数として扱えます。
    print("red: ${color.red}, green: ${color.green}, blue: ${color.blue}")
  }
}
  • Swift
// Associated Value(enum)の定義
enum Color {
  case red
  case green
  case blue
  case rgbColor(red: Int, green: Int, blue: Int)
}

let color = Color.rgbColor(red: 255, green: 255, blue: 255)
switch color {
case .red:
  print("red")
case .green: 
  print("green")
case .blue: 
  print("red")
case let .rgbColor(r, g, b):
  // case letと記述することによって、switchに指定したcolorの各プロパティが
  // それぞれr、g、bに渡されます。
  print("red: \(r), green: \(g), blue \(b)")
}


遅延初期化

Kotlinではby lazyによって遅延初期化ができます。
Swiftではlazy varによってほぼ同様のことが実現できます。ただし、Swiftではvarのため値の再代入が可能となります。

  • Kotlin
val name: String by lazy {
  print("initialized")
  "hoge"
}

val name1 = name // 初回アクセス時だけinitializedがprintされる
val name2 = name // 2回目以降は初回の評価結果のキャッシュが返却される

// 🙅valなので再代入できない。
name = "fuga"
  • Swift
lazy var name: String = {
  print("initialized")
  return "hoge"
}()

let name1 = name // 初回アクセス時だけinitializedがprintされる
let name2 = name // 2回目以降は初回の評価結果のキャッシュが返却される

// varなので再代入できる。
name = "fuga"