Zenn
Closed67

Rustを書いてみる

nanasinanasi

出力

ログ出力: println!マクロ

  • 文字列以外は出力できない
  • プレースホルダ{}を使うことで数値とかを埋め込める
println!("num is {}", 10); // num is 10
nanasinanasi

デバッグ用に変数を出力するdbg!マクロもある。
この際所有権の関係(?)で、参照を渡す必要がある。

let msg: String = "Hello";
dbg(&msg);

&をつけなかった場合、所有権がdbg!に移るっぽい?

nanasinanasi

↑と思いきや、所有権が移る場合と移らない?場合がありそう。
String型は移るが、&str型やその他リテラルは移らない?

nanasinanasi

println!のフォーマットはstd::fmtのものと同じらしい。
また、プレースホルダにはいくつかの構文がある。

  • {?}: デバッグ(dbg!)と同じように出力する
  • {引数名}: 引数に名前をつけ、その名前で順序を決定する
  • :: フォーマット指定子と別のとを区切る
println!(
  "{index}: {suit:?} {rank}",
  index = i + 1,
  suit = card.suit,
  rank = card.rank
);

なお、Rustに他言語の名前付き引数はない模様。
じゃあこの構文は一体...?

詳細はドキュメントを参照。

nanasinanasi

変数を直接埋め込むこともできるらしい。

let num = 10;
println!("num is {num}"); // num is 10

ただしこれはprintln!の機能なので、通常の変数宣言等に応用はできない。
どうして...

nanasinanasi

リテラル

  • 数値(i32とかu32とか)
    • iは符号付き(負の数OK)
    • メモリサイズに応じで8とか64とかもある
  • 文字(char): 'で囲む
  • 文字列: "で囲む
    • &strStringがあるらしい?
nanasinanasi

変数

let 変数名: 型名 = 初期値;で宣言できる。

let num: u8 = 10;

変数はデフォルトで不変。
可変にするならmutをつける。

nanasinanasi

シャドーイング: 同名の変数を再定義する
この際前の変数を参照することもできる。

let num = 10;
let num = num + 1;
dbg!(num); // 11
nanasinanasi

↑これのおかげで同じコードをコピペしても動くのか...
JavaScriptだと同じ名前の変数の宣言はダメだから新鮮な気分

nanasinanasi

もしかしてVSCodeだと、可変の変数は下に線が出る?
何かを変更する(可変の)メソッドもそうな気がする

nanasinanasi

letを使った変数宣言は、トップレベル(main関数外)には書けない。
グローバルな変数はconstとかstaticを使うらしい。

nanasinanasi

入力を受け付け、それを数値に変換する:

// 入力を受け付ける
let mut ans_input = String::new();
std::io::stdin().read_line(&mut ans_input).unwrap();

// 数値にする
let ans_input = ans_input.trim().parse::<i32>().unwrap();
nanasinanasi
  • trim()は文字列のメソッドで、改行(\n)を取り除く
  • unwrap()Result型のメソッドで、値を取り出してるっぽい
  • ::って何...?
nanasinanasi

parse::<i32>()ってもしかして、型を書くことでメソッドの挙動が変わってる?
もしそうならTypeScriptにはない感覚

nanasinanasi

ResultokメソッドはOption型を返すらしい。
この2つは何が違うんだ...?

nanasinanasi

if

  • 式なので変数に代入できる
  • 条件式を囲む()は不要
  • dbg!を条件式部分に入れる技がある
if dbg!(1 + 1 == 2) {/* 略 */}
nanasinanasi

if letっていう、Scalaのunapplyみたいなやつもある。
Rustではこれをやるのに、

  • パターンマッチング
  • if let

の2通りがあるみたい。

nanasinanasi

クレート

クレート=Rust版npmパッケージ

追加にはcargo addを使う

ターミナル
cargo add rand

このコマンドはCargo.tomlに依存関係を追記する。
また、Cargo.lockというロックファイルも作ってくれる。

追加したクレートはuseで使える。

use rand::Rng;
// ...
dbg!(rand::rng().random_range(0..100));
nanasinanasi

制御構文

  • if式: 前述
  • while, loop
  • match: パターンマッチ

