Rust для разработчиков на C#
Если ты зарабатываешь на жизнь C#, хорошая новость: многое в Rust покажется знакомым. В Rust есть обобщения с ограничениями, async/await, мощное сопоставление с образцом, цепочки итераторов, которые читаются почти как LINQ, и один отличный консольный инструмент (Cargo), который делает то же, что dotnet. Ты уже умеешь мыслить типами, интерфейсами и лямбдами. Эти знания пригодятся.
Главный сдвиг касается памяти и ошибок. В Rust нет сборщика мусора. Вместо этого у каждого значения один владелец, а компилятор проверяет владение и заимствование до запуска программы. Ещё в Rust нет null (вместо него Option) и нет исключений (вместо них Result). Именно на эти три изменения уйдёт большая часть времени на обучение. Эта страница сопоставляет то, что ты знаешь по C# и .NET, с Rust, чтобы ты мог потратить время на то, что действительно ново.
Если тебе нужно общее сравнение с C++, Go и Python, загляни в урок Rust против C++, Go и Python.
Понятия языка: из C# в Rust
Заголовок раздела «Понятия языка: из 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>, nullable-ссылочные типы |
Option<T>: либо Some(value), либо None |
Option |
?? и ?. |
.unwrap_or(...), .map(...), .and_then(...) и ? |
Option |
Исключения, throw, try/catch |
Result<T, E> + оператор ? |
Result, Свои типы ошибок |
Исключение для настоящей ошибки в коде (например, InvalidOperationException) |
panic! (останавливает поток) |
Паника |
IDisposable + using |
Drop, который срабатывает автоматически, когда владелец выходит из области видимости (RAII) |
Область видимости, Умные указатели |
| Сборщик мусора | Владение и заимствование, проверяемые при компиляции | Владение, Заимствование |
record |
struct с #[derive(Debug, Clone, PartialEq)] |
Структуры, Популярные трейты |
Выражение with для record |
Синтаксис обновления структуры: 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-макросы |
Макросы, JSON и Serde |
| Рефлексия | Во время работы почти нет. Вместо неё макросы и derive генерируют код при компиляции |
Макросы |
unsafe и указатели |
Блоки unsafe и сырые указатели |
Unsafe |
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, если преобразование может не удаться) |
Преобразования |
Перегрузка операторов (operator +) |
Реализуй трейты из std::ops, например Add |
Продвинутые трейты |
Арифметика checked / unchecked |
checked_add, wrapping_add, saturating_add |
Переполнение целых чисел |
decimal |
Встроенного десятичного типа нет; используй крейт rust_decimal или храни копейки в целом числе |
Типы данных |
Коллекции
Заголовок раздела «Коллекции»Стандартная библиотека покрывает почти всё из 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 |
SortedSet<T> |
BTreeSet<T> |
HashSet |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque и BinaryHeap |
Stack<T> |
Vec<T> с push / pop |
Векторы |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (сначала наибольший; оберни в Reverse, чтобы сначала был наименьший) |
VecDeque и BinaryHeap |
LinkedList<T> |
LinkedList<T> есть, но нужен редко; VecDeque обычно быстрее |
VecDeque и BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>> или крейт dashmap |
Общее состояние |
Одна вещь, за которой стоит следить: как и у Dictionary, у HashMap в Rust нет фиксированного порядка. Если выводишь содержимое, сначала отсортируй его или используй BTreeMap.
Инструменты: из dotnet в cargo
Заголовок раздела «Инструменты: из dotnet в cargo»Эта часть нравится большинству разработчиков на C# сразу. Cargo в одном инструменте делает работу CLI dotnet, MSBuild и NuGet.
| .NET | Rust | Примечания |
|---|---|---|
CLI dotnet |
cargo |
Собирает, запускает, тестирует, добавляет пакеты, публикует. См. Cargo и крейты |
.csproj |
Cargo.toml |
Короткий, легко править руками |
packages.lock.json |
Cargo.lock |
Создаётся автоматически |
.sln с несколькими проектами |
Рабочее пространство Cargo (workspace) | Один 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 |
Отладочные сборки работают намного медленнее; замеряй в 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-комментарии /// |
Doc-комментарии /// в Markdown + cargo doc |
Примеры кода в документации запускаются как тесты. См. Документация |
| xUnit / NUnit / MSTest | Встроенные #[test] и cargo test (cargo nextest — популярный более быстрый раннер) |
См. Тестирование |
| Moq | mockall или написанные вручную фейки, реализующие твой трейт |
|
| Coverlet (покрытие кода) | cargo llvm-cov или cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
См. Производительность |
| dotnet-trace, профилировщики | cargo flamegraph, perf или любой нативный профилировщик |
Вот небольшой Cargo.toml, чтобы сравнить его с .csproj:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Библиотеки и фреймворки: «Что использовать вместо…?»
Заголовок раздела «Библиотеки и фреймворки: «Что использовать вместо…?»»Это первый вопрос, который задаёт любая команда на .NET: «Мы используем Entity Framework. А что есть в Rust?» Вот ближайшие аналоги. За Rust не стоит одна компания с «стеком по умолчанию», как Microsoft за .NET, поэтому часто приходится выбирать между двумя-тремя хорошими крейтами от сообщества.
Доступ к данным
Заголовок раздела «Доступ к данным»| .NET | Rust | Примечания |
|---|---|---|
| Entity Framework Core | Diesel: ORM и построитель запросов; запросы проверяются на типы при компиляции. Миграции через diesel_cli |
Зрелый (2.x), по умолчанию синхронный (есть diesel-async) |
| Entity Framework Core (асинхронный, в стиле сущностей) | SeaORM: асинхронный ORM с сущностями, связями и миграциями (sea-orm-cli) |
Ближе всего к 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. В уроке Базы данных все три показаны на работающем коде.
Веб и сеть
Заголовок раздела «Веб и сеть»| .NET | Rust | Примечания |
|---|---|---|
| ASP.NET Core Web API | axum (мы делаем такой в проекте веб-API), actix-web, Rocket | axum и actix-web широко используются в продакшене |
| Minimal APIs | axum: маршруты — обычные обработчики async fn |
Стиль очень похож |
| Конвейер middleware | Слои tower и крейт tower-http (CORS, сжатие, трассировка, таймауты) |
axum использует tower напрямую |
| Встроенное внедрение зависимостей | State в axum, обычные конструкторы и трейты |
См. строку про DI ниже |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | WebSocket в axum (фича ws) или tokio-tungstenite |
Встроенного протокола в стиле SignalR нет |
| gRPC (Grpc.AspNetCore) | tonic (с prost для protobuf) |
|
HttpClient |
reqwest |
По умолчанию асинхронный, есть необязательный блокирующий клиент |
| JWT bearer-аутентификация | jsonwebtoken |
Повседневные строительные блоки
Заголовок раздела «Повседневные строительные блоки»| .NET | Rust | Примечания |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
Стандарт; также умеет TOML, YAML и другое. См. JSON и Serde |
ILogger / Serilog |
tracing (+ tracing-subscriber) или log + env_logger |
Структурированное логирование со спанами. См. Логирование |
IConfiguration / appsettings.json |
config или figment |
Слои из файлов и переменных окружения, затем десериализация через serde |
DI-контейнер (IServiceCollection) |
Обычно ничего: передавай зависимости в конструкторы, используй трейты для того, что хочешь подменять в тестах | shaku существует, но контейнеры используют немногие проекты |
| AutoMapper | Пиши реализации From / Into |
Явно и проверяется компилятором. См. Преобразования |
| FluentValidation / DataAnnotations | validator или garde (валидация через derive) |
|
| Polly (повторы) | backon или middleware повторов из tower |
|
| MediatR | Прямого аналога нет | Обычные трейты или каналы между задачами |
| Hangfire / фоновые задачи | tokio::spawn для задач внутри процесса; apalis для постоянных очередей задач |
|
CancellationToken |
Удали future или используй CancellationToken из tokio-util |
Десктоп, игры и прочее
Заголовок раздела «Десктоп, игры и прочее»| .NET | Rust | Примечания |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
Все до версии 1.0; API ещё меняются. См. Rust и WebAssembly |
| WPF / WinForms / MAUI | Tauri (веб-интерфейс + бэкенд на Rust), Slint, egui, Iced |
Самые стабильные — Tauri 2 и Slint 1.x. Официального GUI-тулкита нет |
| Unity | Bevy или Rust внутри Godot через крейт godot |
Bevy ещё до 1.0 и ломает API между релизами |
| ML.NET | candle, burn, linfa |
Намного моложе ML-инструментов Python |
| Azure SDK для .NET | Официальный Azure SDK для Rust: azure_core, azure_identity, azure_storage_blob, … |
Охватывает меньше сервисов, чем SDK для .NET |
Бок о бок: класс становится struct + impl
Заголовок раздела «Бок о бок: класс становится 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Данные и поведение живут в двух отдельных блоках. Конструкторов нет — есть только функции, которые возвращают Self. Каждый метод сразу говорит, читает ли он значение (&self) или изменяет его (&mut self). А поскольку deposit изменяет счёт, переменная должна быть объявлена как let mut. В C# любой метод может поменять любое поле; в Rust изменения видны в сигнатуре.
Бок о бок: интерфейс становится трейтом
Заголовок раздела «Бок о бок: интерфейс становится трейтом»В C# у тебя мог бы быть интерфейс IShape с двумя классами и 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.00В C# каждый вызов через интерфейс — виртуальный. В Rust выбираешь ты: Box<dyn Shape> использует виртуальный вызов (см. Трейт-объекты), а обобщённая функция вроде fn print_area<T: Shape>(s: &T) компилируется отдельно для каждого типа, вообще без виртуального вызова. Приятный бонус: свой трейт можно реализовать для типов, которые писал не ты, даже для String или i32. Так Rust делает то, что в C# делают методы расширения.
Бок о бок: null становится Option
Заголовок раздела «Бок о бок: null становится Option»C# с nullable-ссылочными типами предупреждает о null, но во время работы null всё равно существует:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("email не указан");
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!("email не указан"), }
// `if let` — короткая форма, когда важен только один случай. if let Some(email) = find_email(2) { println!("найден {email}"); } else { println!("email не указан"); }
// unwrap_or работает как оператор ?? в C#. let email = find_email(2).unwrap_or("неизвестно".to_string()); println!("{email}");}Вывод:
email не указаннеизвестноВ Rust нет NullReferenceException, потому что нет null, который можно разыменовать. Компилятор просто не даст вызвать .to_uppercase() у Option<String>. Вспомогательные методы описаны в уроке Option, а короткие формы — в уроке if let и while let.
Бок о бок: try/catch становится Result и ?
Заголовок раздела «Бок о бок: 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 stringТекст invalid digit found in string («в строке недопустимая цифра») печатает сама стандартная библиотека Rust, поэтому он на английском.
Главное отличие: в C# любой метод может бросить исключение, и сигнатура об этом не говорит. В Rust Result — часть сигнатуры, поэтому вызывающий код не сможет забыть его обработать. Для своих типов ошибок смотри урок Свои типы ошибок. В приложениях крейт anyhow даёт что-то близкое к «поймать любое исключение» (см. anyhow и thiserror).
Бок о бок: LINQ становится цепочкой итераторов
Заголовок раздела «Бок о бок: 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Выручка: 550Итераторы в Rust ленивые, как отложенное выполнение в LINQ: ничего не происходит, пока ты не вызовешь что-то вроде collect(), sum() или не запустишь цикл for. collect() — это твой ToList(). В отличие от LINQ, цепочки итераторов обычно компилируются в тот же машинный код, что и написанный вручную цикл, так что за красивый стиль не приходится платить. Сортировка разобрана в уроке Сортировка и поиск.
Бок о бок: using/IDisposable становится Drop
Заголовок раздела «Бок о бок: 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 разблокируется, когда удаляется его guard. Нет финализаторов и пауз сборщика мусора; очистка происходит в предсказуемый момент.
Обо что споткнётся разработчик на C#
Заголовок раздела «Обо что споткнётся разработчик на C#»Все, кто приходит из C#, натыкаются на одни и те же кочки. Знать их заранее полезно.
-
Присваивание перемещает, а не копирует ссылку. В C#
var b = a;для класса даёт две ссылки на один объект. В Rustlet b = a;перемещает владениеStringилиVecвb, иaбольше нельзя использовать:fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} and {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} and {copy}");| ^^^^^ value borrowed here after moveКомпилятор говорит, что значение
ownerуже перемещено. Исправить можно заимствованием (let copy = &owner;) или осознанным клонированием (owner.clone()). Сначала прочитай урок Владение — там объясняется это правило. -
Анализатор заимствований. Одновременно может быть много читателей (
&T) или один писатель (&mut T), но никогда и то и другое. Обычные для C# схемы — например, объекты, которые ссылаются обратно на родителя, или много объектов, меняющих один общий объект, — требуют умных указателей или другой архитектуры. Приготовься к нескольким неделям раздражения. Потом станет намного легче. -
Stringи&str. В C# один типstring. В Rust два:Stringвладеет своим текстом,&strзаимствует его. Функции обычно принимают&str, а структуры обычно хранятString. См. Строки. -
Нет наследования. Нельзя написать базовый класс с общими полями и переопределяемыми методами. Для общего поведения используй трейты (с методами по умолчанию), а общие данные клади в структуру, которую содержат другие структуры.
-
Нет исключений. Ошибки — это значения. Ты будешь часто писать
?, и твойmainтоже может возвращатьResult. Вызывать.unwrap()во время обучения нормально, но это версия Rust для «проигнорировать исключение и упасть». -
Нет null. Поначалу
Optionповсюду кажется лишней работой. Через какое-то время заметишь, что ошибки с null-ссылками пропали. -
Явные клоны. Rust никогда не копирует значение в куче у тебя за спиной. Если видишь
.clone(), значит, делается настоящая копия. Это фича: стоимость видна. -
Время компиляции. Чистая сборка проекта с множеством крейтов занимает больше времени, чем сборка .NET. Инкрементальные сборки и
cargo check(который пропускает генерацию кода) намного быстрее; используйcargo check, пока редактируешь. -
Для async нужна среда выполнения. В .NET среда выполнения и пул потоков есть всегда. В Rust
async fnничего не делает, пока её не запустит исполнитель, поэтому ты добавляешь tokio и пишешь#[tokio::main]надmain. См. Async/Await.
Путь обучения для разработчиков на C#
Заголовок раздела «Путь обучения для разработчиков на C#»Этот сайт не обязательно читать по порядку. Для разработчика на C# этот маршрут пропускает то, что ты уже знаешь, и тратит время там, где Rust отличается:
- Первые шаги: установи Rust и запусти
cargo new. - Переменные и типы данных: пробеги глазами, в основном чтобы узнать
let mutи числовые типы. - Владение, затем заимствование, затем срезы: самые важные для тебя страницы. Не торопись.
- Строки:
Stringи&str. - Структуры и перечисления, затем match и сопоставление с образцом.
- Option и Result, затем свои типы ошибок.
- Трейты, обобщения, популярные трейты и трейт-объекты.
- Замыкания и итераторы: твой LINQ.
- Векторы, HashMap и VecDeque и BinaryHeap.
- Модули, Cargo и крейты и тестирование.
- Времена жизни и умные указатели.
- Многопоточность, общее состояние и async/await.
- JSON и Serde и логирование, а потом собери проект веб-API.
Практика
Заголовок раздела «Практика»В обоих упражнениях дан фрагмент на C#. Перенеси его на Rust.
1. Перенеси record и запрос LINQ
Заголовок раздела «1. Перенеси record и запрос LINQ»Перенеси этот код на C#. Он должен вывести названия товаров, которые есть в наличии и стоят меньше 20. Подсказка: сделай struct с #[derive(Debug, Clone, PartialEq)], затем используй filter, map и collect в Vec<&str>. У Vec<&str> есть метод .join(", ").
var products = new List<Product>{ new("Блокнот", 5, true), new("Наушники", 45, true), new("Ручка", 2, false), new("Кружка", 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("Блокнот", 5, true), product("Наушники", 45, true), product("Ручка", 2, false), product("Кружка", 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(", "));}Вывод:
Блокнот, КружкаНебольшая вспомогательная функция играет роль позиционного конструктора record. Атрибуты derive дают то, что record в C# даёт бесплатно: вывод через {:?}, копирование через .clone() и сравнение через ==.
2. Перенеси Dictionary с исключениями
Заголовок раздела «2. Перенеси Dictionary с исключениями»Этот код на C# берёт один товар из словаря остатков и бросает исключение, если что-то не так. Перенеси его так, чтобы take возвращала Result<u32, String>, а не бросала исключение. Подсказка: stock.get_mut(item) возвращает Option<&mut u32>. Преврати его в Result с помощью .ok_or(...), а затем используй ?.
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}, осталось {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}, осталось {left}"), Err(e) => println!("ошибка: {e}"), } }}Вывод:
взяли: яблоки, осталось 2ошибка: груши закончилисьошибка: нет товара «киви»TryGetValue с параметром out превращается в get_mut, который возвращает Option. Два throw становятся значениями Err, а try/catch — выражением match. &mut stock в вызове ясно показывает, что take изменяет словарь.
- Многое из C# переносится: обобщения, async/await, лямбды, сопоставление с образцом и итераторы в стиле LINQ — у всего этого есть близкие аналоги в Rust.
- Главные сдвиги: владение вместо сборщика мусора,
Optionвместо null иResult+?вместо исключений. - Классы становятся
struct+impl, интерфейсы — трейтами, а наследование — композицией плюс трейты.using/IDisposableпревращается в автоматическийDrop. - Cargo заменяет CLI
dotnet, MSBuild и NuGet; clippy и rustfmt заменяют анализаторы иdotnet format; тестирование встроено. - Вместо EF Core смотри на SeaORM или Diesel, вместо Dapper — на sqlx, вместо ASP.NET Core — на axum или actix-web. Варианты для GUI, игр и ML моложе.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что Rust использует вместо null?
2. Твоя команда использует Dapper и любит писать SQL вручную. Какой крейт Rust ближе всего?
3. Что в Rust ближе всего к блоку using с IDisposable из C#?
4. В C# var b = a; для класса копирует ссылку. Что делает let b = a; со String в Rust?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Rust for C#/.NET Developers — Microsoft (на английском): собственное руководство Microsoft, с гораздо большим количеством примеров на C# и Rust бок о бок.
- Язык программирования Rust: официальная книга (перевод на русский). Для разработчиков на C# важнее всего главы 4 (владение), 9 (ошибки) и 10 (обобщения и трейты).
- Getting Started with Diesel и документация SeaORM (на английском): два главных ORM, если ты ищешь замену Entity Framework.
- sqlx на docs.rs (на английском): выбор в стиле Dapper, с SQL, проверяемым при компиляции.
- Are We Web Yet? и Are We GUI Yet? (на английском): обзоры веб- и GUI-библиотек Rust от сообщества.