はじめに

本書はAtCoderのコンテストにRustで参加するためのガイドブックです。

AtCoderとは?

AtCoderは、オンラインで参加できるプログラミングコンテスト(競技プログラミング)のサイトです。リアルタイムのコンテストで競い合ったり、約3000問のコンテストの過去問にいつでも挑戦することが出来ます。 (AtCoderのトップページより引用)

Note: 競技プログラミングはプログラミングで解決できるような問題をなるべく早く正確に解く競技です。競技プログラミングについてよく知らないが興味があるという方は、インターネット上に初心者向けの詳しい情報がたくさんありますので検索してみてください。AtCoderで開催されているものでは、大きく分けて二種類あります。

  • 与えられる問題に対して、その解を出力するようなプログラムを書く競技 (アルゴリズム系)
    • 定期開催のもの: AtCoder Beginner Contest (ABC), AtCoder Regular Contest (ARC), AtCoder Grand Contest (AGC) があり、難易度は通常 ABC < ARC < AGC です。
    • 定期開催の他、企業によって開かれるコンテストもあります。そういったコンテストで上位成績をとると、その企業への就職、インターン、アルバイトなどで多少優遇されることがあります (コンテストによります) 。
    • 例: 「整数Nが与えられます。N以下の正整数から等確率に1つを選ぶとき、それが奇数になる確率を求めなさい」 (AtCoder Beginner Contest 142 A問題)
    • 例: 「N人の身長が与えられます。Kcm以上の人の人数を出力してください」 (AtCoder Beginner Contest 142 B問題)
  • 与えられる問題に対して、少しでも良い解を出力するようなプログラムを書く競技 (マラソン系)
    • 定期開催のものはまだありません。企業が自社の取り組みやそこでの課題をテーマに出題することが多いようです。
    • 例: 「ある観測データが与えられるので、可能な限り圧縮するプログラムとそれを解凍するプログラムを書いてください」 (Wethernews Programming Competition)

なぜRustなのか?

AtCoderで使える言語は非常にたくさんあります。どの言語を使ってもよいですし、問題によって使い分けても構いません。その中でなぜRustを使うのか、そのメリットとデメリットをまとめてみました。できるだけ一般論で比較するよう心がけますが、競技プログラミングにおけるC++人口がそれなりに多いことと、Rustはその特性上C++と比較されることが多いので、具体的にC++との比較になっている部分も多くあります。

メリット

高速である

AtCoder含め、競技プログラミングでは「実行時間制限」とよばれるものがあります。この時間内にプログラムの実行が終わらないと「TLE (Time Limit Exceeded)」という判定が付いて誤答扱いとなります。多くの場合は想定されている解法であれば多少の余裕をもって解けるように設定されていますが、非常にたくさんの言語が使える都合上全ての言語で公平になるようにはできません。遅い言語に合わせて設定すると速い言語では強引な解法でゴリ押しできてしまうことがありますし、速い言語に合わせると遅い言語では想定されている解法でも通せないということになります。いずれにせよ、基本的には速い言語であるほうが計算時間的には有利です。 (もちろん遅い言語と言われるものにも、例えば書き易さであったり、ライブラリが充実していたり、なにかしらのメリットがあるはずです。どちらかが絶対的に有利ということではありません。)

Rustは最速と言われるC/C++並みに速いとされていますので、(少なくともAtCoderでは) 速度面で不利になることはないと言えるでしょう。

信頼性が高い

信頼性は、ここではRustの公式トップページに倣いメモリ安全性、スレッド安全性、バグの起こしにくさであるとします。競技プログラミングで特に大事になってくるのはメモリ安全性とバグの起こしにくさです。

Reliability Rust’s rich type system and ownership model guarantee memory-safety and thread-safety — and enable you to eliminate many classes of bugs at compile-time.

