書いて覚えるSwift入門

第23回型は苦しい(かもしれない)役に立つ

なぜSwiftは静的な型を採用したのか?

前回紹介したとおり、Swiftの公式サイトのAbout Swiftには、

  • 安全(Safe)
  • 高速(Fast)
  • 豊かな表現力(Expressive)

という3つの特長が挙げられています。ところでSwiftは静的型をしています。つまりSwiftの生みの親たちは静的型がこの3つの特長に有利だと判断したということですが、なぜ静的型を採用すると安全で高速で表現力が豊かになるのかきちんと型られた、もとい語られたことは意外に少ないように筆者には思われます。本記事では他の言語の例も交えつつ、Swiftの型に対する姿勢を見ていくことにしましょう。

varでも変えられないもの

では早速実例を見てみましょう。PlaygroundかREPLで、次のように入力してみてください。

var number = 20
number += 1
number *= 2
number += 0.195

最後のところでerror: binary operator'+=' cannot be applied to operands of type 'Int' and 'Double'というエラーが出ているはずです。整数に浮動小数点数は足してはダメだということです。

今度はRubyで同じことをしてみましょう。以下はirbで実行してみた例です。

irb(main):001:0> number = 20
=> 20
irb(main):002:0> number += 1
=> 21
irb(main):003:0> number *= 2
=> 42
irb(main):004:0> number += 0.195
=> 42.195

一見Rubyの方が便利に思えます。が、さらに続けてこう打ってみましょう。

irb(main):005:0> number = "#{number}km"
=> "42.195km"
irb(main):006:0> number *= 2
=> "42.195km42.195km"

いつの間に数値だったnumberは文字列になってしまっています。

今度はSwiftに戻って次のようにしてみましょう。

var number:Double = 20
number += 1
number *= 2
number += 0.195

今度は42.195になったはずです。さらに続けて

number = "\(number)km"

としてみると、error: cannot assign value of type 'String' to type 'Double'で止まります。

この観察から、Swiftの型の特徴が見えてきます。

  • 型は変数宣言の時点で決まる
  • 型を明示しなくともよい。しない場合は推論される
  • 変数に異なる型の値を代入することはできない

