5
4

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の基本をPythonと比較しつつ学んでみる #1

Posted at

普段Pythonなどをメインにお仕事をしていますが、Rustのごく基本的な文法や挙動などをPythonと比較しつつ学んでいってみます(前も入門書消化したタイミングで少し記事にしたりしていますが復習も兼ねて書いておきます)。

※Rust初心者なため誤解している点などがあるかもしれません。その辺はご容赦ください。

※長くなりそうなので記事を分割します。本記事は1記事目となります。

環境の準備

Rust自体のインストールやらrust-analyzer関係とかは以前記事にしていたのでそちらのリンクのみ貼っておきます。

少しインストールされているバージョンが古くなっている気がするのでアップデートしておきます。

$ rustup update

バージョンを確認してみると以下のバージョンとなりました。

$ rustc --version
rustc 1.68.0 (2c8cc3432 2023-03-06)

また、記事執筆用に適当なプロジェクトをCargoで作成しておきます。

$ cargo new rust_basics

あとはVS Codeで対象のプロジェクトフォルダを追加して進めていきます。VS Code上でRun | Debugというメニューが出てくるのでそちらを使用して実行していきます。

image.png

RustをVS Code上で触るのがかなり久々なので一応Cargoコマンドで作成されたmain.rsで実行してみてHello, world!が表示されることを確認しておきます。

fn main() {
    println!("Hello, world!");
}

image.png

問題無さそうなのでこれで進めていきます。

コンソール出力

Pythonのprint関数に該当するものはRustではprintln!となります。!はマクロ用の記述となり、println!関数というよりかはprintln!マクロという扱いになります。

fn main() {
    println!("Hello, world!");
}
Hello, world!

Pythonのように複数の引数を指定することはストレートにはできないようです(エラーとなります)。基本的に第二引数とかに関しては文字列のフォーマット処理用という感じのようです。

fn main() {
    println!("Hello", ", world!");
}
2 |     println!("Hello", ", world!");
  |              -------  ^^^^^^^^^^ argument never used
  |              |
  |              formatting specifier missing

Pythonの文字列のformatメソッドのように文字列中に変数などを入れたい場合には文字列中に{}の括弧を記述し、変数を第二引数以降に指定します。

fn main() {
    let cat_name: String = "タマ".to_string();
    let cat_age: i32 = 5;
    println!("猫の名前は{}です。歳は{}です。", cat_name, cat_age);
}
猫の名前はタマです。歳は5です。

Pythonのf-stringsのように変数を文字列内に挿入してコンソール出力で扱うような書き方もできます(v1.58.0以降でサポートされているようです)。

fn main() {
    let cat_name: String = "タマ".to_string();
    let cat_age: i32 = 5;
    println!("猫の名前は{cat_name}です。歳は{cat_age}です。");
}
猫の名前はタマです。歳は5です。

ただし{}の括弧内でPythonのようにメソッドを呼び出したりなどのプログラム制御は本記事執筆時点ではサポートしていない?ようです。例えば以下のように{}の括弧内でlenメソッドを呼びだしてみるとエラーになります。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    println!("{arr.len()}");
}
error: invalid format string: expected `'}'`, found `'.'`
 --> src\main.rs:3:19
  |
3 |     println!("{arr.len()}");
  |               -   ^ expected `}` in format string
  |               |
  |               because of this opening brace
  |
  = note: if you intended to print `{`, you can escape it using `{{`

このようなケースでは恐らく第二引数に値を指定するか一旦変数に別途設定する形が必要になるかと思われます。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    println!("{}", {arr.len()});
}
3

Pythonの文字列のformatメソッドのようにキーワード引数的な指定もできるようです。

fn main() {
    let cat_name: String = "タマ".to_string();
    let cat_age: i32 = 5;
    println!(
        "猫の名前は{cat_name}です。歳は{cat_age}です。",
        cat_name = cat_name,
        cat_age = cat_age,
    );
}
猫の名前はタマです。歳は5です。

