7
4

More than 3 years have passed since last update.

Rustに入門する〜4日目 リファクタリング〜

Posted at

初めに

3日目の続き、
直さないといけないのはわかっていたけど動くからで放置していたものをきれいにしていきます。

ちなみにこの記事を書いている途中で知ったのですが、
今まで読んでた公式のガイドは古いバージョンだったみたいです...
Url親のたどったらなんか出てきた...
https://doc.rust-jp.rs/book/second-edition/

&strStringどっちを使うか

C言語はある程度やっていたのでstr&strについては言わずもがなという感じでしたが、
Stringという概念はなかったのでさて&strStringはなんだとなるわけです。
それこそStringcharの概念は色々な言語にそれぞれあるかと思いますが
&strを含む3種類を持つ言語は多分これが初めてです。

文字列
http://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/strings.html

またこれを理解するにあたってヒープの文字が見えたのでスタックとヒープの記事を事前に読んでおきました。
4章まで読めばとりあえず書けそうって思ったので実は5章以降読んでないのを忘れてました。

C言語を昔趣味でやってた時なんとなくこれらは違うくらいで、
スタックは早い!ヒープは動的なものが入るメモリ領域であまり使わない方が良い。
くらいの認識でしっかり理解はできていなかったのでこの機会に少し踏み込みましょう。
(徐々に確保領域が大きくなるmallocfreeをループさせてプログラムが死んだ...なんて思い出が)

スタックとヒープ
https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/the-stack-and-the-heap.html

上記以外も諸々見ていますが、ざっくりの違いは以下でした。
(他にも僕が情報源として見ていないだけで色々あると思いますが)

  • 可変長であるか(String)、固定長であるか(&str)
  • 変更可能であるか(String)、変更不可であるか(&str)
  • 借用となるか(String)、参照となるか(&str)

可変長ということは伸縮のための仕組みを持っているはずなのでStringはコストが高そうです。
特に伸長は、一回確保した後連続する領域で確保できない場合のコストが凄そう。

また、引数で渡す際に&strは参照として引き渡せる点も大きい...と思ったんですが
たぶん&Stringで渡せば解決する問題ですね。

またこれに加えそれぞれの型変換を行う際には以下のようになるようです

  • &str -> Stringの変換はメモリ確保のコストがかかる
    • 元の文字列の複製を行う為
  • String -> &strへの変換はほぼコストがかからない
    • &strStringで確保された領域を参照で挿すだけ?

上記の記事にもありましたが文字列を定義する場合&'static strな点を含めると、
通常参照のみであれば&strを使うべきでしょうか。

"Hello there." は文字列リテラルで、 &'static str 型を持ちます。

ただ&strの場合はあくまで参照を返すはずなので、
関数の返り値などはStringとして返さないと実態がないことになるので(rust的にはそもそもコンパイルエラーが出そうだが)、
諸々踏まえ使えそうであれば&strとして使っていくことになるでしょうか

Deserializeした値を入れる先に参照型があると死にましたがこれも多分同じ理由ですね。
fn main()fn a()-> &strを呼び出すなんてことがもしできたら、
aの関数の中で確保した文字列の参照を返したとしても、aの関数の文字列が終了した途端にその参照元は消えるわけですからね。
(詳しくは上記のスタックとヒープ)

await?って何

あちこちで出てきたんですがとりあえずunwrap()でもなんか知らんけどいけるやんけ!って放置してました。
.await.unwrap()でやってるとチェーンが長くなるのでサンプル通り.await?を理解して使えるようになります。

公式のガイドだと以下のページに載ってました

エラー委譲のショートカット-演算子
https://doc.rust-jp.rs/book/second-edition/ch09-02-recoverable-errors-with-result.html

どうやらこれを使うとResult<T,E>で返してくれるようです。
今までunwrap()を使っていたのはエラー処理に気を使わず動かすために...というよりとりあえず実装お手軽!と思ってやってました。
Resultだとmatchで処理しないといけないし...と思っていたのので。
つまり今まではunwrap()でエラーならpanic!()で落ちるという処理をErr(e)として返却してくれるという形で使えそうです。

さて、改めて理解したところで、
Result<T,E>は今までunwrap()してきたところを丁寧に理解して戦う必要がありそうです。

Result型への理解を深める

Result<T,E>は今までunwrapするかmatchさせてエラーを出力するだけだけだったのでしたが、
返り値として返そうとするとめちゃくちゃ大変でした。

fn a()->Result<T,E>を定義する際にTについては本来返そうとしている型を入れるだけでしたが、
Eにも適切な型を当てなければいけないのですがこれが厄介です。

例えばreqwest::ClientBuilder::new().build()?とするだけの関数であれば
Eにはreqwest::Errorを設定すれば良いのですが、
例えばこの前後にファイルを開きつつ?演算子で処理しようとするとio::Errorが発生する可能性があります。
なのでEにはreqwest::Errorio::Errorの両方が入るようにしないといけないみたいです。

これを解決するための方法はいくらかあるみたいです。

  • エラーハンドリングをこなし返却されるエラーの方が一定になるようにする
  • failure等ライブラリを使う
    • どのライブラリが最適かは結構変わっているっぽい?
  • 複数のエラー型を含む独自のエラー型を定義する

なお今回は上記の立ち向かう選択肢を取らず逃げる選択肢を取ろうと思います。
つまり、
reqwest::Errorについては一時的な通信エラーの可能性があるので?演算子を利用し、
serde_json::error:Errorについては構造による永続的なエラーの可能性が高いためunwrap()とする。
ということにします。

こうすることでこの関数のエラーの返却はreqwest::Errorのみとなりました。
(いつか正式に対応します...)

JSONのデシリアライズの際にkey名を指定する

serde_jsonの機能というよりSerialize/Deserializeの機能であるみたいです。

Field attributes
https://serde.rs/field-attrs.html

こんな感じのJSONがあったときに

{
    "dateTime" : "xxxxx"
}

以下の定義をしてlet sc = serde_json::from_str(&json).unwrap();のような感じでデシリアライズをすると、
sc.dateTimeではなくsc.date_timeとして呼び出せます。

pub struct SampleClass{
    #[serde(alias = "dateTime")]
    pub date_time: String
}

4日目を終えて

さくっと軽く綺麗にしてと思ったらResultの部分が想像異常に重かったです。
お試しでプログラム作ってみたいよ!って人はResult型に関しては脳死でunwrap()した方がいいとしか思えなかった。
よくはないですが、こういうとりあえず動く部分に影響しないところはなかなかモチベーションが上がらない。
(特にRustで初めて作っている部分でもあるので)

次回はSlackのAPIを叩く予定ですが、
3日目のカレンダーから情報を取る際に終日の取れ方がなんか変という点の解消や所用で少し間が開くと思います。

7
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4