JavaScriptにおけるfor文はない。
Rustにおけるforfor..in(JSのfor..ofみたいなやつ)を指す。

for (n in 1..10) {/* ... */}
nanasinanasi

↑に出てきた1..10Range
これは1以上10未満、つまり1 ~ 9を指す。

1 ~ 10を指したい場合、1..=10と書ける。

nanasinanasi

列挙型

enumを使うと列挙型を定義できる。
値は型名::列挙子で取り出せる。

enum Suit {
  Heart,
  Diamond,
  Spade,
  Club,
}

let suit: Suit = Suit::Diamond;

Suit::DiamondとかはSuit型の値になるっぽい...?

nanasinanasi

#[derive(Debug, Clone, Copy, PartialEq)]ってなんだろう?
多分dbg!での出力に必要な何かっぽいんだけど

nanasinanasi

構造体

structを使って定義できる。
メンバーは名前: 型,で定義する。

struct Card {
  suit: Suit,
  rank: i32,
}

インスタンスは構造体名 { メンバー }で作る。
メンバーには.を挟んでアクセスできる。

let card = Card {
  rank: 8,
  suit: Suit::Diamond,
};

dbg!(card.rank); // 8

メンバー名と同じ名前の変数がある場合:

let card = Card { rank, suit };
nanasinanasi

無名関数

mapみたいな関数を受け取るメソッドはRustにもある。

無名関数の構文: |引数1, 引数2...| 処理
処理に書いた値が戻り値になるっぽい。
処理部分はブロック{}で囲むこともでき、その場合も最後に書いた値が戻り値になる。

// 同じみのマッピング: map
let numbers: Vec<usize> = input
  .split_whitespace()
  .map(|str| str.parse().unwrap())
  .collect(); // Vecに変換する?

// 全てが条件を満たすか?: all
let flash = hand.iter().all(|c| c.suit == first_suit);
nanasinanasi

配列

配列は特定の型のオブジェクトの集合。
固定長で、連続したメモリ領域に格納される。

生成には[]が使える。
各要素は,で区切り、最後の;の後に要素数を書ける。

let array: [i32; 1] = [0];
let array = [0; 500];

詳細

nanasinanasi

Vec

Vecは長さを変えられる配列(コレクション)。
Vec::new()で作成できるほか、別のオブジェクトのcollectメソッドなどから作成する方法もある。

let vec: Vec<i32> = Vec::new();
let vec = Vec::<i32>::new();

可変(let mut)の場合はpushなどのメソッドが使える。

let mut vec: Vec<i32> = Vec::new();
vec.push(14);
dbg!(vec.first().unwrap()); // 14

詳細

nanasinanasi

配列とVecは、iter()メソッドでイテレーターに変換?できる。
これはよくforループで使う。

また、イテレーターのenumerate()メソッドでは、要素のインデックスを一緒に取得できる。

nanasinanasi

Vecを作成するときに別の変数を使った場合、その値の所有権はVecにムーブする。
また、Vecの要素の所有権はVec自身が持っている。

let v = String::from("Hello");
let vec = vec![v]; // move
dbg!(v); // Error!
dbg!(vec);
nanasinanasi

/targetディレクトリは消してもいい by ChatGPT
ディスク容量が心配だし、作り終わったやつから消していこうかな

nanasinanasi

関数

fn 関数名(引数) -> 戻り値の型 { 処理 }で定義する。
呼び出しは関数名(引数}

fn hello(name: &str) -> &str {
  println!("Hello, {name}!");
  name // 引数のnameをそのまま返す
}
// ...
hello("world");
  • 戻り値の型を省略した場合は()になる
  • 戻り値の型は推論されない
  • 最後にセミコロン;なしで書いた値が戻り値になる(ブロックの仕様っぽい)
nanasinanasi

仮引数を可変にする場合、引数名の前にmutをつける。
可変参照を受け取る場合、型部分に&mutをつける。

fn add(mut a: f64, b: f64) {}
fn add(a: &mut f64, b: f64) {}
nanasinanasi

変数の不変参照は&で取得できる。
可変参照は&mutで取得できるが、変数が可変(let mut)で宣言されている必要がある。

