[Xcode/Swift] Swift ⇄ Kotlin 脳内スイッチ・チートシート②

前回の記事はこちら:

[Xcode/Swift] Swift ⇄ Kotlin 脳内スイッチ・チートシート①

宣言方法の違い(文法スニペット対比)

3.1 変数・定数(型注釈・型推論・初期化)

Swift:

  • let=定数、var=変数。型は : 型名

Kotlin:

  • val=定数、var=変数。型は : 型名

※ どちらも型推論あり

例:

// Swift
let count: Int = 3          // 型注釈
var name = "Alice"          // 型推論 (String)
var scores: [Int] = []      // 空配列は注釈入れておくと安心

// Kotlin
val count: Int = 3          // 型注釈
var name = "Alice"          // 型推論 (String)
var scores: MutableList<Int> = mutableListOf()

空のコレクションは型注釈を付けるとエラーになりにくい。


3.2 関数宣言(デフォルト引数・名前付き引数・可変長)

Swift:

  • 引数ラベル(外部名)で自然に名前指定。_ でラベル省略。
  • 可変長(varargs):Int...

Kotlin:

  • 呼び出し側で name = 値 と書く。
  • 可変長(varargs):vararg Int

※ デフォルト引数:両方とも 使える。

例:

// Swift
func greet(to person: String = "Guest", times: Int = 1) {
    for _ in 0..<times { print("Hi, \(person)") }
}

greet(to: "Alice", times: 2)   // ラベル必須
greet(times: 3)                // person はデフォルト
greet(to: "Bob")               // times はデフォルト

func sum(_ numbers: Int...) -> Int { numbers.reduce(0, +) }
sum(1, 2, 3) // 6

// Kotlin
fun greet(person: String = "Guest", times: Int = 1) {
    repeat(times) { println("Hi, $person") }
}

greet(person = "Alice", times = 2)  // 名前付き
greet(times = 3)                    // person はデフォルト
greet("Bob")                        // 位置引数でもOK

fun sum(vararg numbers: Int): Int = numbers.sum()
sum(1, 2, 3) // 6

3.3 クラス/構造体/データクラス(イニシャライザ/コンストラクタ、copy/memberwise init)

Swift:

  • struct(値型)には初期化が自動付与。class(参照型)は init が基本。

Kotlin:

  • data class は主コンストラクタでプロパティ宣言+copy が自動。class は必要に応じてプロパティと constructor

例:

// Swift (Struct:init 自動生成)
struct User {
    var name: String
    var age: Int = 0
}
let user1 = User(name: "Alice")        // age はデフォルト 0
let user2 = User(name: "Bob", age: 20) // もう一つのパターン

// Swift(class:init 必要)
class Person {
    let name: String

    init(name: String) {
        self.name = name
    }
}

// Kotlin(data class:copy 付き)
data class User(
    val name: String,
    val age: Int = 0
)
val user1 = User("Alice")
val user2 = User("Bob", 20)
val user3 = user2.copy(age = 21)   // 一部だけ差し替えて新インスタンス

※ Swift struct代入で丸ごとコピーされるイメージ。Kotlin data classcopy で差分を作る。


3.4 プロパティ(計算型・遅延・バックフィールド/willSet/didSet vs get()/set()

Swift:

  • 計算プロパティ:var value: Int { get {…} set {…} }get だけなら省略可)
  • 遅延:lazy var(参照型で使いやすい)
  • 変更監視:willSet / didSet

