15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustの難しいコンセプトをまとめてみた

Posted at

概要

長すぎて気が遠くなるRust公式ガイドブックから要点だけとって自分なりにまとめてみました。
例題となるコードや豆知識的なものも詰まっています。初心者には障壁となる「難しい」ものなので主にメモリの話が多いです。

英語の勉強に役立つかもしれないので英語版もどうぞ (uwu*)
https://qiita.com/0_Akise/items/e87e706b0875dbe15f32

所有権(Ownership)

  • C、C++などの言語に存在するメモリリークなどのメモリ周りの問題を防ぐため設けられたRustにしか存在しない特徴的なコンセプトです。
  1. Rustにおいて、宣言された値はシステム上に所有者(Owner)と呼ばれる一つの変数を持ちます。
  2. 所有者は同時に一つしか存在できません。
  3. 所有者がスコープ外(主に{}のことを言う)に行くと、宣言された値はドロップ(消滅、メモリから解除)されます。
  • 後ほどまた説明しますが、所有者の共有(Shared Ownership)が許される場合があります。Rc<T>, Arc<T>などがその例です。これらの場合を除けばほとんどの場合、所有者は上のルールに従います。

  • 所有権のドロップ

fn main() {
	let s = String::from("hello"); // 変数 s が 「String 型 "hello"」 の所有者です。
}
// スコープ外({}の外)になったので、s はドロップされました。もう使えません。
  • 所有権を関数に渡す場合
fn takes_ownership(s_b: String) { // s_b には渡されたものの所有権も一緒に移動されます。
	println!("{}", s_b);
}

fn main() {
	let s_a = String::from("hello"); // s_a は "hello" の所有権を持ちます。
	takes_ownership(s_a); // s_a を関数に渡します。
}
  • 所有権を関数から渡される場合
fn gives_ownership() -> String { // 返り値を持っています。
	let s_b = String::from("hello"); // s_b は "hello" の所有権を持ちます。
	return s_b; // 関数が呼び出されたところに値と一緒に所有権も返します。
}

fn main() {
	let s_a = gives_ownership(); // s_a は関数から所有権をもらいます。
	println!("{}", s_a);
}
  • 所有権を借用する場合(参照渡し)
fn print_length(s_b: &String) { // s_b は渡されたものの所有権を”借りて”もらいます。
	println!("string length: {}", s_b.len());
}

fn main() {
	let s_a = String::from("hello");
	print_length(&s_a); // s_a の所有権を s_b に”貸して”あげます。
	println!("{}", s_a); // 値が移動していないので s_a はまだ問題なく使えます。
}

値の移動(Value Moves)

  • Rust言語では、宣言された値に対して所有者は同時に一つしか存在できません。もし値を他の変数に代入するなど値が移動する場合、所有権も一緒に動くことになります。
let s1 = String::from("hello");
let s2 = s1; // 値の移動が発生
// 値が s1 から s2 に移動, 所有権を s1 から s2 に移動させました。
	
println!("{}", s1); // エラー: value "s1" used here after "move"
  • 値をクローン(Clone)する
let s1 = String::from("hello");
let s2 = s1.clone(); // 値をクローン

println!("{}{}", s1, s2); // 大丈夫
  • 移動先の値を利用する
let s1 = String::from("hello");
let s2 = s1; // 値の移動が発生

println!("{}", s2); // 大丈夫
  • 参照を利用する
let s1 = String::from("hello");
let s2 = &s1; // s1 の所有権を s2 に貸してあげる

println!("{}{}", s1, s2); // 大丈夫

