https://youtu.be/20XxkNtgLR4
今回はThe Basics
の 型エイリアス
について見ていきます。シンプルな機能で、押さえておくべきところもほとんどない感じもしますけれど、それだけにあまり着目しない機能でもあると思うので、せっかくのこの時間を使って見渡せる限り眺めてみようと思います。よろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #125
00:00 開始 00:15 前回の補足 01:00 スティッキービットの挙動 03:42 setuid と setgid の挙動 03:57 setuid はスクリプトに設定しても効果なし 04:31 バイナリーファイルで setuid の効果を試す 06:11 期待通りに動かないので試しに sudo で実行してみる 09:01 setuid が適切に動くように修正 10:52 setuid, setgid についてのまとめ 12:53 往生際悪く調べてしまう癖 13:41 型エイリアス 15:06 既存の型の別名を定義 15:29 既存の型を文脈に応じて分かりやすく 18:24 型エイリアスの型安全性に対する疑問 21:15 C 言語の typedef 21:59 別の型として扱ってくれたら便利なのでは? 23:54 型エイリアスの具体例を挙げてみる 25:01 型エイリアスが異なる型と判定されたとしたら 26:53 型エイリアスと型拡張 28:11 考えられる型エイリアスの使い道 29:01 質疑応答 29:14 タプルや関数型を簡略化する —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #125
今日からザ・ベーシックスのタイプエイリアスの話に入っていこうと思うのですが、その前に前回や前々回に話した内容の補足をしたいと思います。前回、試してみたけれどもうまくいかなかった箇所についてです。
まず、前々回の補足を前回やってみたのですが、うまくいかなかった部分がありました。そのあたりを少し補足してから、タイプエイリアスの話に移っていこうと思います。今回試していたのは、パーミッションのお話でしたが、すっかり忘れてしまっていてうまくいかなかった部分です。具体的には stickybit
と setuid
、setgid
のあたりです。
まず stickybit
についてですが、stickybit
がセットされているディレクトリは、テンポラリーディレクトリとして一般的に設定されています。この stickybit
がセットされている場合、全てのユーザーがそのディレクトリ内のファイルに読み書き可能な設定になります。例えば、テンポラリーディレクトリにファイルを作成し、それを誰でも読み書き可能な状態にしたとします。
$ touch testfile
$ chmod 777 testfile
この状態で stickybit
がセットされていると、他のユーザーが作成したファイルを削除することはできません。root
ユーザーが作成したファイルで試してみると以下のようになります。
$ su - root
$ touch testfile
$ chmod 1777 testfile
この場合、他のユーザーが testfile
を削除しようとすると、パーミッションエラーになります。
次に setuid
と setgid
の話ですが、前回これをシェルスクリプトで試してみたところ、うまくいかなかったことがありました。シェルスクリプトを作って実験してみたけれども、ユーザーの切り替えがうまくいかなかったのは、シェルスクリプト自体に setuid
が設定されていなかったからです。
setuid
を試す場合には、実際のバイナリを作成する必要があります。例えば、以下のように C 言語でバイナリを作成する必要があります。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("現在のユーザーID: %d\\n", getuid());
return 0;
}
このソースコードをコンパイルしてバイナリを作成し、それに setuid
を設定します。
$ gcc -o testprogram testprogram.c
$ chmod u+s testprogram
これで実行してみることで、setuid
の動作を確認することができます。
とりあえず今日はこんな感じで、前回の補足をした上で、次回はいよいよ本題のタイプエイリアスの話に入っていこうと思います。 それで、int main
でvoid
で、それでここでprintf("%d", getuid())
だったかな。これで30と書いてあげて、ちゃんと書けたかな。えーと、gcc
で、あ、u
か。long
を返すからって言ってんのか。何か書式文字列も忘れてしまった。%u
なんてあったっけ、みたいな感じですが、コンパイル通った。で、a.out
を実行すると、これで501っていうのが入ってきてますけど、これで自分のユーザーIDが501なのかな。出てくるかな。出てこないね。まあいいや。
これをchmod
でユーザーにsetuid bit
を設定して、a.out
を実行します。これでs
になりますね。今実行する限りでは501のままなんですけど、これでchown
でユーザーをルートに変えると、ルートが所有することになります。こうすると、a.out
を実行しても、あれ、501か。なんか違うね。getuid
じゃなかったか。間違えたらしい。もう一回確認しようかな。
getuid
。エフェクティブユーザーIDね。これでできたはずなんだけど、うまく動かなかったな。su
コマンドでスーパーユーザーにしてから実行してみますか。スーパーユーザーで実行。どうかな。実行してるプロセスの、あれ数じゃなかったっけ。待ってますね。じゃあ、sudo
ですかね。sudo -su
で。あ、そっかそっか、ユーザー変えてからね。あ、そっか、パスが変わっちゃうのかな。これはゼロ。これはね、そもそも実行するユーザーが違うのでね。euid
じゃなくてuid
だったかな。ちょっとやってみよう。
これでコンパイルをして、あー、あれだね。su rm setuid bit
。で、これでコンパイルかけて、で、これでsetuid bit
を動かすと今501でしょ。で、chmod
でsticky bit
をつけて動かすと、あれ、変わんなかったな。え、おかしいな。一応連中してきて今日望んだんですけどね。うまくいかなかったな。setuid bit
がsticky bit
ついてて、あっ、今そうだ、権限が。chown
だ、間違えた。sudo chown root a.out
。これでうまくいくでしょ。あれ、うまくいかないね。
こういうのね、諦めが悪いのは良くないですよね。そのおかげで、あれ、ちょっとうまく行ってないね。chown
ユーザープラスuid
でしょ。これで。で、a.out
。あ、illegal user
。あっ、own
じゃない、chmod
ね。で、これでa.out
にuid bit
が付いて、ルートが持ってるっていう状態。で、これでa.out
を動かすと、あれ、変わんないね。あれおかしいな。やっぱuid
違うのかな。この際の悪さは裏目に出るときもあれば、いいこともありますけどね。
えーと、int main
でしょ。で、sys/types.h
でしょ。uid_t uid = getuid();
、uid_t euid = geteuid();
。で、gid_t gid = getgid();
、だからこれでgid_t egid = getegid();
。
ユーザーIDとグループIDを取っているんですけど、これでprintf("%d %d %d %d", uid, euid, gid, egid);
とやってあげると、順番に出てくることになるでしょう。これをコンパイルするにあたって、所有権が違うから、chown
でいいか。まず直して、gcc
でコンパイルかけて、今権限どうなってんだ。a.out
が自分の所有権で全部リセットされてるね。ってことは、chmod
で、モッドの前にオーナー変えようかな。sudo chown root a.out
。で、a.out
を実行すると、このときは501、501、20、20ね。
で、これでsudo chmod u+s a.out
。これでsetuid bit
がついた状態でルートが持ってて、で、これでa.out
を実行すると、やっぱゼロになる。そう、ルートのユーザーID、エフェクティブユーザーIDはゼロなわけです。今自分自身は熊谷っていうアカウントなんですけど、実行ユーザ権限はルートなので、ルートができることは何でもできるバイナリが作られたっていう状況になる。これがsetuid
。
で、同じようにsetgid
っていうのがあって、グループにsetbit
つけて、で、これでね、やってあげるとa.out
でグループも、ほら、ゼロになる。こういうやつです。これがスクリプトに対してフラグを設定しても、結局ね、例えばさっきのchmod +x test.sh
がね、動くようにしてあげても、例えばこれで、そうね、シェルスクリプトのね、foo.sh
は実行するってやってあげても。これは変わらない原因っていうのは、結局ね、こうやって書いてますけど実際に実行する時にはね、/bin/sh
に対してtest.sh
を渡すみたいな動きになるので、そうするとね、sh
コマンドが別にsetuid bit
が付属してるわけじゃないのでね、結局のところエフェクティブUIDが変わるっていうことはないっていうね。
これでルート権限でCGIを動かしたかったためにCをやったことがあったな。Perlとかじゃできないんでね。以上が今日のお話でした。 そうそうそう、あのコメントちょっと読みますけど、そうなんですよね。なんかうまくいかないけど、ちょっとやめておこうとか思いつつも、ついつい戻ってしまって延々と時間ばっかり食ってるみたいな。そういう裏目に出ちゃうことってありますが、一長一短でね。急いでるときにこんなことされてたら周りは苛立つとは思うんですけど、こういったのもある意味探求心的なところもあってね。こうやっていくことによって何か見つけられることもあったりするんでね。まあまあ状況に応じて後回しにしたりできる人が賢いのかな。
はい、じゃあちょっと本題いきましょうね。そろそろね。タイプエイリアス、おなじみの機能なんですけれども、あんまり活用もしてない機能かなというのが個人的にはあります。なんかね把握しやすい機能だとは思うんですけど、把握した後ね、それを積極的に使っていくっていう発想まで至っていないなぁと。何気なく使えちゃう分ね。この勉強会でも何回もタイプエイリアスが出てくるたびにお話ししていて、その中で話を聞かせてもらって長い型を見やすくする。まあいろんな事情で型名が長くなっちゃうっていう状況があったりするんで、それをシンプルに置き換えていくよっていう使い方。
あの時はRxSwift
のすごい長い名前の型、それの話を聞いて「なるほどなぁ」って思った気がするんですけど、まあでもね、そういったくらいなわけですよ。まあ型エイリアスだからそんなもんだろうとは思うんですが、せっかくなのでね、ゆっくり見ていって、いい使い道とか発想の足しにでもなったらいいなぁと。この話をしていきます。
まず型エイリアスとは何かっていうと、既存の型の別名を定義するもの。これに尽きるんですよね。とりあえずね。尽きるよね。あ、ちょっと尽きない?いや、尽きるか。まあ気悪いな。後で話そう。
で、typealias
キーワードで定義して、既存の型を文脈に応じてより適切な名前で参照したい時に役立つ。まあ確かにこれが重要なポイントなのかもしれないですね。既存の型を文脈に応じてね、よりわかりやすくっていうのは例えばね、文脈、例えばそうだな、Tweet
っていう構造体があったとして、そのツイートのID
は何ですよっていうのをプロパティに持たせるとき、int
型って持たせますよね。あとツイートのテキストとか、まあいろいろとパラメータ持たせるわけですけど、このくらいなら大したことない気もしますが、ID
はID
型だよねみたいにしたい時、例えばそうだな、typealias ID = Int
として、このツイート型ではInt
はID
として変えるよ、みたいにしてあげると、これでねコンパイルが通るし、ぱっとコードを見た時にID
はID
型なんだなぁ、みたいな。これの利点としては、ツイート型でイニシャライズする時にコード補完でも、こうやってねID
型を取るよ、みたいに表示される。これがまあいいところ。まあ誤解はなくなりますよね。とりあえずね。
まあこういったパラメータ名と型名が一致する場合は大した問題ないと思うんですけど、例えばこれがID
じゃなくてKey
だった時、キーはID
型を取るのか、みたいに読めるようになるっていうのかな。こういうふうにね。とりあえずコード補完でこのあたりちゃんとね、Int
じゃなくてタイプエイリアスに置き換えた型の名前が出てくるっていうのは、プログラマーにとって役に立つ付加情報というのかな。制約することで、Int
っていう広大な範囲ではなくて、ID
を取るんだよっていう意味を制約することで含めていく、みたいな。といった感じの使い方ができるので、文脈に応じてね、より適切な名前がある時には役に立つ、といったものなのかな。
まぁこれくらいなんですよ、自分の中の認識として今のところ持ってるタイプエイリアスね。ただですよ、そんな中でね、大事なコンセプトの一つに型安全というのがありますよね。インスタンスはそのインスタンスの型を想定しているところにしか使えないっていう大事な原則があるわけですけれど、number = 10
、要はねInt
型の番号があった時に、これを渡せちゃうんですよね。タイプエイリアスなんでね、所詮ね。ID
型を求めているところにInt
型を渡せる。まあこれで問題ないと言えば問題ないですけれど、ちゃんとね動くんでね。ただ、コンパイル時に型が違うかどうかを検出するタイミングを意識してますよね。例えば、そうだなぁ、ありえないというか、例えばvalues
っていうのがあった時に、うっかり全然意味をなさないものを渡しちゃってたとします。間違ってね、勘違いで渡しちゃってたときにコンパイルエラーにならないじゃないですか。
これがね、struct
としてタイプエイリアスではなくてstruct
で例えばこうやってID
型が定義されていたとして、こうかな。そうね、こうやって定義されていたとして、それでID
型を使ってたとすると、ここにねInt
を渡そうとしたらコンパイルタイムでエラーになるじゃないですか。こういったところを考えると、まあまあそんな間違いがないところだからといってもタイプエイリアスにするメリットっていうのがちょっと思い浮かばないなって言う感じがするんですよね。 分かるんですよ、とりあえず楽なのは。でも、構造体を作ったらいいんじゃないかなって思うんですよね。例えば、「ツイート型」をJSONとして保存したいなって時には、今は Codable
があるので、Codable
を実装してあげれば、難しいことを考えずにアーカイブもできるし、永続化もできます。そういった点から、便利であるのは分かるんですけど、使いどころが難しいと感じることもありますね。
さて、これでコンパイル全体が通っていると思います。共感してくれる方もいるかと思いますが、C言語の typedef
の話をしますね。typedef
は型を定義するものです。無名構造体に名前を付けたり、インテジャー(整数型)を typedef
すると、元は int
なんだけど、異なる型として宣言できるのです。
例えば、typedef struct 名前 { ... } 名前;
のように定義することができます。これにより、無名構造体に名前をつけて扱うことができるのです。Int
を typedef
すると、それは int
とは異なる型として宣言でき、コンパイルエラーになることがあります。
Swiftでは、なぜそうしなかったのかなって思うこともありますね。型チェックをしてくれたら、四行もかけてID型を作らなくてもよくなるのではないでしょうか。名前が違うということは役割が違うと捉えても良いと確かに思います。
例えば、単位系で同じ Int
でも、キログラムとセンチメートルでは違いますよね。型として違うものと認識させることができれば、センチメートルにキログラムを渡すことがなくなります。typealias
ではなく typedef
だと、型が異なるため型チェックが厳密になり、誤った型を渡すことを防げるのです。
そう考えると、Swiftのtypealias
の活躍範囲も広がると思います。例えば、String
型が SubSequence
に対して Substring
を返す状況を考えてみます。typealias
を使って String
の SubSequence
を定義すると、エレメント型が一致しないとき、コンパイルエラーになるかもしれません。
例えば、次のようなコードの場合です。
private var buffer: String
typealias Element = String.SubSequence
こういったふうに定義すると、buffer
のスタートインデックスなど操作時に型チェックが必要になるかもしれません。しかし、型が違うことで認識されると、正しく型変換が行われます。
例えば、意図せず異なる型の値を代入しようとした場合、すぐに気づくことができます。こうした型チェックの利点を生かしていくことで、コードの可読性も安全性も向上します。
最近のSwiftのバージョンアップで、String
のインデックス表現も書きやすくなると聞いていますので、さらに便利になるでしょう。 このレシーバーをわざわざ書かなくてよくなるらしいです。まあ、こうやって全然違うストリングの型も入っちゃうっていうのが面倒ですよね。もともとこういったことが嫌で型安全を追求していたんじゃないかなと思います。そうすると型エイリアスを作っちゃった型には、明示的な型変換が必要になるというコードになっても問題ないですし、場合によっては型エイリアスを宣言したそのスコープ内だけでは省略できるような言語仕様でもいいと思うんですよね。外では絶対に変換が必要で、という感じです。
こういったもう少し型安全に配慮した型エイリアスになってくれると、もう少し使いどころがありそうな感じがしますね。エクステンションも同様です。例えば、型エイリアスとしてid
型をint
で作成し、エクステンションでid
に対してsomething
のような関数を追加すると、a
がid
型の10、b
がint
型の10としたとき、a
に対してsometing
が呼べるのは当然としても、b
がsometing
呼べるのはどうなのという問題がありますね。
例えば、以下のようなコードです。
typealias ID = Int
extension ID {
func something() {
print("Something")
}
}
let a: ID = 10
let b: Int = 10
a.something() // OK
b.something() // これも呼べてしまう
こういうふうに、コードで出てくる値が10の場合、明示的に型をvoid
として返すようにすればreturn void
でint
を0とか返せば、0と出ますよね。何か変わったのかな、以前はボイドが出てたか、何も出てなかったかのような気がします。でもまあいいや。
とりあえず、型エイリアスはこうやって完全に分身が作られます。今のところ活用の幅としては、コード補完でちゃんと型エイリアスのものが出るくらい。例えば、戻り値をid
型と書いた場合、QuickHelpにぱっとid
が表示されるというくらいです。これが全然出なかった頃もあり、そのときはそれこそ意味がなかったと思いますが、今のところその程度の役割を持っています。ドキュメント的なものが変わるのがメリットかなぐらいの認識ですね。
次回ももう少し型エイリアスについてお話ししようと思っているので、そのときに何かあれば話してもらえればと思います。ここで、コメントを少し拾ってみましょう。
タプルを時々タイプエイリアスする、確かにそうですね。タイプエイリアスがないとタプルは結構つらいです。型を精密に覚えていないと手戻りが激しいし、特にクロージャーを含むタプルとかは大変です。関数型もそうで、パラメーターと戻り値の型が複雑な場合は、コールバック関数をタイプエイリアスしたりすることがありますね。複雑な型に対してタイプエイリアスを使うのは良い手だと思います。
タプルや関数型のような名前の付いていない型にはエクステンションできないので、これを防げるかもしれません。13行目14行目のような例で、片方にエクステンションしたはずが両方で使えてしまうというようなことも言語仕様的に防げるのは良いことです。
今日は時間になったので、また次回に続けましょう。これで今日の勉強会を終わりにします。お疲れ様でした。ありがとうございました。