今日の内容
- シーザー暗号とは
- 型変換
- 関数定義
- クロージャ
はじめに
前回は変数の型について学びました。今回は、シーザー暗号を通して、よりRustを詳しく学びます。main関数以外の初めての関数定義や、型変換、クロージャーなんかにも少し触れてみます。
シーザー暗号ってなに
シーザー暗号とは、暗号の一種で、とても簡単な暗号です。各文字をそれぞれ同じ文字数だけシフトして暗号化するという手法です。
今回は、3文字右にずらし、また変換はアルファベットのみとします。
「abcdefghijklmnopqrstuvwxyz」⇩
「defghijklmnopqrstuvwxyzabc」
たとえば、「hello, world!」を暗号化すると、「khoor, zruog!」となります。
以下は、Rustで文字列を暗号化して表示し、そのあとその文字列を復元してまた表示するプログラムです。
fn encrypt(s: &str, shift: i16) -> String {
let code_a = 'a' as i16;
let code_z = 'z' as i16;
let mut result = String::new();
for c in s.chars() {
let mut code_c = c as i16;
if code_a <= code_c && code_c <= code_z {
code_c = (code_c - code_a + shift + 26) % 26 + code_a;
}
result.push((code_c as u8) as char);
}
return result;
}
fn main(){
let enc = encrypt("hello, world!", 3);
let dec = encrypt(&enc, -3);
println!("{} => {}", enc, dec);
}
/******** 出力結果 *******
khoor, zruog! => hello, world!
*************************/
※encはencode(データを一定の規則に基づいて変換すること)で、decはdecode(特定のルールに基づいて変換されたデータを元の状態に戻すこと)の略
※encrypt「暗号化」
関数の定義と呼び出し
今回、初めて値を返す関数を定義しました。まずは関数の定義方法を学びましょう。
Rustで戻り値や引数を指定する関数を定義するには以下のように記述します。
fn 関数名 (実引数) -> 戻り値型 {
}
つまり、上のプログラム内のencrypt関数は、「&str型のsとi16型のshiftを引数として、戻り値はString型である」となります。
また、main関数内ではencrypt関数の呼び出しがされています。関数の呼び出しは
関数名(仮引数);
となります。
実引数および仮引数はこちらから
上のプログラムを見てみると、main関数内で以下のように呼び出されています。
fn main(){
let enc = encrypt("hello, world!", 3);
let dec = encrypt(&enc, -3);
}
encrypt関数は、戻り値がString型であると定義したので、変数enc、decはString型になります。
Rustで関数の値を戻すには、関数の末尾で「値」と書くか、「return 値;」と書きます。前者で書く場合は、セミコロンを付けないことがポイントです。
ここで、&str型とString型について少し触れます。
&str型とString型はどちらも文字列リテラルを表す型です。
「文字列リテラル」とは、プログラム内に記述される文字列を示す定数のことです。
ここで、char型と比べてみましょう。
記法 | 型 | 意味 |
---|---|---|
'A' | char | 文字を表す |
"A" | &str | 文字列リテラル |
Rustでは文字と文字列を明確に区別します。
ここで文字とは、1文字を表すデータ型のことです。そして文字列は、0個以上の文字を保持できる配列のようなものです。
型変換
encrypt関数内の処理を見てみると、最初の行が
let code_a = 'a' as i16;
となっています。
「'a'」は、文字aの文字コードを得ます。そして、「as i16」は、「'a'をi16型に型変換する」ということです。
もし、Unicodeに対応させたいなら、u32に変換します。これは、C言語などのプログラミング言語では、文字(char型)は1バイト(8ビット)ですが、Rustでは4バイト(32ビット)になっているからです。
このように、「as」は型を強制的に変換したいときに使います。
上のプログラムでは、「code_a」に'a'の文字コードをi16型に変換して束縛し、「code_z」に'z'の文字コードをi16型に変換して束縛しています。
プログラムを理解する
さて、それでは必要な知識はほとんどそろったので、プログラムの内容を理解しましょう。
まず、main関数から始まります。
let enc = encrypt("hello, world!", 3);
encrypt関数を呼び出し、実引数「s」に"hello, world!"と、「shift」に3を渡します。
let code_a = 'a' as i16;
let code_z = 'z' as i16;
変数「code_a」に'a'のi16型、「code_z」に'z'のi16型を束縛します。
let mut result = String::new();
可変な変数「result」という空の文字列を作成します。この「result」に、暗号化したものが入ります。
for c in s.chars() {
文字列「s」を一文字ずつ端まで繰り返し、繰り返す毎にその文字が変数「c」に束縛されます。
let mut code_c = c as i16;
変数「code_c」に変数「c」をi16型に変換したものを束縛します。文字列「s」の各文字を、i16型に変換して、シフトの操作を可能にします。
if code_a <= code_c && code_c <= code_z {
code_c = (code_c - code_a + shift + 26) % 26 + code_a;
}
変数「code_c」が変数「code_a」以上または変数「code_z」以内、つまり、文字列「s」の各文字が、それぞれa~zのどれかであることを確認します。もしtrueなら、変数「code_c」を変数「shift」の数右にずらした文字で置き換えます。この式は少し複雑に見えますが、「code_c - code_a」の部分で、'a'~'z'を、それぞれ0~25に変えて、「+shift」で3~28に変えて、「%26」でその数を26で割った余り(0~25)を求め、「+code_a」でその余りを'a'~'z'に変えています。途中の「+26」は、変数「shift」が-3である時(復元するとき)に、たとえば「0-3」を26で割った余りは「-3」になってしまうので、それを防ぐためにあります。
result.push((code_c as u8) as char);
これは、文字列「result」に、push()内の文字を追加する、ということです。()内は、変数「code_c」を一度u8型に変換し、それをchar型に変換しています。i16からcharに変換はできないので、一度u8にしてから変換しています。
return result;
文字列「result」を返し、関数を抜けます。この返り値(戻り値)がmain関数内の変数「enc」になります。
let dec = encrypt(&enc, -3);
ここまでとほぼ同じです。違うのは、渡す値が、暗号化された文字列と、変数「shift」の値が-3であることです。
println!("{} => {}", enc, dec);
"変数「enc」 => 変数「dec」"を出力します。
クロージャ
クロージャとはなんでしょうか?
Rust公式サイトには次のように書いてあります。
クロージャ: 環境をキャプチャできる匿名関数
環境をキャプチャ? 匿名関数?
まず、簡単な理解として、「クロージャは、関数の仲間」だと考えてください。
クロージャは引数を指定できます。クロージャを呼び出すと、定義した処理を行います。しかし、関数にはあった「名前」が、クロージャにはないのです。よって、「匿名関数(無名関数)」と呼ばれています。
さらに、クロージャは引数以外の変数を実行時の環境ではなく、自身が定義された環境において解決します。なので、「環境をキャプチャ」と言います。
以下は、クロージャを活用したシーザー暗号を作るプログラムです。
fn encrypt(s: &str, shift: i16) -> String {
let code_a = 'a' as i16;
let conv = |c| (((c-code_a+shift+26)%26+code_a)as u8)as char;
let enc1 = |c| if 'a' <= c && c <= 'z' {conv(c as i16)} else{c};
return s.chars().map(|c| enc1(c)).collect();
}
fn main(){
let enc = encrypt("hello, world!", 3);
let dec = encrypt(&enc, -3);
println!("{} => {}", enc, dec);
}
/******** 出力結果 *******
khoor, zruog! => hello, world!
*************************/
Rustでのクロージャの書式は以下のようになります。
let 名前 = |引数| 定義;
また、上のプログラムでは、charsメソッドを使って文字列を1文字ずつに分割し、一文字ずつに対してmapメソッドを使って処理を行い、最後にcollectメソッドで再び連結しています。
おわりに
おつかれさまでした。今回は、100DaysOfCode 七日目として、シーザー暗号を通してRustをより詳しく学びました。次回は、配列について学びます。
ご精読、ありがとうございました