これは様々なプログラミング言語が様々なアイデアで対処している部分です。たとえばメモリの確保と解放を正しく行うために、C++であればスマートポインタやコンテナを用意したり、他の言語ではガベージコレクタという実行時の機構を用意したりしています。RustでもC++のスマートポインタやコンテナと同様なものを用意し、自分でメモリの確保と解放を行わなくてよいようにできています。ガベージコレクタを利用すると確かに安全でメモリ管理に関してほとんど何も考えなくてよいものの、そのためにほとんどのデータをヒープに置いて参照経由で扱わなければならず、実行速度にも多少影響します。かといってC++のスマートポインタやコンテナは間違った使い方が簡単にできてしまいます。特にイテレータはコレクションに対する操作を行うための標準的なツールにもかかわらず実質的に単なるポインタと同様なので、しばしば無効なイテレータが発生します。例えば (少々意図的な例ですが) 次のように簡単に問題を起こせてしまいます。

#include <iostream>
#include <vector>
#include <string>
using namespace std::string_literals;
int main() {
    std::vector<std::string> v = {"hello"s, "world"s};
    v.shrink_to_fit();
    for (auto const &i: v) {
        if (i == "hello") v.push_back("c++"s); // イテレータを無効化してしまう
        // 未定義動作なので、プログラム全体が何を起こすか分からない
        std::cout << i << std::endl;
    }
}

このようなことはRustではコンパイルエラーとしてコンパイル時に検出されます。

let mut v = vec!["hello", "world"];
for &i in &v {
    if i == "hello" {
        v.push("rust");
        // E0502: cannot borrow `v` as mutable because it is also borrowed as immutable
        // 4 | for &i in &v {
        //   |           --
        //   |           immutable borrow occurs here
        //   |           immutable borrow later used here
        // 5 |     if i == "hello" {
        // 6 |         v.push("rust");
        //   |         ^^^^^^^^^^^^^^ mutable borrow occurs here
    }
    println!("{}", i);
}

また、複雑なアルゴリズムにバグはつきものです。例えば、添字計算をしていてちょっとした書き間違いで配列のサイズを超えたところを参照してしまったといったことは度々起こりえます。こういうとき、例えばC/C++では配列外参照をしてしまったプログラムがどのように動作するかの保証がなく、segmentation faultとだけ表示されて異常終了したり、たまたまうまくいってしまったり、手元で正解するケースがサーバーでは誤答となったりし得ます。こういったよく分からない動作が起きてしまうとバグの原因特定が難しくなったり無関係なところを原因と思い込んだりしてしまい、デバッグにかなりの時間を費やしてしまうこともしばしばあります。この配列外参照の例ではRustは必ずエラーを起こしますし、どこで起こしたかも表示してくれます。それが自分のコードでない場合でも (-gオプション付きでビルドされたかCargoでデバッグビルドされたバイナリであれば) バックトレースを表示させることで呼出元となる自分のコードを特定できます。


# #![allow(unused_variables)]
#fn main() {
let x = vec![1, 2, 3];
let y = 3;
let _z = x[y]; // 配列外参照!
#}
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3', ...
stack backtrace:
  (...中略...)
  16: alloc::vec::{{impl}}::index<i32,usize>
             at ...
  17: test::main
             at .\test.rs:4
  (...中略...)
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

多くの問題をコンパイル時に発見できる

RustはC/C++並みの速度を確保するため、実行時にやらなければいけないことをなるべく減らす方針の言語です。たとえば先ほど触れたように、多くの言語にあるガベージコレクタがありません。それだけならばC++と変わりありませんが、速度と安全性を両立させるためにRustではできるだけ多くのことをコンパイル時に確認する仕組みになっています。C++が受け入れてしまうような危険なコードもコンパイルエラーにします。

それ以外にも、例えば整数型同士の暗黙の型変換がない (例えu8からu32であっても) こともある種のミスを防ぐのに役立ちます。たとえばC++で総和をとるためにaccumulate()関数を利用する際、気をつけなければオーバーフローしないはずのところでオーバーフローを起こしてしまいます (もし符号付き整数型であったなら未定義動作にもなってしまいます) 。

#include <iostream>
#include <limits>
#include <numeric>
#include <vector>
int main() {
    // long long で表せる最大値を`large`とする。
    long long large = std::numeric_limits<long long>::max();

    std::vector<long long> s = {large};

    // sには`large`しか入っていないので、総和は普通に`large`になるはず。ところが
    // 総和の型をint型だと思って計算してしまいオーバーフローを起こしてしまう。特
    // に何のエラーもなし。
    std::cout << std::accumulate(s.begin(), s.end(), 0) << std::endl;

    // 正しくは以下の通り。
    std::cout << std::accumulate(s.begin(), s.end(), 0ll) << std::endl;
}