余談ですが恐らくprintln!のlnは軽く調べた感じlineの意味かと思われます(新しい行に対して出力を行うという意味合い的に)。

strとString

NumPyとかで固定長の文字列を扱うケースなどを除いて、Pythonだと文字列は基本的に可変長のstr型を扱う形となります。

一方でRustではstrは固定長の型となり、可変長の文字列は別途Stringという型があります。String型は文字列の長さの情報などを持ったポインタといった性質を持つため異なる文字列の長さを扱うことができます。

関数などで引数にstr型を指定すると、引数に指定される文字列の長さはケースバイケースで変動するため固定長のstr型では扱うことができずエラーになってしまいます。

fn print(input_str: str) -> () {
    println!("{input_str}");
}

fn main() {
    print("Hello, world!");
}
 --> src\main.rs:6:11
  |
6 |     print("Hello, world!");
  |           ^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `str`
  = note: all function arguments must have a statically known size

str型の代わりに可変長のString型を引数の型に指定しておくとコンパイルが通ります。ただし通常の""のクォーテーションで作成した文字列はstr型となるためto_stringメソッドでString型に変換する必要があります。

fn print(input_str: String) -> () {
    println!("{input_str}");
}

fn main() {
    print("Hello, world!".to_string());
}
Hello, world!

もしくはstr型を使いつつも&の記号を引数部分に付与して借用とすることでもエラーを回避できます。値の変更などをしないケースであればこちらもシンプルかもしれません。

fn print(input_str: &str) -> () {
    println!("{input_str}");
}

fn main() {
    print("Hello, world!");
}
Hello, world!

整数と浮動小数点数間の計算

Pythonだとintやfloat同士を直接計算できます。例えば以下のような計算を行うことができます。

value1: float = 10.5
value2: int = 20
value3: float = value1 + value2
print(value3)
30.5

一方でこういった計算はRustだとエラーで弾かれます。

fn main() {
    let value1: f64 = 10.5;
    let value2: i32 = 20;
    let value3: f64 = value1 + value2;
    println!("{value3}");
}
error[E0277]: cannot add `i32` to `f64`
 --> src\main.rs:4:30
  |
4 |     let value3: f64 = value1 + value2;
  |                              ^ no implementation for `f64 + i32`
  |
  = help: the trait `Add<i32>` is not implemented for `f64`
  = help: the following other types implement trait `Add<Rhs>`:     
            <&'a f64 as Add<f64>>
            <&f64 as Add<&f64>>
            <f64 as Add<&f64>>
            <f64 as Add>

こういった場合には型を揃える必要があるため、20といった整数値を20.0などに変更して浮動小数点数同士の計算となるように調整する必要があります。

fn main() {
    let value1: f64 = 10.5;
    let value2: f64 = 20.0;
    let value3: f64 = value1 + value2;
    println!("{value3}");
}
30.5

変数の型を途中で変えたくなった・・・といった場合にはasを使ってキャストすることができます。

fn main() {
    let value1: f64 = 10.5;
    let value2: i32 = 20;
    let value3: f64 = value2 as f64;
    let value4: f64 = value1 + value3;
    println!("{value4}");
}
30.5

数値のbit数の差

Pythonだと(NumPyとかを使わなければ)整数などの数値の上限サイズをあまり意識しなくとも扱えます(必要に応じて大きなサイズが自動で割り振られて快適にプログラミングができる一方でメモリ的にはあまり効率的ではない面もあります)。

一方でRustでは数値のbit数を意識する必要が出てきます。8bitの整数であればi8、32bitの整数であればi32、64bitの浮動小数点数であればf64、符号無しの8bitの整数であれはu8といった型を扱う必要があります。

変数で型の指定を省略した場合にはデフォルトでは整数であればi32、浮動小数点数であればf64の型が割り振られます。

