Rust.md
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
```md
|
||||
---
|
||||
title: "Rust Tutorial (für Python-Erfahrene)"
|
||||
tags: [rust, tutorial, programming]
|
||||
created: 2026-03-27
|
||||
---
|
||||
|
||||
# Rust Tutorial (für Python-Erfahrene)
|
||||
|
||||
Dieses Tutorial ist als **hands-on Lernpfad** gedacht: viele kleine Programme, die du direkt ausführen und variieren kannst. Du bekommst dabei die wichtigsten Rust-Konzepte (Ownership/Borrowing, Lifetimes-Grundlagen, Traits, Enums/Pattern Matching, Fehlerbehandlung, Collections, Generics, Module/Crates, Testing, Async-Basics).
|
||||
|
||||
---
|
||||
|
||||
## 0) Setup: Was brauche ich, wie führe ich Rust aus?
|
||||
|
||||
### Installation (Compiler + Tooling)
|
||||
Rust wird normalerweise über **rustup** installiert (inkl. `rustc`, `cargo`, Standardbibliothek).
|
||||
|
||||
- **Windows/macOS/Linux:**
|
||||
- https://rustup.rs öffnen und Anweisungen folgen
|
||||
- Danach prüfen:
|
||||
```bash
|
||||
rustc --version
|
||||
cargo --version
|
||||
rustup --version
|
||||
```
|
||||
|
||||
### Editor (empfohlen)
|
||||
- **VS Code** + Extension: **rust-analyzer**
|
||||
- Alternativ: IntelliJ Rust, Neovim + rust-analyzer, etc.
|
||||
|
||||
### Neues Projekt erstellen und ausführen
|
||||
Rust nutzt **Cargo** als Build-Tool + Paketmanager.
|
||||
|
||||
```bash
|
||||
cargo new hello_rust
|
||||
cd hello_rust
|
||||
cargo run
|
||||
```
|
||||
|
||||
Wichtige Cargo-Kommandos:
|
||||
```bash
|
||||
cargo build # kompiliert (debug)
|
||||
cargo build --release
|
||||
cargo run # build + run
|
||||
cargo test # tests
|
||||
cargo fmt # formatieren (rustfmt)
|
||||
cargo clippy # lints (clippy)
|
||||
cargo doc --open # docs generieren + öffnen
|
||||
```
|
||||
|
||||
Projektstruktur:
|
||||
- `src/main.rs` (Binary)
|
||||
- `Cargo.toml` (Dependencies, Name, Version, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 1) Hello World + Grundsyntax
|
||||
|
||||
`src/main.rs`:
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, Rust!");
|
||||
}
|
||||
```
|
||||
|
||||
**Wichtig:** Makro-Aufruf erkennst du am `!` (`println!`, `vec!`, `format!`, …).
|
||||
|
||||
---
|
||||
|
||||
## 2) Variablen, Mutabilität, Typen
|
||||
|
||||
Rust ist **statisch typisiert**, aber oft mit **Typinferenz**.
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let x = 5; // immutable
|
||||
// x = 6; // Fehler
|
||||
|
||||
let mut y = 5; // mutable
|
||||
y = 6;
|
||||
|
||||
let z: i32 = 42; // expliziter Typ
|
||||
let pi: f64 = 3.1415;
|
||||
|
||||
println!("{x} {y} {z} {pi}");
|
||||
}
|
||||
```
|
||||
|
||||
### Shadowing (anders als mut)
|
||||
```rust
|
||||
fn main() {
|
||||
let s = "42";
|
||||
let s = s.parse::<i32>().unwrap(); // shadowing: neuer s mit anderem Typ
|
||||
println!("{s}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3) Strings: `&str` vs `String` (Python-Vergleich)
|
||||
|
||||
- `&str`: **String Slice**, meist “geliehener” Text (z.B. Stringliteral)
|
||||
- `String`: **besitzender**, heap-allocierter String (änderbar)
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let a: &str = "hi"; // slice
|
||||
let mut b: String = String::from("hi"); // owned
|
||||
b.push_str(" there");
|
||||
|
||||
println!("{a} / {b}");
|
||||
}
|
||||
```
|
||||
|
||||
Sehr häufig konvertieren:
|
||||
```rust
|
||||
let s = "abc".to_string();
|
||||
let t = String::from("abc");
|
||||
let u: &str = &t; // String -> &str
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) Funktionen
|
||||
|
||||
```rust
|
||||
fn add(a: i32, b: i32) -> i32 {
|
||||
a + b // kein Semikolon => Ausdruck
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let r = add(2, 3);
|
||||
println!("{r}");
|
||||
}
|
||||
```
|
||||
|
||||
Mehr Rückgabewerte via Tupel:
|
||||
```rust
|
||||
fn div_mod(a: i32, b: i32) -> (i32, i32) {
|
||||
(a / b, a % b)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) Kontrollfluss: `if`, `loop`, `while`, `for`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let n = 7;
|
||||
|
||||
if n % 2 == 0 {
|
||||
println!("even");
|
||||
} else {
|
||||
println!("odd");
|
||||
}
|
||||
|
||||
for i in 0..3 {
|
||||
println!("i={i}");
|
||||
}
|
||||
|
||||
let items = vec!["a", "b", "c"];
|
||||
for (idx, item) in items.iter().enumerate() {
|
||||
println!("{idx}: {item}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`loop` kann Werte zurückgeben:
|
||||
```rust
|
||||
fn main() {
|
||||
let mut i = 0;
|
||||
let result = loop {
|
||||
i += 1;
|
||||
if i == 3 {
|
||||
break i * 10; // liefert 30
|
||||
}
|
||||
};
|
||||
println!("{result}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) Structs, Methoden, `impl`
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(name: impl Into<String>, age: u32) -> Self {
|
||||
Self { name: name.into(), age }
|
||||
}
|
||||
|
||||
fn birthday(&mut self) {
|
||||
self.age += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut u = User::new("Alice", 30);
|
||||
u.birthday();
|
||||
println!("{u:?}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7) Enums + Pattern Matching (sehr wichtig)
|
||||
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
enum Message {
|
||||
Quit,
|
||||
Write(String),
|
||||
Move { x: i32, y: i32 },
|
||||
}
|
||||
|
||||
fn handle(m: Message) {
|
||||
match m {
|
||||
Message::Quit => println!("bye"),
|
||||
Message::Write(text) => println!("text={text}"),
|
||||
Message::Move { x, y } => println!("move to {x},{y}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
handle(Message::Write("hello".into()));
|
||||
handle(Message::Move { x: 1, y: 2 });
|
||||
}
|
||||
```
|
||||
|
||||
`match` ist exhaustiv: alle Fälle müssen behandelt werden (oder `_`).
|
||||
|
||||
---
|
||||
|
||||
## 8) Ownership, Borrowing, References (Kern von Rust)
|
||||
|
||||
Rust verhindert Data Races und Use-After-Free durch Regeln:
|
||||
|
||||
### Ownership-Regeln (vereinfacht)
|
||||
1. Jeder Wert hat **genau einen Owner**.
|
||||
2. Wenn der Owner aus dem Scope läuft, wird der Wert **gedroppt**.
|
||||
3. **Move**: Zuweisung/Übergabe kann Ownership übertragen.
|
||||
|
||||
Beispiel: Move vs Copy
|
||||
```rust
|
||||
fn main() {
|
||||
let a = String::from("hello");
|
||||
let b = a;
|
||||
// println!("{a}"); // Fehler: a wurde gemoved
|
||||
|
||||
let x = 5; // i32 ist Copy
|
||||
let y = x;
|
||||
println!("{x} {y}");
|
||||
}
|
||||
```
|
||||
|
||||
### Borrowing: `&T` (immutable) und `&mut T` (mutable)
|
||||
```rust
|
||||
fn len(s: &String) -> usize {
|
||||
s.len()
|
||||
}
|
||||
|
||||
fn add_exclamation(s: &mut String) {
|
||||
s.push('!');
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut s = String::from("hi");
|
||||
println!("{}", len(&s));
|
||||
|
||||
add_exclamation(&mut s);
|
||||
println!("{s}");
|
||||
}
|
||||
```
|
||||
|
||||
Borrowing-Regeln:
|
||||
- beliebig viele `&T` **oder**
|
||||
- genau ein `&mut T`
|
||||
- aber nicht beides gleichzeitig (im gleichen Gültigkeitsbereich)
|
||||
|
||||
Typischer Fehler (und Fix durch Scopes):
|
||||
```rust
|
||||
fn main() {
|
||||
let mut s = String::from("abc");
|
||||
|
||||
let r1 = &s;
|
||||
let r2 = &s;
|
||||
println!("{r1} {r2}");
|
||||
|
||||
let r3 = &mut s; // ok, weil r1/r2 danach nicht mehr benutzt werden
|
||||
r3.push('d');
|
||||
println!("{r3}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9) Slices: Teilansichten von Daten
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let a = [10, 20, 30, 40];
|
||||
let mid: &[i32] = &a[1..3]; // 20,30
|
||||
println!("{mid:?}");
|
||||
|
||||
let s = String::from("hello world");
|
||||
let w: &str = &s[0..5];
|
||||
println!("{w}");
|
||||
}
|
||||
```
|
||||
|
||||
Achtung bei UTF-8: String-Slicing nur an gültigen Byte-Grenzen von Codepoints.
|
||||
|
||||
---
|
||||
|
||||
## 10) Collections: `Vec`, `HashMap`, `HashSet`
|
||||
|
||||
### `Vec<T>`
|
||||
```rust
|
||||
fn main() {
|
||||
let mut v = vec![1, 2, 3];
|
||||
v.push(4);
|
||||
|
||||
for x in &v {
|
||||
println!("{x}");
|
||||
}
|
||||
|
||||
if let Some(last) = v.pop() {
|
||||
println!("popped {last}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `HashMap<K,V>`
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("alice", 10);
|
||||
m.insert("bob", 7);
|
||||
|
||||
*m.entry("alice").or_insert(0) += 1;
|
||||
|
||||
if let Some(score) = m.get("alice") {
|
||||
println!("alice={score}");
|
||||
}
|
||||
|
||||
for (k, v) in &m {
|
||||
println!("{k} => {v}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11) Option und Result: Fehlerbehandlung ohne Exceptions
|
||||
|
||||
Rust nutzt **`Option<T>`** (statt `None`) und **`Result<T, E>`** (statt Exceptions).
|
||||
|
||||
### Option
|
||||
```rust
|
||||
fn first(v: &[i32]) -> Option<i32> {
|
||||
v.first().copied()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let v = vec![1, 2, 3];
|
||||
match first(&v) {
|
||||
Some(x) => println!("first={x}"),
|
||||
None => println!("empty"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Result + `?` Operator
|
||||
```rust
|
||||
use std::fs;
|
||||
|
||||
fn read_file(path: &str) -> Result<String, std::io::Error> {
|
||||
let text = fs::read_to_string(path)?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match read_file("Cargo.toml") {
|
||||
Ok(t) => println!("len={}", t.len()),
|
||||
Err(e) => eprintln!("error: {e}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`?`**: Wenn `Err`, returnt die Funktion früh; wenn `Ok`, entpackt.
|
||||
|
||||
---
|
||||
|
||||
## 12) Iterators (sehr “pythonic”, aber typisiert)
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let nums = vec![1, 2, 3, 4, 5];
|
||||
|
||||
let squares: Vec<i32> = nums.iter()
|
||||
.map(|x| x * x)
|
||||
.collect();
|
||||
|
||||
let even_sum: i32 = nums.iter()
|
||||
.filter(|x| *x % 2 == 0)
|
||||
.sum();
|
||||
|
||||
println!("{squares:?} even_sum={even_sum}");
|
||||
}
|
||||
```
|
||||
|
||||
Ownership dabei:
|
||||
- `iter()` gibt `&T`
|
||||
- `into_iter()` konsumiert und gibt `T` (Owned)
|
||||
- `iter_mut()` gibt `&mut T`
|
||||
|
||||
---
|
||||
|
||||
## 13) Generics + Traits (Rusts “Interfaces”)
|
||||
|
||||
### Generics
|
||||
```rust
|
||||
fn first<T: Clone>(v: &[T]) -> Option<T> {
|
||||
v.first().cloned()
|
||||
}
|
||||
```
|
||||
|
||||
### Trait definieren und implementieren
|
||||
```rust
|
||||
trait Greeter {
|
||||
fn greet(&self) -> String;
|
||||
}
|
||||
|
||||
struct Person { name: String }
|
||||
|
||||
impl Greeter for Person {
|
||||
fn greet(&self) -> String {
|
||||
format!("Hi, {}!", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn say_hello(g: &impl Greeter) {
|
||||
println!("{}", g.greet());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let p = Person { name: "Alice".into() };
|
||||
say_hello(&p);
|
||||
}
|
||||
```
|
||||
|
||||
Trait Bounds:
|
||||
```rust
|
||||
fn print_debug<T: std::fmt::Debug>(x: T) {
|
||||
println!("{x:?}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14) Module, Crates, Sichtbarkeit (`pub`)
|
||||
|
||||
### In einer Datei
|
||||
```rust
|
||||
mod math {
|
||||
pub fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
|
||||
fn secret() {} // privat
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("{}", math::add(1, 2));
|
||||
}
|
||||
```
|
||||
|
||||
### Mehrere Dateien (typisches Layout)
|
||||
- `src/main.rs`
|
||||
- `src/math.rs`
|
||||
|
||||
`src/main.rs`:
|
||||
```rust
|
||||
mod math;
|
||||
|
||||
fn main() {
|
||||
println!("{}", math::add(1, 2));
|
||||
}
|
||||
```
|
||||
|
||||
`src/math.rs`:
|
||||
```rust
|
||||
pub fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15) Lifetimes (Grundidee, praxisnah)
|
||||
|
||||
Lifetimes sagen dem Compiler: “Wie lange sind Referenzen gültig?”
|
||||
|
||||
Beispiel: Funktion gibt eine Referenz zurück:
|
||||
```rust
|
||||
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
|
||||
if a.len() >= b.len() { a } else { b }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let x = "short";
|
||||
let y = "a bit longer";
|
||||
println!("{}", longer(x, y));
|
||||
}
|
||||
```
|
||||
|
||||
Als Anfänger-Regel:
|
||||
- Wenn du **Owned** (`String`, `Vec<T>`) zurückgeben kannst, ist das oft einfacher.
|
||||
- Lifetimes brauchst du vor allem bei APIs, die Referenzen zurückgeben.
|
||||
|
||||
---
|
||||
|
||||
## 16) Smart Pointers: `Box`, `Rc`, `Arc`, `RefCell` (Überblick)
|
||||
|
||||
- `Box<T>`: heap allocation, Ownership bleibt eindeutig
|
||||
- `Rc<T>`: shared ownership **single-thread**
|
||||
- `Arc<T>`: shared ownership **thread-safe**
|
||||
- `RefCell<T>`: “interior mutability” (Borrow-Regeln zur Laufzeit)
|
||||
|
||||
Mini-Beispiel `Rc`:
|
||||
```rust
|
||||
use std::rc::Rc;
|
||||
|
||||
fn main() {
|
||||
let a = Rc::new(String::from("hello"));
|
||||
let b = Rc::clone(&a);
|
||||
|
||||
println!("a={}, b={}", a, b);
|
||||
println!("strong_count={}", Rc::strong_count(&a));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17) Concurrency (Basics): Threads + Channels
|
||||
|
||||
```rust
|
||||
use std::thread;
|
||||
use std::sync::mpsc;
|
||||
|
||||
fn main() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
tx.send("hello from thread").unwrap();
|
||||
});
|
||||
|
||||
let msg = rx.recv().unwrap();
|
||||
println!("got: {msg}");
|
||||
|
||||
handle.join().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18) Async (Basics): `async/await` mit Tokio
|
||||
|
||||
Async braucht i.d.R. eine Runtime (z.B. Tokio).
|
||||
|
||||
`Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
`src/main.rs`:
|
||||
```rust
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let h = tokio::spawn(async {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
42
|
||||
});
|
||||
|
||||
let result = h.await.unwrap();
|
||||
println!("result={result}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19) Testing
|
||||
|
||||
```rust
|
||||
fn add(a: i32, b: i32) -> i32 { a + b }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adds() {
|
||||
assert_eq!(add(2, 3), 5);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ausführen:
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20) “Rust für Python-Dev”: mentale Übersetzungen
|
||||
|
||||
- Python-Liste ↔ `Vec<T>`
|
||||
- Dict ↔ `HashMap<K,V>`
|
||||
- `None` ↔ `Option<T>`
|
||||
- Exceptions ↔ `Result<T,E>` + `?`
|
||||
- Duck typing ↔ Traits + Generics
|
||||
- Mutable Default überall ↔ Rust: **immutable by default**
|
||||
- Garbage Collector ↔ Ownership (Drop am Scope-Ende)
|
||||
|
||||
**Idiom:** Daten lieber so modellieren, dass `match` alle Fälle abdeckt (Enums sind stark).
|
||||
|
||||
---
|
||||
|
||||
## 21) Mini-Projekt 1: CLI “todo” (ohne externe Crates)
|
||||
|
||||
Ziel: einfache Aufgabenliste im Speicher (kein Persistenz).
|
||||
|
||||
```rust
|
||||
use std::io::{self, Write};
|
||||
|
||||
fn main() {
|
||||
let mut todos: Vec<String> = Vec::new();
|
||||
|
||||
loop {
|
||||
print!("todo> ");
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut line = String::new();
|
||||
if io::stdin().read_line(&mut line).is_err() {
|
||||
println!("input error");
|
||||
continue;
|
||||
}
|
||||
let line = line.trim();
|
||||
|
||||
if line == "quit" {
|
||||
break;
|
||||
} else if line == "list" {
|
||||
for (i, t) in todos.iter().enumerate() {
|
||||
println!("{}: {}", i + 1, t);
|
||||
}
|
||||
} else if let Some(rest) = line.strip_prefix("add ") {
|
||||
todos.push(rest.to_string());
|
||||
} else if let Some(rest) = line.strip_prefix("done ") {
|
||||
if let Ok(idx) = rest.parse::<usize>() {
|
||||
if idx >= 1 && idx <= todos.len() {
|
||||
todos.remove(idx - 1);
|
||||
} else {
|
||||
println!("index out of range");
|
||||
}
|
||||
} else {
|
||||
println!("usage: done <number>");
|
||||
}
|
||||
} else {
|
||||
println!("commands: add <text> | list | done <n> | quit");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22) Mini-Projekt 2: JSON holen (Reqwest)
|
||||
|
||||
`Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
```
|
||||
|
||||
`src/main.rs`:
|
||||
```rust
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HttpBin {
|
||||
url: String,
|
||||
origin: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), reqwest::Error> {
|
||||
let data: HttpBin = reqwest::get("https://httpbin.org/get")
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
println!("{data:#?}");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 23) Nächste Schritte / gute Referenzen
|
||||
|
||||
- Offizielles Buch: https://doc.rust-lang.org/book/
|
||||
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
|
||||
- Standard Library Docs: https://doc.rust-lang.org/std/
|
||||
|
||||
---
|
||||
|
||||
## Vorschlag für deinen Lernpfad (kurz)
|
||||
1. Kapitel 1–7 (Syntax, Structs/Enums)
|
||||
2. Ownership/Borrowing (Kapitel 8–10) – viel üben
|
||||
3. Option/Result + Iterators
|
||||
4. Traits/Generics + Module
|
||||
5. Tests + kleines Projekt
|
||||
6. Dann Async/Concurrency nach Bedarf
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen (damit ich es besser anpassen kann)
|
||||
- Willst du Rust eher für **CLI**, **Backend**, **Embedded**, **WebAssembly** oder **Data/Perf** nutzen?
|
||||
- Betriebssystem/Editor?
|
||||
- Soll ich daraus **mehrere Obsidian-Notizen** machen (z.B. “Ownership”, “Result/Option”, “Traits”, …) mit internen Links?
|
||||
```
|
||||
Reference in New Issue
Block a user