1. Qiita
  2. Items
  3. Swift

[iOS] 新言語SwiftがObjective-Cよりも良いところ

  • 1741
    Stock
  • 17
    Comment
Stocked
More than 1 year has passed since last update.

(更新memo)
OptonalValueについては、詳細を以下の記事にまとめなおしました。
[Swift] OptionalValueの便利さ /「?」と「!」でより堅牢なコードへ

Swift

さきほどWWDCにて新言語 Swiftが発表されました。
The Swift Programming Language (iBooks Store) で言語ガイドが公開されていたのでザッと目を通してみました。

Objecitve-Cと比較してSwiftがイケてそうなところをパッと気になったところだけ書いていってみます。

変数/定数の型推論がある

Objective-Cのように明示的に型を書かなくても型を推論してくれます。
推論で問題ないケースも多いと思うのでタイプ数がかなり減らせそうですね。
( 変数を宣言する際はvar、定数を宣言する際はletで宣言します。 )

// 型推論
var name   = "Shinji Ikari" // 変数の型は推論によりString型になる 
var age    = 14             // 変数の型は推論によりInteger型になる 
let height = 141.5          // 定数の型は推論によりDouble型になる

とはいえ、もちろん明示的に型を指定することもできます。

let weight:Float = 43  // Float型
let favoriteWord:String  = "最低だ俺って" //String型

ちなみに暗黙的な推論はありますが、暗黙的な変換はありません。
例えば文字列と数値を連結した文字列を作るコードは以下のようになります。

let prefix   = "iOS"
let version  = 8
let result   = prefix + String(version) // 自分で明示的にversionをString型に指定

文字列の操作がObjCよりも簡単

Objective-Cでは、文字列操作のAPIが説明的すぎるインタフェースだったので
普段使いするには若干面倒くさいところがありました。

Swiftでは(ObjCに比べると)文字列操作が簡単にできる記法が用意されています。
+で連結したり、\(値)という記法で値を文字列中に展開したりできます。


let firstName = "Shinji"
let lastName  = "Ikari"
let fullName  = firstName + lastName // "ShinjiIkari"
let message   = "僕は\(firstName) \(lastName)です。" // 僕はShinji Ikariです

ちなみにObjective-Cで同様の処理を書くと以下のようになります。

file.m

// Objective-Cの場合

// 文字列の連結
NSString *firstName = @"Shinji";
NSString *lastName  = @"Ikari";
NSString *fullName  = [firstName stringByAppendingString:lastName];
NSString *message   = [NSString stringWithFormat:@"僕は%@ %@です。",firstName,lastName];

Swiftが簡潔で読みやすいですね。
ちなみに\()記法は以下のように中に式を書くこともできます。

"それは\(a + b)です" // 内部で連結
"関数の実行結果は「\( otherFunc(100) )」です" // 内部で関数呼び出し

ArrayやDictionaryでprimitiveな値を使える

以下のコードを見て「え?普通じゃん」と思うかもしれません。

let first  = 1
let second = 2
let numbers  = [ first, second]

しかしObjecitve-Cを使っていた身としては大変ありがたいコードなのです。
なぜかというとObjective-CではPrimitive値とオブジェクト(NSObject)を開発者自身でコンテキストによって使い分ける必要がありました。

例えば数値を扱う際にしてもNSIntegerは値、NSNumberはオブジェクトと
どちらを利用するかを、コンテキストを見ながら考える必要があります。

特にNSArrayやNSDictionary等はオブジェクトしか扱うことができないため、それらを利用する際にオブジェクトと値を変換することが多かったと思います。
このようなコンテキストによる値とオブジェクトを使い分けることは手間がかかり
場合によっては、バグがでる原因にもなることがありました。

例えば以下のようなコードはObjecitve-Cではエラーとなります。

file.m
NSInteger first  = 1;
NSInteger second = 2;
NSArray *numbers  = @[ first, second ]

なぜならNSIntegerは値であり、オブジェクトではないからです。
これらを気にせず書けるSwiftは素敵ですね。

ArrayにDictionaryに型を持たせることができる

Swiftでは配列に型を持たせることもできます。

let nameList : String[] = ["Ikari", "Asuka", "Ayanami"]

Objecitve-CはNSArrayの中身を全てid型で扱うという大胆なアプローチをとっていました。
柔軟で便利ではあったのですが。状況によっては中身の型を明示して堅牢にしたいケースもあると思います。

Objective-CでNSArrayの中の型に制約をつけたいときは静的には難しいので、動的なAssertionでごまかしたりしてたのではないでしょうか。

