どうもラクーンホールディングスのシュウヤです!
なぜこの記事を書こうと思ったのか?
私はRustが好きで個人でよく書いています。しかし、分かり易く効率的なコードの書き方を理解していませんでした。分かりやすく効率的にコードを書けないことは、パフォーマンスや可読性の低下を引き起こします。このことから、分かり易く効率的な書き方、つまりイディオムを紹介することが誰かのためになると考えました。
Idiomsとデザインパターンの違いは?
イディオムは、特定のプログラミング言語で頻繁に使われる、効果的かつ効率的なコードの書き方やテクニックのことを指します。他方で、デザインパターンはソフトウェア開発における共通の問題に対する効率的な書き方やテクニックで、特定の言語に依存しません。なので、Rustという言語を極めるという意味ではイディオムを学ぶ必要があるのですね。
この記事の目的
- Rustの美しい書き方を学べる
目次
- 引数には借用型を利用しよう
- 文字列は+でなくフォーマットで連結しよう
- Defaultトレイトを使ってコンストラクタを抽象化しよう
- Optionはflattenを使って値を安全に取得しよう
- 一時的にmutにしたい場合はスコープを使ったり再定義をしよう
引数には「所有型の参照」よりも「借用型」を利用しよう。
例えば、String,&String,&strのうちどれを引数にするかと聞かれれば、&strを利用しようということです。それぞれの特徴についてみていきます!
String(所有型)
Stringは言うまでもなく、所有権を持つことからメモリを新しく確保するのでパフォーマンスは悪いです。また、所有権を移動してしまうともう使えなくなり、柔軟性もありません。今回の例だと、データのアドレス: 100,文字列の長さ: 10、確保されたメモリのキャパシティ: 15(例えば)を持っている状態となります。
fn print_string(hoge : String){
println!("{}",hoge);
}
fn main(){
let name_string = String::from("shuya");
let name_and_str = "shuya";
print_string(name_string);
// &strとStringで型が違うためコンパイルエラー
print_string(name_and_str);
// 関数に呼び出されて所有権が移動したのでコンパイルエラー
print_string(name_string);
}
&String(所有型の借用)
Stringのデータを参照するためコピーをする必要がありません。そのためパフォーマンス的にはかなり良いです。しかし、String自体が実データとその情報のアドレスを持っています。それを更に参照するということから実データにたどり着くまでに余分なステップが必要となります。また、&strと型が一致しません。今回の例だと&StringはこのStringオブジェクトのメモリアドレス(1000番)を指し示します。
fn print_string(hoge : &String){
println!("{}",hoge);
}
fn main(){
let name_string = String::from("shuya");
let name_and_str = "shuya";
print_string(&name_string);
// &strと&Stringは型が一致しないのでコンパイルエラー
print_string(name_and_str);
// 所有権は移動しないので参照できる
print_string(&name_string);
}
&str(借用型)
直接参照する上にその文字列の所有権を持ちません。この結果パフォーマンスとしては最高となります。&strは任意のStringから得られるだけでなく、文字列リテラル(ソースコードに直接書かれた文字列)や他の&strからも簡単に作成できるため最も柔軟です。今回の例だと開始位置のアドレス: 100と長さ: 5を持ちます。
fn print_string(hoge : &str){
println!("{}",hoge);
}
fn main(){
let name_string = String::from("shuya");
let name_and_str = "shuya";
print_string(&name_string);
print_string(&name_and_str);
print_string(&name_string);
}
文字列は+でなくフォーマットで連結しよう
+で文字列を連結する
連続して使うと、各結合のたびに新しい文字列が作成され、メモリの再割り当てが発生する可能性がありパフォーマンスは悪いです。また、両方のオペランドが String 型である必要があり柔軟性もないです。
let name = "Alice";
let age = 30;
let score = 88.6;
// わざわざ全部 .to_string()にしないといけない。
let s = "Name: ".to_string() + name + ", Age: " + &age.to_string() + ", Score: " + &score.to_string();
println!("{}",s);
formatを使う
パフォーマンスとしては、内部で一度に最終的な文字列のサイズを計算し、適切なバッファを確保します。これにより、文字列の再割り当てを減少させ、パフォーマンスを向上させます。また、異なる型のデータを一つの文字列に結合する場合は自動でやってくれるので柔軟性があります。複数の値を {} プレースホルダを用いて一つの書式指定された文字列に結合することができ、結果としてコードがより読みやすく、保守しやすくなります。
let name = "Alice";
let age = 30;
let score = 88.6;
// format!を使った場合
let s = format!("Name: {}, Age: {}, Score: {}", name, age, score);
println!("{}",s);
Defaultトレイトを使ってコンストラクタを抽象化しよう
Rustの多くの型にはコンストラクタ(関連型関数)があります。ただし、これは型に固有です。そのため、Rustは「new()メソッドを持つすべてのもの」を抽象化することはできないため、コードが助長になります。例えば、名前、メールアドレス、都道府県番号、パスワードを構造体のメンバー変数として設定したとき、Default Traitを使わないのと下記のように冗長な書き方になります。コードを余分に書いているためミスが生じたり保守性も下がるでしょう。
// User構造体の定義。構造体の中身をみるにはDebugを入れる必要があります。
#[derive(Debug)]
struct User {
user_name: String,
mail_address: Vec<String>,
prefecture_code: i32,
password: String,
}
// すべてのコンストラクタのデフォルトの値を設定しないといけません。
impl User {
fn new() -> Self {
User {
user_name: String::from(" "),
mail_address: Vec::new(),
prefecture_code: 0,
password: String::from(" "),
}
}
}
fn main() {
// new関数を使用してUserを初期化
let conf = User::new();
println!("{:?}", conf);
}
これをDefalut Traitにすると下記のように書き直せます。
二度も書き直す必要がないため生産性の高いコードを書くことが出来ると考えます。
// User構造体の定義。Defalutを追記
#[derive(Debug,Default)]
struct User {
user_name: String,
mail_address: Vec<String>,
prefecture_code: i32,
password: String,
}
fn main() {
// defaultを使うことでシンプルに書ける
let conf = User::default();
println!("{:?}", conf);
}
Optionはflattenを使って値を安全に取得しよう
Some(1),None,Some(3)を含むvectorを考えます。これらをunwrapを使って、直接データを取得しようとすると、Noneが存在する場合にパニックを起こしてしまいます。
let options = vec![Some(1), None, Some(3)];
for opt in options {
let value = opt.unwrap(); // Noneの場合、パニックを引き起こす
println!("{}", value);
}
他方で、flatten()メソッドを使うことでSomeの値だけを反復処理することが出来ます。
let options = vec![Some(1), None, Some(3)];
for value in options.into_iter().flatten() {
println!("{}", value); // Noneは自動的に除外される
}
一時的にmutにしたい場合はスコープを使ったり再定義をしよう
よく見られる問題は、これ以降変更しない場合や、一時的にデータを変更するだけの場合でもmutのままになっているコードです。これらは意図せずデータを変更するリスクが残ります。例えば下記のコードを例にとってみます。
データをこれ以降変更しないのにmutのまま
let mut data = vec![1,2,3];
data.push(10);
println!("{:?}",data)
一時的にデータを変更するだけなのにmutのまま
let mut data = vec![1,2,3];
data.push(10);
data.pop();
// ミュータブルなものとしてそのまま処理が終わってしまっている
println!("{:?}",data)
したがって、それぞれイミュータブルとして再定義したり、スコープ内で設定する必要があります。
データをこれ以降変更しない場合
let mut data = vec![1, 2, 3];
data.push(4);
let data = data; // イミュータブルとして再定義
println!("{:?}", data); // 出力: [1, 2, 3, 4]
一時的に利用したい場合
let data = {
let mut data = vec![1,2,3];
data.push(10);
data.pop();
data
};
println!("{:?}",data);
いかがでしたでしょうか?
少しはより効率的に美しく書ける方法を学べたかと思います。
皆さんのお力になれれば幸いです。