Open7

Rust学習メモ ー Tour of Rust で気になった点をまとめていく

yojiyoji

Tour of Rust(リンク)で気になった点、勉強になった点、疑問点をまとめていく。

きっかけ

Rustを用いたWebアプリの機会をいただけそうなので、勉強を始めようと考えた。
Rustについては正直全く知らないので、いろはから学ぶのに良さそうなTour of Rustを読んでいく。

目的

Rustの習得の脚がけにする。
主に以下の事項を達成できるようにする。
・基本的な文法を知ること。
・Rustにおいて重要な概念やライフサイクルを理解すること。
・Rustの良さを把握すること。(できれば)

バックエンド側のパフォーマンスも気になるところなので、Rustのライフサイクルについては注意して学習する。

yojiyoji

第一章 基礎

言語特徴

型推論型の言語である。 → 明示的な型宣言がそんなに必要ない?

変数の可変と不変を大変気にする言語である。
→ 後ほど追求する予定。

let mut a = 44;

「mut」が可変属性を付与する。
→ 「mutable」の略と想定
(日頃からTypescriptをよく使うので、letの後ろに何かをつける文法は少し慣れない…)

→ 文字列で試してみたら、警告文が出た。

文字列はすでに可変であると言うこと?

文字列の章があるので、先延ばし。

符号なし(u8など)と符号あり(i64など)の型がある。

→ メモリを気にするシステムに近い言語の特徴、C言語に近い?

スライス型について

コンパイラが決定して把握するコレクション型
→ コンパイルまでするまでわからない → コンパイルするまでは長さが無限の配列

文字列型について

文字列型はスライス型の延長に位置する型になっている。

実行時に、その文字列やスライスの長さが決定する。
→ 文字列型は構成する文字に関するスライス型であると考える

13i32

上のように、数字の後ろに型を続けて記述できる。(知らなかったら混乱しそう)

タプル型

「()」で囲んだデータの集合の型。
要素番号をピリオドで繋ぐ書き方をする。(こちらも中々みたことがない)

tupple.0 

unit:空のタプル = ()
┗ あまり使われないらしい…

配列

定義時に、型と配列の長さを同時に渡す。(独特かも)

let numbers: [u32; 4] = [0, 1, 2, 3];

なお、スライス型は 「; 4」の部分が必要ない。

関数

返り値について

タプル型(()で囲んだ型)を使うことで、複数の値を返すことができる。

yojiyoji

第二章 基本制御フロー

コメントアウト

「//」で行をコメントアウト可能

if文

条件式は「()」で囲む必要がない。
以降、while文やmatch文も同様。

// "199未満"が出力
let x = 198

if x < 100 {
	println!("{}未満", 199);
}

loop文

無限ループする構文。(初めてみた)

// xが常に増え続ける
loop {
	x += 1
}

while文などと同様、breakで抜け出すことができる。

for文

for in で記述できる(Typescriptも似たような記述方法がある。すぐ慣れそう。)

for x in 0..5 {
	println!("{}", x)
}

0 .. 5 : 0から5の一個手前まで(つまり、0から4まで)のイテレータ

0 ..= 5:0から5までのイテレータ

match文

switch文の強化版みたいなもの?こちらも初めて出会った。

必ず条件を網羅しなければならないという制約がある。

match x {
	0 => {
		// 0に該当するとき
	}
	1 | 2 | 3 => {
		// 1, 2, 3のいずれかに該当するとき
	}
	4..=10 => {
		// 4から10のいずれかに該当するとき
		// イテレータを使用可能
	}
	matched_num @ 10..=100 => {
		// 10から100のいずれかに該当するとき
		// 詳細は下部
	}
	_ => {
		// switch文のdefaultに該当する。「_」で表現。
	}
}

@:この記述の仕方で、イテレータに該当するときにmatched_numと言う名前の変数に紐付けできる。

ブロック式

if文、match文、関数、ブロック({}で囲んだ式のまとまり)は単一の方法で値を返せる。

それぞれの文で文末記号(;)がないものが返される。

  • ブロック式
let x = {
	let a = 2;
	let b = 3;
	a + b
}
  • 関数