なお、Swiftでは合わせてGenericsも導入されています。

Switch文に色んなデータを渡せる

以下のコードを見て「え?普通じゃん」と思うかもしれません。

let company = "apple"
switch company {
case "google":
    let mobile_os = "Android"
case "apple":
    let mobile_os = "iOS"
}

しかしObjecitve-Cを使っていた身としては大変ありがたいコードなのです。
Objective-CではNSArrayNSDictonaryはオブジェクトしか受け取れず不便でしたと前述しましたが…
一方でObjective-Cのswitchは逆に値しか受け取れませんでした。
( そもそもCのswitchだからですね。 )

なので例えばObjecitve-CでNSStringなどでswitch的なことをする際は、「if-elseでがんばる」「がんばってマクロを作る」「enumでなんとかする」などなど、みなさん創意工夫していたのではないでしょうか。

文字列でswtichできる。幸せなことですね。

さらにSwiftのswitchにはwhereで細かい条件を制御することが可能です。

let os_name = "FreeBSD"
switch os_name {
case let os_name where os_name.hasSuffix("BSD"):
    let comment = "os_nameの末尾にBSDがついている."
case "linux":
    let comment  = "os_nameはlinux."
default:
    let comment = "それ以外"
}

swtich文だけで、かなり柔軟な処理ができそうですね。
ちなみにString#hasPrefixのようにprimitiveなStringやNumericな値から直接methodを使えるのもの良いですね。

nilをより堅牢に扱える / OptionalValue

Objective-Cはnilを透過的に許容する言語でした。 そのためObjective-C開発者の皆さん、本来nilが入るべきではない変数にnilが入り込んだ経験あるのではないでしょうか。
もしくは、そうならないようにNSAssertval != nilチェックを小まめにいれたりしたのではないでしょうか。

Swiftでは各変数にnilを許容するかどうか(Optionalな値かどうか)を型の後ろに?をつけることで明示的に指定することができます。

// Sringの後ろの?があるためoptionalValueはStringもしくはnilを入れることができる
var optionalStringFirst : String? = "Hello"
optionalStringFirst = nil // nilを代入可能

var optionalStringSecond: String?  // 初期値を明示しない場合はnilが初期値

言語自体にnilを許容するかどうかを選べる仕様があるということは、静的解析で確認することができます。
これにより実行前にnilの状態を意識することで、素早くバグの芽をつむことが出来ます。

OptionalValueの詳細に関しては以下の記事に具体例を交えながらまとめてみましたのでご覧くさい。
[Swift] OptionalValueの便利さ /「?」と「!」でより堅牢なコードへ

Method Chainなインタフェース(fluent interface)を設計しやすい

Objective-Cではメソッドの呼び出し方が[Object message]という書き方だったため、メソッドチェイン的なインタフェースにするのが難しく、実現するためにはBlocksを使ったhackyな実装になってしまっていました。
( 例えばUnderscore.m, ReactiveCocoa, BoltsFrameworkのようなfluent interfaceなライブラリ達は、みなBlocksで実装されています )

SwiftではObject.message()な記法なので素直にメソッドチェインなインタフェースを書くことができそうです。

enumの表現力が豊か

Objecitve-Cのenumは前述のswitchと同様にC言語のenumそのままのものでした。
一方でSwiftのenumは、それよりも表現力豊かなものになっています。
まずはサンプルを見てみましょう。

enum Company {
    case Apple, Google, MicroSoft
    func getBrowser() -> String {
        switch self {
        case .Apple:
            return "Safari"
        case .Google:
            return "Chrome"
        case .MicroSoft:
            return "IE"
        }
    }
}

let apple = Company.Apple
let mobileOS = apple.getBrowser();

ObjectiveCと比べると以下の点で、便利です。

enumの各値にprefixをつける必要がない

上のコードでは Company.Appleのような表現ができています。これをObjective-Cで書こうとすると以下のようにprefixを付けた冗長な宣言になってしまいます。
( ObjCではprefixをつけなければAppleがスコープ中でglobalな値になってしまう)

file.m
typedef NS_ENUM(NSUInteger, Company) {
   // 値自体がスコープに反映されるため,それぞれCompanyXXXのようにprefixをつける
   CompanyApple,
   CompanyGoogle,
   CompanyMicroSoft
};

enumの値にデータに振る舞いを持たせることができる

apple.getBrowser()のようにenum自体が振る舞いを持つ機能は個人的には是非、多用していきたいと思いました。
enumを利用するということは、値によって振る舞いが異なるロジックがある(ことが多い)のですが
1つのenumを複数箇所でswitch分岐しているコードは見通しが悪く、enumに新たな値を増やす際にバグの原因になりやすく危険なケースも多いです。

