🚀

忙しい人向けのRust

に公開

TL;DR

Rustで書かれたプログラムを読んだり、少しだけ修正する必要が発生したけれど、
Rustについて学んでいる時間なんか残されていない、という方向けに必要な知識を最小限にまとめました。しっかり勉強したい方は他の方の記事や書籍で学んでください。

インストールからHello,worldまで(Linux)

インストール

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Hello,World

$ mkdir rust_projects
$ cd rust_projects
$ cargo new hello
$ cd hello
$ cargo run

ccが見つからないと言われたのでDebianを使っている私はsudo apt install build-essentialで解決しました。

変数

変数の定義方法

変数の定義はlet 変数名=値で行う。
ただし、この方法は同じ変数に再代入できない。
つまり、下記のコードはエラーになる。

let x = 5;
x = 6; //エラー

これを回避するにはlet mut 変数名=値で定義する。

let mut x = 5;
x = 6; //問題なし

基本の型

整数型

プログラム中ではi8, i16, i32, i64, u8, u16, u32, u64, isize, usizeと表記される。
iは符号付き、uは符号なしを表し、数字はビット数を表す。
isize,usizeのビット数は環境に依存する。

浮動小数点数型

プログラム中ではf32, f64と表記される。fは浮動小数点数を表し、数字はビット数を表す。

論理値型

プログラム中ではboolと表記される。true, falseがある。

文字型

プログラム中ではcharと表記される。
ユニコードの文字1つ分のデータを表し、文字コードU+0000からU+D7FFと、U+E000からU+10FFFFまでの値を取りうる。文字列ではない。

タプル型

複数の値を1つの型にまとめる。
タプル内のそれぞれの値は同じ型である必要はないが、位置と型の対応関係は守る必要がある。
分解して値を取り出す方法は以下の2種類がある。

let t = (500, 6.4, 1);
let (x, y, z) = t; //パターンマッチングによる分解
let t = (500, 6.4, 1);
let x = t.0; //インデックスによる分解
let y = t.1;
let z = t.2;

配列型

配列は長さが明確になっている必要がある。
また、全ての要素は同じ型である必要がある。

let a = [1, 2, 3, 4, 5];
let x = a[0];

制御フロー

条件分岐

if

if 条件式1 {
}
else if 条件式2{
}
else {
}

条件式は論理値型である必要がある。
他の言語にあるような以下のコードはエラーとなる。

let num = 3;
if num { something; } //numは論理値型ではないのでエラー

num != 0とかにすれば論理値型になるのでセーフ。

match

Cで言うswitchに近い。
ただし、すべての分岐パターンを明示的に網羅する必要がある。
網羅できていない場合はエラーになる。
_を使うとその他すべてのパターンを表せるので、網羅できてエラーは解決する。

let value = 1;
match value {
    1 => println!("one"),
    3 => println!("three"),
    _ => println!("other"), //上記以外のすべて
}

C言語のswitchで置き換えたイメージは以下のようになる。

int x = 1;
switch (x) {
case 1:
    printf("one\n");
    break;
case 3:
    printf("three\n");
    break;
default:
    printf("other\n");
    break;
}

if let

if let構文はmatch式の分岐が1つで、残りは無視したい場合に使われる。
つまりmatchを使って同じ内容を書くことができる。

let value = 3;
if let 3 = value {
    println!("three");
}

matchで書いた場合は以下のようになる。

let value = 3;
match value {
    3 => println!("three"),
    _ => (),
}

if letelseを追加することもできる。matchで書けばよくない?

let value = 3;
if let 3 = value {
    println!("three");
}
else {
    println!("other");
}
let value = 3;
match value {
    3 => println!("three"),
    _ => println!("other"),
}

let else

まず、let elseが使われる状況の1つの例として、処理が「異常系」に入ったので関数をそこで終了したい場合などを考えます。

例えば、Webサービスで言えば、処理待ちのリクエストを処理しようとしたときに、処理待ちのリクエストがなかった場合。そうした「異常系」の処理をelseのブロックに記述します。
(何を「異常系」とするかは開発者によって異なるでしょうが、この説明には関係ないので無視します。)

let MyRequest::Get(req_data) = req else {
    return None;
};

//以降は処理待ちのリクエストに対して何か行う

let elseの最後の}の後には;が必要なので要注意。

if letで書けば以下のようになります。

if let MyRequest::Get(req_data) = req {
    //処理待ちのリクエストに対して何か行う
}
else {
    None
}