関数は 「return」 のようなものを使用せず、文末記号がない式が返り値になる。

fn sample() -> i32 {
	let c = 42
	let d = 100
	// c + d、つまり142が返り値になる
	c + d 
}
  • if文

三項演算子などを返せる。(文末記号はつけてはダメ)

  • loop

(ブロック式とは異なるが)loop文も値を返す方法がある。

その方法は、break の後ろに返り値を記述する方法である。

// スコープ外の変数を操作したい場合は、スコープ外の変数を可変にしておく必要がある。
let mut y

// xに"100超"が格納される。
let x = loop {
	y += 1
	if y > 100 {
		break	"100超"
	}
}
yojiyoji

第三章 基本的なデータ構造体

構造体

struct

fieldの集合。(構造体のキー名と型を結びつける。)

演算子「.」で中のフィールドの値を取り出し可能。

struct book {
	title: String,
	price: i32,
	pages: i32,
	author: String,
}
  • タプルライクなstruct

キーを省略した書き方。

struct Date(u32, u16, u16);

fn main() {
    let date = Date(1992, 11,9);
    // 1992年11月9日
    println!("{}年{}月{}日", date.0, date.1, date.2);
}
  • ユニットライクな構造体。

フィールドそのものを持たない構造体。(空のタプルをunitと言うことから。)

こちらもほとんど使われないらしい…

enum

列挙型。こちらは様々な言語でもお馴染みの型で、宣言したキーワードまたは値しか値を取れない型。

enum Fruits {
	Apple,
	Banana,
	Orange,
	Grape,
}

fn main() {
  // 下記のスタティックメソッドと同じ呼び方をする。Enumも静的なオブジェクトになるからか?
	let apple = Fruits::Apple;
	
	// そのままではprintすることは不可能。
	println!("{}", apple);
}
  • データを持つ列挙型

列挙型は複数の型を持つことが可能。(他の言語のUnion型がEnumのみで実現できる。)

enum Fish {}

enum Meat {}

enum Vegetable {}

enum Food {
	Fish,
	Meat,
	Vegetable,
}

別名:tagged_union (タグ付き共用型)

→ 複数の型を組み合わせて新しい型を作り出すことができる。

→ 「代数的データ型」を持つと言われている。

? 調べてみたが、意味を掴みかねている。要調査。

メソッド

関数とは異なる点に注意!

メソッドは、特定のデータ型に結びつく関数のこと

  • スタティックメソッド

型そのものに結びつくメソッド。

Javaにも同じ役割のメソッドがある。このメソッドは該当の型をインスタンス化しなくても使えるメソッドであったので、同じ役割であると想定。

// 「::」で型からスタティックメソッドを呼び出せる。
let s = String::from("Hello World")
  • インスタンスメソッド

ある型のインスタンスに紐づくメソッド。

スタティックメソッドと対比して、インスタンス化しないと使用できないメソッドであると想定。

→ 想定通り、インスタンス化しないとエラーになった。

メモリ

(おそらく)Rustを理解する上で重要な概念。

C言語と同じく機械語に近い言語になるため、どこのメモリにデータを配置するかは非常に重要になる。

三種類のメモリを扱う。

データメモリ

固定長、もしくはスタティックなデータを配置する。

スタティック(静的)なデータとは、ライフサイクルにおいて常に存在し続けるデータで、誰でも読み取れる定数などが該当する。

(文字列も基本的には変更されないデータなので、スタティックデータに該当する。)

ライフサイクル中、いつでもアクセスされるためメモリの場所が変わらないことが特徴。

→ 非常に高速にアクセス可能

→ 文字列以外の用途は少ない?グローバルな定数以外思いつかない…

スタックメモリ

関数内で宣言された変数を配置する。

関数が呼び出されている間、メモリ上で位置が変化することがない。

→ 非常に高速にアクセス可能。

ヒープメモリ

プログラム実行中のデータを保持する。

このメモリの上にあるデータは、追加・移動・削除・サイズ調整が許されている。

→ メモリの場所が動く。(動的メモリ)

柔軟性を得ることができる。遅くはないが…

3つのメモリを比較すると、以下のようなイメージ

速さ

データメモリ > スタックメモリ > ヒープメモリ

