🐙

TS使っている人のRust 0から勉強日記1

2024/10/09に公開

こんにちは!普段TypeScriptを使ってフロントエンド開発をしている人です🙌

最近、上司に「TypeScript(ちょっとは)書けるようになったからRustやってみようか^^」と言われ、言われるがままやってみたものの何をやっているんだ状態…

ZennにはたくさんのRustの基礎となる部分の記事や本があったため、同じような内容にはなりますが、この記事の内容は基礎の基礎(+TypeScriptとの比較?)で自分の勉強の記録用に書いています。


Rustってなんだ

そもそもRustってなんなんだから始まったので、とりあえずドキュメントを。
https://www.rust-lang.org/ja

ドキュメントを読むのがすごく苦手なので、開いてすぐRustのメリットが書いてあったことは助かりました。
(このドキュメントを読んでわかる方にはこの記事見なくても…ってなる気はする)

Rustのディレクトリについて

私はいつもVSCodeを使用してRustのimageの入ったコンテナを立てています。
コンテナを無事開くことができたら、

$ cargo init

とすることで 実際に実行する部分のmainを含めた最小構成を作成してくれます。

├── src/
│   └── main.rs
├── Cargo.toml
└── Cargo.lock  (プロジェクトで`cargo build`等の操作を行うと生成される)

クレート(Crate)

クレートとはコードをまとめたもので最小のコンパイル単位です。
(少なくとも一つのクレートから成り立っている。)

クレートにはバイナリクレートライブラリクレートがあります。

バイナリクレートとはmain.rsをエントリーポイントして実行可能なバイナリを生成するクレートです。

ライブラリクレートとはlib.rsをエントリーポイントとして他のクレートから再利用できるライブラリを提供するクレートです。

モジュール(module)

モジュールは、1つのクレート内でコードを整理し、名前空間を提供する単位です。
modをキーワードとして使用します。
Rustではデフォルトで関数や構造体・定数などのモジュールは非公開(private)なので、pubmod(とuse)を使用することで公開されるようになります。
(pubはTypeScriptでいうexportでmodがimport的な…?)

pubとmodの使い方の前に先にどのようなファイルがあるかの確認を。

ファイル名 内容
main.rs cargo devやcargo startなどをすると実行されるルートファイル
通常srcディレクトリ内に存在する
lib.rs ライブラリとして生成されるルートファイル
通常srcディレクトリ内に存在する
build.rs build するときに実行するファイル Cargo.toml と同じ階層に書くこと
mod.rs Rust2015方式のそのディレクトリのエントリーポイントとコードの整理としての役割
(この二つの部分に関してはTypeScriptのindex.tsと一緒)

pub mod useの使い方

例えば、次のようなファイルがあるとします。

command.rs
pub fn commandA () {
    println!("このコマンドはAタイプです。");
}
同じものをTypeScriptで書くとこんな感じ
Commnad.ts
export function commandA(){
    console.log("このコマンドはAタイプです。")
}

このファイルをmain.rsやlib.rsなどの別のファイルで使用したい場合は以下のようになります。

mod command;
fn main () {
    command::commandA();
}
同じものをTypeScriptで書くとこんな感じ
index.ts
import * as command from "./Command";
function main(){
    command.commandA();
}

ここで注意しなければならないのは指定するファイルの深さです。
上の例のディレクトリ構成は以下のように使用したいファイル(main.rs)と指定したいファイル(command.rs)が同一階層にある場合の話です。

src
├── command.rs
└── main.rs

では、使用したいファイルより深い階層のものを使用したい場合はどうしたらいいかというと、mod.rsや指定したいファイルのあるディレクトリ名と同じファイル名を使って指定できるようにしてあげます。
言葉だけでは何言ってんだこいつ状態だと思うので、例をあげてみます。

mod.rsを使用する場合

mod.rsとはRustのedition2018までで使用されていたものです。(今でも使用はできる)
ディレクトリ構成とmod.rsの内容は以下のような感じ。

src
├── commands
│   ├── mod.rs
│   ├── command_a.rs
│   └── command_b.rs
└── main.rs
TypeScriptだとこんな感じ
src
├── commands
│   ├── index.ts
│   ├── CommnadA.ts
│   └── CommandB.ts
└── main.ts
mod.rs
pub mod command_a;
pub mod command_b;
TypeScriptだとこんな感じ
index.ts
export * from './CommnadA';
export * from './CommnadB';

こうすることでmainからの複数のファイルの呼び出しを「mod commands」と1行で呼び出せるようになります。

main.rs
mod commands

fn main(){
    commands::command_a::command_a();
    commands::command_b::command_b();
}
同じファイル名の場合

これはedition2018以降で使用されるようになったもの。
ディレクトリ構成は以下のような感じ。

src
├── commands.rs
├── commands
│   ├── command_a.rs
│   └── command_b.rs
└── main.rs

mod.rsの内容とcommnads.rsの内容は一緒ですが、ファイル名とファイルの置いてある階層が異なります。
今回はcommandsというディレクトリをまとめておきたいので、ディレクトリ名と同じファイル名ディレクトリと同じ階層にファイルを作成します。

