Rust のここが好き、10選

2022/03/03に公開約11,500字

Rust はお気に入りの言語のひとつです。
そんな Rust の具体的にどういうところが好きなのかまとめておこうと思いました。あくまで個人的に好きだという観点で選んでいます。
まとめてみて思ったのですが、技術的に初心者にも利便さが伝わるようなものが多いような気がします。皆さんの「Rust 推し」にお役に立てるかも知れません。

なお、Rust のバージョンは 1.59 を前提にしています。

オブジェクトの可変・不変がスコープ毎に定義できる

関数は引数を mut で受け取らない限り変更はできません。
「預かったものは変更しません」と宣言している関数はホントに変更しないことをコンパイラが保証してくれます。
可変な構造体で操作して、不変な構造体に入れ直すとか、構造体に set_mutable みたいなメソッドを付けて管理したり、ということをしなくてもいいんです。

#[derive(Debug, Default)]
struct Sample {
    index: i8
}

pub fn main() {
    // 可変として定義
    let mut a = Sample::default();
    logging(&a);
    
    a.index = 1;
    logging(&a);
    
    increment(&mut a);
    logging(&a);
}

// ここでは可変
fn increment(s: &mut Sample) {
    s.index = s.index + 1;
}

// ここでは不変
fn logging(s: &Sample) {
    // s.index = 2; // compile error
    println!("{s:?}");
}

Open playground

Option 型

最近の言語では珍しくないのですが、地味に大事なのがこの Option 型です。
Some/None で値の ある/ない を区別することで自然と null が不要になります。
また、Result 型との相互変換も簡単にできるので後述の ? オペレータとの組み合わせで便利に使えるようになっています。

pub fn main() {
    let a = read_num("123");
    println!("数値: {a:?}");
    
    let b = read_num("xyz");
    println!("数値じゃない: {b:?}");
    
    let c = get_num("54");
    println!("パース成功: {c:?}");
    
    let d = get_num("x");
    println!("パース失敗: {d:?}");
}

// Option を Result に変換
fn get_num(s: &str) -> Result<u8, &'static str> {
    read_num(s).ok_or("Not number!")
}

// Result を Option に変換
fn read_num(s: &str) -> Option<u8> {
    parse_num(s).ok()
}

fn parse_num(s: &str) -> Result<u8, core::num::ParseIntError> {
    s.parse()
}

Open playground

Result 型

失敗するかもしれない処理の場合、例外を投げるのではなく Result 型を使うことになっています。
パターンマッチで成功と失敗とで分岐させてもいいのですが、? オペレータで即時リターンさせるのが便利です。

pub fn main() {
    let a = divide(216);
    println!("分割結果: {a:?}");

    let b = divide(7);
    println!("分割結果: {b:?}");

    let c = divide(10);
    println!("分割結果: {c:?}");
}

fn divide(x: i64) -> Result<i64, &'static str> {
    // パターンマッチで条件分岐させるやり方
    let a = match div2(x) {
        Some(v) => v,
        None => return Err("Odd number"),
    };
    let b = match div3(a) {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    
    // `?` を使うやり方
    let c = div2(b).ok_or("Odd number")?;
    let d = div3(c)?;
    div3(d)
}

fn div2(x: i64) -> Option<i64> {
    if x % 2 == 0 {
        Some(x / 2)
    } else {
        None
    }
}

fn div3(x: i64) -> Result<i64, &'static str> {
    if x % 3 == 0 {
        Ok(x / 3)
    } else {
        Err("Can not div by 3")
    }
}

Open playground

パターンマッチと if let ...

通常の match 文以外にも、let や引数などの場面でパターンマッチが使えます。
if let ... の形で使うと「マッチした場合」のみの動作にフォーカスできます。

#[derive(Debug)]
struct Member {
    name: String,
    id: u32,
}

pub fn main() {
    log_option(&Some("これ"));
    log_option(&None);

    log_members(&[
        Member {
            name: "田中".to_string(),
            id: 8,
        },
        Member {
            name: "佐藤".to_string(),
            id: 9,
        },
    ]);

    log_bytes("マッチする場合", &[12, 23, 34]);
    log_bytes("マッチしない場合", &[4, 3, 2, 1]);
}

fn log_option(o: &Option<&str>) {
    println!("\nOption のマッチ");
    if let Some(v) = o {
        println!("取り出した値 {v}");
    }
}

fn log_members(members: &[Member]) {
    println!("\n構造体の中身の値を取り出す");
    members.iter().for_each(|&Member { ref name, id }| {
        println!("{name} {id}");
    });
}