参照から実際の値を取得する=参照外し(デリファレンス?)には*を使う。

fn add(a: &mut i32, b: i32) {
  *a += b;
}

let mut a = 10;
add(&mut a, 3);
nanasinanasi

関数などの説明はJSDoc方式で書けるっぽい。
/** */の中に説明を書く。

nanasinanasi

戻り値の値の所有権は、その戻り値を使うやつ(例: 変数)に移る。
スコープから抜けるから破棄されるというわけではない。

fn gen_vec() -> Vec<String> {
  let vec = vec![String::from("abc")];
  vec
}
// ...
let vec = gen_vec(); // vecに所有権が移る
dbg!(vec); // OK
nanasinanasi

関数定義の引数名の前につけるmutは、引数自体を可変にするもの。
これはScalaのvalvarに似ているかも。

fn sample(mut a: i32) { // 引数aを可変にする
  a += 5; // OK
}

このa引数を変えても、関数の外には影響しない
あくまでも関数内での、ローカル変数を変更するだけなイメージ。

nanasinanasi

逆に可変参照を示す&mutは、可変の変数を扱うイメージ。
参照は変数のショートカットと考えると、ただlet mutで宣言された変数を扱っているだけに見えてわかりやすい。

以下のコードでは、initの所有権をvecの参照が指す先の変数に渡している。

fn change(vec: &mut Vec<i32>, init: Vec<i32>) {
  *vec = init; // 参照外しすると変数が出てくる
}

let mut vec = vec![1, 2, 3];
let init: Vec<i32> = vec![4, 5, 6];
change(&mut vec, init); // initに入ってるVecの所有権をvecに渡す
dbg!(vec); // OK
nanasinanasi

自動で参照外ししてくれる場合がある。
例えばi32型の+演算子は、*をつけなくてもつけてもOK。

fn add(x: &i32) {
  // どっちでも動く
  dbg!("{}", x + 1);
  dbg!("{}", *x + 1);
}

また、メソッド呼び出し時のself(JSのthis?)は参照を受け取る。
そのため(&vec).method()のように明示的に参照であることを示すこともできるが、冗長なためvec.method()でOK。

つまり、メソッドの呼び出しは参照からでもいける。

fn add(x: &Vec<i32>) {
  x.iter(); // 参照からメソッドを呼び出す
  (&x).iter(); // これはいったい...?
}
nanasinanasi

Vecのインデックスアクセスも自動参照外しの対象らしい。

もしかしてVecの操作って*いらない?と思ったけど、Vecの参照とVecの比較なんかに使うらしい。

let mut v1 = vec![1, 2, 3];
let mut v2 = vec![1, 2, 3];
let v1_ref = &v1;
let v2_ref = &v2;
let result = if *v1_ref == v2 { "equal" } else { "not equal" };

by この記事

nanasinanasi

所有権

  • 値には所有権を持つ所有者が必ず1つある
  • 変数は所有権を持てる
  • 所有権の保持者は値を読み書きできる
  • 他の変数は参照を借用(borrow)できる
nanasinanasi

可変参照がある場合、所有権の持ち主も値を変更できない。
これは、Rustが複数の箇所から書き込みが可能な状態を許さないため。

let mut x = 5;
let y = &mut x; // 可変参照を取得
x += 10; // yはまだ生きているのでエラー
*y += 10;

なお、以下はエラーが出なかった。
yの使用箇所がもうないため?

let mut x = 5;
let y = &mut x; // 可変参照を取得
*y += 10;
x += 10; // yは今後使われないのでOK?
nanasinanasi

所有権はいくつかの方法でムーブ(移動)できる。

  • 変数へ代入する
  • 関数の実引数として使う

ムーブによって所有権を失った変数は、値への読み取りも書き込みもできなくなる。
スコープから抜けたなどで所有者を失った値は、メモリから解放される。

そして、Copyトレイトを実装しているものはムーブではなくコピーが行われる。
例えばi32などの型が挙げられる。

nanasinanasi

Copyトレイトを実装しているものはムーブではなくコピーが行われる

このため、例えば変数の代入を行っても所有権は移動しない。

