シンプルなCLIのTodoアプリ作りを通して、rustの勉強をした
はじめに
CLI でシンプルな Todo App を作る事が出来ました。
こちら
の記事を参考にさせて頂きました。非常に分かりやすく説明されていて、とても勉強になりました。ありがとうございます。
完成後のリポジトリも貼っておきます。
記事執筆時点で実装した機能は 追加
と 更新
、ファイルへの保存
のみです。
ただし、今後私自身が勉強のために機能を追加していく予定です。上記リポジトリも更新されると思います。下記のコミット内容が今回の記事に該当します。
バージョン
rustup: 1.25.1
cargo: 1.63.0
ディレクトリ作成
今回の作業ディレクトリを作ります。
$ cargo new todoapp-cli
作成したディレクトリに移動します。
$ cd todoapp-cli
src
というディレクトリと、Cargo.toml
というファイルがあると思います。 src
ディレクトリ配下に main.rs
というファイルが入っていると思います。今回はこちらのファイルにコードを書いていきます。
現時点で下記コマンドを実行すると、Hello world!
と返ってくると思います。
$ cargo run
引数をどうやって読み取るか
今回目指す Todo アプリは、引数 [1] を 2 つ入力します。
1 つ目がアクション、2 つ目がアイテム(Todo の内容)です。今回は下記の標準クレートを利用します。
main.rs
の main()
に下記コードを記述します。
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
println!("{:?}, {:?}", action, item);
nth()
で、各場所の引数を読み取る事が出来るそうです。
nth(0)
はプログラムそのものを指すので、nth(1)
が第一引数( action
)になり、 nth(2)
が第二引数( item
)となります。
expect()
[2] [3] は、値を返す、または存在しない場合(ここで言うと引数を書かなかった場合)は、プログラムを終了して下記のような指定のメッセージを返すものです。
thread 'main' panicked at 'Please specify an item', src/main.rs:52:40
rust
では引数無しでプログラムを実行出来ます。なので、実際に引数に値を入力したかどうか確認する必要があります。
それでは実際に引数を 2 つ渡してみます。引数は --
の後ろに入力します。
第一引数に hello
、第二引数に world
と入力して実行してみます。
$ cargo run -- hello world
下記のように入力した引数が表示されていれば成功です。
"hello", "world"
引数を入力して、それを表示する処理を行いました。自分が思ったように動いてくれると嬉しいものですね。
構造体を作り、なおかつメソッドを定義する
先ほどの引数の処理は比較的簡単に実装出来ました。問題はここからです。構造体 [4] という箱?、型の集合体?みたいな物を作って、その構造体の振る舞い(メソッド) [5] を定義します。
The Rust Programming Language 日本語版
では、下記のように記されていました。
私はここで何度も頭が爆発しました。おそらくまだ理解出来ていないと思いますが、私の中では前述の通り 構造体が箱みたいなもので、その箱がどういう振る舞いをするのか定義
するのが メソッドの定義
と解釈しました。
他のプログラミング言語知識がある方は、似たような処理に覚えがあるかもしれません。
今回作成する Todo アプリは、Todoの追加
& Todoの更新
、そしてその Todo をファイル形式でローカルストレージに保存します。
それら 追加
更新
保存
の各振る舞いを定義する構造体をまずは作る必要があります。
構造体の作り方
標準ライブラリにある HashMap
を利用するので、main.rs
の一番上に下記コードを追記します。
use std::collections::HashMap;
main
関数の外に下記コードを入力します。
構造体名の頭文字は大文字で記述します。
struct Todo {
map: HashMap<String, bool>,
}
いきなり出てきた HashMap
に驚いている方いると思います。私は何が起きてるのか分かりませんでした。構造体よりも意味が分かっていません。Rust By Example 日本語版
や、rust 公式ドキュメントを見てみました。
- ハッシュマップ
- Struct std::collections::HashMap
- Hash table
key
で値を参照するのがハッシュテーブルらしいです。このハッシュテーブルが、rust では HashMap
で実装されているようです。
HashMap<String, bool>
という書き方は、String
と boolean
のキーで値を保持すると解釈しました。
下記のような出力結果をイメージしました。
Learning rust
や Shopping
が String
キー、true
が boolean
キーです。
{
"Learning rust": true,
"Shopping": true
}
これで、構造体が出来ました。次はどのような振る舞いをさせるか、メソッドを定義していきます。この時点で頭から煙が出てしまいますね。rust 楽しいです。
構造体にメソッドを定義する
メソッドは通常の関数 fn
のように記述出来ます。
記述は似ていますが、書く場所は構造体の外で、第一引数は self
[6] パラメータを受け取ります。
self
は呼び出されたメソッドの構造体自身を指す、と解釈しています。
型も構造体と同じになるので、記述しなくて済んでいるのではないかなと思います。
impl
ブロックにメソッドの定義を記述します。
impl Todo {
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
}
impl
は、振る舞いを定義する際に利用する、と解釈しています。今回の Todo アプリでは、構造体の振る舞い、つまりメソッドを定義していて、下記の
The Rust Programming Language 日本語版
では、型にトレイトの振る舞い [7] を定義しています。
これを踏まえた上で、上記コードを見ると、Todo
構造体に insert
メソッドを定義しています。
上記コードは、構造体への参照 &mut self
とキー key: String
を受け取り、HashMap の insert メソッド [8] を利用して map に値を入れている、と解釈しています。
構造体は新しい値を追加して変更されていくので、mut
[9] で可変にしています。
そして、&
は参照 [10] を意味します。ここでは、値そのものではなく、値が格納されている場所へのポインタ
をイメージします。
なので、厳密には値を保持しておらず、値が格納されている場所を示してるだけ、だと思っています。
&mut self
は、私の中で 2 つのどちらが正しい意味なのか分かっていません。2 つとも正しくない場合もあります。orz
値の更新が可能な構造体への参照
更新可能な値への参照を持つ構造体
&
が mut
に掛かってるように見えるが・・・
-
&
+mut self
-
&mut
+self
執筆時点(2022/8/26)では理解できていません。
参照や、値の借用と合わせて 所有権
[11] という rust 独自のルールがあります。これは、変数が値の所有者という意味で捉えています。
所有権には規則があります。
- rust の各値は、所有者と呼ばれる変数と対応している。
- いかなるときも所有者は一つ。
- 所有者がスコープから外れたら、値は破棄される。
他のプログラミング言語をまともに触った事がないので、この所有権がどうユニークなのか、調べてみました。
- C 言語などは、メモリ管理をプログラマー自身が全て管理する。
- Java 言語などは、ガベージコレクション?というシステムが導入されており、プログラミング言語側で良い感じにメモリを管理してくれる。
メモリ管理をプログラマーが全て行う。そうなると、メモリの解放忘れ?みたいなヒューマンエラーが起こりうる。
ガベージコレクションはすごく楽。プログラミング言語側で全てやってくれるので。しかし、低速・・・楽だが、C 言語などと比べると遅いらしいですね。
上記 2 つの問題点を解決するべく、所有権という機能が生まれたみたいです。
rust はガベージコレクションを導入していません。なので Java 言語などよりは早いです。
所有者がスコープから外れたら、値は破棄される
という rust の規則があります。なので、解放忘れなどが発生してもコンパイル時に分かる。
ここらへん、なんとなくイメージは出来てきましたが、理解が出来ていないようでコンパイルで殴られています。rust 楽しいです。
所有権に関する詳しい内容は前述した The Rust Programming Language 日本語版
を見てみてください。今回の Todo アプリ作りの過程で 値を所有して削除するか、もしくは保持するために参照するか
考えながら進みます。
ファイルに保存する
DB などを利用せずにデータを残すために、ローカルに プレーンテキストファイル
を生成してそこに記録を保存していきます。
impl
ブロック内に下記のコードを追記してください。先ほど記述した insert
メソッドの外に記述します。
impl Todo {
// ここに insert メソッドがある
+ fn save(self) -> Result<(), std::io::Error> {
+ let mut content = String::new();
+ for (k, v) in self.map {
+ let record = format!("{}\t{}\n", k, v);
+ content.push_str(&record)
+ }
+ std::fs::write("db.txt", content)
+ }
}
->
は、関数から返される型を注釈しています。上記コードでは Result
を返します。
map
を繰り返し処理し、format!
マクロで各文字列を整えています。\t
がタブ出力で \n
が改行を意味します。
その後、content.push_str
でフォーマットされた文字列をコンテンツにプッシュしています。
std::fs::write("db.txt", content)
は、db.txt
というテキストファイルに先ほどのコンテンツを書き込んでいくという意味です。この書き方だと、todoapp-cli
ディレクトリ直下に db.txt
というファイルが生成されます。
私は data
というディレクトリを作成し、その中に生成するようにしたかったので、下記のように書き直しました。
impl Todo {
// ここに insert メソッドがある
fn save(self) -> Result<(), std::io::Error> {
let mut content = String::new();
for (k, v) in self.map {
let record = format!("{}\t{}\n", k, v);
content.push_str(&record)
}
- std::fs::write("db.txt", content)
+ std::fs::write("./data/db.txt", content)
}
}
ディレクトリ構成のルールなどは全く分かっていません。プロジェクト直下にデータファイルをそのまま保存するのに個人的違和感があったためこのようにしています。
私はこの save
メソッドを定義する際に引数の self
が参照ではなく実体であることに気付きました。
save
が self
の所有権を持つことで、マップによる誤更新を防ぐのが目的かと思っています。
main 関数で構造体を利用する
冒頭で記述した引数の操作の下に下記を記述します。
fn main() {
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
println!("{:?}, {:?}", action, item);
+ let mut todo = Todo {
+ map: HashMap::new(),
+ };
+ if action == "add" {
+ todo.insert(item);
+ match todo.save() {
+ Ok(_) => println!("todo saved"),
+ Err(why) => println!("An error occurred: {}", why),
+ }
+ }
}
Todo
構造体で todo
をインスタンス化しています。そして mut
で変更可能な状態としています。
if action == "add"
は、第一引数が add
の場合の処理という意味です。
todo.insert(item);
は、先ほどの todo
インスタンスの insert
メソッドをドット記法で呼んでいます。引数に item
を渡しています。insert
メソッドはだいぶ序盤に書いたのでここでもう一度下記に記載しています。insert
の第一引数 &mut self
は構造体自身なので、第二引数の key
に先ほどの item
が渡されます。
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
match todo.save()
は、成功したらOk
、 失敗したら Err
を返します。
match
[12] [13] とは、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれる制御フロー演算子だそうです。
この時点で、Todo の追加とファイルへの保存が出来ます。
$ cargo run -- add "test todo1"
todo saved
と返ってきたら追加処理はうまく動いています。
$ cat ./data/db.txt
test todo1 true
と返ってきたら、テキストファイルへの保存もうまく動いています。私は data
ディレクトリにテキストファイルを保存しているので上記コマンドでしたが、保存先に応じて適宜書き直してください。
自分の書いたコード通りに処理が進んでいるので楽しいですね。私は、この時点で insert
メソッドの定義内に save
メソッドを定義してしまったり、タイピングミスなどを繰り返し長い道のりでした。コンパイル時に細かく指摘してくれるので一つのエラーに長時間悩まされる事は今のところありません。rust 楽しいです。
Todo を上書きせずに新しく作る
今の状態だと、追加 Todo を追加する度に上書きしています。これでは不便ですね。Todo は複数ある状況が多いと思います。次は、上書きしないようにしていきます。
下記のコードを impl
ブロック内に追記します。
impl Todo {
fn insert(&mut self, key: String) {
// insertメソッド中身
}
fn save(self) -> Result<(), std::io::Error> {
// saveメソッド中身
}
+ fn new() -> Result<Todo, std::io::Error> {
+ let mut list = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .read(true)
+ .open("./data/db.txt")?;
+ let mut content = String::new();
+ list.read_to_string(&mut content)?;
+ let map: HashMap<String, bool> = content
+ .lines()
+ .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
+ .map(|v| (v[0], v[1]))
+ .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
+ .collect();
+ Ok(Todo {map})
+ }
}
上記コードは
参考
にしている記事の内容によると、結構無理やりいろんな書き方を導入したみたいです。他言語で目にするクロージャー、イテレータ、ラムダ関数などを rust がサポートしていますという紹介が目的のようです。私は他言語の知識をほとんど持っていないので分かりませんでしたが・・・
fn new() -> Result<Todo, std::io::Error>
は、Todo
もしくは、std::io::Error
を返す new
関数を定義しています。引数に self
が無いので、これはメソッドではありません。impl
ブロック内で記述した理由は、impl
が implementation (実装)
という意味にもある通り、Todo
構造体の機能として new()
を実装するからです。
下記の部分で、db.txt
をどのように開くかを設定しています。
let mut list = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("./data/db.txt")?;
-
.write(true)
で、ファイルの書き込みを許可 -
.create(true)
で、ファイルが存在しなかった場合に作成する事を許可 -
.read(true)
で、ファイルの読み込みを許可 -
.open("./data/db.txt")?;
で、./data/db.txt
ファイルを開く事を許可
OpenOptions
[14] は、デフォルトで false
です。
また、.open("./data/db.txt")?;
の ?
[15] [16] が気になったので調べてみたところ、エラー処理の一種です。例によって The Rust Programming Language 日本語版
に記載があったので、下に URL 貼ってます。詳しい内容はご確認ください。
-
Result
を返す関数内でしか使えない -
match
式とほぼ同じように動作する -
Result
がOk
ならOk
の中身を返す、Err
なら、return
と同じような動作をして、エラーは呼び出し元のコードに委譲される
let mut content = String::new();
list.read_to_string(&mut content)?;
list.read_to_string(&mut content)?;
は、ファイル内を全バイト読み込み、content String
に追加します。
read_to_string
メソッドを利用するために、ファイル先頭に use std::io::Read;
を記述します。
次は下記コードの解説です
let map: HashMap<String, bool> = content
.lines()
.map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
.map(|v| (v[0], v[1]))
.map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
.collect();
.lines()
[17] は、文字列の各行に対するイテレータを作成します。
.map
で 3 行処理していますが、この動きが理解出来ませんでした。| |
これがクロージャ?という機能らしいです。
以下Rust By Example 日本語版
よりクロージャの特徴を抜粋 [19] [20]
- その場限りの用途で使われる
- 呼び出しは関数の呼び出しと全く同じ
- 入力変数の名前は必ず指定しなければならない (入力変数を囲むのに
||
を利用) - 外側の環境にある変数を捕捉する事が出来る (
捕捉
だからスコープ外の変数を取り込む事ができる?)
んー、難しい・・・
line.splitn(2, '\t')
[21] は、タブ文字で 2 つに分けるという意味です。
メソッドに collect::<Vec<&str>>()
[22] を追加して、分割された文字列を借用した文字列スライスの Vector
型に変換するように map
関数に指示しています。
map(|v| (v[0], v[1]))
は、タプルに変換していると思います。
map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
は、先ほど変換したタプルの 2 つの要素を String
と boolean
に変換しています。 from_str()
メソッドを利用するために、ファイルの先頭に use std::str::FromStr;
を追記します。
Ok(Todo {map})
で、 呼び出し元に構造体を返します。
下記コードを main
関数内に追記します。
fn main() {
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
// こちらのコードは引数の動作確認で利用しただけなのでコメントアウトか削除
// println!("{:?}, {:?}", action, item);
+ let mut todo = Todo::new().expect("Initialisation of db failed");
let mut todo = Todo {
map: HashMap::new(),
};
if action == "add" {
todo.insert(item);
match todo.save() {
Ok(_) => println!("todo saved"),
Err(why) => println!("An error occurred: {}", why),
}
}
}
早速 2 回以上 add
アクションで Todo を追加してみます。
$ cargo run -- add "todo1 cooking"
todo saved
$ cargo run -- add "todo2 shopping"
todo saved
$ cat ./data/db.txt
todo1 cooking true
todo2 shopping true
上記のような結果になったと思います。上書きしなくなるだけで Todo アプリっぽくなりますね。もうお腹いっぱいかもしれませんが、せっかく true
or false
で Todo のフラグを立ててるので、完了した Todo は false
に更新したいです。rust 楽しいです。
完了した Todo を false に更新する
complete
というメソッドを新しく追加します。
impl Todo {
fn insert(&mut self, key: String) {
// insertメソッド中身
}
fn save(self) -> Result<(), std::io::Error> {
// saveメソッド中身
}
fn new() -> Result<Todo, std::io::Error> {
// new中身
}
+ fn complete(&mut self, key: &String) -> Option<()> {
+ match self.map.get_mut(key) {
+ Some(v) => Some(*v = false),
+ None => None,
+ }
+ }
}
Option<()>
[23] 型は、値がある場合は Some()
、値がない場合は None
を返します。 None
はエラーという意味ではなく、値が無い事を示しているだけです。エラーを表す場合は、 Result<T,E>
を使う場合が多いらしいです。
self.map.get_mut(key)
[25] は、コレクション内に値が存在しなければ None
を返します。
Some(*v = false)
の *
は、参照外し [26] です。参照ではなく実体を false
に更新しています。
complete
メソッドの定義は出来ました。次は main
関数で利用するために、コードを追記します。 insert
メソッドの書き方同じく、第一引数に与えられる action
を見て処理を行うようにします。if
を使って条件分岐します。
fn main() {
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
// こちらのコードは引数の動作確認で利用しただけなのでコメントアウトか削除
// println!("{:?}, {:?}", action, item);
let mut todo = Todo::new().expect("Initialisation of db failed");
let mut todo = Todo {
map: HashMap::new(),
};
if action == "add" {
todo.insert(item);
match todo.save() {
Ok(_) => println!("todo saved"),
Err(why) => println!("An error occurred: {}", why),
}
+ } else if action == "complete" {
+ match todo.complete(&item) {
+ None => println!("'{}' is not present in the list", item),
+ Some(_) => match todo.save() {
+ Ok(_) => println!("todo saved"),
+ Err(why) => println!("An error occurred: {}", why),
+ }
+ }
+ }
}
todo.complete(&item)
を match
式で処理します。 None
の場合はエラーメッセージを表示するようにしています。 todo.complete
メソッドに渡すのは item
の参照である &item
です。元の所有者は save
だが、今この瞬間だけ complete
が item
を借りている状況になると解釈しています。
なので、 次の None => println!("'{}' is not present in the list", item)
で、 item
を利用出来ます。
&item
があれば Some(_)
を処理します。todo.save()
を呼び出して、ファイルに保存します。保存が成功したら Ok(_)
を表示、失敗したら Err(why)
でエラーメッセージを表示します。
これで Todo のトグルを切り替える処理が完了しました。試してみるために、一度 ./data/db.txt
を削除します。
$ rm ./data/db.txt
そして実際に、複数 Todo を追加して、そのうち一つの complete してみます。
$ cargo run -- add "todo1 javascript"
$ cargo run -- add "todo2 rust"
$ cargo run -- complete "todo2 rust"
$ cat ./data/db.txt
todo1 javascript true
todo2 rust false
上記のように、2 つ追加して上書きされておらず、そのうち一つを complete
アクションで完了とすると、 false
に変わっています。最後に、 cat
コマンドで確認しています。
下記のようにコマンドを打つと完了ステータスから保留ステータスに更新出来ます。
$ cargo run -- add "todo1 rust"
$ cat ./data/db.txt
todo1 javascript true
todo2 rust true
保存先を JSON ファイル形式に変更
これで追加、更新、保存が出来る Todo アプリを作れました。すでに形にはなっていますが、プレーンテキスト形式で保存しているデータを json
ファイルにしたいと思います。この時 serde_json
を利用するので、 Cargo.toml
ファイルの dependencies
に下記を追記します。
[dependencies]
serde_json = "1.0.60"
impl
ブロックの new()
関数を更新します。
impl Todo {
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
fn save(self) -> Result<(), std::io::Error> {
let mut content = String::new();
for (k, v) in self.map {
let record = format!("{}\t{}\n", k, v);
content.push_str(&record)
}
std::fs::write("db.txt", content)
}
fn new() -> Result<Todo, std::io::Error> {
- let mut list = std::fs::OpenOptions::new()
+ let list = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
- .open("./data/db.txt")?;
+ .open("./data/db.json")?;
- let mut content = String::new();
- list.read_to_string(&mut content)?;
- let map: HashMap<String, bool> = content
- .lines()
- .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
- .map(|v| (v[0], v[1]))
- .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
- .collect();
- Ok(Todo {map})
+ match serde_json::from_reader(list) {
+ Ok(map) => Ok(Todo { map }),
+ Err(e) if e.is_eof() => Ok (Todo {
+ map: HashMap::new(),
+ }),
+ Err(e) => panic!("An error occurred: {}", e),
+ }
}
fn complete(&mut self, key: &String) -> Option<()> {
match self.map.get_mut(key) {
Some(v) => Some(*v = false),
None => None,
}
}
}
list.read_to_string(&mut content)?;
で、コンテンツを String に割り当てる必要がないので、 let list = std::fs::OpenOptions::new()
で mut
を外しています。
.open("./data/db.json")?;
のファイル拡張子を json
へ変更しました。
OpenOptions
で read(true)
にしているため serde_json::from_reader(list)
[27] で、ファイルなどから読み込む事が可能です。
この match
式は、最初にファイルの存在有無を確認し、ファイルが無ければ新しくハッシュマップを作り返す処理に見えます。
Err(e) => panic!("An error occurred: {}", e)
で利用している panic!
マクロ [28] は、エラーメッセージを表示して処理を巻き戻すそうです。そんな事が出来るとはすごいですね。Cargo.toml
ファイルに追記する事で、巻き戻しではなく異常終了に切り替えることも出来るそうですが、今回は特に設定していません。
save メソッドを更新
map
を json
ファイルで保存するときにも、 serde
を使いたいので save()
の中身も更新します。
impl Todo {
fn insert(&mut self, key: String) {
self.map.insert(key, true);
}
- fn save(self) -> Result<(), std::io::Error> {
- let mut content = String::new();
- for (k, v) in self.map {
- let record = format!("{}\t{}\n", k, v);
- content.push_str(&record)
- }
- std::fs::write("db.txt", content)
- }
+ fn save(self) -> Result<(), Box<dyn std::error::Error>> {
+ let content = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open("./data/db.json")?;
+ serde_json::to_writer_pretty(content, &self.map)?;
+ }
fn new() -> Result<Todo, std::io::Error> {
let list = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("./data/db.json")?;
match serde_json::from_reader(list) {
Ok(map) => Ok(Todo { map }),
Err(e) if e.is_eof() => Ok (Todo {
map: HashMap::new(),
}),
Err(e) => panic!("An error occurred: {}", e),
}
}
fn complete(&mut self, key: &String) -> Option<()> {
match self.map.get_mut(key) {
Some(v) => Some(*v = false),
None => None,
}
}
}
Box<dyn std::error::Error>
の Box
[29] [30] は、ヒープ領域に値を確保する最も簡単な方法で、簡単に言うとヒープ上に置かれた値へのポインタです。ファイルを開く際、ファイルシステムのエラーか、もしくは Serde のエラーか、どちらを返すか分からないので、呼び出し元がエラーを処理出来るようにエラーそのものではなく、エラーへのポインタを返します。 dyn
[31] は、トレイトを使う際の構文です。
serde_json::to_writer_pretty(content, &self.map)?;
[32] で、第一引数で write(true)
を確認して、第二引数のデータ構造を json
として出力します。
最後に、 use std::io::Read;
と use std::str::FromStr;
は必要無いので、ファイルから削除してください。
終わりに
お疲れ様でした。javascript を某プログラミング学習サービスでかじった程度のプログラミング知識なので、的外れな解釈があるかもしれません。他の言語習得時にも、チュートリアルとして Todo アプリを作るケースが多そうだったので、今回 rust の入門として行いました。完成ソースコードも冒頭に貼っているので、コード全体の確認にご利用ください。rust 楽しいです。
Discussion
素晴らしい記事をありがとうございます!
この記事に沿ってコーディングを進めていく過程で、
どういう仕組みだ?これはなんなんだ!?
という疑問が出てきて、Rustの理解度が少し深まったように感じます。
この記事を読み終えたあとに、メゾットの追加とそれに伴って必要になったsave()メゾットの若干の仕様変更を行ってみました!
途中、所有権やjson周りの扱いで躓きましたが、それを乗り越えて少し自信がつきました!
私のような初心者には、本当に有り難い記事でした!
Daikiさん、ありがとうございました!
今後も精進していきます!
記事の閲覧とコメントありがとうございます!
励みになるお言葉まで頂き光栄です。ありがとうございます!
記事執筆当時は趣味プログラミングでRustを触れていましたが、現在は業務でTSを触る機会を頂きなかなかRustを触れていないのが現状でした...
ろふぇん様にコメント頂きまた僕もモチベーションが湧いてきたので頑張りたいと思います😊
一緒に頑張りましょう🔥