& 参照 (共有参照、Shared Reference)

  • 場合によっては、関数などに参照を渡したほうが値そのものを渡すより効率的な場合があります。
  1. 参照は固定された微小なデータサイズ(ポインターと同じサイズ)を持っています。参照を渡すとそのポインターだけコピーすることになるので、値が持つデータすべてをまるごとコピーするより軽くなります。特に値が大きいデータを複数持つ構造体とかの場合がそうです。
  2. 値を渡すと、暗黙的に毎回その値が持つデータすべてをコピーするようになっています。1番で記述したように値が大きいデータを持ち、Cloneトレイとを実装するとなるとかなり非効率的なのがわかります。参照を渡すことで、そのような構造体を指すポインターだけをコピーすることになるので、データすべてをコピーすることを回避できます。
  3. 参照を渡すとコード内複数のところで同じデータに対し、所有者を移動したり所有権を含みデーターをまるごとコピーすることなくそのデーターにアクセスできるようになります。データを改変することなく、ただ閲覧したいだけなら参照がいいでしょう。
  • 留意すべきなのは、参照渡しが必ず効率的であるとは限りません。Copyトレイトを実装する小さくて簡単なデータ型(整数, ブールとかの単体)を扱うときは値を渡してもデータをコピーするとき非常に軽い故に負荷が劇的に増えたりすることがなく、むしろ参照を渡すとなるとその参照が指しているポインターに行ったり来たりするので効率が下がる場合があります。

  • & 参照は不変参照(Immutable Reference)よりも共有参照(Shared Reference)と呼ばれることが推薦されています。後ほど説明しますが、場合によっては&参照が可変性を持つ場合があり、これを内部可変性(Interior Mutability)と呼びます。内部可変性とはとても初心者には向いていない話なんですが、メモリ管理やコンカレンシー(マルチスレッド)において非常に多く活用される、あるいはシステムそのものが内部可変性から作り上げられたこともあります。主にCell<T>, RefCell<T>, Mutex<T>, RwClock<T>などのスマートポインタ(Smart Pointer)やアトミックなどがそれに当てはまります。そして`UnsafeCellから成るシステム全般は共有参照&`を通じて値を改変できるようになるため、「不変参照」という用語は多少不適切かもしれません。

参照解除(Dereference)

  • 参照解除オペレーター*****で参照を解除することができます。これで「値への参照」を「値そのもの」に変えることができますね。
let s = Stirng::from("hello"); // データ型 : String
let s_ref = &s;                // データ型 : &String
let s_val = *s_ref;            // データ型 : String <- & が消えた!
// s_val にエラーが発生します。s は "hello"の所有者ですが、s_ref は s への参照(所有権を借りている)であるだけなので所有権自体は持っていません。
// なので s_ref から s_val へそもそも以てしない所有権を移動できず、エラーが起きてしまいます。
  • エラーが起こらない場合もあります。主にCopyトレイトが設けられている基本的なデータ型がそうです。
let x = 42;         // データ型 : i32
let x_ref = &x;     // データ型 : &i32
let x_val = *x_ref; // データ型 : i32 <- & が消えた!
// i32 データ型は Copy トレイトを実装するため、暗黙的に値がコピーされ x_val に代入されています。

値 VS 参照

  • 「値を渡す」ことと「参照を渡す」ことは所有権が動くか動かないかの違いがあります。記述したように、値に所有者は一つしか存在できません。これは値を複数活用するときちょっとした工夫が必要になります。例を見てみましょう。

  • 「値を渡す」:このアプローチはSomeStructを使うとき毎回複製(Clone)しなければなりません。これはSomeStructがもし巨大なデータを含む構造体だったりその構造体を使う関数が頻繁に呼び出されたら効率的でなくなってしまいます。

#[derive(Debug)]
pub struct SomeStruct {
	num: i32,
}

fn print_some_struct_val(the_struct: SomeStrcut) { // パラメータを見てみましょう
	println("{:?}", the_struct);
}

