Page icon

第3回

2021/07/19
About Swift
Empty
Empty
Empty
12 more properties
今回は、前回に引き続いて Swift.org
About Swift
の中から
Swift の機能
について眺めていきます。Swift に搭載されている Objective-C から比べて先進的になったあたりを紹介する会になりそうです。よろしくお願いいたしますね。
———————————————————————— 熊谷さんのやさしい Swift 勉強会 #3
00:00 開始 00:38 振り返り 01:09 Swift の機能 06:11 型推論 14:05 型パラメーターと絡めた型推論 18:23 名前空間 23:43 予約語と同名のシンボルを使う 26:44 列挙型を用いた名前空間 34:37 汎用的な型を安易に拡張しない 35:50 モジュールによる名前空間 38:23 メモリーは自動で管理 40:27 Unmanaged 43:10 セミコロンは省略可能 44:39 名前付きパラメーター 51:10 名前付きパラメーターによる可読性向上 53:53 次回の展望 ————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #3
今回は前回の続きです。Swift.orgというSwiftの情報がまとまっているウェブサイトがありますが、そこの「About Swift」、本当に入り口の部分ですね。そこについてお話ししていて、今回はその続きとなります。
前回は、Swiftの概要や、言語としてどのように運営されているのか、そしてSwiftが達成しようとしている「セーフ」「ファスト」「エクスプレッシブ」という三つの目標についてお話ししました。この辺りに興味がある方は、前回のアーカイブを見ていただければと思います。
今回はSwiftの機能について見ていこうと思います。Swiftと言えば、どういう言語を思い浮かべるのでしょうか。おそらく「iOSを作る言語」くらいの認識かなと思いますが、それはさておき、Swiftの機能について「About Swift」に書いてある内容を整理していきます。
まず、Swiftにはコードの読み書きをしやすくする機能があると書かれています。その一方で、真のプログラミング言語に必要な制御を提供する、簡単でありかつ本格的な言語とも述べられています。そう言われると確かにそんな感じだなと思うのですが、例えば私が思うSwiftの印象として、まず初心者が非常にとっつきやすい言語であると感じます。なんとなくさらさらと書いていける、そんな書きやすい、初心者にもなじみやすい言語です。
それと並行して、GenericsやSwiftの世界でいう「プロトコル思考」など、本格的なコードが書ける機能も揃っていて、初心者がステップアップしたときにも探求心を満たせるような、そんな言語になっているような気がします。そんなところをざっくりと話しましたが、具体的にどのようなものがあるかというと、型推論、名前空間、自動メモリ管理、セミコロンを省略して書ける、名前付きパラメータなどが挙げられます。
これらについて詳しく見ていこうかと思いますが、例えば型推論について説明すると、型推論そのものはそんなに難しいものではありません。ざっくりと書くと、変数を用意するときに、従来のObjective-CやCをイメージすると一番分かりやすいかと思いますが、変数宣言のときに型を書いて、その型の変数と宣言する。しかし、Swiftでは文脈を見て型が推論されるので、わざわざ型を書かなくても良くなります。
例えば、整数リテラルが与えられているときに、その変数が整数と捉えられるので、わざわざ
int
型と書かなくても良いのです。もう少し具体的な例を示すと、以下のようになります。
var number = 42 // 型を指定しなくても `Int` 型と推論 var text = "Hello" // 型を指定しなくても `String` 型と推論
これらの変数は型を明示的に書かなくても文脈から推論されます。また、配列の例を挙げると、以下のような場合も同様です。
var numbers = [1, 2, 3] // `Int` 型の配列と推論 var texts = ["a", "b", "c"] // `String` 型の配列と推論
このように、型を明示的に書かなくても、文脈から推論されるのが型推論の特徴です。
また、型推論の強力な点は、例えば配列に混合データが入ってきた場合には、適切に推論してくれるところです。以下の例を見てみましょう。
var mixedArray = [1, "two", 3.0] // `Any` 型の配列と推論
この場合、
mixedArray
は整数だけでなく文字列や浮動小数点数も含むため、
Any
型の配列として推論されます。このような柔軟な型推論が、Swiftを使いやすいものにしています。
これがSwiftの型推論の基本的な仕組みです。 Swiftというプログラミング言語について説明します。Swiftでは、プログラマーの意図と異なる動作をする可能性があり、そのため誤解を招く恐れがあります。こういった場合には型推論に任せず、明示的に型を指定することが勧められます。例えば「これは配列ですよ」と具体的に書くことが求められることがあります。ここから、Swiftの安全性の特徴が見えてきます。
他の例として、
int
型と
String
型が
CustomStringConvertible
に準拠している場合、これらを配列にすることも可能です。型推論はあくまでも一般的な型に導くため、例えば全てが
Double
型になる場合があります。その一方で、整数リテラルは何も指定しなければ
int
型として扱われますが、
Double
型にしたい場合には明示的に指定します。
C言語に慣れている人は、整数リテラルを
10
、不動小数点数を
10.0
のように考えるかもしれませんが、Swiftでは
10
Double
型として解釈されることもあります。不動小数点数にしたい場合、文脈でそれが
Double
型であると分からない場合には、明示的に
as double
のように指定します。
配列の中に一つでも
Double
型が含まれていると、それに従って他のリテラルも
Double
型として型推論されるのが一般的です。また、不動小数点数を受け取るメソッドがある場合、たとえば
sum
というメソッドは
Double
型を取るという文脈から、整数リテラル
10
Double
型として推論されます。
型推論についてさらに話すべきことがいろいろありますが、特にリテラルに関してはまた別の回に取り上げたいと思います。
型推論の面白い例をもう少し紹介します。理解が難しい場合は飛ばしてもらっても構いません。例えば、
struct Value
という型を作った場合、これをジェネリックにして型パラメータを取るようにしたとします。この時、複数のイニシャライザを用意しておくことができます。
Swiftでは型拡張によって型パラメータに基づいて特定の機能を実装することが可能です。ここで、
Bool
型を取るイニシャライザを作るとしましょう。例えば以下のようなコードになります:
struct Value<T> { init() {} init(_ value: Bool) {} } let instance1 = Value<Int>() let instance2 = Value<Double>(10.0)
Value<Double>
のように指定することで、型パラメータが
Double
であることが自動的に判定されます。このように、型推論を利用して型パラメータを省略することができる場合もあります。
以上のように、Swiftの型推論は非常に強力で、文脈から多くのことを推測して適切な型を決定します。これにより、コードの記述が簡潔になり、プログラマーがより直感的にコードを記述することが可能になります。 これを型推論って言っていいのか、まあ分からないですが、たぶん型推論の分野かなと思います。こういうふうに型推論は単純に変数、たとえばストリング型を省略するというところから始まって、こういう高度なところまで発展していくのはなかなか興味深い機能だったりします。これが一つ、Swiftの新しい機能として用意されているものになります。
ここまでで何かありますか?「なるほどね」という感じですかね。こんな感じでいろいろ喋っていきますけど、全然途中で割り込んでもらってもOKですし、「これ、よく分からない」といったことがあれば聞いてもらえればその都度補足していきますので、ぜひ話しかけてくださいね。まず型推論、話戻ってもOKですからね。全然ふと思い立った瞬間に話しかけてもらえればOKです。
じゃあ、次は名前空間の話をしていきましょうか。このSwiftの名前空間の話なんですけど、名前空間があるってAppleが公式にSwiftが登場したときに話した内容なんですが、今でもこのSwiftに用意されているものが名前空間ではないと捉える人もいると思います。だから「Swiftは名前空間がないよ」っていう人と「Swiftは名前空間があるよ」っていう人が両方存在する、そんなところな気がします。そのあたりは何をもって名前空間とするかっていうところなのでね、そういうもんでしょう。
名前空間、今時の名前空間を持っている言語ってC++以外だと、C#もありますね。結構普通なものになっているかな。Objective-Cにはなかったですね。とりあえず、Perlとかはパッケージで名前空間を作ります。他の言語はよくわからないけど、Swiftも同じようなノリでパッケージで名前空間を作ったり、型で名前空間を作ったりしますが、純粋なネームスペースみたいな書き方はSwiftにはないです。
では、Swiftでどういうふうに名前空間を作るかというと、型かな。例えば、スタンダードという名前はあんまり良くないですが、たとえば
Numeric
みたいに書いて、構造体またはクラスでも列挙型でも何でもいいんですけど、型を使って名前空間を定義していくという特徴を持っています。これによって名前空間そのものに意味を持たせるというか、
Numeric
という単語に数値というドメインを作る意味合いで定義するわけです。
それで、
Numeric
の中だとどんなのがいいかな。例えば
static let
かな。
0
みたいにね。こういうふうなのを作って、
Numeric
を返す。こういうふうに書いてあります。
static let
だから代入しないといけなかった、間違えた、こうか。こんな感じです。それっぽく書いておくと、
var value
0
みたいにね。何の変哲もない型の定義を書いただけになるわけですが、ある人の中ではこれをネームスペースと捉えて、このプロパティ
0
はネームスペース
Numeric
に存在している、みたいな捉え方をします。
この辺り明確に語られていないので、人それぞれ認識が違うところがあります。型がそのまま名前空間になっていると。例えば、この名前空間の中に
enum
を実装してあげて、
enum
として
(type)
を作る。ケースとして
integer
real
という風に作ってあげる。こうすると、この
type
Numeric
的な名前空間に所属するという書き方ができるようになります。
この名前空間がなかったときには、そこで定義しないといけなくて、これだと何の
type
か分からないので
NumericType
みたいに書いていましたが、名前空間ができるようになったからこういう書き方しなくてよくて、所属できると。所属できることによって
Numeric
type
ですよっていう表現ができるようになったというのが名前空間の話です。 はい、では続きに移りたいと思います。せっかくなので、いくつか紹介しておきますね。人によっては純粋な名前空間を作りたいというときに、列挙型を使う方もいらっしゃるようです。
例えば、以下のように列挙型を作って、その中で必要なものを定義していくという方法があります。なぜここで列挙型を選ぶのかというと、列挙型はケースがなければインスタンス化することができないという特性を利用します。インスタンスを作れないので、予期しない使われ方をしないというわけです。こうすることで、
World
そのものはインスタンス化できないため、純粋に名前空間の代用として使えるという考え方ですね。
enum World { struct SomeStructure { // 必要なものをここに定義 } }
これが一般的に推奨されるか否かはわかりませんが、一応参考までに紹介しておきます。この列挙型で名前空間を作るという考えについては、文化によるのかなと感じます。
さらに、Swiftの拡張機能(extension)を使って、既存の型に自分で機能を追加する方法もあります。しかし、これには注意が必要で、自分で定義したものと他の人が定義したもの、または標準ライブラリと混ざってしまうと混乱を招く可能性があります。
例えば、String型にextensionでエンコード関連のメソッドを追加した場合、以下のように定義することができます。
extension String { var encodedString: String { // エンコード処理をここに記述 return self // 一例としてselfを返します } }
このように定義することで、どの文字列に対しても
encodedString
を使えるようになりますが、既存の機能と混ざってしまって気持ち悪いと感じることがあるかもしれません。その場合、名前空間を作って区別することもできます。
例えば、以下のように列挙型で名前空間を作ったケースです:
enum Ex { static func encodedString(from string: String) -> String { // エンコード処理をここに記述 return string // 一例としてstringを返します } }
こうすることで、
Ex.encodedString(from: "example")
のように使うことができ、標準の機能と混ざらずに済みます。
また、RxSwiftなどのライブラリでは、全ての機能を
.rx.
という名前空間で分けている例があります。これにより、
RxSwift
の機能だと明示的にわかるようになるわけです。
エクステンション機能は非常に強力ですが、それゆえに他の定義と衝突するリスクもあります。特に、Swift本体が新たな機能を追加した場合、自分の定義と衝突する可能性があるため注意が必要です。
大規模なプロジェクトや長期にわたるメンテナンスを考えるときには、名前空間をしっかり意識して、安易に汎用的な型に機能を追加するのではなく、独自のストラクトやクラスで機能を分けることが推奨される場合もあります。
また、Swift言語にはモジュールという概念があります。アプリやフレームワークのターゲット1個がモジュール、プレイグラウンド全体が1個のモジュールなどとして扱われます。このモジュールも名前空間として機能しており、たとえば以下のように
import Foundation
を使って、必要な機能をインポートすることができます。
import Foundation
このように、モジュールも名前空間として活用することができます。 インポートファウンデーションってやると、このファウンデーション自体が名前空間を形成しているんです。なので、ファウンデーションの中には何が実装されているか見てみると、たとえば
NSString
とか
URL
がありますね。ファウンデーションの中で定義されている
URL
も含まれます。
普段は
URL
を定義するときに特に特別な書き方をするわけではなく、普通に
let url = URL(string: "<https://example.com>")
などとして使いますが、実際にはこの
URL
はファウンデーションの名前空間に所属しています。名前空間を使って指定することも可能です。
例えば、
Foundation.URL
という形式で名前空間を指定して使うこともできます。そのため、名前空間が異なっていれば、同じ名前のものを明記しなくても使える仕組みになっています。
次に、メモリ管理について話します。前回の勉強会で ARC(Automatic Reference Counting)の話をしましたが、これに関連する内容です。スコープ内でオブジェクト志向、特に古いオブジェクト志向の言語によくあるメモリの確保やアロケーション、リテインやリリースを明示的に書かなくても大丈夫です。
Swiftでは、例えば
let url = URL(string: "<https://example.com>")
のように書くだけで、自動的にメモリを管理してくれます。スコープ内で使われるオブジェクトは自動的にリテインされ、スコープを抜けた時に自動的に解放されます。
しかし、自動的なメモリ管理だけでなく、手動でも管理することができます。Swiftには
Unmanaged
という仕組みが用意されており、
Unmanaged.passRetained
Unmanaged.passUnretained
を使うことで、自分でリテインやリリースを制御することが可能です。
例えば、
unmanaged = Unmanaged.passRetained(someObject)
とすることで、そのオブジェクトの参照カウンターを手動で管理できます。これにより、高度なメモリ制御が可能になります。
次に、セミコロンについてです。Swiftでは行末にセミコロンを書かなくても良いですが、書いてもエラーにはなりません。例えば:
let number = 10;
という風に書けます。セミコロンは1行に複数のステートメントを記述する場合に使うことができます。例えば:
let a = 1; let b = 2; let c = a + b
また、Swiftの強力な機能の一つに名前付きパラメータがあります。名前付きパラメータは、関数を呼び出すときに引数の名前を指定することができ、コードの可読性が向上します。例えば:
func convert(value: Int, toType type: String) { // 関数の実装 } convert(value: 42, toType: "String")
このように、名前付きパラメータを使うことで、引数の意味が明確になり、コードが読みやすくなります。最近では、この機能を採用する言語が増えてきています。
以上、Swiftの名前空間、メモリ管理、セミコロンの使用、名前付きパラメータに関する基本的な概念を紹介しました。 とりあえずリターンを完成させると良いでしょう。リターンを完成させることで、Swiftのコードが正しく機能します。
次に、具体的な使い方を見ていきましょう。たとえば、引数にラベルを付けると、次のようなコードになります。
func convert(value: Int, to type: Double.Type) -> Double { return Double(value) }
このように定義してあげると、使うときにどうなるかと言いますと、タイプを指定する必要がありますが、以下のように書けます。
convert(value: 10, to: Double.self)
この段階で、
value
には10が入り、これが変換されて
Double
型になります。ラベルがあることで、引数の意味が明確になります。ラベルがない場合、どの値がどの引数に対応しているかを直感的に判断するのが難しくなります。
また、Swiftでは外部引数名という考え方もあります。たとえば、
convert
関数をもう少し読みやすくするために外部引数名を使うと、次のようなコードが書けます。
func convert(_ value: Int, to type: Double.Type) -> Double { return Double(value) }
この場合、以下のように呼び出すことができます。
convert(10, to: Double.self)
外部引数名を利用することで、コードがさらに読みやすくなります。特にGitHub上でコードレビューをする際に、引数の意味が明確になるのは非常に便利です。
ここまでで、ラベル付き引数と外部引数名のメリットが理解できましたでしょうか。引数にラベルを付けることで読みやすさを保ち、そのラベルを使って外部引数名を設定することで、Swiftのコードは非常に表現力豊かになります。
次に、APIデザインガイドラインについても触れておきます。適切な名前を付けることで、コードの読みやすさとメンテナンス性が飛躍的に向上します。このガイドラインを参考にすることで、Swiftのプログラムを迅速かつ楽しく書くことができるようになります。
この勉強会では、名前付きパラメーターのメリットについて具体例を交えながら説明してきました。これがSwiftという言語の強力な特徴の一つです。次回の勉強会では、さらに実装的な詳細について見ていきたいと思います。
本日はこれで時間いっぱいになりました。次回は、今回の概念的な話から一歩進んで、より実装にフォーカスした内容をお届けします。
今回のSwift勉強会はこれで終了です。皆さん、1時間お疲れ様でした。