LoginSignup
8
7

More than 3 years have passed since last update.

Rust で C# 9.0 と同じ(ような)プログラムを書いてみよう

Last updated at Posted at 2020-12-08

この記事は Rust 3 Advent Calendar 2020 9日目の記事です。

動機

C# 1.0 と C# 9.0 で同じプログラムを書いてみよう という記事の C# 9.0 のコードを Rust に写経したらどうなるか?という試みです。

環境

stable-x86_64-unknown-linux-gnu
rustc 1.48.0 (7eac88abb 2020-11-16)

end と入力するまで文字列を受け取って最後に入力した文字列から表示する

Iteratorを用いることで下記のように、とてもエレガントで直感的な記述が可能となります。また、ある入力を除外したり変換したい場合にfiltermapなどの関数を使うことでスマートに書けますね。

main.rs
use std::io;

struct ReadLine;

impl Iterator for ReadLine {
    type Item = String;
    fn next(&mut self) -> Option<Self::Item> {
        let mut buf = String::new();
        io::stdin().read_line(&mut buf).ok()?;
        Some(buf.trim().into())
    }
}

fn main() {
    ReadLine
        .take_while(|s| s.ne("end"))
        // .filter(|s| s.ne("a"))
        // .map(|s| format!("{}!", s))
        .collect::<Vec<_>>()
        .iter()
        .rev()
        .for_each(|s| println!("{}", s));
}

特定のメッセージを受け取ったらイベントを発行するクラス

C# 9.0 のEventHandlerEventArgsが何ものかよくわからなかったので、とりあえず構造体のメンバにクロージャを渡して、ある条件でそのクロージャを呼ぶという実装を考えてみました。Rust においてクロージャはFnFnMutFnOnceトレイトによって実現されている訳ですが、それらのトレイトについてはこちらの記事や、こちらの記事を読むことで理解が深まると思います。

また、条件分岐にはmatches!マクロを使用していますが、それについては matches! マクロの定義match 式のドキュメントが参考になります。

main.rs
struct MessageCollector<F: Fn(String)> {
    greet: Option<F>,
}

impl<F: Fn(String)> MessageCollector<F> {
    fn new() -> Self {
        Self { greet: None }
    }

    fn add_message(&self, message: &str) {
        if matches!(message, "おはようございます" | "こんにちは" | "こんばんは")
        {
            if let Some(greet) = self.greet.as_ref() {
                greet(message.into());
            }
        }
    }
}

fn main() {
    let greet = |message| {
        println!("{} って言ったね!", message);
    };
    let mut message_collector = MessageCollector::new();
    message_collector.greet = Some(greet);
    message_collector.add_message("Hello");
    message_collector.add_message("world");
    message_collector.add_message("こんにちは");
}

非同期でインターネットからテキストのダウンロード

HTTP リクエストの定番はreqwestクレートですね。2020 年 12 月現在reqwestのバージョンは 0.10.9 ですが、tokioバージョン 0.2.* にしか対応していないようです。(現在tokioの最新バージョンは 0.3.5)

Cargo.toml
[dependencies]
tokio = { version = "0.2", features = ["full"] }
reqwest = "0.10"
main.rs
use reqwest::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let body = reqwest::get("https://example.com").await?.text().await?;
    println!("{}", body);
    Ok(())
}

テキストファイルに書かれた URL のリストからサイトのタイトルタグの行を取得

非同期イテレータは Rust ではStreamというそうです。C# 9.0 の実装とはちょっと違いますが、非同期イテレーションの例ということで…。

async-std

非同期ランタイムにasync-stdを使用した場合の例。parallel-streamという便利クレートのおかげで非同期イテレーションが簡単に書けます。

Cargo.toml
[dependencies]
anyhow = "1.0"
async-std = { version = "1.7", features = ["attributes"] }
parallel-stream = "2.1"
surf = "2.1"
main.rs
use anyhow::Result;
use async_std::fs;
use parallel_stream::prelude::*;
use surf;

#[async_std::main]
async fn main() -> Result<()> {
    let url_list = fs::read_to_string("urllist.txt")
        .await?
        .lines()
        .map(String::from)
        .collect::<Vec<_>>();

    url_list
        .into_par_stream()
        .map(|url| async move {
            let body = surf::get(&url).await.ok()?.body_string().await.ok()?;
            for line in body.lines() {
                if line.contains("<title>") {
                    return Some((url, line.to_string()));
                }
            }
            None
        })
        .for_each(|result| async {
            if let Some((url, title)) = result {
                println!("{}", url);
                println!("{}", title);
            }
        })    
        .await;

    Ok(())
}

tokio

非同期ランタイムにtokioを使用した場合の例。非同期イテレーションにはfuturesクレートのfutures::streamを使います。

Cargo.toml
[dependencies]
anyhow = "1.0"
tokio = { version = "0.2", features = ["full"] }
reqwest = "0.10"
futures = "0.3"
main.rs
use anyhow::Result;
use futures::{stream, StreamExt};
use tokio::fs;

#[tokio::main]
async fn main() -> Result<()> {
    let url_list = fs::read_to_string("urllist.txt")
        .await?
        .lines()
        .map(String::from)
        .collect::<Vec<_>>();

    stream::iter(url_list)
        .map(|url| async move {
            let body = reqwest::get(&url).await.ok()?.text().await.ok()?;
            for line in body.lines() {
                if line.contains("<title>") {
                    return Some((url, line.to_string()));
                }
            }
            None
        })
        .for_each_concurrent(3, |result| async {
            if let Some((url, title)) = result.await {
                println!("{}", url);
                println!("{}", title);
            }
        })
        .await;

    Ok(())
}

入力 2 つを整数かどうか判定して、さらに偶数かどうかも判定してメッセージを出しわける

これは前述のmatch式の例ですね。つよい。

main.rs
use anyhow::Result;
use std::io;

fn main() -> Result<()> {
    let x1 = read_line()?.parse::<i32>().ok();
    let x2 = read_line()?.parse::<i32>().ok();

    let message = match (x1, x2) {
        (Some(v1), Some(v2)) if is_even(v1) && is_even(v2) => {
            format!("両方偶数!!入力した値は {} と {} ですね!", v1, v2)
        }
        (Some(v1), Some(v2)) => format!("入力した値は {} と {} ですね!", v1, v2),
        (Some(_), None) | (None, Some(_)) => "おしいね…".into(),
        (None, None) => "まだまだだね".into(),
    };

    println!("{}", message);
    Ok(())
}

fn is_even(value: i32) -> bool {
    value % 2 == 0
}

fn read_line() -> Result<String> {
    let mut buf = String::new();
    io::stdin().read_line(&mut buf)?;
    Ok(buf.trim().into())
}

まとめ

C# 9.0 を写経してみたら Rust の便利な構文やクレートがあることに気付きました。

8
7
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
8
7