OK
let value = 42;
let tmp = value; // コピーが作成される
dbg!(value);

このvalueに例えばVecを入れるとエラーになる。

nanasinanasi

*で参照外しを行っても、出てくるのは所有権ではなく変数へのショートカット
参照外しした値を変数に入れて所有権を得てみようとした結果、エラーが出た。

fn use_vec(vec_ref: &Vec<String>) {
  let value = *vec_ref; // 参照外し
}

エラー内容:

cannot move out of *vec_ref which is behind a shared reference

shared referenceは直訳すると共有参照だけど、不変参照を指してるっぽい?

nanasinanasi

ちなみにRustでも参照の参照は作れるっぽい。
その場合自動参照外しは再帰的に行われるっぽい。

let vec = gen_vec();
let vec_ref = &vec;
let vec_ref_ref = &vec_ref;

dbg!(vec_ref_ref); // ["abc"]
nanasinanasi
  • 所有権は値に対して発生する
  • 参照は変数に対して発生する
  • 借用は所有者から参照を借りること
nanasinanasi

参照も値の一種なので、所有権という概念が発生する。

  • 不変参照はCopyトレイトを実装している
  • 可変参照はCopyトレイトを実装していない
let a = 10;                  // immutable object
let a_ref = &a;              // reference
let a_ref_copy = a_ref;
print!("{} {} {}", a, a_ref, a_ref_copy); // borrow check!! - OK

参考

nanasinanasi

Copyはマーカートレイトの一種。
マーカートレイトはコンパイラに指示を与えるためだけの空のトレイト。

nanasinanasi

スライス

スライスは対象から一部を切り出したビュー。
文字列や配列、Vecからは[start..end]という記法でスライスを作成できる。

スライスは基本的に参照型で、例えば&[f64]のようになっている。
また、不変のスライスと可変のスライスがある。

型強制という仕組みによって、Vecや配列はスライスに自動で変換可能。

fn use_slice(slice: &[i32]) {
  // 普通の配列と同じように使える
  dbg!(slice.len());
}
// ...
let vec = vec![0; 10]; // Vecを作成するマクロ
use_slice(&vec[0..5]);
use_slice(&vec); // 型強制
nanasinanasi

なお、&strStringのスライス型。
"で作ったリテラルは&str型になる。

nanasinanasi

mutをつけないことによる制約はあまり意味ない?

let vec = vec![1, 2, 3];
let mut vec = vec;
change(&mut vec); // OK

JavaScriptのconstな変数がletで再宣言できるような不思議な感覚。
そもそも変数の仕組みが全然違うからそりゃそうだけど...

nanasinanasi

mutは値が変更可能か=&mut selfなメソッドが呼べるかにも影響する。
そんな重要なところが簡単に変えられるのは驚き。

だけど、その分JavaScriptみたいに関数によってオブジェクトが変更されるのか読み取りしかされないのかがわからないということは、不変参照と可変参照があるのでない。
TypeScriptでreadonlyとかと比べると、考え方の違いがある?

nanasinanasi

Vecの要素の所有権はvec自身が持つ。
要素の所有権はムーブできない。

let vec = vec![String::from("hoge"), String::from("fuga")];
let tmp = vec[0]; // cannot move out of index of `Vec<String>`

ただし、Copyトレイトを実装している場合は、ムーブではなくコピーが発生する。

let vec = vec![1, 2, 3];
let tmp = vec[0]; // ムーブではなくコピー
dbg!(vec[0]); // ムーブではなくコピー
nanasinanasi

何も#deriveしていない構造体の場合、メンバーの所有権はムーブできる。

let vec = Memory { slots: vec![] };
let tmp = vec.slots; // move
dbg!(&vec.slots); // borrow of moved value: `vec.slots`
nanasinanasi

Vecの要素や構造体のメンバーへの参照も取得できる。
これも値への参照というよりは、メンバーへの参照を取得している感じ。

let mut vec = Memory { slots: vec![] };
let tmp = &mut vec.slots; // vec.slotsの参照を取得
*tmp = vec![1.0];
dbg!(&vec.slots); // [1.0]
nanasinanasi

