Page icon

第125回

2022/06/17
The Basics
Empty
Empty
Empty
Empty
12 more properties
今回は
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行目のような例で、片方にエクステンションしたはずが両方で使えてしまうというようなことも言語仕様的に防げるのは良いことです。
今日は時間になったので、また次回に続けましょう。これで今日の勉強会を終わりにします。お疲れ様でした。ありがとうございました。