Kotlin:

  • 計算プロパティ:val/varget()/set(value) を付ける
  • 遅延:by lazy { ... }(スレッドセーフ既定)、lateinit var(後初期化用、プリミティブ不可
  • 変更監視:カスタム set(value) { … } 内で field(バックフィールド)を操作

例:

// Swift
struct Rect {
    var width: Double
    var height: Double
    var area: Double { width * height } // 計算プロパティ

    lazy var cache = [String: Any]()    // 初アクセスで生成

    var name: String = "rect" {
        willSet { print("will set to \(newValue)") } // 呼ばれるタイミング: 値がセットされる直前
        didSet  { print("did set from \(oldValue)") } // 呼ばれるタイミング: 値がセットされた直後
    }
}

// Kotlin
class Rect(
    var width: Double,
    var height: Double
) {
    val area: Double
        get() = width * height          // 計算プロパティ

    val cache: MutableMap<String, Any> by lazy { mutableMapOf() }

    var name: String = "rect"
        set(value) {
            println("will set to $value")
            field = value               // バックフィールド必須
            println("did set from $field")
        }
}

3.5 可視性とモジュール(internal/public vs internal/public/module 概念)

Swift:

  • デフォルトは internal(同一モジュール内で可視)。public は外部から見えるが継承・オーバーライドは不可、外部で継承を許すなら open。他に fileprivate / private

Kotlin:

  • デフォルトは publicinternal同一モジュール内private/protected あり。クラスの継承は open を付けないと不可能(デフォルト final)。

例:

// Swift
public class A {}     // 他モジュールから見えるが継承は不可
open class B {}       // 他モジュールからの継承OK
internal struct C {}  // 同一モジュールのみ
private func f() {}

// Kotlin
open class A          // open で継承許可(デフォは final)
internal class B      // 同一モジュールのみ
private fun f() {}

module:Kotlin は Gradle のモジュール単位。Swift も SwiftPM のパッケージ/ターゲット単位で概念は近い。


3.6 ジェネリクス(型制約・where 句 ↔ 上界/下界・reified

Swift:

  • <T: Protocol>、複数は where

Kotlin:

  • <T: Interface>、複数は where T: A, T: B
  • reified:Kotlin の inline 関数だけで使える“実行時に型を取り出せる”仕組み。Swift には直接的な対応なし(is/as? はあるがジェネリック実行時型情報は基本消える)。

例:

// Swift:最大値(Comparable 制約)
func maxOf<T: Comparable>(_ a: T, _ b: T) -> T { a > b ? a : b }

// Swift:複数制約
func encodeJSON<T>(_ v: T) -> Data where T: Encodable { try! JSONEncoder().encode(v) }

// Kotlin:Comparable 制約
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b

// Kotlin:複数制約
fun <T> encodeJson(v: T): String where T : Any = /* 省略 */

// Kotlin:reified(型でフィルタ)
inline fun <reified T> List<*>.only(): List<T> =
    this.filterIsInstance<T>()

val xs: List<Any> = listOf(1, "a", 2)
val ints = xs.only<Int>()  // [1, 2]

3.7 演算子とオーバーロード、等価性(Equatable/Hashableequals/hashCode

Swift:

  • Equatable/Hashable に準拠すると ==/集合キーが使える。Comparable< 等。独自演算子は static func を定義。

Kotlin:

  • ==構造的等価equals)、===参照等価compareTo 実装で < 等が使える。operator 修飾子で演算子オーバーロード。

例:

// Swift
struct Point: Equatable, Hashable, Comparable {
    let x: Int, y: Int
    static func < (lhs: Point, rhs: Point) -> Bool {
        (lhs.x, lhs.y) < (rhs.x, rhs.y)
    }
}

let a = Point(x: 1, y: 2)
let b = Point(x: 1, y: 2)
print(a == b) // true

// Kotlin
data class Point(val x: Int, val y: Int): Comparable<Point> {
    override fun compareTo(other: Point): Int =
        compareValuesBy(this, other, Point::x, Point::y)
}

val a = Point(1, 2)
val b = Point(1, 2)
println(a == b)  // true(構造)
println(a === b) // false(参照)

※ Kotlin で data classequals/hashCode/toString/copy を自動生成。Swift の struct合成実装が付く(単純な Stored Property のみの場合)。


型の考え方

4.1 値型中心 vs 参照型中心(コピーセマンティクスと不変性)

Swift:

  • 値型(struct/enum)中心。代入でコピー(実装は多くが COW:必要になったときだけコピー)。不変(let)推奨でバグ減。

Kotlin:

  • 参照型中心data class も参照型)。**イミュータブルに“運用”**する(valcopy)。

例:

// Swift
struct Counter { var value = 0 }
var c1 = Counter()
var c2 = c1        // コピー
c2.value = 10
print(c1.value)    // 0(影響しない)

// Kotlin:copy で不変運用
data class Counter(val value: Int = 0)
val c1 = Counter()
val c2 = c1.copy(value = 10)
println(c1.value)  // 0(影響しない)

※ 設計目安:ドメインの“データ”は不変振る舞いは別へ(UseCase/Service)。Swift は struct、Kotlin は data class + copy。


4.2 代数的データ型で表現力を上げる(switch 網羅性 vs when 網羅性)

大目的:状態/結果/イベントを型で漏れなく表す。

コツ:else を書かないで網羅させる習慣をつけると、追加時にコンパイルが気付いてくれる。

例:

// Swift:関連値付き enum
enum AuthState {
    case signedOut
    case signingIn(progress: Double)
    case signedIn(userID: String)
}

func render(_ s: AuthState) {
    switch s {
    case .signedOut:
        print("Sign in")
    case .signingIn(let p):
        print("Loading \(p)")
    case .signedIn(let id):
        print("Welcome \(id)")
    }
    // すべて列挙しないとコンパイルエラー(網羅性)
}

// Kotlin:sealed interface + data class
sealed interface AuthState {
    data object SignedOut: AuthState
    data class SigningIn(val progress: Double): AuthState
    data class SignedIn(val userId: String): AuthState
}

fun render(s: AuthState) = when (s) {
    AuthState.SignedOut -> println("Sign in")
    is AuthState.SigningIn -> println("Loading ${s.progress}")
    is AuthState.SignedIn  -> println("Welcome ${s.userId}")
    // else 不要(sealed の網羅性)
}

4.3 Null安全の実務運用(アンラップ戦略・早期 return・!! 禁止令)

基本ルール:

  • 入口で早期 return(Guard/Early return)
  • 変換で非 null に寄せる(デフォルト値・マッピング)
  • !!(Kotlin)や ! 強制(Swift as!/try!/!)は極力使わない
  • チーム規約例:!! 禁止、レビューで即指摘。どうしても必要なら直前で理由をコメント

例:

// Swift:guard で早期 return
func load(userID: String?) -> String {
    guard let userID else { return "guest" }
    return "user: \(userID)"
}
// Swift: チェーンとデフォルト
let length = user?.name?.count ?? 0

// Kotlin:Elvis で変換
fun load(userId: String?): String =
    "user: " + (userId ?: return "guest")
// Kotlin: チェーンとデフォルト
val length = user?.name?.length ?: 0

4.4 型変換・キャスト(as?/as! vs as?/as

Swift:

  • as? … 失敗すると nil(安全)
  • as! … 失敗するとクラッシュ(危険)

Kotlin:

  • as? … 失敗すると null(安全)
  • as … 失敗すると 例外ClassCastException

例:

// Swift
let any: Any = "Hello"
if let s = any as? String {
    print(s.count)
}
// let n = any as! Int  // 危険:クラッシュ

// Kotlin
val any: Any = "Hello"
val s = any as? String
println(s?.length)
// val n = any as Int   // 危険:ClassCastException

※ 実務:安全キャスト(as?)→ null ハンドリングが基本。失敗は型設計の見直しサイン