Rustではこのようなことは最初から型の不一致によりコンパイルエラーとなります。

let s = vec![std::i64::MAX];
let x: i32 = s.into_iter().sum();
// E0277: the trait bound `i32: std::iter::Sum<i64>` is not satisfied
// 3 | let x: i32 = s.into_iter().sum();
//   |                            ^^^ the trait `std::iter::Sum<i64>` is not implemented for `i32`

ジェネリクスとトレイトの仕組みも強力です。たとえばジェネリックな関数が型変数Tをもつとき、このTのとりうる型を特定のトレイト (=機能一覧) を実装しているものだけに制限することができます。逆にTに対してできることはその特定のトレイトが定める機能のみです。従って、一度コンパイルが通った関数はその制約を満たす限りのどのようなTを与えても関数の内部でコンパイルエラーとなることはありません。特にライブラリを整備するにあたってはこれはとてもありがたいことです。実際に使ってみなくても、コンパイルさえ通れば、将来的に作られうるどんなユーザー定義型を与えようともその関数が正しく呼び出せることが保証されます。C++のテンプレートなどでは実際に具体的な型を与えて始めて様々な検証をするので、使う段階になってからでないとエラーが発見できません。このことは、後述するコンパイルエラーの分かりやすさにも繋がっています。

コンパイルエラーが分かりやすい

これは少々主観的な話になるのかもしれませんが、Rustのコンパイルエラーは読みやすく分かりやすいという評判があります。実際にコンパイルエラーが発生したとき、まずエラーが起きた場所はもちろんとして、エラーが関連する他の場所 (例えば以前に借用された場所など) などをアスキーアート的な手法で視覚的に分かりやすく表示してくれます。さらに、なぜそれが間違っているのか/それをどのように修正することができるかのヒントが提示されることもあります。例えば、先ほどのエラー全体は次のようになっていました。

error[E0277]: the trait bound `i32: std::iter::Sum<i64>` is not satisfied
--> (filename):3:32
|
3 |     let x: i32 = s.into_iter().sum();
|                                ^^^ the trait `std::iter::Sum<i64>` is not implemented for `i32`
|
= help: the following implementations were found:
            <i32 as std::iter::Sum<&'a i32>>
            <i32 as std::iter::Sum>

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

先ほど少し触れたように、トレイト/ジェネリクスの仕組みによりエラーの所在が明確化されています。関数が呼び出せないのは引数に関数が設定した制約を満たさないものを与えているからだと分かります。動的型付け言語やC++のテンプレートでは、仕組み上実際に実行または実体化してみなければエラーの存在が分かりませんので、与えられた引数が関数の求める条件を満たしていなかったのか関数の方に根本的な書き間違いがあるのかがインタプリタ/コンパイラには分かりません。必然的に実際にエラーを起こしたライブラリの内部の実装部分を指し示しながら、ここで呼び出されていて、ここで呼び出されていて、...と辿っていくしかなく、しかもそのうちのどれが悪いのかが分からないので関係がありそうなところを全て表示していくしかありません。

一部のエラーには詳細な説明が用意されており、rustc --explain (エラーコード)とすることにより、そのエラーが何を言っているのか/どういうコードでそのエラーが発生するのかなどをもりこんだ詳細な解説を読むこともできます。たとえば上のエラーのE0277にも用意されています。

$ rustc --explain E0277
You tried to use a type which doesn't implement some trait in a place which
expected that trait. Erroneous code example:

```
// here we declare the Foo trait with a bar method
trait Foo {
    fn bar(&self);
}

// we now declare a function which takes an object implementing the Foo trait
fn some_func<T: Foo>(foo: T) {
    foo.bar();
}

fn main() {
    // we now call the method with the i32 type, which doesn't implement
    // the Foo trait
    some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied
}
```
(以下略)

抽象化のための機能を数多く備えている