使用頻度

ヒープメモリ > スタックメモリ > データメモリ

構造体のメモリの置き方

インスタンス化された構造体のデータはスタックメモリに格納される。

構造体内のStringは、テキストをヒープ領域に配置し、このテキストへの参照アドレスをスタックメモリに格納する。

→ テキストはあくまで「変更の可能性があるもの」で、このテキストのメモリ上の場所(参照アドレス)は変わらないのでスタックメモリに配置される、と言う認識。

yojiyoji

第四章 ジェネリック型

そもそもジェネリック型とは

structやenumを部分的に実装できるようにする型。

(例:型を柔軟に当てたい、特殊な状況を型に含めたい、など。)

※ ジェネリック(Generic)は、「一般的な」や「共通」の意味を持つ英単語

struct GenericStruct <T> {
	item: T,
}

fn main() {
	// turbofish演算子を使った明示的な型指定の書き方
	let genericStruct1 = GenericStruct::<i16> { item: 10 };
	
	// rustの型推論は、ジェネリック型も推測してくれる
	let genericStruct2 = GenericStruct { item: 10 }
	
	// やろうと思えば入れ子も可能(バグの温床になりかねないのでできるだけ避ける)
	let genericStruct3 = GenericStruct {
		item: GenericStruct {
			item: 10
		}
	}
}

Rustには「null」がない

他の言語ではよく用いられる、項目や値がないことを示すnullがRustにはない。

代わりに、Noneを用いる。

特徴的なジェネリック型

Option

ジェネリックなEnum型の一つ。

上記で述べた、値がないことを表現するNoneと必ず存在する値であることを示すSomeで構成される。

enum Option<T> {
	None,
	Some(T),
}
fn main() {
	// SomeおよびNoneはOption固有の表現で、省略して記述することが可能
	let x = Some(100);
	let y = None;
	
	// match文で分岐可能
	let z = match x {
		Some(i) => i,
		None => -1
	}
}

Result

失敗する可能性のある値を返すことができるジェネリックなEnum型。

enum Result<T, E? {
	Ok(T),
	Err(E),
}
fn sample_func(i: i32) -> Result<i32, String> {
	if i < 100 {
		Ok(i)
	} else {
		Err(String::from("100未満ではない"))
	}
}

fn main() {
	// 
	let result = sample_func(99);
	
	// こちらもmatchで分解可能
	match result {
		Ok(i) => println!("成功 {}",i),
		Err(e) => println!("失敗"),
	}
}

💡 main関数もResultを返すことができる。

エラーハンドリング

Result型はTypescriptなどの非同期処理と同様にエラーハンドリングができる。

エラーハンドリング用のメソッドもデフォルトで定義されている。

// 以下は等価
do_something_that_might_fail()?

match do_something_that_might_fail() {
	Ok(v) =. v,
	Err(e) => return Err(e),
}

ベクタ型(コレクション型)

ジェネリック型には、コレクション型(複数の値を取り扱うのに有用な型)を含む。

他言語では配列型や辞書型、集合型がある。

Vec(ベクタ)は可変サイズのリストである。

iter()メソッドでベクタのイテレータを作成可能。

→ for文などでベクタを回せるようになる

fn main() {
	// pushするなら、Vecは可変でなければならない(ベクタのメモリの長さが変化する可能性があるため)
	let mut sample_vec = Vec::new();
	sample_vec.push(1);
	sample_vec.push(2);

	// マクロを使った定義方法。
	let sample_vec2 = vec![String::from("a"), String::from("b")];	
}

ベクタはヒープメモリに作成される。最初はデフォルト長の長さの領域が確保される。

デフォルト長で足りなくなった場合、データを再割り当てする。

値を簡単に取り出す方法

Option型のSomeと、Result型のOkを即座に取り出したい場合、unwrapメソッド簡単に(分岐構文を使わずに)取り出すことが可能。

ただし、NoneやErrを考慮しないメソッドのため、失敗することがある

もし失敗する場合は、unwrapメソッドはpanic!(プログラム失敗のメッセージを表示し、プログラムを終了させる処理)を実行する。

// 以下は等価, Resultも同様