fn main() {
	let some_struct: SomeStruct = SomeStruct { num: 3 }
	print_some_struct_val(some_struct.clone()); // ここ
	print_some_struct_val(some_struct);         // ここ
}
  • 「参照を渡す」:このアプローチはSomeStructを使うときにデータすべてのコピーを生成する代わりに、参照が構造体に指しているポインターだけをコピーすることになります。これはその構造体を使う関数とかが所有権をもらわなくてもデータにアクセスできるようにしてくれます。ただし、参照は値そのものとは違ってコード内で長生きできず、ライフタイムがとても短いのが問題です。処理のど真ん中で参照がドロップしちゃうとバグが起きたりするかもしれません。そして、データが簡単かつ軽いものだとその値を複製するよりもポインター間を行ったり来たりするほうが非効率的です。なので参照を使うのは主にデータを読み取って出力したりするときによく使われます。
#[derive(Debug)]
pub struct SomeStruct {
	num: i32,
}

fn print_some_struct_ref(the_struct: &SomeStruct) { // パラメータを見てみましょう
	println!("{:?}", the_struct);
}

fn main() {
	let some_struct: SomeStruct = SomeStruct { num: 3 }
	print_some_struct_ref(&some_struct); // ここ
	print_some_struct_ref(&some_struct); // ここ
}

可変性(Mutability)

  • Rustではmutキーワードなしで宣言された変数やポインターはその値を改変/修正できません。これはRustが目指すメモリ安全性を確保するため設けられた所有権と借用システムに深く関わっていて、これらが実装されることでデータレーシングや誤ったデータ割当などのプログラミングでは頻繁に起こる問題を排除してくれます。Rustは多言語と違い、可変性を明示的(はっきり)にすることで「いつ」「どこで」可変性を付与するかをきちんと認識し、もっと安全かつ効率的なコードをかけるようにしてくれます。
let x = 10;
x = 20; // エラー、x は不変です。

let mut y = 10;
y = 20; // y は可変なので問題ありません。

&mut 参照(排他参照、Exclusive Reference)

  1. &: 「不変借用」とも呼ばれる「共有参照」を作ります。一般的な場合、共有参照は「値への参照」でありデータへのアクセスはできるも改変はできません。共有参照はドロップされない限りデータへの複数の参照が許され、複数のコードで使えるようになります。
  2. &mut: 「可変借用」とも呼ばれる「排他参照」を作ります。一般的な場合、排他参照は共有参照と同じですが決定的に値の改変ができるようになります。ただし、排他参照は同時に一つしか存在できません。これは複数のところで同じ値を改変するいわゆる「データレーシング」を防ぐため一つに制限されています。これをもとに、&mut は「可変参照」よりも「排他参照」と呼ぶことが推薦されています。

"排他"とは基本、"ほかはダメ、これだけOK"という意味です。&mut は同時に存在できる参照の数が一つしかないため、「可変」よりも「排他」を使うことが勧められています。

  • &&mut の違い
let mut s = String::from("hello");

let s_ref = &s; // 共有参照
println!("s_ref: {}", s_ref);
// Output: s_ref: hello

let s_mut_ref = &mut s; // 排他参照
s_mut_ref.push_str(", world!"); // データの改変
println!("s_mut_ref: {}", s_mut_ref);
// 出力: s_mut_ref: hello, world!
  • このコードはコンパイルされません。
let mut s = String::from("hello");

let s_ref = &s;
let s_mut_ref = &mut s;

println!("s_ref: {}, s_mut_ref: {}", s_ref, s_mut_ref);
// s_ref と s_mut_refに同時にアクセスしようとしています。これはデータレーシング状態になるのでRustでは許されません。
  • 使い方
fn add_one(x : &mut i32) { *x += 1; } // &mut を参照解除して x の値にアクセスします。

fn main() {
	let mut y = 42;
	add_one(&mut y); // y は 43
}

Copy VS Clone

  • CopyCloneとはRustが定義するトレイト(Trait)であり、どのように値がコピー(複製)されるかを定義したものです。
  1. Copy: Copyトレイトが実装されるということは、そのデータは複製方法としてメモリ上に占めている領域をそのままコピーすることができるということです。これは主に整数型などの基本的なデータ型には実装されていて、関数とかに渡されるとき必ずコピーが渡されるようになります。
  2. Clone: Cloneトレイトはデータ型がもっと複雑かつ大きい複製過程を経る必要があるものに実装します。例えば「String」や「Vec」、「Copyを実装するフィールドが存在しない構造体」などに実装することができます。
  • CopyCloneはデータをメモリ上でどのように管理するかに決定的な違いがあります。Cloneの場合、そのデータの完全に新しい複製を作るに対して、Copyは同じ値に指すポインターを2個作ります。これは二重解除などのメモリバグを起こす場合があります。