例えば次のようなさまざまな機能があります。Rustは後発の言語ですので、他のプログラミング言語に備わっている優れた機能も参考にして多数の機能が導入されています。

  • 強力な型推論

    型推論アルゴリズムとしては非常に有名なHindley/Milnerのアルゴリズムをベースにした賢い型推論器を持ちます。言語設計的に型推論を制限している箇所 (例: 関数の引数) や、型推論だけでは決定できず型を明示する必要があることもあります (例: iter.collect::<Result<_, _>>()?) 。それでもほとんどのケースでは推論され、型の明示は最低限で済みます。

  • 代数的データ構造

    Rustの列挙型 (enum)は各バリアントに値を持てます。いわゆる直和型や代数的データ型と呼ばれるものです。タグ付き共用体 (tagged union) と呼ばれることもあります。例えば一方向連結リストの構造は次のように表せます。

    
    # #![allow(unused_variables)]
    #fn main() {
    enum List<T> {
        /// 先頭要素とそれ以降の要素によるリストを持つ。
        Cons(T, Box<List<T>>),
    
        /// リストが空であることを示す。
        Nil,
    }
    #}
  • トレイト

    JavaやC#のインターフェイスと似ていますが、より柔軟に利用することができます。Rustではいわゆる「オブジェクト指向言語」が持つ継承の仕組みをもたない代わりに強力な列挙型とトレイトの仕組みを活用します。例えば単純な継承関係であれば次のような方法で置き換えられます。多くの場合、1.と2.を用いれば十分対応できることでしょう (参考) 。

    1. 実装クラスが数個かつパラメータも多くはない

      列挙型 (直和型)を使います。 むしろこのような場合は最初から継承よりも直和型が適切である場合が多いです。 実際、KotlinやScalaではJVM上で抽象クラスとそのサブクラスを直和型のように扱うためにsealed classという機能を備えています。

    2. 実装の共有

      トレイトのデフォルト実装を使います。 フィールドの共通化は、代わりにgetterメソッドをトレイトに記述しておきます。 むしろこれにより各フィールドやメソッドの役割が明確になり、無意味に複雑なコードが生まれにくくなります。

      ただし、getterを書く際に「1つのオブジェクトに対する可変参照は高々1つしか存在できない」という原則が障害となることがごく稀にあります。

    3. 特定のメソッドだけ必要

      trait object (Box<_>)でdynamic dispatchを行ないます。

    なお、DerefDerefMutトレイトを実装すると、メソッド呼出時に自動的に対象の型への型変換が行われ、簡単に対象の型のメソッドを呼び出すことができるようになります。これを継承における「親クラスからのメソッドの受け継ぎ」に転用することは可能ですが、これはアンチパターンとされることも多いです (参考) 。

    その他また、Rustのトレイトは型クラスのように考えることもできます (参考) 。

  • パターンマッチング

    パターンマッチングは、端的に言うならばタプルや構造体・列挙型などを (構造に合わせて分岐しながら) 分解し、中身を取り出す機能です。特に先に触れた列挙型を扱うにあたっては、パターンマッチは非常に強力な機能です。最近でも少しずつ改善が行なわれており、 Rust 1.26でslice patternが追加されたり1.39でifの制限の取り払われたりしました。これらの改善もあり、特にML系言語に馴染みのある方は近い感覚で扱えるのではないでしょうか。

  • 衛生的マクロ

    衛生的とは識別子が衝突する心配がないことです。マクロ内で宣言されたローカル変数がマクロ外から参照できてしまうことも、その逆にマクロ内で参照する変数がたまたま展開先のローカル変数を参照してしまうこともありません。また、マクロの各引数が何を受け取るのかを指定することができるため、例えば式を受け取ると指定すればその引数はひとまとまりの式として扱われます。この式の値を利用するときに、前後の関係で式としての解釈が壊れることもありません (例えば $x = 1 + 2 のとき 2 * $x2 * 1 + 2 ではなく 2 * (1 + 2) に相当します) 。

    このように、Rustのマクロは積極的に利用しても比較的安全です。特にRustでは多くなりがちなボイラープレートを短くまとめるなど役に立つ機会も多いでしょう。またマクロの入力はパターンマッチで行われるため受け付ける文法が比較的自由であり、この後に述べるRustのデメリットのうち特に冗長性に関わることは可読性を保ったままマクロで解決できることも多いです。

  • block expression

    block expression とはブロックを式として扱える仕組みのことです。ブロックの式としての値はそのブロックの末尾にある式の値になります。これがあると、例えば別の変数の初期化にしか使わない一時変数のスコープを絞ることができます。地味ではありますが、あると便利に感じる機能です。

  • シャドーイング (の推奨)

    一般的にシャドーイングはすべきでないとされる傾向がありますが、Rustではむしろシャドーイングが推奨されています。 これにより、たとえば処理の途中で一度変数に格納するときにもわざわざprefixやsuffixが付いた別の変数を宣言しなくて良くなりますし、あるいは再束縛することで不要になったmutを消すこともできます。

    
    # #![allow(unused_variables)]
    #fn main() {
    // この配列を数値にしてかつソートしたい
    let list = vec!["2", "4", "1", "5", "3"];
    let list = list.into_iter().map(|x| x.parse().unwrap());
    // ソートしたいので mut とする
    let mut list: Vec<i32> = list.collect();
    list.sort();
    let list = list; // ソートしたので mut は不要、消す
    // list.push(4); // => 既に mut ではないので変更できない
    assert_eq!(list, [1, 2, 3, 4, 5]);
    #}

    一般にシャドーイングするべきではないとされる理由は様々にありますが、一番はやはり混同しやすくなるからと考えられます。その点Rustは強い静的型付け言語ですので、仮に混同したとしてもどこかのタイミングで型エラーになるという期待ができます。

    また、変数の個数を抑止するという効果もあります。block expression等と適宜組み合わせることで変数の数やスコープはさらに小さく保つことができ、多少関数の実装が長くなっても見通しが悪くなりにくいと言えます。競技プログラミングではmain関数が長くなりがちなので一層嬉しいのではないでしょうか。

