日が経過しています。情報の鮮度にご注意ください
説明
なぜJavaScriptではないのかと聞かれることがありますが、それはスクリプト言語であり、型システムなどがなく、Swiftとは比較にならないからです。
私はフロントエンド開発者で、C、Java、Python言語については入門レベルの理解しかありませんが、TypeScript(以下TS)の機能と使用に関しては上級レベル(自負)です。そのため、私が驚く点が、他の方々にとっては「ただの当たり前の言語機能」に思えたり、「TypeScriptの設計者は天才なのか?」と感じられるかもしれません。
私はSwiftとTSの違いすべてに無理に驚くわけではありません。なぜなら、一部の違いはTS自身の不足によるものであり、またコンピュータ言語では一般的な設計(例えば数字をIntとDoubleで区別し、Number型だけにしないなど)もあるからです。驚くべきはJSの設計(結局1週間で急いで設計されたものです)であって、Swiftではありません。
私の考えでは、言語のルールや例外、予約語が多すぎる場合、それは良い言語とは言えません。
はじめに
この記事はSwiftの公式言語紹介の順番に沿って驚きを記述しており、驚かなかったり理解できない内容(例:附加宏)は省略しています。
基本知識
コメント
驚きポイント:XCodeには`/** */`ブロックコメントのショートカットがなく、`cmd + /`の行コメントしかありません。
VSCodeのブロックコメントショートカットも押しにくく、Alt + Shift + Aです。この機能はあまり使われていないのでしょうか?
もちろん、VSCodeとXCodeどちらも、/**と入力してEnterを押すと、自動的にブロックコメントが生成されます。
オプショナルバインディング
驚きポイント:デバッグ中に`if`文で常に`true`となる値をテスト用に書こうとしてもできません。`if`文のオプショナルバインディングは、必ずオプショナル型の値でなければなりません。
1 | |
こんな必要ある?
コレクション型
驚きポイント:配列メソッドの中で、`sort` はその場でソートを行うのに対し、`sorted` は新しい配列を返します。
一つの言語がフレームワークの仕事までこなすとは、さすがAppleのスタイルだ。これに類する驚きは他にもたくさんあるが、いちいち驚いている場合ではない。
制御フロー
if式
驚きポイント:`if-else`で同じ変数に代入する場合に対処するため、`if`式の書き方が用意されています。同様に、`switch`にも式形式が存在します。
Swiftはやりすぎなところがある:
1 | |
Switchのすごい判定
驚きポイント:switchには暗黙のフォールスルーがない
しかしこれは理解できるし、より合理的だ。普通の人が、わざわざ break を書かずに case 1 から case 2 に自動的に進んでほしいと思うだろうか?
驚きポイント:switchはオブジェクトの値を判定できる!
これは実際には feature であり、直感的により合理的で、まさにプロフェッショナル向けだ。
TSでは、switchのcase文が全等(===)判定を使用するため、通常switchの括弧内にオブジェクトを渡すことはない。オブジェクトは参照を判定するものであり、TSではオブジェクトの等価性を判定する必要がある場面は稀で、ましてやswitch文で判定することはまずない。
しかしSwiftでは、case文で判定する値を「キャプチャ」できる(正式名称は「パターン」と呼ばれる):
1 | |
TSにおいても「複数マッチ」がサポートされていますが、Swiftのcaseにおける複数マッチとは全く異なることに注意が必要です。
Swiftの複数マッチでは、カンマ区切りの内容のいずれかが一致すればcase文が実行されます。しかし、TSのカンマ区切りのマッチは、本質的に最後の項目のみにマッチします。これはTSにおけるカンマ区切りの文が最後の値のみを返すためです:
1 | |
関数
関数のラベル付き引数
驚きのポイント:仮引数(ラベル付き引数)は同名でも良い???さらに規定(また制限が来た、参った):可変引数の後ろの引数は必ず引数ラベルが必要(もし1つだけなら実引数が仮引数になる)省略できない。
1 | |
クロージャ
クロージャのN種類の省略記法
驚きポイント:クロージャのシンタックスシュガーが多すぎてここでは全て列挙しませんが、最も衝撃的なのはたった1つの `>` 記号だけで表現できる形式です。
このように書ける本質的な理由は、String型に > という名前の関数が存在するからです。そうです、記号も関数になり得るのです!この点については後ほどさらに驚くべき内容を紹介します。
1 | |
本当に信じられない。
returnの省略
驚きのポイント:「単一行のreturnは省略可能」と理解できるかもしれないが、「複数行でもreturnを省略できる」ことは絶対に理解できないでしょう
TypeScriptにおけるreturnの省略は、Swiftの通常の書き方と同じで、単一行のreturn省略:
1 | |
通常のSwift構文:
1 | |
しかし!SwiftUIでの書き方:
1 | |
驚くべきことに、これもトレーリングクロージャ関数です。VStackは関数であり、クロージャパラメータが最後かつ唯一のパラメータとして渡されるため、VStackの括弧を省略できます。しかし!ここでは2つのText関数呼び出しを返しているのに、returnが書かれていません。なぜでしょうか?
それはViewBuilderを使用しているからです。この仕組みは後述するresultBuilderと同じシンタックスシュガーです。
自動クロージャ
驚きポイント:一見普通のクロージャ代入に過ぎないようですが、その遅延評価能力には驚かされました。
自動クロージャについて最初に学んだ時、この例で「遅延評価」能力について説明されました:
1 | |
これはごく普通のクロージャーで、TSと同じくcustomerProvider変数がクロージャーに代入されているだけだと私は考えます。同じ実装をTSで書くとこうなります:
1 | |
そしてもちろん、それが呼び出されたときに初めてクロージャーのロジックが実行されます。これこそ遅延評価というものです!
しかし、その遅延評価の真価が発揮されるのは、クロージャーが引数として渡される場合です。
ごく普通の明示的なクロージャー呼び出しは、TSの呼び出し方法とほぼ同様で、クロージャーを引数として受け取り、書き方も中括弧の中に記述します:
1 | |
しかし、customerProvider を @autoclosuer とマークすると状況が変わります。この場合、serve 関数の呼び出しは次のように記述できます:
1 | |
最後の serve 関数の呼び出しに注目してください。その引数 customer は文 customersInLine.remove(at: 0) です。TSでは、どんな場合でも呼び出しスタックはまずこの値を評価してから serve 関数を呼び出します。しかしSwiftでは、この書き方は上記の形式と単に見た目が異なるだけで、ロジックは同じです。つまり、まず serve 関数が実行され、その後内部でこの文が実行されます(customerProvider が呼び出された時点で)。これこそが噂の「遅延評価」というものでしょう。
このようなケースを多く書いていると、以下のようなコードに頻繁に出くわすことになります:
1 | |
ここでは、括弧内の文が先に実行されるのか、外側の関数が先なのか判断が難しい場合があります。そのため、Swiftの公式ドキュメントでも以下のように説明されています:
自動クロージャを過度に使用すると、コードの可読性が低下する可能性があります。コンテキストや関数名は、計算が延期されていることを明確に示す必要があります。
本当に信じられない、推奨されていないなら最初から設計すべきじゃなかったのに!
列挙型
驚きポイント: 他の言語では単なる状態判断のためのオプション的な型に過ぎない列挙型が、Swiftでは最もよく使われる第一級の型であり、Structの機能を一部置き換えることができるなんて信じられますか?
驚きポイント2: 列挙型は値型です。
TypeScriptにおける列挙型はごく普通の「列挙型」にすぎず、単に値を列挙するだけで、主に状態マシンで状態を記述するために使用されます:
1 | |
もちろん、TSが提供する様々な機能には、実行時とコンパイル時の違いなどもありますが、ここでは触れません。TSでは、列挙型の代わりに完全にオブジェクトを使用することができます:
1 | |
関連値
驚きポイント: Swiftのこの列挙型設計、一体何を考えてるの!
Swiftにおいて、列挙型の重要性は最優先です。すべてのケースを列挙可能(CaseIterableプロトコルに準拠が必要)、ケースを関数として扱い、呼び出し時に値を渡して列挙型インスタンスに処理させることができます。これを「関連値」と呼びます:
1 | |
そして使用時に値を渡すことができます:
1 | |
列挙型が最もよく使われるのはswitch文の中です。驚くべきswitchと同様に驚くべきcaseを組み合わせて、次のように書くことができます:
1 | |
最初に見た時、正直かなり抽象的で、実際の使用場面も想像できませんでした(私自身そんなことをしたことがなかったので)。
暗黙的な代入
驚きポイント: 列挙型の型宣言は、列挙型自体の型(キーと値のペア)ではなく、ケースの型を宣言している。
Swiftにおける暗黙的な代入は、TypeScriptやその他のC系言語と同様で、最初の数字がnであれば、その後の値はn+1となります。
しかしSwiftはさらに一歩進んでいて、宣言した型に基づいて暗黙的に値を代入します。デフォルトでは行われません。例えば:
1 | |
TSでは、Swiftで指定できるInt型(実際にはcaseの型を指す)さえ指定できず、enumの型(通常はRecordで別途定義されたキーバリューペア)しか指定できません。
構造体とクラス
衝撃の事実:構造体が値型だって???
この一見しっかり者の構造体が、実は値型(パフォーマンス最適化のためにImmutableの仕組みを使用)だったとは:
1 | |
波括弧付きのもの、どう見ても値には見えない!信じられない!驚き!
プロパティ
様々なxxプロパティ/ラッパー/オブザーバー
驚きポイント:Swiftがやりすぎている。計算プロパティ、ストアドプロパティ、プロパティラッパー(TSにもある)、プロパティオブザーバー(TSにもあり、ラッパーと一緒)といった概念は、通常フレームワークが提供するものだ。例えばVueの`computed`や`watch`など。しかしSwiftはStructとクラスの中でこれらを自前で実装している。信じられない!
1 | |
なお、属性ラッパーはグローバル変数には使用できません。
サブスクリプト
驚きポイント: これについては特に言うことはありません。「サブスクリプト」という概念自体が既に驚きで、コレクション、リスト、ディクショナリの要素にアクセスするためのショートカットを提供しています。さらに、サブスクリプトは関数のように複数の値を渡すこともできます。
基本的に、サブスクリプトの呼び出し方法は、データ構造(前述のコレクション、リスト、ディクショナリ)に対する関数呼び出しで値をアクセスすると考えられますが、違いは関数呼び出しが () を使用するのに対し、サブスクリプトは [] を使用することです。
継承
驚きポイント: これまでのすべての驚きポイントと同様に、Swiftはさまざまな目的や効果を実現するために、あたかも随意にキーワードを追加しているかのようです。そのため、継承に関連する予約語(もちろん、「予約語」と呼べるものではありません。Swiftでは予約語もすべて使用可能で、バッククォートで囲めば良いのです。これについては後ほど驚きポイントとして取り上げます)が数多く存在します。例えば: `final`、`override`、`open`、`required` など。
初期化プロセス
Structのメンバーワイズイニシャライザ
驚きポイント: Structは値型であるため、従来のClassのイニシャライザルールとは初期化時に違いがあります。
本来、StructとClassはどちらもごく普通のデータ構造で、TSのClassと何ら変わりません。しかし、Swiftはここでも一風変わったことをしています。
Structは値型であるため、特別な点として「メンバーワイズイニシャライザ」と呼ばれる初期化形式があります。これは、Structに init が全く定義されていない場合、そのプロパティを初期化するために引数を渡す形式でinitでき、明示的に記述する必要がないというものです。
1 | |
しかし、このルールはClassには適用されません。Classは明示的にinitコンストラクタメソッドを持ち、プロパティを初期化する必要があります。
クラスの指定イニシャライザとコンビニエンスイニシャライザ
驚きポイント: え?イニシャライザには2種類あるの???
クラスの指定イニシャライザは、TSにおける通常のconstructと同じ役割を持つinitメソッドです。しかし、そのコンビニエンスイニシャライザは…initの前にconvenienceキーワードを付ける(また来た!)ものです:
1 | |
実はここでSwiftのドキュメントは、なぜ便利イニシャライザが必要なのかを明確に読者に伝えず、直接クラスの初期化、継承プロセスとイニシャライザのデリゲーションについて説明しています。ここで初心者の方々に、なぜ便利イニシャライザというものが存在するのかを説明します。とてもシンプルな理由です:クラス内の複数の指定イニシャライザは互いに呼び出し合うことが許されていません。これだけの理由です。呼び出したくてたまらない?初期化プロセスを簡素化したい?それなら便利イニシャライザを使いなさい!
通常の書き方:
1 | |
上記のように、self.b を2回書くのは面倒だと感じませんか?そこで、2番目の init() では、このようにしたいと考えています:
1 | |
コンパイラエラーが報告されました:Designated initializer for 'A' cannot delegate (with 'self.init'); did you mean this to be a convenience initializer?
この場合、convenienceイニシャライザを使用する必要があります:
1 | |
「便利」が目的です~
失敗可能イニシャライザ
驚きポイント: イニシャライザが失敗することもある?
実は、失敗可能イニシャライザとは、初期化プロセス(init関数の呼び出しプロセス)でエラーが発生する可能性があるものを指します。
したがって、イニシャライザがエラーを投げる場合、TSのように外側でtry-catchする必要はなく、直接明示すればよいです。方法はinitの後に疑問符を付けてinit?とし、エラーが発生する可能性のある場所でnilを返し、インスタンス化時にnilかどうかを判断するだけです。
Swiftでは通常のイニシャライザは値を返しません。これはTSと同じです——ただしTSのイニシャライザは値を返すことができ、返される値がオブジェクト型の場合、thisオブジェクトを置き換えます。これはさらに驚きかもしれません。
オプショナルチェーン
驚きポイント: Q: どのような場合に関数に()を書いているのに実行されない?A: オプショナルチェーンのシナリオです。
余計な説明は抜きにして、例を見てみましょう:
1 | |
エラー処理
驚きポイント:列挙型と同様に、Swiftのtry-catch(実際はdo-catch)の設計は非常に複雑です。
catch文は任意のエラーをキャッチできるだけでなく、特定のエラータイプもキャッチ可能で、isを使用したりswitch文のように複数のcatch分岐でマッチングが行えます。これは驚きです:
1 | |
並行処理
驚きポイント:ドキュメントを読んでいた時、最初は `withTaskGroup` が Group Task を行う組み込みメソッドだとは気づきませんでした。。。
await の使い方は TS の await と全く同じで、快適です!
拡張機能
驚きポイント:拡張機能はおそらく Swift で最も強力な設計です。組み込みオブジェクトでもサードパーティ製でも、どんなオブジェクトでも、自由に、低コストで、複雑な宣言やキーワードを一切必要とせずに拡張できます。
TS では既存のクラスを拡張する場合、プロトタイプチェーンを操作し、オブジェクトの this をそのクラスに向ける必要があります。
しかし Swift では、単に extension を書くだけで、好きなメソッドやプロパティを自由に追加できます。それらの this はインスタンスを指すか、拡張型(つまり静的)メソッドやプロパティになります。
まさに圧倒的な自由度です。
プロトコル
驚きポイント:プロトコルは Struct の抽象化問題を解決するために導入されたと思います。どの言語でもクラス自体は継承可能(言い切りすぎかもしれませんが)なので、余計な「プロトコル」を実装する必要はなく、抽象クラスで十分です。ただ副次的に、Swift はクラスの単一継承を制限すると同時に、プロトコルをクラス、Struct、列挙型のすべてで活用できるようにしました。
これ以上言うことはありません。言いたいことは上記に尽きます。
ジェネリクス
驚きポイント:ジェネリクスには「関連型」という概念があり、これは宣言時に付与するジェネリクスと同様の役割ですが、より強力です。
Swift のジェネリクス関連型は次のようなものです:
1 | |
なぜこのように設計されていないのか:
1 | |
おそらく、この項目が非常に長くなり、エレガントでなくなる可能性があるためでしょう。例えば:
1 | |
名前の後ろに書くと、ジェネリック宣言が頻繁に画面からはみ出してしまい、非常に扱いづらい。
不透明型とカプセル化プロトコル
驚くべき点:コンパイラには型が何であるかを知らせつつ、呼び出し側には具体的な型を知らせず、ただそれが特定のプロトコルに準拠していることだけを知らせる効果を実現できます。
基本的に、不透明型とカプセル化プロトコルが実現しようとしている効果は、一方で呼び出し側に実装詳細を隠蔽しつつ、コードをリファクタリングする際に多くの変更を必要としないようにすることです。例えば:
1 | |
ここで、makeShapeが返す型は、Shapeプロトコルに準拠する任意の型であればよく、明示的にCircle型を返す必要はありません。このようにすることで、後からShapeに準拠する新しいStructを追加しても、この関数はそれを返すことができます(これはSwiftUIでよく使われる方法でもあります。例えばsome Viewなど)。
メモリ安全性
驚きのポイント:Swiftは値の参照渡しが可能なため、同じメモリ領域への同時読み書きが発生する可能性があります(これは理にかなっていますよね?)
1 | |
上記の実行はエラーを発生させますが、コンパイラは警告を出しません。このようなケースは多く存在するため、特に注意が必要です。TSには参照渡しの概念が存在しないため、このような問題は発生せず、簡単です~
高度な演算子
驚きのポイント: 記号を関数名として使用した関数を再定義/カスタマイズすることで、同じクラスや構造体のインスタンス間で相互操作を実現できます。
例を挙げます:
1 | |
このデザインは素晴らしい、どうして思いつかなかったんだろう。すごいすごい。
注意が必要なのは、演算子メソッドには独自の属性があることです。例えば、+ は二項演算子であり中置演算子でもあるため、2つの関数を受け取ります。- は中置演算子(二項演算子)にも、前置演算子(単項演算子、func の前に prefix を追加する必要あり)にもなるため、オーバーロード可能です。
しかしSwiftでは、代入演算子 = はオーバーロードできず、三項条件演算子(a ? b : c)も同様にオーバーロード不可と規定されています。
この演算子は非常に一般的で、任意のSwift組み込みオブジェクト/Foundationオブジェクトにおいて、== のような等価判定の演算子関数を見かけることができます。
さらに、独自の演算子を実装することさえ可能です!
1 | |
すごい。
結果ビルダー
驚きポイント:Swiftは「エレガントさ」と「再利用性」のために手段を選ばない。
以下のコードを見栄え良くするために、Swiftは@resultBuilderという魔法のようなものを「発明」しました。例:
通常の書き方:
1 | |
Swiftは、上記の三項演算子による判定が煩雑で、複雑な条件分岐の場合にはコードが長くなり可読性が低下すると指摘し、よりエレガントな解決策を求めました。そこで生まれたのが(私の理解が正しければ)結果ビルダーです:
1 | |
そして、上記の c 変数に次のように値を代入します:
1 | |
再利用、エレガント、効率的、最高!
字句構造
驚きのポイント:Swiftには予約語が非常に多いにもかかわらず、予約語を識別子として使用することが許可されています。
方法は、バッククォートで囲むだけです:
1 | |
しかし、キーワードを除けば、x と x は同じ変数です。
人生の重要な選択に直面したとき、最善の方法を誰かが教えてくれて、貴重な時間を無駄にせずに済めばと、私はよく願っています。だからこそ、自分の経験を踏まえて頻繁にブログを書き、広大なインターネットのこの小さな片隅に、私にとって一度きりの人生経験を記録し、助けを求める人々の力になれればと思っています。