bit数の範囲を超えた値を設定してしまうとエラーとなります。たとえば以下の例ではi8を指定しつつ値に128を設定しているもののi8の型では-128~127までの範囲の値しか取れないのでエラーとなります。

fn main() {
    let value: i8 = 128;
    println!("{value}");
}
error: literal out of range for `i8`
 --> src\main.rs:2:21
  |
2 |     let value: i8 = 128;
  |                     ^^^
  |
  = note: the literal `128` does not fit into the type `i8` whose range is `-128..=127`

配列とベクタ

Pythonではlistで動的に変更の効く配列のデータ、tupleで変更の効かない配列のデータを扱うことができます。

一方でRustだとarray型の方は変更が効かない配列となりPythonのtupleに近い性質を持ちます。RustのVec<T>型の方が変更が効く配列となるためPythonのlistに近い挙動をします。

array型の方は: [<値の型>; <値の件数>]といった型の指定の仕方が必要になります。型と件数の間がコンマではなくセミコロンが必要なので注意してください。

たとえばi32の型の値を3件持つ配列の場合以下のような定義になります。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
}

上記のコードはPythonで言うとtupleを使う形で以下のようなコードに近い形になります。

from typing import Tuple

arr: Tuple[int, int, int] = (10, 20, 30)

配列の長さはlenメソッドで参照することができます。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    println!("{}", arr.len());
}
3

また、iterメソッドでループを回すこともできます。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    for int_value in arr.iter() {
        println!("{int_value}");
    }
}
10
20
30

配列の値を直接println!マクロで表示することはできません。以下のようにエラーになります。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    println!(arr);
}
error: format argument must be a string literal
 --> src\main.rs:3:14
  |
3 |     println!(arr);
  |              ^^^
  |
help: you might be missing a string literal to format with
  |
3 |     println!("{}", arr);
  |              +++++

error: could not compile `rust_basics` due to previous error

f-stringsのような記述などでもエラーになります。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    println!("{arr}");
}
 --> src\main.rs:3:16
  |
3 |     println!("{arr}");
  |                ^^^ `[i32; 3]` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `[i32; 3]`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

こういった場合にはエラーメッセージにも書かれていますが{:?}という指定でフォーマットする必要があります。

fn main() {
    let arr: [i32; 3] = [10, 20, 30];
    println!("{:?}", arr);
}
[10, 20, 30]

Rustの配列は固定長でしたが、可変長のものを使いたい場合にはベクタ型の利用が必要になります。ベクタ型はPythonのlistに近い性質を持ちます。型の指定はVec<値の型>といった記述が必要になります。

また、ベクタ型を生成するにはvec!マクロなどを使います。他にも生成方法は色々ありますがこのマクロを使うことが多そうです。マクロの後には[]の括弧とともにコンマ区切りで初期値を指定します。

例えばi32の整数の値を格納するベクタ型で値が10, 20, 30の3つの値を持つベクタを作成するには以下のような記述になります。

fn main() {
    let int_vector: Vec<i32> = vec![10, 20, 30];
}

配列と同じようにベクタもiterメソッドでループを回したりlenメソッドでベクタの長さを取得したりなどができます。

ループを回す例
fn main() {
    let int_vector: Vec<i32> = vec![10, 20, 30];
    for int_value in int_vector.iter() {
        println!("{int_value}");
    }
}
10
20
30
lenメソッドで長さを取る例
fn main() {
    let int_vector: Vec<i32> = vec![10, 20, 30];
    println!("{}", int_vector.len());
}
3

Rustではデフォルトでは変数がイミュータブルで宣言されるのでベクタへ値を追加したい場合などにはmutの指定を追加してミュータブルで変数を宣言する必要があります。追加に関してはpushなどのメソッドが用意されています(jsの配列などのようにベクタの末尾に追加されます)。

fn main() {
    let mut int_vector: Vec<i32> = vec![10, 20, 30];
    int_vector.push(40);
    for int_value in int_vector.iter() {
        println!("{int_value}");
    }
}
10
20
30
40