この問題に対応するためObjective-Cの場合は、コードの見通しをよくするためにenumをラップしただけのクラスを作成していたりしましたが、それだけのための1つのクラスを作るのは若干手間でもありました。

なのでenum自体に振る舞いをつけさせるというアプローチは、その問題に対するシンプルな答えだと思います。

enumに副次的な値を持たせることができる / Associated Values

さらにSwiftではEnumに引数のようにコンテキストごとに値を持たせることができます。
この機能を利用するには定義するときに、受け取りたい値の型を指定しておきます。
例えば以下のようにServerResopnseのResult,Errorそれぞれが値を持てるようにするには以下のようにenumを定義しておきます。

enum ServerResponse {
    case Result(String, String)
    case Error(String)
}

このenumの実際の利用シーンは以下のようになります。

let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")

switch success {
case let .Result(sunrise, sunset):
    let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
case let .Error(error):
    let serverResponse = "Failure...  \(error)"
}

こちらの機能も利用すればObjective-CではClassを使わなければ表現できなかったようなことが
enumだけで実現できることが増えそうですね。

関数の引数にデフォルト値をとれる

まずは通常の関数の宣言を見てみましょう。
postBlog関数は2つの引数「String型のtitle」「String型のbody」を受け取り、返り値として受け取る関数です。

func postBlog(title: String, body: String) -> Bool {
  ...
  return true;
}
let bool = postBlog("眠い","本文です")

上記の例で、Bodyのデフォルト引数を"空っぽ"に指定した場合は以下のようなコードになります。

func postBlog(title: String , body: String = "空っぽ") -> Bool {
  ...
  return true;
}
let bool = postBlog("眠い") // postBlog("眠い","空っぽ"')と同値

上記のコードを見て「普通じゃん」と思う方もいるかもしれません。
しかしObjective-Cでデフォルト引数を実現するには涙ぐましい書き方が必要でした。
例えば同様のコードをObjCで書いた場合は以下の様なコードになっていました。

file
// Objective-Cでがんばってデフォルト引数的なものを作る場合
-(void)postWithTitle:(NSString *)title body:(NSString)body{
  ...
}

-(void)postWithTitle:(NSString *)title{
  [self postWithTitle:title body:@"空っぽ"];
}

上記の書き方は引数が増えるとつらくなっていきます。。
自然な書き方でデフォルト引数が使えるのは幸せなことです。

Closureの書き方がわかりやすい

Objective-CもBlocksが導入されたことでClosure処理をかけるようになったのですが
後付ということもあり、引数の受け取り方が通常のメソッドや関数と書き方が違い、また
直感的ではないため、覚えづらいところがありました。

その点Swiftは引数の受け取り方や書き方が通常の関数と同じです。(まぁそりゃそうでしょうが)

以下は、ある配列の中身を全て2倍にした配列を作る例です。
Array#mapにClosure渡して実装しています。


let numbers = [1, 2, 3, 4]
let doubledNumbers = numbers .map({
    (number: Int) -> Int in
    let result = 2 * number
    return result
    })
// doubledNumbers : [ 2, 4, 6, 8]

{}でスコープを渡して1行目で引数宣言をしています。
引数を受け取る(number: Int) -> Intは通常の関数の引数宣言でも同様の書き方なので
ObjCのように覚えにくかったり、混乱することはないと思います。

また言語自体にArray#mapがあるのもいいですね。
クロージャーで関数を定義するには以下のようになります。

// 引数で受け取った2つの数の和を返すだけの関数
func addTowNumbers(numA:Int , numB:Int) -> Int {
    return numA + numB
}
let result = addTowNumbers(1,3) 
// result: 4

わかりやすいですね。比較という意味でObjective-Cでも同様の内容のコードをBlocks書いてみましょう。

int (^addTowNumbers)(int) = ^(int numA,int numB) {
    return numA + numB;
};
int result = addTowNumbers(1,3);

Objective-Cでは記号が多く、なんなかソラで書くのが難しいです。
Swfitは簡潔ですね。

Classの定義が直感的

コードを見れば初見でも何をやっているか違和感なく入ることができます。

class Human {
    var name: String    
    init(name: String) {
        self.name = name
    }
    func sayHello() -> String {
        return "Hello. I'm \(self.name)."
    }
}
let tarou = Human("Tarou Yamada")
ikari.sayHello() // Hello. I'm Tarou Yamada