私は今のTypeScriptでのディレクトリ構成と同じようにしたいので、mod.rsを使用しています…
私の事情はさておき、サブタイトルにある「use」についてまだ何も触れていません。

pubとmodだけでも他のファイルやディレクトリ先で使用することができるのですが、ディレクトリの階層が長くなるとそれだけ指定する関数などまでの一文が長くなります。
「冗長なのは嫌だ!!!!」って時に使用するのがuse(やpub use)です。

例えばこんなディレクトリ階層の時

src
├── commands.rs
├── commands
│   ├── a.rs
│   ├── a
│   │   └── test1.rs
│   ├── b.rs
│   ├── b
│   └── test2.rs
└── main.rs

useを使用せずにmain.rsでcommandsの関数を呼び出すには、

commands.rs
pub mod a;
pub mod b;
main.rs
mod commands;
fn main(){
    commands::a::test1::test1();
    commands::b::test2::test2();
}

main関数内での呼び出しが長い…
(今回名付けがめんどくさかったので)短いファイル名だからいいもののもっと長いファイル名ばかりだったとすると辿り着くまでがすごく遠く感じますね。

useを使用してみた場合どうなるか

main.rsで使用する場合
main.rs
use commands::a::test1;
use commands::b::test2;

fn main() {
    test1();
    test2();
}
TypeScriptだとこんな感じ
main.ts
import {test1} from "./commands/a/test1";
import {test2} from "./commands/a/test2";
function main(){
    test1();
    test2();
}
mod.rsなどで使用する場合
command.rs(mod.rs)
pub mod a;
pub mod b;

pub use a::test1;
pub use b::test2;

この時のmain.rsは

main.rs
mod commands;
fn main(){
    commands::test1();
    commands::test2();
}

//または
use commands::{test1,test2};
fn main(){
    test1();
    test2();
}

modを使用した場合::でネストすることができないので、関数内での呼び出しで呼び出し先まで指定していましたが、useを使用することで関数内での呼び出しがスッキリしましたね。
個人的には一文が長いと読む気が失せちゃうのでuseを使った方が見やすくていいかなと…

型定義

結構ざっくりとしか書いていないので、この内容もあるよって思う方はぜひコメントに書いていただけると助かります…。

データ型

用語 型の種類
Vec<要素の型>,[T;N] 配列 ([T;N]はTがN個の配列 )
Option<要素の型> 値があるかないかわからないもの
char,String,str(&str) 文字列型
i8,u8,i16,u16,i32,u32,i64,u64,isize,usize 整数型
f32,f64 浮動小数点型
bool ブーリアン型
(型名,型名...) タプル型 複数の型を一つにまとめられる

書き方

プロパティ(値を持っているもの)の宣言方法は以下のようになります。

struct 型名 {
    プロパティ
}

//例
pub struct Student {
    pub id:i16,
    pub name:String,
    pub address:String,
}

関数の宣言方法は以下のようになります。

impl 型名 {
    関数
}

//例
impl Student {
    pub fn new(id:i16,name:String) -> Self {
        Self {
            id,
            name,
            address:Default::default()
        }
    }
} 

トレイト(trait)

トレイトとは型に共通の振る舞いを定義するための機能です。複数の方に対して共通のメソッドを実装することができます。

書き方は以下のようになります。

trait 型名 {
    関数など
}

//例
trait Example {
    fn example(&self);
}

struct Student {
    pub id:i16,
    pub name:String,
    pub address:String,
}

impl Example for Student {
    fn example(&self){
        println!("{}の学籍番号は{}です", self.name,self.id);
    }
}

所有権について

私は、コードを書き始めてここの理解がまず第一の壁になりました。

一言で所有権とは?

メモリ管理と安全性の保証のためのもの。所有権というものにより、ガベージコレクション(GC)なしでメモリリークやデータ競合を防ぐことができます。

私はガベージコレクションというものが何かわかっていなかったので下のサイトでふんわり内容把握しました。
https://wa3.i-3-i.info/word1176.html

なぜRustにはあってTypeScriptにはないのか

理由は単純(?)でRustはガベージコレクションがなく、TypeScriptにはランタイム環境にガベージコレクションが内蔵されているからです。
ちょっと話はそれますが、なぜガベージコレクションの有無が言語によって分かれるのか気になったので、ある場合とない場合のそれぞれのメリットをChatGPTに聞いてみたところ、以下のような返答が返ってきました。(ちょっと長いので気になる方は見てください)

ChatGPTからの返事

< ガベージコレクションありの時 >
メモリの簡素化
手動でメモリの解放の管理をする必要がないので開発者の負担軽減、開発効率の向上、コードの簡素化が可能となります。

メモリリークの防止
GCが不要になったオブジェクトを検出し、自動的にメモリを解放するため、メモリリークのリスクが低くなります。

柔軟なメモリ操作
GC環境では、オブジェクトの参照やコピーが柔軟に行えます。

< ガベージコレクションなしの時 >
パフォーマンスの向上
GCがない場合、メモリ管理のオーバーヘッドがないため、リアルタイムアプリケーションでのパフォーマンスが向上します。
メモリの割り当てと解放を手動で行うことでリソース管理の精度を高めることができます。