ゼロコスト抽象化を追求している

Rustの言語デザインやライブラリは、一定の使いやすさを実現しつつも、使いやすさのために実行時の高速性を犠牲にはしないというゼロコスト抽象化 (zero-cost abstraction)を追求しています。

他の言語では「イテレータではなくfor文で書いた方が速い」、「async/awaitを使うと遅い」といったことは珍しくありません。これはその言語の選択で、動作が遅くなるとしてもコードを簡潔に簡単に書けることを優先したと言えます。一方でRustでは抽象化によってパフォーマンスが犠牲になるのなら、その抽象化はできるだけ採用しません。最適化の余地 (自由度) を残すためには関数群は基礎的なものにとどめる必要があり、特定の機能を実現するために冗長なコードを書く必要があります。結果的にコードは長く面倒になり手間もかかりますが、そうしてでもパフォーマンスをとることができるように設計されています。

このことは「簡潔な構文や関数によってその機能が必要とするコストを隠してしまう」ことを避けているとも言えます。つまり、本当にパフォーマンスが必要なときに最適化を検討するべき「コスト」の部分が明確化されているということでもあります。

デメリット

現れる概念が比較的難しい

先に見たように、Rustでは、いままで他の言語ではコンパイラが検証していなかったようなことをコンパイル時に検証します。そのためにRustでは所有権や借用をはじめとする独特の概念が導入されており、それらの概念の理解そのものが難しいとされることも多いようです。これらの概念が課す多数のルールがなぜ存在するのかを理解することは、仕組みをある程度理解していなければ難しいものです。

たとえば要素への参照をもったままVec本体の可変参照をとることはできませんが、これは可変参照と共有参照は同時に存在できないというルールからです。ではなぜこのようなルールがあるのでしょうか。たとえばVecに要素を追加するとキャパシティが足りないときにメモリの再確保とデータの移動が行われるので、無効な参照が生み出せてしまうからです。しかしこういった事情を知らないと、単にRustがよく分からない制約を課してくるだけの書きにくい言語だと感じてしまうかもしれません。一方でこの難しさを隠せている言語もあります。たとえばJavaやC#などオブジェクトを参照で扱うような言語では、要素への参照を得ても単にそのオブジェクトへの参照が一つ増えるだけで、可変長配列の領域そのものへの参照を得るわけではないということもあるかもしれません。そのような言語では、要素そのものは可変長配列が管理するメモリ上にあるわけではないので、要素への参照をもったまま可変長配列を伸ばしても問題は起こりません。そのかわり、おそらく別の部分の犠牲 (アクセスに必ず参照を介することのコストであったり、参照型と値型の挙動の違いによる難しさであったり) があります。