some_option.unwrap()

match some_option {
	Some(v) => v,
	None => panic!("エラーメッセージ"),
}
yojiyoji

第5章 データの所有権と借用

独特な仕組みと枠組み コードをエラーになりにくくするもの?

「所有権」とは

束縛

以下の行為は、「変数に束縛する」と呼ぶ。

(宣言時の代入で、インスタンスを束縛することができる)

// fooへ構造体のインスタンスを束縛する行為
let foo = Foo { x: 100 };

※ 束縛されるとメモリリソースが作成され、以降ライフタイムが終了するまでRustのコンパイラに確認され続ける。

→ 確認される意味は?

所有権

インスタンスが束縛されている変数をリソースの所有者と呼び、所有し続ける権利を所有権と呼ぶ。

なぜ所有権を考慮する必要があるか

Javaなどの言語では、参照されなくなったりして使用されなくなるインスタンスをガベージコレクションという機能でメモリ上から削除する。

→ メモリの過剰な使用を防ぎ、メモリリークを起こさないようにする。

Rustでは、このガベージコレクション機能が存在しない

代わりにリソースの解放とメモリ上からの削除を行うための機能として、この所有権を利用する。

流れとしては以下。

  1. 変数に束縛され、コンパイラによる監視が始まる

  2. (スコープ内での変数の使用など、ここではコンパイラは監視のみ実施)

    所有権が生きている間は使われなくなっても解放されることがない。

  3. スコープの終わりで、リソースの解放とメモリ上からの削除(ドロップ)を実施

  4. 監視を終了

階層的なドロップ

ドロップ対象が階層的な構造をしているとき、ドロップは親飲みに実施される

→ 順番に解放されていき、一回のみで完結する。

// 解放される時は、foo -> foo.bar の順番

let foo = Foo { bar: Bar { k: 100 } }

所有権の移動と回収

move(移動)

所有権を別の変数に譲渡する行為。

所有権を移動すると、元のスコープでは所有権を所持せず、変数を使用できなくなる

主に関数の実引数として渡す時はmoveに該当する

所有権を返す

移動した所有権を関数から返すことも可能。

所有権を返す行為は、関数の返り値を束縛することで達成できる。

所有権の借用(参照)

所有権は譲渡する方法以外に、元の変数から借り入れる(元の変数にも所有権が残る)方法がある。

let foo = Foo {x: 100 };
let bar = &foo;

// 借用の場合、それぞれの変数がドロップされる。借用元のみがドロップされる訳ではない。

借用は連鎖的に実施することも可能であり、借用した変数の一部を借用することなども可能。

下記に記載する借用ルールが守られていれば、連鎖的な借用の制限はない。

所有権の可変な借用

借用するとき、対象の変数を変更可能な状態で受け取ることができる。

この時、同一インスタンスへの2つ以上の箇所からの変更が可能になる可能性がある。

これはデータ競合を引き起こす可能性があるため、Rustでは禁止されている行為である。

データ競合防止のため、可変な借用が行われている間、借用元の変数の移動および変更ができなくなる。

→ 借用先の変数のドロップを待つか、下記の返却を待つ必要がある。

let mut foo = Foo { x: 100 };
let bar = &mut foo;

// この間で、fooの関数への引数渡しや、変更は禁止されている。以下の行為はコンパイルエラーになる。
// foo.x = 10;

bar.x = 200;
// barがここでドロップされるため、以降はfooへの変更および移動が可能になる。

foo.x = 10;

参照外し

可変な借用などで、元の変数への変更や移動ができなくなる。

変更や移動を行うためには可変先の借用のドロップを待つしかないが、これでは時間がかかりすぎたり、途中で変更したい場合など困る可能性がある。

Rustでは参照を外す(借用取りやめ)の方法がある。

let mut foo = 100;
let bar = &mut f;
// 参照外しした変数
let baz = *bar;

// fooへの直接的な変更は不可能
// foo.x = 10;

// 代わりに参照外しした変数への変更を行う
baz.x = 10;

// 100 10が印字される
println!("{} {}",bar, baz); 

