C#開発者のためのRust
ふだんC#を書いているなら、うれしいお知らせがあります。Rustの多くの部分は、きっとなじみやすく感じるはずです。Rustには、制約付きのジェネリクス、async/await、強力なパターンマッチング、LINQによく似たイテレータのメソッドチェーンがあります。さらに、dotnet と同じ仕事をこなす優れたコマンドラインツール(Cargo)もあります。型、インターフェース、ラムダで考えることには、もう慣れていますよね。その知識はそのまま活かせます。
大きく変わるのは、メモリとエラーの扱いです。Rustにはガベージコレクタがありません。その代わりに、すべての値には所有者がひとりだけいて、コンパイラがプログラムの実行前に所有権(ownership)と借用(borrowing)をチェックします。また、Rustには null がなく(Optionを使います)、例外もありません(Resultを使います)。学習時間のほとんどは、この3つの違いに使うことになるでしょう。このページでは、C#と.NETで知っていることをRustに対応づけていきます。そうすれば、本当に新しい部分に時間をかけられます。
C++、Go、Pythonとの全般的な比較が知りたい場合は、Rust vs C++・Go・Pythonを見てください。
言語の概念:C#からRustへ
Section titled “言語の概念:C#からRustへ”この表は、旅行の会話帳のように使ってください。いちばん右の列は、Rust側を説明しているこのサイトのページへのリンクです。
| C# | Rust | 詳しくは |
|---|---|---|
フィールドとメソッドを持つ class |
データ用の struct + メソッド用の impl ブロック |
構造体 |
interface |
trait |
トレイト |
| 抽象クラス、継承 | 継承はない。コンポジション(構造体の中に構造体を入れる)+ デフォルトメソッド付きのトレイトを使う | トレイト、デザインパターン |
異なるクラスを入れた List<IShape> |
Vec<Box<dyn Shape>>(トレイトオブジェクト) |
トレイトオブジェクト |
| 拡張メソッド | 自分で定義したトレイトを、自分が書いていない型(String など)に実装する |
トレイト |
where T : IComparable<T> 付きのジェネリクス |
トレイト境界付きのジェネリクス:T: Ord または where T: Ord |
ジェネリクス |
null、Nullable<int>、null許容参照型 |
Option<T>:Some(value) か None のどちらか |
Option |
?? と ?. |
.unwrap_or(...)、.map(...)、.and_then(...)、そして ? |
Option |
例外、throw、try/catch |
Result<T, E> + ? 演算子 |
Result、独自のエラー型 |
本物のバグを表す例外(InvalidOperationException など) |
panic!(スレッドを止める) |
panic |
IDisposable + using |
Drop。所有者がスコープを抜けると自動で実行される(RAII) |
スコープ、スマートポインタ |
| ガベージコレクタ | 所有権と借用。コンパイル時にチェックされる | 所有権、借用 |
record |
#[derive(Debug, Clone, PartialEq)] を付けた struct |
構造体、よく使うトレイト |
recordの with 式 |
構造体更新記法:Order { total: 5, ..old } |
構造体 |
enum(名前付きの整数だけ) |
各バリアントが独自のデータを持てる enum |
列挙型 |
switch 式、is パターン |
match(すべてのケースを網羅したかコンパイラがチェックする) |
match、パターンマッチング |
タプル (int, string) |
タプル (i32, String) |
タプル |
ref / out パラメータ |
&mut T、またはタプルや Option を返す |
借用 |
ラムダ、Func<T>、Action<T>、デリゲート |
クロージャ。Fn、FnMut、FnOnce というトレイトで表される |
クロージャ、Fn・FnMut・FnOnce |
event |
チャネル、またはボックス化したクロージャ(コールバック)のリスト | チャネル、Fn・FnMut・FnOnce |
LINQ:Where、Select、Sum、OrderBy、ToList |
イテレータアダプタ:filter、map、sum、sort_by(Vec に対して)、collect |
イテレータ、並べ替えと検索 |
IEnumerable<T> と yield return |
impl Iterator<Item = T>、または Iterator を実装した独自の型 |
独自のイテレータ |
async Task<T> / await |
async fn は Future を返す。実行にはtokioのようなランタイムが必要 |
async/await |
Task.Run、Thread、lock |
std::thread::spawn または tokio::spawn。Mutex<T> は守るデータを包み込む |
並行処理、共有状態 |
プロパティ(get; set;) |
プロパティはない。pub フィールドか、ゲッター・セッターのメソッドを使う |
構造体 |
string と StringBuilder |
&str(テキストを借用したビュー)と String(所有していて、伸ばせる) |
文字列 |
文字列補間 $"Hi {name}" |
format!("Hi {name}") と println!("Hi {name}") |
出力 |
var |
let(デフォルトで不変。変更したいときは let mut と書く) |
変数 |
const、readonly |
const、そして普通の let(最初から読み取り専用) |
変数 |
static readonly フィールド |
static、または起動時に作る値なら LazyLock |
変数 |
namespace と using ディレクティブ |
モジュール(mod)と use |
モジュール |
| アセンブリ / プロジェクト | クレート(ライブラリまたはバイナリ) | Cargoとクレート |
[Serializable] のような属性 |
#[derive(Serialize)] のような属性と、deriveマクロ |
マクロ、SerdeでJSONを扱う |
| リフレクション | 実行時のものはほぼない。代わりにマクロと derive がコンパイル時にコードを生成する |
マクロ |
unsafe とポインタ |
unsafe ブロックと生ポインタ |
unsafe Rust |
Span<T>、ReadOnlySpan<char> |
スライス:&[T] と &str |
スライス |
ToString() のオーバーライド、IFormattable |
Display を実装する(すると .to_string() もタダで使える) |
よく使うトレイト |
IEquatable<T>、IComparable<T>、GetHashCode |
PartialEq/Eq、PartialOrd/Ord、Hash(たいていderiveで済む) |
よく使うトレイト |
implicit / explicit 変換演算子 |
From/Into(失敗する可能性があるなら TryFrom) |
型変換(From・Into) |
演算子オーバーロード(operator +) |
Add など、std::ops のトレイトを実装する |
高度なトレイト |
checked / unchecked の算術 |
checked_add、wrapping_add、saturating_add |
整数オーバーフロー |
decimal |
組み込みの10進数型はない。rust_decimal クレートを使うか、金額を最小単位(セントなど)の整数で持つ |
データ型 |
コレクション
Section titled “コレクション”System.Collections.Generic にあるものは、ほとんど標準ライブラリでまかなえます。名前は変わっても、考え方は同じです。
| C# | Rust | 詳しくは |
|---|---|---|
List<T> |
Vec<T> |
ベクタ |
T[](長さ固定) |
長さがコンパイル時にわかるなら [T; N]、そうでなければ Vec<T> |
配列 |
Dictionary<K, V> |
HashMap<K, V> |
HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V>(キーが並んだまま保たれる) |
HashMap |
HashSet<T> |
HashSet<T> |
HashSetとBTreeMap |
SortedSet<T> |
BTreeSet<T> |
HashSetとBTreeMap |
Queue<T> |
VecDeque<T>(push_back / pop_front) |
VecDequeとBinaryHeap |
Stack<T> |
push / pop で使う Vec<T> |
ベクタ |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T>(大きいものから順に出る。小さいものから出したいなら Reverse で包む) |
VecDequeとBinaryHeap |
LinkedList<T> |
LinkedList<T> もあるが、必要になることはまれ。たいていは VecDeque のほうが速い |
VecDequeとBinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>、または dashmap クレート |
共有状態 |
ひとつ注意があります。Dictionary と同じく、Rustの HashMap にも決まった順序はありません。中身を表示するときは、先に並べ替えるか、BTreeMap を使いましょう。
ツール:dotnetからcargoへ
Section titled “ツール:dotnetからcargoへ”ここは、ほとんどのC#開発者がすぐに気に入る部分です。Cargoは、dotnet CLI、MSBuild、NuGetの仕事をひとつのツールでこなします。
| .NET | Rust | メモ |
|---|---|---|
dotnet CLI |
cargo |
ビルド、実行、テスト、パッケージの追加、公開。Cargoとクレートを参照 |
.csproj |
Cargo.toml |
短くて、手で編集しやすい |
packages.lock.json |
Cargo.lock |
自動で作られる |
複数のプロジェクトを含む .sln |
Cargoワークスペース | 最上位の Cargo.toml ひとつにメンバーのクレートを並べる |
| NuGet / nuget.org | crates.io | 公開されたすべてのクレートのAPIドキュメントが docs.rs にある |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app(ライブラリなら cargo new --lib) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
デバッグビルドは実行がかなり遅い。ベンチマークはリリースビルドで |
dotnet watch |
cargo watch または bacon(cargo install でインストール) |
|
dotnet tool install -g |
cargo install |
|
global.json(SDKのバージョン) |
rustup と rust-toolchain.toml ファイル |
|
| Roslynアナライザー | cargo clippy |
役に立つリントが何百もある公式ツール |
dotnet format |
cargo fmt |
全員にひとつの標準スタイル |
| Visual Studio / Rider | VS Code + rust-analyzer、またはJetBrains RustRover | rust-analyzerで補完、定義へのジャンプ、その場でのエラー表示が使える |
XMLドキュメントコメント /// |
Markdownで書く /// ドキュメントコメント + cargo doc |
ドキュメント内のコード例はテストとして実行される。Rustのドキュメントを参照 |
| xUnit / NUnit / MSTest | 組み込みの #[test] と cargo test(より速いランナーとして cargo nextest が人気) |
Rustのテストを参照 |
| Moq | mockall、またはトレイトを実装した手書きのフェイク |
|
| Coverlet(コードカバレッジ) | cargo llvm-cov または cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
パフォーマンスを参照 |
| dotnet-trace、プロファイラ | cargo flamegraph、perf、または任意のネイティブプロファイラ |
.csproj と比べられるように、小さな Cargo.toml を載せておきます。
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }ライブラリとフレームワーク:「代わりに何を使えばいいの?」
Section titled “ライブラリとフレームワーク:「代わりに何を使えばいいの?」”これは、どの.NETチームも最初にする質問です。「うちはEntity Frameworkを使っているけど、Rustには何があるの?」というものですね。ここでは、いちばん近いものを紹介します。.NETにはMicrosoftがいますが、Rustには「標準の構成」を決める1つの会社がありません。そのため、良いコミュニティ製のクレート2〜3個の中から選ぶことがよくあります。
データアクセス
Section titled “データアクセス”| .NET | Rust | メモ |
|---|---|---|
| Entity Framework Core | Diesel:ORM兼クエリビルダー。クエリはコンパイル時に型チェックされる。マイグレーションは diesel_cli で |
成熟している(2.x)。標準では同期(diesel-async もある) |
| Entity Framework Core(非同期、エンティティ中心) | SeaORM:エンティティ、リレーション、マイグレーション(sea-orm-cli)を備えた非同期ORM |
EFにいちばん近い感覚。sqlxの上に作られている |
| Dapper | sqlx:普通のSQLを書くと、query! マクロがコンパイル時に実際のデータベースと照らし合わせてチェックする。マイグレーションは sqlx-cli で |
非同期で、とても人気。ORMではない |
| ADO.NET | sqlx、tokio-postgres、rusqlite(SQLite) |
より低レベルのドライバ |
| StackExchange.Redis | redis |
EFが気に入っていたなら、まずSeaORMを試しましょう。Dapperで自分でSQLを書くのが好きだったなら、sqlxを。コンパイル時の保証をいちばん強くしたいなら、Dieselを試してください。Rustのデータベースでは、3つとも動くコード付きで紹介しています。
Webとネットワーク
Section titled “Webとネットワーク”| .NET | Rust | メモ |
|---|---|---|
| ASP.NET Core Web API | axum(プロジェクト: AxumでWeb APIで実際に作ります)、actix-web、Rocket | axumとactix-webは本番環境で広く使われている |
| Minimal APIs | axum:ルートは普通の async fn ハンドラ |
スタイルがとても近い |
| ミドルウェアのパイプライン | tower のレイヤーと tower-http クレート(CORS、圧縮、トレース、タイムアウト) |
axumはtowerを直接使っている |
| 組み込みの依存性注入(DI) | axumの State、普通のコンストラクタ、トレイト |
下のDIの行も参照 |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | axumのWebSocket(ws フィーチャー)、または tokio-tungstenite |
SignalRのようなプロトコル層は組み込まれていない |
| gRPC(Grpc.AspNetCore) | tonic(protobufには prost) |
|
HttpClient |
reqwest |
標準で非同期。ブロッキング版のクライアントもオプションである |
| JWTベアラー認証 | jsonwebtoken |
日常的な部品
Section titled “日常的な部品”| .NET | Rust | メモ |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
定番。TOML、YAMLなどにも対応。SerdeでJSONを扱うを参照 |
ILogger / Serilog |
tracing(+ tracing-subscriber)、または log + env_logger |
スパン付きの構造化ログ。ログ出力を参照 |
IConfiguration / appsettings.json |
config または figment |
ファイルと環境変数を重ねて、serdeでデシリアライズする |
DIコンテナ(IServiceCollection) |
たいていは何も使わない。依存するものはコンストラクタに渡し、テストで差し替えたいものにはトレイトを使う | shaku もあるが、コンテナを使うプロジェクトは少ない |
| AutoMapper | From / Into の実装を書く |
明示的で、コンパイラがチェックしてくれる。型変換(From・Into)を参照 |
| FluentValidation / DataAnnotations | validator または garde(deriveを使ったバリデーション) |
|
| Polly(リトライ) | backon、またはtowerのリトライ用ミドルウェア |
|
| MediatR | 直接対応するものはない | 普通のトレイト、またはタスク間のチャネル |
| Hangfire / バックグラウンドジョブ | プロセス内のタスクなら tokio::spawn、永続的なジョブキューなら apalis |
|
CancellationToken |
Futureをドロップする、または tokio-util の CancellationToken を使う |
デスクトップ、ゲーム、その他
Section titled “デスクトップ、ゲーム、その他”| .NET | Rust | メモ |
|---|---|---|
| Blazor WebAssembly | Leptos、Yew、Dioxus |
どれもバージョン1.0前で、APIはまだ変わる。RustとWebAssemblyを参照 |
| WPF / WinForms / MAUI | Tauri(Web UI + Rustのバックエンド)、Slint、egui、Iced |
いちばん安定しているのはTauri 2とSlint 1.x。公式のGUIツールキットはない |
| Unity | Bevy、または godot クレートを使ってGodotの中でRustを使う |
Bevyは1.0前で、リリースのたびにAPIが変わる |
| ML.NET | candle、burn、linfa |
PythonのMLツールに比べるとずっと若い |
| Azure SDK for .NET | 公式のAzure SDK for Rust:azure_core、azure_identity、azure_storage_blob、… |
.NET SDKよりも対応しているサービスが少ない |
並べて比較:クラスは struct + impl になる
Section titled “並べて比較:クラスは struct + impl になる”実際のコードを見てみましょう。C#で書いた小さな銀行口座です。
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner}の残高は{account.Balance}です");
public class BankAccount{ public string Owner { get; } private decimal balance;
public BankAccount(string owner, decimal balance) { Owner = owner; this.balance = balance; }
public void Deposit(decimal amount) => balance += amount; public decimal Balance => balance;}Rustではこうなります。
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` はただの慣習で、キーワードではありません。 fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`:このメソッドは口座を変更します。 fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`:このメソッドは読むだけです。 fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{}の残高は{}です", account.owner, account.balance());}実行結果:
Adaの残高は150ですデータと振る舞いは、2つの別々のブロックに分かれています。コンストラクタはなく、あるのは Self を返す関数だけです。どのメソッドも、値を読むだけ(&self)なのか変更する(&mut self)のかを最初に宣言します。そして deposit は口座を変更するので、変数は let mut でなければなりません。C#ではどのメソッドでもどのフィールドでも変更できますが、Rustでは変更することがシグネチャに表れるのです。
並べて比較:インターフェースはトレイトになる
Section titled “並べて比較:インターフェースはトレイトになる”C#なら、IShape インターフェースと2つのクラス、そして List<IShape> を使うかもしれません。
var shapes = new List<IShape> { new Circle { Radius = 1 }, new Rectangle { Width = 2, Height = 3 } };foreach (var s in shapes) Console.WriteLine($"{s.Area():F2}");
public interface IShape { double Area(); }
public class Circle : IShape{ public double Radius { get; init; } public double Area() => Math.PI * Radius * Radius;}
public class Rectangle : IShape{ public double Width { get; init; } public double Height { get; init; } public double Area() => Width * Height;}Rustでは、インターフェースはトレイトになり、それぞれの構造体が自分の impl ブロックでトレイトを実装します。
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
fn main() { // Box<dyn Shape> が「何らかの IShape への参照」の役割をします。 let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 2.0, height: 3.0 }), ];
for shape in &shapes { println!("{:.2}", shape.area()); }}実行結果:
3.146.00C#では、インターフェースの呼び出しはすべて仮想呼び出しになります。Rustでは自分で選べます。Box<dyn Shape> は仮想呼び出しを使います(トレイトオブジェクトを参照)。一方、fn print_area<T: Shape>(s: &T) のようなジェネリック関数は型ごとに別々にコンパイルされるので、仮想呼び出しはまったく起きません。うれしいおまけもあります。自分で定義したトレイトを、自分が書いていない型、たとえば String や i32 にも実装できるのです。C#の拡張メソッドにあたることを、Rustはこうやって実現しています。
並べて比較:null は Option になる
Section titled “並べて比較:null は Option になる”C#のnull許容参照型はnullについて警告してくれますが、実行時には null はまだ存在します。
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("メールアドレスが登録されていません");
Console.WriteLine(FindEmail(2) ?? "不明");Rustでは、値がないかもしれないものは Option<T> 型になります。None の場合を処理するまでは、普通の値として使うことができません。
fn find_email(user_id: u32) -> Option<String> { if user_id == 1 { } else { None }}
fn main() { match find_email(1) { Some(email) => println!("{}", email.to_uppercase()), None => println!("メールアドレスが登録されていません"), }
// ひとつのケースだけ気にするなら、`if let` が短く書けます。 if let Some(email) = find_email(2) { println!("見つかりました: {email}"); } else { println!("メールアドレスが登録されていません"); }
// unwrap_or はC#の ?? 演算子のように働きます。 let email = find_email(2).unwrap_or("不明".to_string()); println!("{email}");}実行結果:
メールアドレスが登録されていません不明Rustには NullReferenceException がありません。参照をたどる先のnullが、そもそも存在しないからです。コンパイラは、Option<String> に対して .to_uppercase() を呼ぶことを許しません。便利なヘルパーメソッドはOptionのページで、短い書き方はif let / while letで学べます。
並べて比較:try/catch は Result と ? になる
Section titled “並べて比較:try/catch は Result と ? になる”C#では、パース(文字列から数値への変換)は例外を投げることがあり、その例外は呼び出し元のどこかでキャッチします。
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "thirty"));}catch (FormatException e){ Console.WriteLine($"エラー: {e.Message}");}Rustでは、失敗する可能性のある関数は、そのことを戻り値の型で示します。? 演算子は「失敗していたら、今すぐそのエラーを呼び出し元に返す」という意味です。例外を上に伝えていくのと同じ役割ですが、それがコードの上で目に見えます。
use std::num::ParseIntError;
fn total_seats(a: &str, b: &str) -> Result<i32, ParseIntError> { let first: i32 = a.parse()?; let second: i32 = b.parse()?; Ok(first + second)}
fn main() { for (a, b) in [("12", "30"), ("12", "thirty")] { match total_seats(a, b) { Ok(total) => println!("合計: {total}"), Err(e) => println!("エラー: {e}"), } }}実行結果:
合計: 42エラー: invalid digit found in stringinvalid digit found in string は標準ライブラリが返す英語のメッセージで、「文字列に数字として不正な文字が含まれています」という意味です。
大きな違いはここです。C#ではどのメソッドも例外を投げるかもしれず、シグネチャを見てもわかりません。Rustでは Result がシグネチャの一部なので、呼び出す側が処理を忘れることがありません。独自のエラー型については独自のエラー型を見てください。アプリケーションでは、anyhow クレートを使うと「どんな例外でもキャッチする」に近いことができます(anyhowとthiserrorを参照)。
並べて比較:LINQ はイテレータのチェーンになる
Section titled “並べて比較:LINQ はイテレータのチェーンになる”よくあるLINQのクエリです。
var orders = new List<Order> { new("Ada", 120), new("Grace", 80), new("Linus", 200), new("Ken", 150) };
var bigSpenders = orders .Where(o => o.Total > 100) .OrderByDescending(o => o.Total) .Select(o => o.Customer) .ToList();
Console.WriteLine(string.Join(", ", bigSpenders));Console.WriteLine($"売上: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);Rust版では、イテレータのアダプタとクロージャを使います。
struct Order { customer: String, total: u32,}
fn main() { let orders = vec![ Order { customer: "Ada".to_string(), total: 120 }, Order { customer: "Grace".to_string(), total: 80 }, Order { customer: "Linus".to_string(), total: 200 }, Order { customer: "Ken".to_string(), total: 150 }, ];
// Where(...).ToList() let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect(); // OrderByDescending(...):並べ替えはイテレータではなく Vec に対して行います。 big.sort_by(|a, b| b.total.cmp(&a.total)); // Select(...) let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// Sum(...) let revenue: u32 = orders.iter().map(|o| o.total).sum(); println!("売上: {revenue}");}実行結果:
Linus, Ken, Ada売上: 550Rustのイテレータは、LINQの遅延実行と同じく遅延評価(lazy)です。collect() や sum()、for ループのようなものを呼ぶまで、何も実行されません。collect() が ToList() にあたります。LINQと違って、イテレータのチェーンはたいてい手書きのループと同じ機械語にコンパイルされます。きれいな書き方をしても、速度の面で損をしないのです。並べ替えについては並べ替えと検索のページで説明しています。
並べて比較:using/IDisposable は Drop になる
Section titled “並べて比較:using/IDisposable は Drop になる”C#では、リソースを適切なタイミングで解放するために using を忘れずに書く必要があります。
using (var conn = new Connection("orders-db")){ Console.WriteLine($"{conn.Name}に問い合わせ中");}Console.WriteLine("ブロックのあと");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"{Name}への接続を閉じます");}Rustでは、後片付けは自動です。値の所有者がスコープを抜けると、Rustがその値の Drop 実装を呼びます。忘れるものが何もありません。
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("{}への接続を閉じます", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("{}に問い合わせ中", conn.name); } // ここで `conn` がスコープを抜けるので、drop() が実行されます。
println!("ブロックのあと");}実行結果:
orders-dbに問い合わせ中orders-dbへの接続を閉じますブロックのあとこれはRAIIと呼ばれる仕組みで、Rustはファイル、ソケット、ロック、そしてメモリそのものまで、すべてをこの方法で管理します。ファイルは File の値がドロップされたときに閉じられます。Mutex は、ガードがドロップされたときにロックが外れます。ファイナライザもGCによる停止もなく、後片付けは予測できるタイミングで行われます。
C#開発者がつまずきやすいところ
Section titled “C#開発者がつまずきやすいところ”C#から来た人は、みんな同じところでつまずきます。前もって知っておくだけで、ずいぶん楽になります。
-
代入は「ムーブ」で、参照のコピーではありません。 C#では、クラスに対して
var b = a;と書くと、ひとつのオブジェクトを指す参照が2つになります。Rustでは、let b = a;はStringやVecの所有権をbにムーブし、aはもう使えなくなります。fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner}と{copy}");}error[E0382]: borrow of moved value: `owner`--> src/main.rs:4:16|2 | let owner = String::from("Ada");| ----- move occurs because `owner` has type `String`, which does not implement the `Copy` trait3 | let copy = owner;| ----- value moved here4 | println!("{owner}と{copy}");| ^^^^^ value borrowed here after moveこのエラーは「ムーブ済みの値
ownerを借用しようとしている」という意味です。直すには、借用する(let copy = &owner;)か、意図してクローンします(owner.clone())。まず所有権を読んでください。このルールを詳しく説明しています。 -
借用チェッカー。 読み取り用の参照(
&T)はいくつでも持てますが、書き込み用の参照(&mut T)を持てるのはひとつだけです。両方を同時に持つことはできません。親を指し返すオブジェクトや、ひとつの共有オブジェクトを多くのオブジェクトが変更する、といったC#ではよくあるパターンには、スマートポインタか別の設計が必要になります。最初の数週間はイライラするかもしれませんが、そのあとはずっと楽になります。 -
Stringと&str。 C#の文字列型はstringひとつですが、Rustには2つあります。Stringはテキストを所有し、&strはテキストを借用します。関数はたいてい&strを受け取り、構造体はたいていStringを持ちます。Rustの文字列を見てください。 -
継承がありません。 共通のフィールドを持つ基底クラスを作って、メソッドをオーバーライドすることはできません。共通の振る舞いには(デフォルトメソッド付きの)トレイトを使い、共通のデータは、ほかの構造体に含める構造体にまとめます。
-
例外がありません。 エラーは値です。
?をたくさん書くことになりますし、mainもResultを返せます。学習中は.unwrap()を使ってもかまいませんが、これは「例外を無視してクラッシュさせる」のRust版だと覚えておきましょう。 -
null がありません。 最初は、あちこちに
Optionがあるのが手間に感じます。でもしばらくすると、null参照のバグに出会わなくなったことに気づくはずです。 -
クローンは明示的に。 Rustは、ヒープ上の値を勝手にコピーしません。
.clone()が書いてあれば、そこで本当にコピーが作られます。コストが目に見えるのは、むしろ長所です。 -
コンパイル時間。 多くのクレートを使うプロジェクトのクリーンビルドは、.NETのビルドより時間がかかります。インクリメンタルビルドと
cargo check(コード生成を省略する)はずっと速いので、編集中はcargo checkを使いましょう。 -
asyncにはランタイムが必要です。 .NETでは、ランタイムとスレッドプールがいつも用意されています。Rustでは、
async fnはエグゼキュータが実行するまで何もしません。そのため、tokioを追加してmainに#[tokio::main]を付けます。async/awaitを見てください。
C#開発者のための学習ルート
Section titled “C#開発者のための学習ルート”このサイトを順番どおりに読む必要はありません。C#開発者なら、次のルートで進むと、すでに知っていることは飛ばして、Rustならではの部分に時間をかけられます。
- Rustを始める:Rustをインストールして
cargo newを実行します。 - Rustの変数とRustのデータ型:ざっと目を通しましょう。主に
let mutと数値の型を覚えるためです。 - 所有権、次に借用、そしてスライス:あなたにとっていちばん大事なページです。じっくり時間をかけてください。
- Rustの文字列:
Stringと&strの違い。 - 構造体と列挙型、次にmatchとパターンマッチング。
- OptionとResult、次に独自のエラー型。
- トレイト、ジェネリクス、よく使うトレイト、トレイトオブジェクト。
- クロージャとイテレータ:これがあなたのLINQです。
- ベクタ、HashMap、VecDequeとBinaryHeap。
- モジュール、Cargoとクレート、テスト。
- ライフタイムとスマートポインタ。
- 並行処理、共有状態、async/await。
- SerdeでJSONを扱うとログ出力。そのあとWeb APIのプロジェクトを作ってみましょう。
どちらの問題も、C#のコード片をRustに移植(ポーティング)します。
1. recordとLINQのクエリを移植する
Section titled “1. recordとLINQのクエリを移植する”このC#のコードを移植してください。在庫があって、価格が20未満の商品の名前を表示します。ヒント: #[derive(Debug, Clone, PartialEq)] を付けた struct を作り、filter、map を使って Vec<&str> に collect します。Vec<&str> には .join(", ") メソッドがあります。
var products = new List<Product>{ new("Notebook", 5, true), new("Headphones", 45, true), new("Pen", 2, false), new("Mug", 12, true),};
var cheap = products.Where(p => p.InStock && p.Price < 20).Select(p => p.Name);Console.WriteLine(string.Join(", ", cheap));
record Product(string Name, int Price, bool InStock);解答を見る
#[derive(Debug, Clone, PartialEq)]struct Product { name: String, price: u32, in_stock: bool,}
fn product(name: &str, price: u32, in_stock: bool) -> Product { Product { name: name.to_string(), price, in_stock }}
fn main() { let products = vec![ product("Notebook", 5, true), product("Headphones", 45, true), product("Pen", 2, false), product("Mug", 12, true), ];
let cheap: Vec<&str> = products .iter() .filter(|p| p.in_stock && p.price < 20) .map(|p| p.name.as_str()) .collect();
println!("{}", cheap.join(", "));}実行結果:
Notebook, Mug小さなヘルパー関数が、recordの位置指定コンストラクタの役割をしています。deriveを付けると、C#のrecordが自動で用意してくれるものが手に入ります。{:?} での表示、.clone() でのコピー、== での比較です。
2. 例外を使うDictionaryを移植する
Section titled “2. 例外を使うDictionaryを移植する”このC#のコードは、在庫の辞書から商品を1つ取り出し、問題があれば例外を投げます。take が例外を投げる代わりに Result<u32, String> を返すように移植してください。ヒント: stock.get_mut(item) は Option<&mut u32> を返します。.ok_or(...) で Result に変えてから、? を使いましょう。
var stock = new Dictionary<string, int> { ["りんご"] = 3, ["なし"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"{item}という商品はありません"); if (count == 0) throw new InvalidOperationException($"{item}は在庫切れです"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "りんご", "なし", "キウイ" }){ try { Console.WriteLine($"{item}を1つ取りました。残りは{Take(item)}個です"); } catch (Exception e) { Console.WriteLine($"エラー: {e.Message}"); }}解答を見る
use std::collections::HashMap;
fn take(stock: &mut HashMap<String, u32>, item: &str) -> Result<u32, String> { let count = stock .get_mut(item) .ok_or(format!("{item}という商品はありません"))?;
if *count == 0 { return Err(format!("{item}は在庫切れです")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("りんご".to_string(), 3); stock.insert("なし".to_string(), 0);
for item in ["りんご", "なし", "キウイ"] { match take(&mut stock, item) { Ok(left) => println!("{item}を1つ取りました。残りは{left}個です"), Err(e) => println!("エラー: {e}"), } }}実行結果:
りんごを1つ取りました。残りは2個ですエラー: なしは在庫切れですエラー: キウイという商品はありませんout パラメータを使う TryGetValue は、Option を返す get_mut になります。2つの throw は Err の値になり、try/catch は match になります。呼び出し側の &mut stock を見れば、take が辞書を変更することがはっきりわかります。
- C#の多くはそのまま活かせます。ジェネリクス、async/await、ラムダ、パターンマッチング、LINQ風のイテレータには、どれもRustによく似たものがあります。
- 大きな違いは、ガベージコレクタの代わりに所有権、nullの代わりに
Option、例外の代わりにResult+?を使うことです。 - クラスは
struct+implに、インターフェースはトレイトに、継承はコンポジションとトレイトになります。using/IDisposableは自動で動くDropになります。 - Cargoが
dotnetCLI、MSBuild、NuGetの代わりになり、clippyとrustfmtがアナライザーとdotnet formatの代わりになります。テストは最初から組み込まれています。 - EF CoreならSeaORMかDiesel、Dapperならsqlx、ASP.NET Coreならaxumかactix-webを見てみましょう。GUI、ゲーム、MLの選択肢はまだ若い分野です。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. Rustでは null の代わりに何を使いますか?
2. あなたのチームはDapperを使っていて、SQLを手で書くのが好きです。いちばん近いRustのクレートはどれですか?
3. C#の IDisposable を使った using ブロックに、いちばん近いRustの仕組みはどれですか?
4. C#では、クラスに対する var b = a; は参照をコピーします。Rustで String に対して let b = a; と書くと、どうなりますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- Rust for C#/.NET Developers — Microsoft(英語):Microsoft自身によるガイドです。C#とRustを並べた例がさらにたくさん載っています。
- The Rust Programming Language 日本語版:公式の本の日本語訳です。C#開発者にとって特に大事なのは、4章(所有権)、9章(エラー処理)、10章(ジェネリクスとトレイト)です。
- Getting Started with Diesel(英語)とSeaORM documentation(英語):Entity Frameworkの代わりを探しているなら、この2つが主なORMです。
- docs.rsのsqlx(英語):Dapper風の選択肢で、SQLをコンパイル時にチェックできます。
- Are We Web Yet?(英語)とAre We GUI Yet?(英語):RustのWebとデスクトップGUIのライブラリをまとめたコミュニティのサイトです。