Slice型

  • Sliceとはコレクション(配列とか)の中を覗き「ここからここまで」といった連続した区間のことです。所有権を一切持たず、代わりにそのコレクションへの覗き込んだ分だけの一時的なビューを提供します。もととなるコレクションをコピーしたり改変することなくコレクションを閲覧したり色々できるようになるため、メモリ効率的なコードを書くのに重要なRustだけが持つコンセプトです。

  • Rustには2つの重要なスライス型があり、「配列スライス」と「Stringスライス」です。

  • 配列スライス

let numbers = [1, 2, 3, 4, 5, 6];
let numbers_slice = &numbers[0..4]; // 0 から 4 までの配列スライスを作る、&に注目

println!("Slice is {:?}", numbers_slice);
// 出力: Slice is [1, 2, 3, 4]
  • Stringスライス
let text = String::from("Hello, World!");
let text_slice = &text[0..5]; // 0 から 5 までのStringスライスを作る、&に注目

println!("Slice is {:?}", text_slice);
// 出力: Slice is Hello

ライフタイム(Lifetime)

  • ライフタイムは「参照」タイプが存在する場合登場するコンセプトです。ライフタイムは参照がいつまで「生きているか」を検証するもので、これを活用することで「メモリから解除されてからそのメモリにアクセスする(use-after-free)」などのバグを回避できます。次の例では、関数 longer のパラメータと返り値に同じライフタイム 'a で結び付けられ(bound)返り値が参照だとしても課せされたとき、すぐ解除されるのではなくパラメータに渡された値と同じ期間生存できるようになります。
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
	if s1.len() > s2.len() { return s1; }
	else { return s2; }
}

fn main() {
	let some_text = String::from("Hello");
	let other_text = String::from("Hello, World!");
	
	let longer_text = longer(&some_text, &other_text);
	// longer_text がもらう返り値が参照であってもここでドロップされません。
	println!("longer: {}", longer_text);
}
// some_text, other_text, longer_text 全部ここでドロップします。
  • 構造体での活用
struct Person<'a> {
	name: &'a str, // &str が参照である故ライフタイムが必要です。 
	age: u32,
}

impl<'a> Person<'a> {...}
  • 複数のライフタイム
fn join_strs<'a, 'b>(s1: &'a str, s2: &'b str) -> String {
	format!("{}{}", s1, s2)
}

dyn (Dynamic)

  • dyn は動的ディスパッチ(トレイトオブジェクト)に使われます。
trait Animal { fn speak(&self); }
struct Cat;

impl Animal for Cat {...}

fn make_animal_speak(animal: &dyn Animal) { // ここ
	animal.speak();
}

fn main() {
	let cat = Cat;
	
	cat.speak(); // 構造体から呼び出し
	make_animal_speak(&cat); // 構造体のオブジェクトを参照として渡す
}