Rustが課すルールにも理由がありますので、そういった事情について意識的に考えることは他の言語や競技プログラミング以外の文脈でも活きる有意義なものではあると思います。単にAtCoderである程度の競技プログラミングをするだけであれば、C#やJavaといった言語でもほぼ正解できるよう調整されているようなので、どちらを取るかは好みといっていいかもしれません。

素早く書くことにはあまり向かない仕様

Rustの安全指向や標準ライブラリの設計方針などは、時間をかけて大規模なプログラムを書くときや堅牢なプログラムを書くときには非常に役に立ちます。一方で競技プログラミングでは、一般のプログラミングと異なり、次のような特徴があります。

  • 早くプログラムを完成させることが重要
  • 入力のフォーマットや扱う値の範囲・個数などが定まっている
  • スレッドを起動してやりとりするようなことは通常ない
  • 後日提出したプログラムを見直したり保守することは通常ない (ライブラリを除く)

したがって、Rustの様々な設計は、必要以上に煩雑に感じることがあります。例えば、Rustでの競技プログラミングを始めようとした方で、標準入力をとるのがとても面倒で挫折し (かけ) た、という方も度々見かけます。例えば、空白区切り二つの整数を読み取ってその和を計算するプログラムは、工夫をしなければ次のようになります。

fn main() {
    let (a, b) = {
        let mut s = String::new();
        std::io::stdin().read_line(&mut s).unwrap();
        let mut iter = s.split_whitespace().map(|i| i.parse::<i32>().unwrap());
        (iter.next().unwrap(), iter.next().unwrap())
    };

    println!("{}", a + b);
}

「二つの空白区切りの数字を読むだけでこれほど多くのコードが必要なのか」と思われるかもしれませんが、こうなっているのには次のような設計があります。

  • 空白区切りの入力を任意の型の値として読み込むことができない。 stdには整数を読む機能などはなく、かならず一行単位 (read_line()) または全体 (read_to_string()) を文字列として読んでから処理する必要があります。なおread_to_string()を使うときは、use std::io::Read;が必要であることと、手元でテストするときにEOF (Ctrl + D (macOSやLinux) またはCtrl + Z (Windows)) を入力するまで入力が終了しないこと、に注意が必要です。
  • 入力はバッファをとって、そこに書き込む。 read_line(&mut s)の部分にあたります。入力をStringに入れて返す関数よりも、バッファを受けとって書き込む方式の方が、必要に応じてバッファを事前にアロケートしておける分、パフォーマンス的には柔軟なのです。とはいえ競技プログラミングでは高々O(log n)回のリアロケーション (この挙動はドキュメント化されていないため変わる可能性もなくはありませんが) にかかるコストを気にする必要はないと思われます。
  • イテレータを上手に扱う必要がある。 受け取った文字列を空白区切りにするためにはsplit_whitespace()という関数を使いますが、これはイテレータを返します。その各要素を整数に変換するためにmap()parse()関数を使いますが、変換先の型を指定するためにparse::<i32>()などの書き方を使う必要があります。itertools を使えばイテレータを直接分解するような書き方ができますがstdに限れば一つずつnext()で要素を取り出すしかありません。
  • stdを含む各ライブラリは大抵明示的なエラーハンドリングを要求する。
    • 何かの理由で標準入力が読み込めない状態になるかもしれません。整数ではない入力を整数にしようとするかもしれません。イテレータの要素が足りないかもしれません。こういったものをRustではOptionResultで表現します。?を使えば視認性を損ねることはない (むしろコードを俯瞰するときのの助けになる) ですが競技プログラミングではunwrap()を使うことになるでしょう。これはエラーが起きたならパニック (i.e. RE) するという乱暴なものですが、入力の形式が決まっている以上エラーになるのは読み間違えたか書き間違えたときのみでしょう。
    • もちろん逆に、明示的なエラーハンドリングは要求しないかわりに失敗時は内部でパニックするような関数もあります。 例えばIndexで境界外アクセスしたときやメモリが足りずにアロケーションに失敗したとき、println!が失敗したときにパニックします。 このように「失敗するとパニックする」ものはその条件をドキュメントに# Panicsという形で書いています。