メモリ使用の最適化
メモリ管理を手動で行うことで、メモリ使用の最適化が可能になります。
メモリ管理のタイミングやパフォーマンスをより正確に制御できるため、プログラムのパフォーマンスを予測しやすくなります。

低レベルシステムとの統合
OSやデバイスドライバなどの低レベルシステムプログラミングでは、メモリ管理の詳細な制御が必要です。GCのない言語では、メモリ管理を直接扱うことができ、ハードウェアリソースに対するより詳細な制御が可能です。

メモリの管理をしなきゃいけないけど、できること増えたりパフォーマンスいいぞって感じなのかな…?

所有権と束縛と参照と可変性

全ての値に「所有者」が存在し、その所有者が所有権を持っています。(1つの値に1つの所有者)
所有者がスコープから外れるとその所有者が持つ値はメモリから解放されます。

『スコープから外れる…?メモリから解放…?』ってTypeScriptばかり使ってきた私はなりました。
メモリの解放は上で書いたガベージコレクションがよしなにしていたのはわかりましたが、スコープってなんだよってことなので軽く追加説明を

スコープとは

ここでいうスコープとは、ある変数やオブジェクトが定義されているブロック(関数、ループ、条件分岐など)のこと。
一つの関数や関数内の{}で囲まれたものってことですね。

ChatGPTにお願いしたら例を出してくれました。

fn main() {
    let x = 5;  // 変数 x がスコープに入る(このブロック内で有効)
    
    {  // 新しいスコープが始まる
        let y = 10;  // 変数 y がスコープに入る(この内側のブロック内で有効)
        println!("Inside block: x = {}, y = {}", x, y);
    }  // この内側のスコープが終了し、y はスコープを抜ける(y はここで無効になる)

    // println!("{}", y);  // y はスコープ外なのでエラーになる
    println!("Outside block: x = {}", x);
}  // ここで main 関数が終了し、x はスコープを抜ける(x も無効になる)

束縛

変数やオブジェクトの生成の際に変数名などが特定の値に束縛されます。
たとえば、変数xに値5を束縛するという意味は、xが値5の所有権を持つことを意味します。
これによってコードを読んでも誰が所有者か判断できますね。

let x: i32 = 5;

参照

Rustでは所有権を使用してオブジェクトの受け渡しをします。
例えば,以下のような感じ

let a: i32 = 10;
let b = a;
// println!("{}", a);  // a は所有権を渡したのでエラーになる
println!("{}", b);

では所有権を渡さずになんとかしたい場合どうするかというと、仮の所有権を作成して渡します。その一つの方法が参照です。
これは&を使用します。

let a: i32 = 10;
let b = &a;
println!("{},{}", a,b);

可変性

Rustでは標準でオブジェクトを不変で束縛します。
上で書いたようにTypeScriptのletとは違い、定義した変数やオブジェクトをある条件で変更したいなどの場合はその変数が可変であることを示しておく必要があります。これはmutを使用します。

let mut a: i32 = 10;
let b = &mut a; //参照かつ可変

借用チェック

参照と可変性について決まりがあります。

  • 不変参照(&だけ)は何個でも同時に存在可能
  • 不変参照(&だけ)と可変参照(&mut)は同時に存在できない
  • 可変参照(&mut)は同時に1つしか存在できない

となります。
この決まりは関数呼び出し時(かつコンパイル時)にチェックされます。
これを借用チェックと呼びます。

fn main() {
    let s = String::from("hello");
    
    // sの不変な参照(借用)を作成
    let r1 = &s;
    let r2 = &s;  // 複数の不変な参照は許可される

    println!("r1: {}, r2: {}", r1, r2);  // 参照を使ってデータを読み取る
}  // r1とr2がスコープを抜けると、不変な参照も無効になる

(簡単すぎるかもしれない)クイズを出します。
以下のprintln!をどちらもできるように順番を変えてみてください。r2は可変参照としたいです。

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &mut s;  
    r2.push_str(", world");
    println!("r1: {}", r1);  // エラー
    println!("r2: {}", r2);
}

答え
 fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    println!("r1: {}", r1);  // r1のスコープはここで終わる
    
    let r2 = &mut s;  // r1が使われた後なので、可変な借用が可能
    r2.push_str(", world");

    println!("r2: {}", r2);
}

先にr1を参照ではなく、そのままprintln!()の中で使用することでr1のスコープを終わらせることができます。


まとめ

なんとなーくのRustとTypeScriptの比較を含めた基礎の基礎でした。

Rustを使っている方や理解している方からしたらものすごく雰囲気しかわかっていないのが伝わってそうですが、Rustを触ったことない方や普段TypeScriptを使用している方に少しでもふんわりとどんなものか伝わってたなら嬉しいです!

私自身、所有権を理解するまでの壁がすごく高く感じる…
次回もう少し深掘りしていきます。(コピーとかクローンとか色々…)
(自分が忘れた時用なんですけど)またfor文の作り方とかもまとめていきたい…!

ここまで読んでいただきありがとうございました!🙌

Discussion