まだ和訳中です(白目

Memory Areas

  • There are 2 types of memory area that the Rust takes care of and these memory areas are in a physical RAMs.

  • Stack Area

    • Stack is used for static, short-lived, and a storage for local data. It has the structure of the most recently allocated data is the first to be deallocated.
    • this is same as vertically stacked up dishes in the kitchen. name of it also came from "stacked dishes", you put the dish at the top, and take the dish at the top not at the middle.
    • Memory allocation on the stack is fast and determined at the compile time. Stack is more efficient than Heap in general.
    • Data stored on the Stack must have a known, fixed data size at compile time which mean it is not suitable for dynamically generated data.
    • Data stored in Stack are: Primitive data types like u32, f64, bool, or Fixed-sized Arrays like [i32; 8], Tuples and Structs with fixed-size types only, Function call frames and local variables with a known size.
  • Heap Area

    • Heap is used for dynamic, long-lived, and a stroage for global data. It is less organized than the stack, and memory management is slower.
    • Heap memory is managed manually, or through smart pointers like Box<T> and Rc<T> in Rust, which automatically handle memory management and prevent memory leaks or dangling pointers.
    • Suitable for data structures that are either large, long-lived, unknown in size, dynamically generated.
    • Data stored in Heap are: Dynamic data structures like Vec<T>, String, HashMap<K, V>, or Dynamic arrays using Box<T> or Rc<[T]>, Structures with dynamic-size types, and Types with a dynamically sized or unknown size at compile time, such as trait objects or closures.
  • Some data structures may be stored on both stack and heap area. for example, a structure that holds both fixed-size data and Vec<T> will have its fixed-size array stored on the stack and the Vec<T> to the heap.

  • The choice between stack and heap allocation depends on the use case, the size and lifetime of the data, and the performance requirements of the application. In Rust, Ownership, Borrowing system and Smart pointers make it easier to manage memory allocation between the stack and the heap, ensuring memory safety and preventing common programming errors like data races and memory leaks.

Smart Pointers

  • Smart Pointers are data structures in Rust that not only act like pointers but also have additional metadata and capabilities. They provide more functionality than a basic reference or raw pointer, such as reference counting and automatic memory management.

  • Box<T>: A box is a smart pointer that allocates data on the heap and provides a simple way to create heap-allocated values. Boxes are useful when you need to move large amounts of data or create recursive data structures. They provide a way to own and manage the memory they point to.

let x = 42;              // has type i32
let x_box = Box::new(x); // has type Box<i32>, llocated the data on the Heap not Stack
let x_val = *x_box;      // has type i32
  • Rc<T>: The Reference counting smart pointer is used when you need to have multiple shared owners of a single value. It keeps track of the number of references to the data and deallocates the memory when the reference count goes to zero. in the following example, x and x_clone shares the ownership of the value 42. and the memory is deallocaated when both of them go out of scope.
use std::rc::Rc;

let x = Rc::new(42);         // has type Rc<i32>
let x_clone = Rc::clone(&x); // has type Rc<i32> self:
let x_val = *x;              // has type i32
let x_clone_val = *x_clone;  // has type i32
  • RefCell<T>: The RefCell smart pointer enforces Rust's borrowing rules at runtime instead of compile-time. It is useful when you need to have mutable reference to immutable data, or when you need multiple mutable references to the same data in a controlled manner. in the following example, we created a RefCell containing the value 42, and then we create the mutable borrow using .borrow_mut() and modified the value of it. this means the RefCell allows us to have interior mutability, where we can modify the value even though x itself is immutable.
use std::cell::RefCell;

let x = RefCell::new(42);              // has type RefCell<i32>, immutable
let mut x_borrow_mut = x.borrow_mut(); // has type RefMut<i32>, mutable
*x_borrow_mut += 1; // x is now 43

let x_deref = *x_borrow_mut;           // has type i32, immutable

Borrow Checker

  • The Borrow Checker is a part of the Rust compiler that enforces the ownership and borrowing rules at compile time. It ensures that your program adheres to Rust's strict memory safety guarantees without needing a garbage collector. The borrow checker verifies that references to data follow Rust's borrowing rules.
  1. At any given time, you can have either one exclusive reference &mut T or any number of shared references &T to a particular piece of data, but not both.
  2. References must always be valid, meaning they cannot outlive the data they point to (no dangling references).
  • The borrow checker analyzes your code to ensure that these rules are followed, helping you catch potential memory safety issues and data races before your code is executed. If the borrow checker finds any violations, it will emit errors or warnings, and your code will not compile until these issues are resolved.

The codes include return keyword for better visibility, but as a general rule for Rust, Do not use return in the real code.

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?