fn log_bytes(msg: &str, bs: &[u8]) {
    println!("\nマッチする場合のみ処理する({msg})");
    if let [a, b, c] = bs {
        println!("three bytes: {a}, {b}, {c}");
    } else {
        println!("unkown bytes: {bs:?}");
    }
}

Open playground

数値の区切り

数値リテラルの途中に '_'(アンダースコア)を書くと無視されるようになっているので、区切り文字として使えます。

pub fn main() {
    let a = 123_000_000_i64;
    assert_eq!(a, 123000000);
    println!("Integer: {a}");
    
    let b = 0x_ABCD_1234_u32;
    assert_eq!(b, 2882343476);
    println!("Integer: {b}({b:x})");
    
    let c = 0.00_000_1_f64;
    assert_eq!(c, 0.000001);
    println!("Float: {c}");
    
    let d = 0b_1011_0010_u8;
    assert_eq!(d, 0xB2);
    println!("Binary: {d}({d:b})");
}

Open playground

型推論

書いた数値リテラルをどこに使うかで u8 とか i16 とかを判別してくれるのはもちろんのこと、
collect などの呼び出し結果をどこで使うかによって型を推測してくれます。

use std::collections::HashMap;

pub fn main() {
    let a = get_unsigned();
    println!("get unsigned: {a}");
    print_unsigned(1);
    
    let list_a = get_list();
    println!("list a: {list_a:?}");
    
    // 同じコードでもこの後どう使うかで型を推測してくれる
    let xs = [1, 2, 3].into_iter().enumerate().collect();
    let ys = [1, 2, 3].into_iter().enumerate().collect();
    
    print_pairs(xs);
    print_hashmap(ys);
}

fn get_unsigned() -> u8 {
    1
}

fn print_unsigned(x: u16) {
    println!("unsigned: {x}");
}

fn get_list() -> Vec<i16> {
    [1, 2, 3].iter().map(|a| a * 2).collect()
}

fn print_pairs(a: Vec<(usize, u8)>) {
    println!("vec: {a:?}");
}

fn print_hashmap(a: HashMap<usize, i16>) {
    println!("hashmap: {a:?}");
}

Open playground

From/Into

自然に変換できると考えられるほとんどの標準ライブラリの型には From/Into が定義されています。
もちろん自分で定義することもでき、From だけを実装すれば、Into はブランケット実装でカバーされているので自分で実装する必要はありません。

use std::collections::HashMap;

struct Position {
    x: i32,
    y: i32,
}

// From を定義すると相手側に Into が自動的に定義される
impl From<(i32, i32)> for Position {
    fn from(pair: (i32, i32)) -> Self {
        Self {
            x: pair.0,
            y: pair.1,
        }
    }
}

pub fn main() {
    println!("\n数値の変換");
    let a = 1;
    let b = convert_to_signed(a);
    println!("u8 -> i16: {a} -> {b}");

    let c = 1;
    let d = convert_to_bigger(c);
    println!("i8 -> i32: {c} -> {d}");

    println!("\n同じコードでもこの後どう使うかで型を推測してくれる");
    let xs = get_nums().into_iter().enumerate().collect();
    let ys = get_nums().into_iter().enumerate().collect();

    print_pairs(xs);
    print_hashmap(ys);

    println!("\n特定の長さの配列に変換");
    print_vec(&[1, 2, 3, 4, 5]);
    print_vec(&[1, 2, 3, 4]);

    println!("\n自前の実装");
    print_position((-2, 3).into());
}

fn convert_to_signed(x: u8) -> i16 {
    x.into()
}

fn convert_to_bigger(x: i8) -> i32 {
    x.into()
}

fn get_nums<A: From<u8>>() -> [A; 3] {
    [1.into(), 2.into(), 3.into()]
}

fn print_pairs(a: Vec<(usize, u32)>) {
    println!("vec: {a:?}");
}

fn print_hashmap(a: HashMap<usize, i16>) {
    println!("hashmap: {a:?}");
}

fn print_vec(vs: &[u8]) {
    if let Ok(array) = vs.try_into() {
        print_5bytes(array);
    } else {
        println!("不明な配列: {vs:?}");
    }
}

fn print_5bytes(bs: [u8; 5]) {
    println!("5 bytes: {bs:?}");
}

fn print_position(p: Position) {
    let x = p.x;
    let y = p.y;
    println!("Position({x}, {y})");
}

Open playground

(おまけ)FromStr を定義しておけば parse できる

use core::str::FromStr;
use core::num::ParseIntError;

#[derive(Debug)]
struct Position {
    x: i32,
    y: i32,
}

#[derive(Debug)]
struct ParsePosError(String);