すぐにわかる利点としては、ソースコードのifelseそれぞれの中身が肥大化しないので、条件分岐を把握しやすいことが挙げられるかと思います。
代わりにこの挙動を理解する必要がありますけどね。

繰り返し

breakcontinueloop以外のループにも使えます。

loop

無限ループ。breakcontinueを使用しない限り永遠に繰り返す。

while

while 条件式 { 処理; }で条件式がtrueの間はブロック内の処理を繰り返す。

for

コレクション型(配列など)の要素について順番に繰り返す。
また、for i in 0..10というような感じでiが0から9までのループを作ることもできる。

let a = [10, 20, 30, 40, 50];

for element in a {
    println!("element is {element}");
}

関数

関数の定義方法

fnキーワードで始まり、関数名の後に引数を書く。
各引数は型を宣言しなければならない。
関数を定義する順番は問題にはならない。(Cで言うプロトタイプ宣言のような問題は起きない)
戻り値を返す場合は、最後のが戻り値になる。
セミコロンをつけるとエラーになる。
関数の途中でreturnキーワードを使って値を返すこともでき、この場合はセミコロンをつける。

fn print_and_plus_one(x: i32) -> i32 {
    println!("x -> {}", x + 1);
    x + 1 //OK
}

fn print_and_plus_one(x: i32) -> i32 {
    println!("x -> {}", x + 1);
    x + 1; //Error
}

構造体とメソッド

構造体の定義方法

structキーワードに続いて構造体名、各フィールドの名前と型を記述する。

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

この構造体のインスタンスを作成する場合は以下のようになる。

let user = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

enumの定義方法

型のグループ化に使われる。

struct Ipv4Addr {
    // 省略
}

struct Ipv6Addr {
    // 省略
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

インスタンス化の際は以下のようになる。

let localhost = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));

enumとmatch

enumとmatchは組み合わせて使われることも多い。
上記の例で説明すると、IpAddrIpv4AddrIpv6Addrによって処理をわけたりできる。

match localhost {
    IpAddr::V4(ipv4) => IPv4の場合の処理,
    IpAddr::V6(ipv6) => IPv6の場合の処理,
}

メソッドの定義方法

implキーワードに続き、メソッドが属する構造体またはenumの名前を指定する。
implブロックは異なる場所で複数定義してもよい。

impl User {
    fn show_username(&self) -> &str {
        return &self.username
    }
}

呼び出すときは、インスタンスの変数名.show_username()になる。
引数はselfだったり、&selfだったり、&mut selfだったりする。
&selfはフィールドの値を変更する必要がなく、参照したいだけの場合に使うことが多い。
&mut selfはフィールドの値を変更する必要がある場合に使う。
selfはこの記事で説明するレベルを超えているので説明しない。

関連関数

構造体やenumに関連付けられた関数の第一引数がselfではない場合は関連関数と呼ばれる。
他の言語では静的メソッドやクラスメソッドと呼ばれるものに近い。

よく使われるenum

Option<T>

enum Option<T> {
    Some(T),
    None,
}

Option<T>Some<T>Noneのどちらかになる。
Some<T>は型Tのデータを1つ持つことを表す。
Noneは値が存在しないことを表す。

例えば、Noneに8を加算する可能性があるコードを書いたとすると、
実行するまでエラーにならない言語もあるが、Rustではコンパイルエラーになる。

let x: i8 = 8;
let y: Option<i8> = Some(8);
let sum = x + y;

Some<T>が保持するデータを取り出したり、Noneである場合の処理などを記述する際にif letlet elseが使われることが多い。

let y: Option<i8> = Some(8);
//let y: Option<i8> = None;

if let Some(value) = y {
    println!("data in y is {value}");
}
else {
    println!("data in y is None");
}
//let y: Option<i8> = Some(8);
let y: Option<i8> = None;
    
let Some(value) = y else {
    println!("data in y is None");
    return ();
};
   
println!("data in y is {value}");

Result<T, E>

enum Result<T, E> {
    Ok(T),
    Err(E),
}

例えばファイルを開くなど、エラーの可能性もある処理に使われる。
Result<T, E>Ok(T)Err(E)のどちらかになる。
Ok(T)は成功を表し、T型の戻り値を格納している。
Err(E)は失敗を表し、E型のエラーを格納している。

最後に

プログラムを書くときはコンパイラのアドバイスに従えばなんとかなることが多いです。

改版履歴

2024.11.27

  • 初版

2025.12.25

  • let elseの説明を追加
  • Option<T>の部分にif letlet elseを使った例を追加
  • 日本語の修正
  • markdownの書き方を修正

Discussion