Objective-CならallocしてからinitWithNameする、もしくはallocと書くのが手間にならないように[Human humanWithName:@"Tarou Yamada"]のようなfactory methodを作っておくことが一般的だったと思います。

もうallocしなくても良いですね。

Propertyの遅延ロード機能

propertyをinit時に初期化するのではなく、初めて利用した時に初期化するための機能です。
これは地味に便利ですね。
init時にはその後利用されるかどうかわからないが、初期化コストがかかるようなpropertyに有効です。

具体的な状況例でいうと例えば以下のような状況を想像してみましょう。

  • Articleクラスはtitle(記事タイトルの文字列)とbodyStorage(本文の読み書きを行うための別クラスBodyStorageのインスタンス)の2つのpropertyを持っている
  • titleはただの文字列なので生成コストは問題にならないがbodyStorageは生成に重い処理がある
  • bodyStorageはinit時点では利用されないし、利用されないままインスタンスが破棄されることもある

上記のような条件の場合init時点でbodyStorageを生成されるのは、利用されるかどうかわからないため無駄になる可能性があります。
そこでbodyStorageが必要になった時点で初めて初期化されるコードを以下のように書きます。

class Article {
    // titleはArticleのinit時点で初期化される
    let title = "記事のタイトル"
    // @lazyをつけることでbodyStorageは必要になるまでインスタンスは生成されない
    @lazy var bodyStorage = BodyStorage()
}

// インスタンス生成,この時点ではtitleのみが初期化されbodyStorageは空
let article = Article()

// titleにアクセス。この時点でもbodyStorageは空
article.title

// bodyStorageにアクセス。ここの時点で初めてBodyStorage()が実行される
article.bodyStorage.store('本文')

Objective-Cで書くと以下のような感じなります。

- (BodyStorage *)bodyStorage {
   if( _bodyStorage ){ return _bodyStorage; }
   _bodyStorage = [BodyStorage new];
   return _bodyStorage;
}

これがSwiftなら@lazyを使えば簡単に書けるのは良いですね。

Operatorのoverloadができる

Operatorのoverloadができるようになりました。
個人的には最近ゲーム開発などしていて、値をオブジェクト化することが多いので助かりそうです。

例えば二次元のベクトルを扱う例を考えてみましょう。
以下は The Swift Programming Language (iBooks Store) に掲載されている例そのままのコードです。

struct Vector2D {
    var x = 0.0, y = 0.0
}
@infix func + (left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x + right.x, y: left.y + right.y)
}

上記のように@infixをattributeとして指定し+演算子を定義すると下記のようにObject同士を+した際に、自分で定義したfunc +が実行されます。( なのでベクトル同士の和が求まる)

let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector

もちろん他の演算子についても可能です。記号を帰る以外にも
また「+=の場合はattributeに@assignment」、「++aのような場合@prefix @assignment」のように様々なoperatorに対応しているようです。

もちろん無闇に利用すると、Objectの挙動がわかりづらくなるケースもあるとは思いますが
選択肢として「operatorのoverload」が増えたのは良いことだと思います。

便利そうな記法

rangeな配列

for i in 0..3 {
    println(i)
}
// 0 1 2 

for i in 0...3 {
    println(i)
}
// 0 1 2 3

わかりやすく書ける数値リテラル

let oneMillion = 1_000_000

まとめ

他にも要素は色々あるけれど

上記の内容はざっくり気になったところをピックアップしただけです。

他にもSwiftではStructclassのような振る舞いを持っていたり、オブジェクトを配列のようにobject[1]などにアクセスするためのsubscript、`Extentions'などなど様々な
機能があります。
(まだ、僕自身ちょこちょこ動かしながら試しているので全部確認できてないです。)
なにわともあれ既に動かせるので、触りながら色々試してみるのが良いと思います。

Objective-Cをあれこれ言っちゃいましたが

この記事ではSwiftをObjective-Cと比較して、Objective-Cに結構厳しめな書き方をしてますが
そもそもSwiftのコンセプトが「Objective-C without the C」である以上、比べると
Objective-CのC言語に引きづられている部分が悪く見えるのは当たり前の話ですね。
( そういう結果にならないと、コンセプトとして意味が無い )

Cから脱却したSwiftはモダンで完成度が高い言語といえると思います。
ここに上げた機能も含め他言語で普通にできるような機能は備えた上で
型やnilに対する考え方などは堅牢にしつつ、使いやすいインタフェースになっています。
これからは、そんなSwiftでiOS , MacOS開発を進めていけるのは素敵なことですね。

Send an edit requestYou can propose improvements about the article to the author 💪
Comments Loading...
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.