偏見にまみれたSwift言語入門

今回RabbitBoxの開発ではじめてSwiftを使用しました。個人的な感想としては「他のモダンな言語から節操なく機能を取り込んだ言語」という印象でした。

最初は若干面食らいますが、他の言語のこの機能というのがわかればスムーズに理解できると思いましたので、これらのポイントを個人的な復習も兼ねて書こうと思います。

RabbitBox - 画像収集と整理アプリ

RabbitBox - 画像収集と整理アプリ

  • Yoshinobu Sugain
  • 写真/ビデオ
  • 無料
apps.apple.com

Swift全体

基本的にはよくあるC/C++系統の書式ですが、Go言語に近い雰囲気だと思います。文末のセミコロンや条件式に括弧が不要だったりします。個人的に好きなタイプです。

また、型推論がとても強力で静的型付け言語であることを忘れそうになります。

この書式を基本に古今東西いろんな言語のプログラミングパラダイムを取り込みまくっているのが、Swiftという言語です。個人的には (C++ + Go + Python + javascript)÷3 みたいな印象です。

変数と定数

変数は当然のように型推論です。後付けされた言語と違い最初から型推論前提で設計されたためか、PythonRubyなどの動的型付け言語に近い使い勝手になっています。Python3のType Hintingにそっくりかもしれません。

var v1 = 1.23
var v2 = funcFoo()
// 型を明示したい場合は後ろにつける
var v3: CGFloat = 1.23

書式が洗練されすぎていて一見するとVariant型のように感じますが、あくまで型推論による 静的型付け 言語です。なので次のようなことはできません。

var v1 = 1.23
v1 = "foo"  // 変数型エラー

また、宣言と初期化を同時に行う必要はありませんが、初期化していない変数にはアクセスできません。

var v1: Int
print(v1)  // エラー

変数宣言のvarとは別にletがあります。letで宣言した定数は初期化後変更できません。

let v1 = 1.23
v1 = 2.00 // ERROR

コレクション

配列

当然基本的なデータ構造は用意されています。配列は次のように宣言します。

let intArray: [Int]
let stringArray: [String] = []  // 型宣言&空で初期化
let array = [1, 2, 3]  // 型推論&初期化

コレクションに関しても未初期化の変数にはアクセスできません。このため、使用前に明示的な初期化が必要です。

var intArray: [Int]
intArray.append(1)  // 未初期化エラー

var intArray2: [Int] = []
// または
var intArray2: [Int] = Array()
intArray2.append(1)  // OK

基本的な操作はPythonをはじめとしたモダンな言語と同様です。

Python

array = ["First", "Second"]
array.append('Third')
array[1]  # "Second"
len(array)  # 3
array[1:]  # ["Second", "Third"]

Swift

var array: [String] = ["First", "Second"]
array.append("Third")
array[1]  // "Second"
array.count  // 3
array[1...]  // ["Second", "Third"]

スライス操作に関してはPythonとやや異なっているため注意が必要です。Pythonの場合、スライス操作では部分列のlistを得られますが、Swiftの場合はArraySliceという特殊なオブジェクトが返ってきます。詳細は別記事にでもしようと思いますが、新規のArrayではなく部分列を参照するために専用のビューオブジェクトが使用されると覚えておいてください。(基本的にはそれほど意識する必要はないと思いますが)

あともう一点、Pythonのリストと非常に似ているのでハマってしまったのですが、SwiftのArrayは参照型ではなく値型です。

例えば、Pythonで次のようなコードを実行するとaとbは同じオブジェクトを参照しているため、bの内容を更新するとaの内容も変更されます。

a = [1,2]
b = a
b.append(4)
print(a)
# [1,2,4]

Swiftの配列は値型であるため別の変数に代入した時点でコピーされます。このため片方の配列を更新してももう片方には影響ありません。

let a = [1,2]
var b = a
b.append(4)
print(a)
// [1,2]

ディクショナリ

ディクショナリ型も用意されています。 静的型付け言語なので格納する型の宣言が必要です。PythonのdictやJavascript連想配列ほどの自由さはなく、C++Javaのハッシュマップに近いです。

Java

HashMap<String,Integer> map = new HashMap<>();

Swift

let map: [String:Int]  // 型宣言
let map: [String:Int] = [:]  // 型宣言&空で初期化
let map = ["key1": 1, "key2": 2]  // 型推論&初期値

配列と同様に値型です。また、アクセス前には初期化が必須です。

制御構文

Pythonとそっくり。以上

If

Python

a = 2
if a == 0:
    print('0')
elif a > 0:
    print('a>0')
else:
    print('a<0')

Swift

let a = 2
if a == 0 {
    print("0")
} else if a > 0 {
    print("a>0")
} else {
    print("a<0")
}

For-in

Python

items = [1, 2, 3, 4, 5]
for item in items:
    print(item)

for i in range(len(items)):
    print(i)

for i, item in enumerate(items):
    print(i, item)

Swift

let items = [1, 2, 3, 4, 5]
for item in items {
    print(item)
}