1つ注意が必要なのは、Rubyに型がないわけではないということ。Rubyの値は必ずクラスという名の型を持っています。変数にどんな型のどんな値でも代入できるだけで。一度宣言した変数に異なる型を代入できない言語を静的型言語(statically typed languages⁠⁠、どんな型でも代入できる言語を動的型言語(dynamically typed languages)と呼びます。C、C++、C#、Objective-C、Javaなどは前者、JavaScript、PHP、Perl、Python、Rubyなどは後者に属します。いわゆるスクリプト言語はほとんど後者に属するのは興味深いところです。ではSwiftはスクリプト言語ではないかというと、以下が動く以上スクリプト言語であるという見方もできなくはありません。

$ cat > ./helloswift
#!/usr/bin/env swift
print("Hello, World!")
$ chmod +x ./helloswift
$ ./helloswift
Hello, World!

にもかかわらずSwiftが静的型を採用しているのは、型推論(type inference)を採用できたことが大きいでしょう。静的型言語も動的型言語もずいぶん昔からありましたが、型推論の導入は前世紀末で、実用的になったのは今世紀に入ってからといっても過言ではありません。なぜ実用的になったのが比較的最近かといえば、コンピュータの性能が上がったから。PlaygroundやREPLはユーザから見たらインタラクティブなので一見インタープリタに見えますが、実は都度コンパイルしているのです。

「やりたいことをやる」にはいちいち型を宣言するのはめんどくさい。しかし「やってほしくないことをやらない」ためには型でやれることを制約したほうがいい。せっかくコンピュータの性能が上がったのであれば、両者のいいとこ取りができた方がよいのは「宣言する」までもないことでしょう。

型ははめてなんぼ

それでは型があった方が本当に安全なのか。やはり実例で見てみましょう。ここでは標準入力に行番号をつけて出力するだけの簡単なお仕事をしてみます。コマンドであればcat -nPerlのワンライナーであればperl -e 'print"$.:$_"while<>'ですが、Swiftではどうでしょう。

var count = 0
while let line = readLine() {
    count += 1
    print("\(count): \(line)")
}

簡単ですね。ではCは?

#include <stdio.h>
int main() {
    char line[64];
    int count = 0;
    while (gets(line)) {
        printf("%d: %s\n", ++count, line);
    }
}

そんなに難しそうには見えませんね。#include<stdio.h>とかint main(){}とか余計なおまじないが入っていたり、char line[64]とかintcountとか変数の型を明示している点を除けばSwiftとさほど違いはないように見えます。

本当に(?)実際にコンパイルして確かめて見ましょう。

$ cc -Wall cat-n.c
$ ruby -e 'puts "1"*42' ¦ ./a.out
warning: this program uses gets(), which is
unsafe.
1: 111111111111111111111111111111111111111111

警告は出ていますが、一応動いているように見えます。しかし……、

$ ruby -e 'puts "1"*4096' ¦ ./a.out
warning: this program uses gets(), which is
unsafe.
Segmentation fault

落ちちゃいました。何が問題だったのでしょう? ソースを見てみると、linechar64個からなる配列として定義されています。なのに4,096文字もある行を喰わせたらバッファーオバーフローするのは当然ですね。文字列ではないのです。文字の配列です。じゃあ文字列を指定すればいい? どこにあるんですかCの文字列なんて!

というわけでgets()ではなくfgets()を使って書き直して見ましょう。

#include <stdio.h>
#define CHARSPERLINE 64
int main() {
    char line[CHARSPERLINE];
    int count = 0;
    while (fgets(line, CHARSPERLINE, stdin)) {
        printf("%d: %s\n", ++count, line);
    }
}
$ ruby -e 'puts "1"*4096' ¦ ./a.out
1: 111111111111111111111111111111111111111111111111111111111111111
……(中略)……
65: 111111111111111111111111111111111111111111111111111111111111111
66: 1

確かにSegmentation faultは出なくなりましたが、1行のはずが66行に分かれてしまいました。明らかにCHARSPERLINEが64と小さいのが原因ですが、ここの適切な値はなんでしょうか?

4096? 65536? そもそもそれってプログラマが頭を悩ませるべきことでしょうか?

気を取り直して、Swift版を試してみましょうか。

$ swiftc cat-n.swift
$ ruby -e 'puts "1"*4096' ¦ ./cat-n
1: 1111…
……(中略)……
1111111111111111111

今度はきちんと1行におさまりました。

Cの問題は一体なんだったのでしょう? ⁠文字列」を扱うプログラムなのに、文字列という型が不在で、⁠文字の配列」という不適切な型を使っていたところなのです。型というのは、それが扱いうる値をきちんと収納できてはじめて意味があるのに実はそうなっていない。それも文字列という、今や数値以上によく扱われる値に対してすら。

今となってはCという言語は静的型と動的型の悪いところ取り言語にどうしても見えてしまいます。Cで「やりたいことをやる」のは多少面倒だけど難しくはない。しかし「やれてはならないことをやれなく」するのは本当に難しい。難しいからこそHeartbleedShellshockのような脆弱性があとをたたない。

そもそも型はなんのためにあるのか。安全で高速なプログラムを実現するためではないのですか? 余計なコードを実行せず、余計なデーターを浪費せず、余計な脆弱性を持ち込ませない。それができない型は面倒なおまじない以下ではないのですか?

もちろん型は安全性や高速性の特効薬ではありません。Swift自体、CやObjective-Cと簡単に相互運用できるようになっていますがこの点から考えれば諸刃の剣。動的にメモリーを確保してくれる静的なStringとて、過大なデータを食わせれば簡単にパンクしてしまいます。とはいえ、Cの置き換えを標榜する言語はSwiftに限らずRustもGoも「まっとうな」文字列型を持っているのは地獄に仏に筆者には見えます。

文字列という基本的な型一つ見ても、適切に設計された型の有用性は実感することができます。それではもっと複雑な型は? たとえばJSONは? 次回はそれを見てみることにしましょう。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