2024年1月27日土曜日

Rustで始める安全第一プログラミング【世はまさに大安全時代】 2024/01/25

https://zenn.dev/hajimari/articles/618c900799fe90

https://zenn.dev/hajimari/articles/618c900799fe90


 Rustを勉強中の、万年駆け出しエンジニアです。

学べば学ぶほど、プログラミングの大きな壁に阻まれ日々苦戦しております。

今回は「Rustで始める安全第一プログラミング」という主題で、各種安全の重要性やRustで何がどのように解決されているのかについて書こうと思っています。

プログラミングにおける"安全"の重要性

プログラミングには危険がいっぱいです。
動的型付け言語であれば、例えばnull/undefinded が関数に渡されてるが、メソッド内ではその中のプロパティにアクセスしようとしてる場合、実行してからエラーが発覚します。

以下は動的型付け言語であるJavaScriptでの例:

function exampleFunction(data) {
    return data.someProperty;
}

// undefinedを渡す
exampleFunction(undefined);

これが本番環境で実行されてしまったら悲劇ですが、静的型付け言語であればコンパイル時にその悲劇を防いでくれます。コンパイルとは主にコンパイラ言語に際し、実行可能な内容に翻訳することを指します。

以下は静的型付け言語であるTypeScript[1]での例:

function exampleFunction(data: { someProperty: string }) {
    return data.someProperty;
}

// 型が正しくないため、この行はコンパイル時にエラーとなる
exampleFunction(undefined);

とにかく、上で説明したかったのは、「プログラミングには危険がいっぱい」ということ。

動的型付け、静的型付けについては今回の記事では特に扱わないので、以下の神記事を読むと理解が深まると思います。

先の例で出した、TypeScriptが注目を集めたのは、強力な型安全があったからです。つまり最近のエンジニアリングのトレンドは、悲劇を未然に防ぐことができる言語なのです。このようなものを型安全と呼びます。

Rustが持つ安全性

先述した内容では、"世はまさに大安全時代である" と説明しました。
例に出したのは型安全でしたが、Rustはその他の安全性もきっちり保証してくれる優秀な言語です。

Rustが持つ安全性は以下です:

  1. 型安全
  2. メモリ安全
  3. スレッド安全
  4. null安全(型安全に内包される?)

型安全

型安全自体の説明もすると長くなるので、null安全の話も混ざっていますが、神記事のリンクを以下に貼っておきます。

「プログラミングにおける"安全"の重要性」の章でも説明しましたが、要するに関数などにおいて期待する型(文字列、数値、オブジェクト、配列、etc...)を指定し、それ以外が渡された際に静的エラーを吐いてくれることを言います。

// Rustの文字リテラル型は&strなので型エラーを吐きます
let str: String = "foo";

// Rustの文字リテラル型は&strなので問題なく動作します
let str: &str = "foo";
// String型を使いたい場合は以下
let str: String = String::from("foo");
let str: String = "foo".to_string();

文字列の型については以下を参照ください。

Rustでは(比較するもんではないかもですが)TypeScriptよりも強力な型安全が保証されており、例えばですがTypeScriptでは多用されるunion型がRustでは存在しません。union型とは、A || B(AまたはB)のような型です。
直和型[2]と呼ばれる型は存在するのですが、長くなるので(以下略)

また、型の章で説明する方がスムーズなのでnull安全についても触れますが、Rustにはそもそもnullが存在しません[3]。これについては、長くな(以下略)。nullが存在しないという奇抜な方法で、強力なnull安全を実現しています。

Rustにおいて、整数型はTypeScriptよりも詳細に分類されています。正の整数の場合、使用可能なビット数に応じてu8u16u32u64u128などの型が用意されています。

型の範囲を超える値を入れた場合、Rustは型エラーではなくオーバーフローエラーを吐きます。

// u8の範囲(0 ~ 255)を超えるため、オーバーフローエラーを吐きます
let num: u8 = 300;

// u16の範囲(0 ~ 65535)以内のため、問題なく動作します
let num: u16 = 300;