for i in 0..<items.count {
    print(i)
}

for (i, item) in items.enumerated() {
    print(i, item)
}

While

想像通りです。

Switch

一度は誰もが思ったであろう「Python君、アタマ固すぎ!」

個人的には一貫した思想のもとにswitch文を拒み続けるPython君も好きですが、Swiftちゃんはそんなこだわり関係なく当然のようにswitch文が使えます。(冗談おいとくと、たぶんパフォーマンス最適化のため必要)

enum Choice {
case a
case b
}

var v = Choice.a

switch v {
case .a:
    print("A")
case .b:
    print("B")
}

特徴的なのが、breakしなくても次のcaseが以降は実行されません。 また、取りうる値を網羅したコードを書かなければビルドエラーとなります。この仕様は非常に便利で、実装のパターン漏れをビルド時点で検出することができバグやロジックの不備を早い段階で検出できます。 パターンを網羅しない場合はdefaultが必須になります。

let v = 2
switch v {
case 1:
    print("1")
case 2:
    print("2")
default:
    print("other")

関数

ここらへんからややクセがでてきますが、定義方法がやや異なるだけで基本的にはよくある関数の仕様と同じです。

func add(x: Int, y: Int) -> Int {
    return x + y
}

let z add(x: 1, y: 2)
// z: 3

関数呼出時に引数名がデフォルト必須になっています。これは好み分かれそうですが、 例えば次のようなC++のコードがあった場合、

int findIdByName(const char* name);
int findIdByIdentifier(const char* ident);
int findIdByPath(const char* path);

findIdByName("hogehoge");

というのがSwiftであれば

func findId(name: String) -> Int
func findId(ident: String) -> Int
func findId(path: String) -> Int

findId(name: "hogehoge")

のように書けます。どちらも同じと言えば同じですが、個人的にはSwiftの方が美しく感じます。

また、次のようにアンダースコアによって引数名が不要な関数も定義できます。

func findId(_ name: String) -> Int
findId("hogehoge")

クラス

基本的にはC++系と同様です。他の言語を触っていれば戸惑うところはあまりないと思います。ただ、継承まわりの機能はC++などと比べ機能が少ないです。(シンプルと捉えるか貧弱と捉えるかは人によると思います。)

class ClassA {
    // 変数
    var num = 10
    var name: String

    init(name: String) {
        // 初期化
        self.name = name
    }

    // 関数
    func increase(v: Int) {
        num += v
    }
}

let a = ClassA(name: "hoge")

構造体

Swiftでは構造体も定義できます。構造体はクラスとほぼ同じ文法で定義できます。

struct StructA {
    // 変数
    var num = 10
    var name: String
}

let a = StructA(name: "hogehoge")

構造体も関数を持てますし、Initializer(init)も定義できます。ほぼクラスと同じ機能なのですが、大きな差としてクラスは参照型、構造体は値型データです。 このため変数への代入時の挙動が異なるほか、構造体は継承が行えないなどの制限があります。

Protocol

JavaC#で言うところのInterface、C++の純粋仮想クラスが非常に近いです。

protocol SimpleProtocol {
    func hoge()
}

class SimpleClass: SimpleProtocol {
    func hoge() {
        print("hoge!")
    }
}

let proto: SimpleProtocol = SimpleClass()
proto.hoge()

Dictionaryのキーに格納すためのHashableやJSONシリアライズに使用されるCodableプロトコルなどSwiftの標準フレームワーク内でも多用されています。

クロージャ

簡単にいうと無名関数です。 キモ可愛いで有名な(?)Javascriptのあいつです。

Javascript

let add = (x, y) => {
    return x+y;
}

Swift

let add = { (x: Int, y: Int) -> Int in
    return x + y
}

実際に使用する際は、引数と返り値の型に関する情報は型推論によって省略できるため、次のような使い方になります。

let add: (Int, Int) -> Int  // 関数型変数の宣言
add = { x, y in  // クロージャの定義側では型に関する宣言を省略
    return x + y
}

サンプルコードでこの構文を初めて見た時、まさかクロージャだとは思わず、調べるのに苦労しました……。「swift in」とかで調べても全然ヒットしないですし。(さらにTrailing Closureという初見殺しの構文があるんですよ)

実はさらにここから引数名とreturnも省略することができ、

let add: (Int, Int) -> Int  // 関数型変数の宣言
add = { $0 + $1 }

ここまで省略しなくても……と最初は思っていましたが、慣れてくるとかなり使います。 例えば、次のように汎用アルゴリズムに関数を渡す場合です。Pythonでいうlamda式の感覚で使用するといい感じなんじゃないでしょうか。

let array = [1,2,3]
let array2 = array.map({ $0 * 2 })

Trailing Closure

日本語だと「後ろに続くクロージャ」といった感じでしょうか。個人的にはSwift内でトップレベルにわかりづらい構文でした。

簡単にいうと、関数の最後のクロージャ型引数の定義を関数呼び出し文の後に続けて記述できるという構文です。意味わからんと思います。

例えば、非同期実行処理は次のように async(execute: ()->Void) という関数に非同期実行したいクロージャを指定します。なので素直に記述すると次のようなコードになります。

DispatchQueue.main.async(execute: {
    print("hogehoge")
})

ただ、Swift開発者はこれに満足できなかったようで、次のように最後の引数がクロージャであった場合、関数呼び出し文の後に記述できるようにしてしまいました。

DispatchQueue.main.async() {
    print("hogehoge")
}

さらに引数がクロージャだけの場合はカッコも省略できます。こうして一見関数呼び出しとは思えないようなコードが誕生します。

DispatchQueue.main.async {
    print("hogehoge")
}

順を追ってみていくと比較的わかりやすいですが、初見で次のようなコードをみるとイミフです。 いつか書こうと思いますが、UIフレームワークのSwiftUIでは多用されているため、そちらを使う場合は理解必須です。

let array = [1,2,3]
let array2 = array.map { $0 * 2 }

Optional型

無効値状態を保持できる変数型です。無効値とはNULL, None, nil, nullptr的なやつらです。Swiftではnilで表現します。

Swiftの変数はクラス型のような参照型変数であっても無効値は代入できません。

var obj = ClassA()
obj = nil  // 一見C++のnullptrのように扱えそうだがエラー

Swiftではデータ型に?をつけることでOptional型を表現できます。例えばIntのOptional型はInt?で次のように宣言します。

var obj: ClassA? = ClassA()
obj = nil  // 無効値を設定

大きな特徴ですが、Optional型は値型に対しても指定できます。Pythonのような動的型付け言語を使用していると忘れそうになりますが、静的型付け言語では通常値型の変数に無効値を指定することはできません。

C++

int v = NULL;  // おそらく0が代入されるだけ。0と無効値の区別はつけられない
string str = "";  // 無効値なのか空文字なのか謎

Swiftでは全てのデータ型に対してOptional型が使用できます。

var v: Int? = nil
var str: String? = nil

例えば何かを検索するような関数があったとき、Pythonでは見つからなかったら当たり前のようにNoneで返すと思います。

def findValue(name):
    for i in items:
        if i.name == name:
            return i.value
    return None

value = findValue('hogehoge')
if value is not None:
    // hogehoge

ただ、通常の静的型付け言語では返値の型を制限する必要があるため、関数ごとに色々工夫する必要があります。

C++

int findValue(const char* name, int& outValue) -> Int {
    for (const auto& i : items) {
        if (i.name == name) {
            outValue = i.value
            return 0
        }
    }
    return -1
}

int value;
if (findValue("hogehoge", value) == 0) {
    // hogehoge
}

SwiftではOptional型により無効値を扱えるため、Pythonのようにシンプルに実装できます。

func findValue(_ name: String) -> Int? {
    for i in items {
        if i.name == name {
            return i.value
        }
    }
    return nil
}
if let value = findValue("hogehoge") {  // Optional型→非Optional型変換の簡易記法(後述)
    // hogehoge
}

Optional型と非Optional型の相互変換

Optional型と非Optional型は別の型です。このため、相互に変換する必要があります。

非Optional型からOptional型への変換は暗黙で行われます。ただし、逆は行われません。

var value: Int = 3
var optValue: Int? = nil

optValue = value  // OK
value = optValue  // エラー

このため、unwrapという操作でOptional型から非Optional型へ明示的に変換する必要があります。

強制unwrap

変数の後ろに!をつけることにより強制的にOptional型を非Optional型へ変換できます。nilが格納されている変数をunwrapするとクラッシュします。 1番最初に紹介しといてなんだけど使わない方がいいと思う。

var value: Int = 3
var optValue: Int? = nil

value = optValue!  // OK
optValue = nil
value = optValue!  // ✨DEATH✨

if-let

前述のunwrapでクラッシュさせないためには、変数がnilでないことを確認すれば良いですよね。ということは次のように書けばいいじゃんというわけです。

if optValue != nil {
    value = optValue!
}

ただ、毎回これ書くのは美しくないと思ったのか、よりスマートな構文が用意されていて次のように書けます。

if let optValue = optValue {  // 変数名はなんでもいい
    value = optValue  // ここのoptValueは非Optional型
} else {
    // 必要であればnilだった時の処理
}

guard-let

「if-letのunwrapもいいけどネストが深くなるのがなぁ……nilが入っている場合はエラーだから処理スキップしたいだけなんだよ。わかってないなぁ……」って思っているなら先ほどと条件を逆にしてこんな書き方もできますよね。

if optValue == nil {
    return // continue, breakなどで中断
}
value = optValue!

まぁこれも当然のごとくスマートな記法が用意されています。

guard let unwrappedValue = optValue else { return }
value = unwrappedValue

さいごに

Swift触りはじめた時に理解しづらかったことを思い出しながら書いてみました。 間違っていることや不正確なこともたくさんあると思いますが、見つけたら優しく教えてください。