追加のメソッドとしてはextendやinsertなどの各種メソッドも他の言語と同じように用意されています。

HashMap

Pythonの辞書に近い性質のものとしてHashMapがあります。キーと値のペアを持つデータとなります。

利用するには標準モジュールのものを読み込む必要があります。PythonだとimportでパッケージをインポートしますがRustではuseを使ってモジュールの読み込みを行います。

また、Pythonの辞書のような初期値の設定は行うことはストレートにはいかず生成後にinsertメソッドなどで値を追加する必要があるようです。

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("first_key", 10);
    map.insert("second_key", 20);
}

for文に渡すとキーと値それぞれが設定される形でループを回すことができます。Pythonだとitemsメソッドを使った時と同じような感じでしょうか。

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("first_key", 10);
    map.insert("second_key", 20);

    for (key, value) in map {
        println!("key: {key}, value: {value}");
    }
}
key: first_key, value: 10 
key: first_key, value: 10 
key: second_key, value: 20
key: second_key, value: 20

lenメソッドで配列やベクタと同様にデータの件数を取得することができます。

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("first_key", 10);
    map.insert("second_key", 20);

    println!("{}", map.len());
}
2

HashMapの内容をそのままprintln!マクロで表示したい場合には配列やベクタと同様に"{:?}"というフォーマットの指定を使います。

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("first_key", 10);
    map.insert("second_key", 20);

    println!("{:?}", map);
}
{"first_key": 10, "second_key": 20}

insertメソッドを使わずにPythonのように初期化時に辞書の各値を設定したい場合には[("キー1", "値1"), ("キー2", "値2"), ...]という値を作成して、そちらを.iter().cloned().collect()のメソッドチェーンでHashMapに変換する・・・という形で近い形で書くこともできます。Rustビルトインだけだとこの辺は少し煩雑ではありますね(将来改善されるかもしれませんが・・・)。

use std::collections::HashMap;

fn main() {
    let map: HashMap<&str, i32> = [("first_key", 10), ("second_key", 20)].iter().cloned().collect();

    println!("{:?}", map);
}
{"first_key": 10, "second_key": 20}

()の括弧はPythonと同様にタプルを生成します。2件分の値を格納したタプルを指定することでキーと値として設定しています(タプルを格納した配列として扱っています)。

iterメソッドではその配列をイテレーターに変換しています。

clonedメソッドでは参照のコピーを作成するメソッドです。cloneメソッドと似ていますがcloneメソッドは値のコピーを作成するのに対してclonedメソッドは参照のコピーを作成します。どうやらイテレーターなどに対してはcloneメソッドが使えないことがあるようなのでclonedメソッドを使っています(str型の文字列などはCloneトレイトを持っておらずcloneメソッドが利用できないので文字列の値を含むイテレーターなどではcloneメソッドが使えないようです。一方で&str型などはCloneトレイトを持っているため参照をコピーする形であるclonedメソッドの方は利用できます)。

最後のcollectメソッドはイテレーターからHashMapへと変換するメソッドです。

少し煩雑で考えることが多いので、現時点ではmaplitなどのサードのクレートとかを使わせていただくと快適なのかもしれません。

HashMapの特定のキーの値にアクセスするにはgetメソッドを使います。Pythonの辞書に対するgetメソッドでキーが無い場合にNoneが返却される挙動と似たような感じでOption型で返却されるので扱うにはmatchステートメントで分岐させてNoneの場合とそれ以外の場合(Some)でそれぞれの分岐を記述する必要があります。

use std::collections::HashMap;

fn main() {
    let map: HashMap<&str, i32> = [("first_key", 10), ("second_key", 20)].iter().cloned().collect();
    let value: Option<&i32> = map.get("first_key");
    match value {
        Some(int_val) => println!("{int_val}"),
        None => println!("キーが見つかりませんでした。"),
    }
}
10

参考文献・サイトなど

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?