借用に関するルール

  • 単一の変数に対して、可変な参照が1つだけある状態か、不変な参照が複数ある状態かのいずれかの状態しか達成できない。両方存在することはできない

    → データ競合防止のためのルール。

  • 参照は、参照元の所有者より長く存在してはならない。

    → 存在しないデータへの参照がある、という矛盾した状態を防ぐためのルール。

明示的ライフタイム

変数の生存期間(ライフタイム)は、基本的にはコンパイラが管理し、常にメモリを整理してくれる。

ライフタイムをコンパイラに任せ切るのではなく、自分たちで管理する方法も存在する。

ライフタイムの共有

「’」演算子で指定可能。

// 引数と返り値のライフタイムを共有する関数になる
fn sample_func<'a>(foo: &'a Foo) -> &'a i32 {
	&foo.x
}

fn main() {
	// fooとyは同じタイミングでドロップされる 
	let mut foo = Foo { x: 100 };
	let y = sample_func(&foo);
}

→ 複雑なライフタイム管理に便利

スタティックライフタイム

スタティック変数はプログラムの終了まで保持し続ける(ライフタイムが続く)変数である。

スタティックライフタイムは、スタティック変数と同じライフタイムという意味で、リソースに付与することが可能。

「’static」演算子で指定可能で、決してドロップされることがない

→ スタティックライフタイムを使用する場合、参照を含む場合はその参照もスタティックライフタイムでなければならない。

(借用に関するルールの二番目のルールを満たすため?)

データ型のライフタイム

データ型のメンバにもライフタイムを指定することが可能。

→ ライフタイムを指定したメンバを持つ構造体が、メンバが参照した参照元よりも長くは存在しないことを示す。

→ 基本的に構造体のライフタイムは、メンバと親は共有する。

struct Foo<'a> {
	i: &'a i32,
}

fn main() {
	let x = 100;
	let foo = Foo {
		i : &x
	}
	// fooはxより長く存在することはない。
}
yojiyoji

Tour of Rust 6章 文字列

文字リテラル

値としての文字を一般に「文字リテラル」という。

文字リテラルの型は「&’static str」である。

つまり、メモリ上の場所を参照しており、ライフサイクルはプログラム終了までを示す。

また、&mutでないため、変更が許可されない

特徴的なメソッド

  • include_str!

ローカルファイルのテキストを変数に落とし込むメソッド。

let long_str = include_str!("sample.txt");

String

ヒープ領域に文字列を持つ構造体

→ 文字リテラルと異なり、拡張や変更が可能である。

メソッド

  • push_str

文字列の最後に文字を追加する。

  • replace

文字(UTF-8バイト)を別の文字で置換する。

  • to_lowercase

文字を全て小文字に変換する。

  • to_uppercase

文字を全て大文字に変換する。

  • trim

空白を切り取る。

文字スライス

常に有効なUTF-8への参照のこと。(所有権そのものは渡されない)

部分的に切り出すことが可能。

let sample = "sample string";

let word1 = &sample[0..3];
let word2 = &sample[5..7];

関数の引数としても文字列

文字リテラルまたは文字列を関数の引数として渡すとき、文字列スライスとして渡される。

→ 所有権が渡らないので、柔軟性が向上

メソッド

  • len

文字列リテラルの長さを取得するメソッド。

  • starts_with

最初の文字を検定するメソッド。

  • ends_with

最後の文字を検定するメソッド。

  • is_empty

文字列の長さが0かを検定するメソッド。

  • find

検索対象が文字リテラルに含まれているかを検定するメソッド。

あれば数字を、なければNoneを返す。


    let a = "hello, world!";
    let find_word = a.find('h');
    
    match find_word {
        Some(i) => println!("{}", i),
        None => println!("None"),
    }
    // 0 
  • concat、join

文字列を連結させる。

文字列の配列に用いられる。

// hello world!
let sample = ["hello", " ", "world", "!"].concat();
  • 文字列変換(to_string)

文字列以外の型を文字列にするメソッド。

失敗の可能性があるので、Resultで返ってくる。

  • parse

文字列や文字リテラルを型付きの値に変更できるメソッド。

Char

常に4バイトで長さを固定されている型。1文字が入る。

(4バイト固定は、Rustでの探索の利便性のため。)