impl From<ParseIntError> for ParsePosError {
    fn from(src: ParseIntError) -> Self {
        Self(src.to_string())
    }
}

impl FromStr for Position {
    type Err = ParsePosError;
    
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let ss: Vec<_> = s.split(',').map(|a| a.trim()).collect();
        if let [x, y] = ss[..] {
            Ok(Self {
                x: x.parse()?, // ParseIntError が発生するが、
                y: y.parse()?, // 自動的に ParsePosError に変換される。
            })
        } else {
            Err(ParsePosError("should be two parts".to_owned()))
        }
    }
}

pub fn main() {
    print_position("3, 5".parse().unwrap());
}

fn print_position(p: Position) {
    let x = p.x;
    let y = p.y;
    println!("Position({x}, {y})");
}

Open playground

特定の generic 型を持つ場合に対してのみ実装を書ける

文法的にはすごく当たり前なんですが、保持している型が特定の場合のみ適用される実装を書けます。
こういうのを自然に書けるのがありがたい。

use core::fmt::Debug;

trait HasPet {
    fn get_pet(&self) -> String;
}

struct User<A> {
    name: String,
    pet: A
}

impl<A: Debug> HasPet for User<A> {
    fn get_pet(&self) -> String {
        format!("{:?}", &self.pet)
    }
}

#[derive(Debug)]
struct Dog {
    name: String
}

struct Cat {
    name: String
}

pub fn main() {
    let user_a = User {
        name: "田中".to_string(),
        pet: Dog { name: "ポチ".to_string() },
    };
    let user_b = User {
        name: "佐藤".to_string(),
        pet: Cat { name: "タマ".to_string() },
    };
    
    println!("{} + {}", user_a.name, user_a.pet.name);
    println!("{} + {}", user_b.name, user_b.pet.name);
    
    log_name(user_a);
    // log_name(user_b); // compile error
}

fn log_name<A: HasPet>(user: A) {
    let pet = user.get_pet();
    println!("{pet}");
}

Open playground

where 節

どういう型を受け付けるかというトレイト境界を <A: X + Y> の形で宣言部に書く方法の他に where 節に書く方法もあります。
宣言部に書くには長すぎる記述を where 節に持ってきて見易くするという利点もありますが、where 節での記述は自由度が高く、宣言部では書けないような表現もできます。

use core::fmt::Debug;

pub fn main() {
    to_i64(vec![1, 2, 3]);
    to_i64(vec![1_u8, 2, 3]);
    to_i64(vec![1_i16, 2, 3]);
    
    to_i64s(vec![2, 4]);
}

fn to_i64<A>(xs: Vec<A>)
where
    A: Debug,
    i64: From<A>, // この書き方は宣言部には書けない
    // A: Into<i64>,
{
    println!("bytes: {xs:?}");
    let ys = xs.into_iter().map(|a| a.into()).collect();
    log_i64s(ys);
}

fn to_i64s<A>(xs: Vec<A>)
where
    Vec<i64>: From<Vec<A>> // この書き方は宣言部には書けない
    // Vec<A>: Into<Vec<i64>> // この書き方も宣言部には書けない
{
    log_i64s(xs.into());
}

fn log_i64s(xs: Vec<i64>) {
    println!("sigs: {xs:?}");
}

Open playground
自作クラスのバージョン

テストを同じファイルに書ける

同じファイル内にテストを書けるので、「こうなるはず」という確認を目の届く範囲にまとめて書けます。ファイルが大きくなってくると自ずと別ファイルに分けることになるかもしれませんが、最初のうちから少しずつ気軽にテストコードを書いておけるのがありがたいです。

pub fn main() {
    let a = to_array::<4>(&[1, 2, 3, 4]);
    println!("{a:?}");
    
    let b = to_array::<3>(&[1, 2, 3]);
    println!("{b:?}");
}

fn to_array<const N: usize>(bs: &[u8]) -> Option<[u8; N]> {
    bs.try_into().ok()
}

#[cfg(test)]
mod test {
    use super::*;
    
    #[test]
    fn convert_to_array() {
        let xs = [1, 2, 3];
        assert_eq!(to_array::<3>(&xs), Some(xs));
        assert_eq!(to_array::<4>(&xs), None);
    }
}

Open playground

この他にもコメントに関数のドキュメントとしてテストを書けるのですが、私はあまり使ってません。ドキュメントとしてちゃんと書かないといけないという勝手な思い込みがあって、ハードルになってます。


以上、ここまでで10選です。
好きなポイントなんて人それぞれだとは思いますが、こういう視点もあるという何らかの気づきのきっかけになってくれればと思います。

Discussion

ログインするとコメントできます