借用されている(参照が作成されている)値の所有権はムーブできない。
そのため、所有権が変わったとき参照はどこを指すのか?という心配はない。

nanasinanasi

forifで簡単に実装できるけど、それでもfindを使ってみたかった

impl Memory {
  fn get_slot(&self, name: &str) -> Option<f64> {
    self.slots.iter().find(|s| s.0 == name).copied()
    // copiedは参照外しみたいなやつ
  }

  fn get_slot_mut(&mut self, name: &str) -> Option<&mut f64> {
    self
      .slots
      .iter_mut()
      .find(|s| s.0 == name)
      .map(|s| &mut s.1) // mapはOption型のメソッド
  }
}
nanasinanasi
  • get_slot: f64の値を返す、読み取り用
  • get_slot_mut: &mut f64を返す、書き込みもする用

こんな設計が許されるのかは知らない。
参照外しの関係で置き換えが簡単じゃないっていうデメリットはある。

nanasinanasi

本ではeval_tokenもメソッドにしてるけど、自己判断で普通の関数にしてみた。
ここではget_slotメソッドを呼び出してる。

fn eval_token(token: &str, memories: &Memory) -> f64 {
  if token.starts_with("mem") {
    let slot_name = &token[3..];
    memories.get_slot(slot_name).unwrap_or(0.0)
  } else {
    token.parse::<f64>().unwrap()
  }
}

この設計がいいのか悪いのかは知らない。

nanasinanasi

if let、ネストが少なくなっていいかもしれない

比較用match:

    match slot {
      Some(slot) => {
        *slot += prev;
        print_output(*slot);
      }
      None => {
        self.slots.push((slot_name.to_string(), prev));
        print_output(prev);
      }
    }

    if let Some(slot) = slot { // 分解
      *slot += prev;
      print_output(*slot);
    } else {
      self.slots.push((slot_name.to_string(), prev));
      print_output(prev);
    }
nanasinanasi

本ではeval_tokenもメソッドにしてるけど、自己判断で普通の関数にしてみた。

「メモリがトークンに紐づく値を評価して返す」のはなんだかおかしな話です。
By 本

そう思ってたから変えてみたけど、やっぱりそうだったのか...
自分の設計を肯定された気分で嬉しい

nanasinanasi

メソッド

基本

  • 構造体に関連する機能群のこと
  • impl 構造体名 {}で定義する
  • 各メソッドはfn メソッド名() {}という感じで普通の関数のように定義できる

selfについて

  • 自身を表す変数で、引数に型なしで書く
  • 所有権 or 不変参照 or 可変参照が欲しいのかで、self or &self or &mut selfを使い分ける
  • selfがないメソッド(他言語のstaticみたいなやつ)もある
struct Memory {
  slots: HashMap<String, f64>,
}

impl Memory {
  // ファクトリみたいなやつ
  fn new() -> Self {
    Self { // Self = Memory
      slots: HashMap::new(),
    }
  }

  // 自身のslotsを参照する
  fn get_slot(&self, name: &str) -> Option<f64> {
    self.slots.get(name).copied()
  }
}
nanasinanasi

HashMap

キーバリューのペアのコレクション。JavaScriptのMapみたいなやつ。
ジェネリック型で、HashMap<Key, Value>形式。

  • get(key) -> Option: キーから値を取得する
  • insert(key, value): ペアを追加する
  • entry(key): Entryオブジェクトを取得する
nanasinanasi

Entry

HashMapの各ペアを示す?型。
同名の型はstd::colections::HashMap以外にも存在するので混同に注意。

entryメソッドで取得でき、値は以下のどちらか。

  • Entry::Occupied: キーがHashMapにある
    • get(): 不変参照を取得する
    • get_mut(): 可変参照を取得する
    • remove(): 削除する?
  • Entry::Vacant: ない
    • insert(value): そのキーに値を追加する
nanasinanasi
match hash_map.entry(key) {
  Entry::Occupied(mut entry) => {
    *entry.get_mut() += num; // 参照を取得できるので代入できる
  }
  Entry::Vacant(entry) => {
    entry.insert(num); // 値を追加する
  }
}
このスクラップは2ヶ月前にクローズされました
ログインするとコメントできます