Note: 入力をとる方法については、2020年言語アップデートで外部クレートとしてproconiotext_io, whitereadなどのクレートが導入されたため、かなり改善されました。リンク先はそれぞれのクレートのドキュメントになっていますので、詳しくはそちらをご覧ください。

他にも、先程少し触れましたが例えば数値型の四則演算や比較を行なうときには基本的に両辺の型が等しくなければいけません。 i32&'_ i32を両辺に持って来ることくらいは許されていますがi32i64をそのまま足したり比較したりはできません。 片方を明示的に変換する必要があります。 これは型システムの都合等ではなく数値型に対しては意図的に制限されています。

さらにスライスのインデックスはusize及びusizeの範囲でなくてはなりません。 isizeでは駄目です。 競技プログラミングでは、非負整数として入力される値であっても、計算途中では符号付き整数の方が扱いやすいので符号付き整数として扱うことが多々あります。その場合arr[(添字の計算式) as usize]のようにusizeに戻す必要があります。 実際AtCoderの提出のうち、"as usize"という部分文字列を持つRustのコードは結構な数が存在します。 地味なところですが、動的計画法など添字を多用するところでは面倒に感じるかもしれません。

一般的には危ないのでコンパイラが許してくれないものの、使い方の制限に照らせば問題ない動作をしたいということもありえます。たとえばこちらでは二次元配列 (Vec<Vec<T>>) における要素の交換を実装しようとしていますが、同じ要素に対する二つの可変参照を持てないというルールによって普通に実装することができません。競技中に「あ、これがやりたい」と思ったことが必ずしもスムーズに実装できない場合がありえます。

Rustで参加する競技プログラマーの中には、こうした煩雑さを改善するためのマクロやヘルパ関数 (もっと便利に標準入力がとれるようにするなど) を定義し、テンプレート (ひな型) として用意している方もいます。インターネット上で公開されている方もいらっしゃいますし、過去のコンテストでの上位Rust参加者の提出などをのぞいてみると、いろいろと参考になるかもしれません。

標準ライブラリが小さい

Rustは比較的新しい言語ですので、インターネット接続環境を前提にしたパッケージ管理システムCargoを標準で持ちます。このため、言語の成長とクレートやRustエコシステムの成長を分離することを目的に、Rustは標準ライブラリを最低限の抽象化とインターフェースとして位置付け、できるだけ小さく保ち続けてきました。かつて標準ライブラリの一部だったり本体にバンドルされていたライブラリ (num, rand, regex など) を積極的に分離することさえしています。ユーザーはCargoを使えば、使いたいパッケージをcrates.ioからいつでも自由にダウンロードできます。

しかし、逆に言えば標準ライブラリだけでは使える機能が非常に制限されるということにもなります。そのため、2020年言語アップデートでいくつかの著名な外部クレートが導入されて利用できるようになりました。2020年4月6日現在、利用可能なクレートの一覧とそれぞれについての簡単な説明がこちらにまとめられています。

コンパイル時間が長くなりがち

様々な解析をコンパイル時に行う都合上、コンパイル時間が長めにかかる傾向があります。Rustではコンパイル速度を速くすることはあまり重要視されていません。特に手元で提出をテストする際、外部クレートを利用するならその外部クレートのビルドも実行することになります。二回目以降のビルドではビルドキャッシュを利用するためコンパイルする必要はありませんが、初回の実行では利用する外部クレートによっては数分単位の時間をとられる可能性があります。つまり、素早く書き上げたコードを手元で軽く実行してみることにすら時間をとられてしまい、提出時刻が数分遅れてしまうということがあり得ます。結果的に手元でコンパイルが通るかどうかをチェックする時間すら惜しいとなってしまうと本末転倒です。なお、外部ライブラリを含むパッケージをコンテスト開始前に一回ビルドしておいて、競技時はそのフォルダをコピーして編集するというふうにすれば回避できます。