型安全な言語では、本番環境で実行する前にエラーを吐いてくれるため、簡単に事前に修正することができるわけですね。

メモリ安全

型安全に関してはTypeScriptの登場で聴き馴染みのある方も多かったと思いますが、Rustではメモリも安全に守ってくれます。メモリ安全とは、自動でメモリ解放を行なってくれる特徴のことです。

メモリは「倉庫」のようなもので、その中には「箱」のようなものが存在します。プログラムが動作する際、箱にデータを入れ、これを倉庫に格納します。
メモリリークとは、不要になったデータが倉庫内に残り続けることで、倉庫が溢れてしまう現象です。Rustでは、不要になった箱の中身(データ)を自動的に空にして倉庫のスペースを確保します。

コレを可能にしているRustの仕組みが、所有権[4]ライフタイム[5]と呼ばれるものなのですが、詳しくは以下の記事を。

この記事は非公式ながら公式サイトでも紹介されている日本語ドキュメントです。

Rustではこのメモリ安全を上記の方法で実現しているため、JavaやPythonとは違いガベージコレクション(以下GC)を使いません。GCはプログラムを走査し、解放できるメモリを解放するという手法ですが、この際に他のスレッドが全て停止している必要があります。

GCを使わず、コンパイル時にメモリ安全を実現しているため、Rustはスレッドを停止することなくプログラムを実行します。

スレッド安全

Rustは並行処理(マルチスレッド)が可能ですが、同じメモリ空間でメモリを確保します。その際、意図しない更新の衝突などが発生する可能性があります。

先ほどの「倉庫(メモリ)」と「箱」の例で説明しましょう。
箱の中身を出し入れする人が同じ倉庫に複数いて、Aさんが箱に「1」という値を入れたとします。

その後Bさんが同じ箱の中身を「2」に変更したとき、Aさんは箱に「1」が入っていると思いながら「2」を取り出してしまいます。

ではどのようにスレッド安全を実現しているのかというと、なんと「所有権」がここでも大活躍してくれます。

以下はRust はどのようにして安全な並列処理を提供するのかという神記事からの引用です。

  1. Rust はスレッドをつかってコードを並列で実行します
  2. 所有権の制約によりスレッド間でのデータの共有が行われないことが保証されるためデータ競合が起こり得ず安全です
  3. スレッド同士のデータの共有をチャンネルというメッセージの送受信器を経由して行う場合、スレッド内のデータはそのスレッドからしか変更されることがないため安全です
  4. スレッド間で可変なオブジェクトを共有する場合、データ競合が発生しない仕組みを利用していることをコンパイル時にチェックするので安全です

参照: Rust はどのようにして安全な並列処理を提供するのか

詳しくは上記の記事を読んでみてください。

Let's 安全第一プログラミング!

ここまでRustが超安全な言語であることが分かったことでしょう。
内部を知るまで、自分は「Rustって尖った言語なんだろうな〜」と思っていましたが、超安心・安全のまぁるい言語です。

Rustに関しては自分もまだ勉強中の身なので、皆で学んで安全第一プログラミングをしませんか?

脚注
  1. TypeScriptではコンパイラによるコンパイルではなく、トランスパイラによるトランスパイルが行われ、結果JavaScriptに翻訳されます。静的エラーはTypeScriptの場合トランスパイラが吐いてくれます。 ↩︎

  2. 直和型: Rustにおいてenumで宣言できる型で、代表的なものにはOption, Resultなどが存在します。とある型に他の型を入れることが可能です。 ↩︎

  3. Rustでnullのような振る舞いを実現したい場合は、Option::Noneを利用しますが、この際のNoneはnullではなくOptionという直和型の列挙子です。 ↩︎

  4. 所有権: Rustでは誰がなんのデータを持っているのかを明確にすることで、いつメモリ解放をすれば良いかコンパイラが検知できるようにします。 ↩︎

  5. ライフタイム: Rustの特殊な概念で、あるデータが解放されるまでの期間を指します。 ↩︎

0 コメント:

コメントを投稿