イントロダクション
こんにちは!最近Rustを学び始めたhihimamu(旧mamuDev)です!
今回は、Bit演算を用いて賢く構造体を定義する方法を備忘録として書き残したいと思います。それではどうぞ!
目次
Rustに関して
Rustとは、WebブラウザFirefoxを開発したMozilla社が携わり、2006年に開発がはじめられたオープンソースのプログラミング言語です。プログラミングコミュニティ「Stack Overflow」が行った調査「Stack Overflow Developer Survey 2022」において、Rustは7年連続で「開発者が最も愛する言語」として選出されている人気の言語です。
Bit演算を用いた構造体定義
ユースケース
現在私はRustの学習のためにパスワードジェネレーターを作っています。主に、構造体、列挙型、メソッド定義、外部トレイト定義、外部クレート利用などの知識習得を目的としています。
パスワードジェネレーターの基本戦略として文字プールを用意しそこからRNGを用いて指定回数文字を選びだす、というものがあると思います。その文字プールの種類として大文字、小文字、英数字、記号とさらにそれらを組み合わせた24通りものパターンがあります。
これを列挙型として定義するのは骨が折れますし、何よりスマートさコンパクトさに欠けますよね。(だよね?)
対処法
ぶっちゃけます。数値データ型フィールドと大文字、小文字、英数字、記号の4つの定数を持った構造体一つで十分なのです。さて、どうやって24通りのパターンを表すか、そうBit演算がここで登場します。
論理和(OR)です。以下のコードで実装します。
use std::opus::BitOr;
#[derive(Debug, PartialEq, Copy, Clone)]
struct Pool {
value: u8
}
impl Pool {
const UPPERCASE: Self = Self { value: 1 };
const LOWERCASE: Self = Self { value: 2 };
const NUMBER: Self = Self { value: 4 };
const SYMBOL: Self = Self { value: 8 };
}
impl BitOr for Pool {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self {
value: self.value | rhs.value,
}
}
}
論理和を用いることで例えば、文字プールを大小文字にしよう!となったとき
let pool: Pool = Pool::UPPERCASE | Pool::LOWERCASE;
というコードを書くとUPPERCASEが持つvalue: 1[01]とLOWERCASEが持つvalue: 2[10]をOR演算し、value: 3[11]となります。
同様に大小文字英数字としたかったら、
let pool: Pool = Pool::UPPERCASE | Pool::LOWERCASE | Pool::NUMBER;
このように表せます。
なぜ論理和なのか、和ではだめなのか
はい、だめです。
Addトレイトを実装している前提でこのような場合を考えましょう。
let pool: Pool = Pool::UPPERCASE + Pool::UPPERCASE;
これはvalue:1 + value: 1が足されvalue: 2となります。おかしいですね、value: 2はLOWERCASEの値です。
そもそもUPPERCASE同士を足すのも変ですが、これではエラーにつながります。
論理和の場合、
let pool: Pool = Pool::UPPERCASE | Pool::UPPERCASE;
これはvalue: 1[01]とvalue: 1[01]の論理和でvalue: 1[01]となります。UPPERCASEとUPPERCASEを足してもUPPERCASEにしかならないということですね。これは良い!
具体的な文字プール(Stringまたは&str)はどうするのか?
ここでもう一つのBit演算、論理積(AND)が登場します。
以下のように実装します。
use std::ops::BitAnd;
impl BitAnd for Pool {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self {
value: self.value & rhs.value
}
}
}
論理積を用いてif文で分岐させると、スマートに文字プールを定義できます。
impl Pool {
fn to_string(pool: Pool) -> String {
let mut res: String = String::new();
if pool & Pool::UPPERCASE == Pool::UPPERCASE {
res.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
if pool & Pool::LOWERCASE == Pool::LOWERCASE {
res.push_str("abcdefghijklmnopqrstuvwxyz");
}
if pool & Pool::NUMBER == Pool::NUMBER {
res.push_str("0123456789");
}
if pool & Pool::SYMBOL == Pool::SYMBOL {
res.push_str("!@#$%^&*()-_=+[{{]}}\\|;:'\"");
}
res
}
}
なぜこれで適切な文字プールが返ってくるかというと、例えば
let pool: Pool = Pool::UPPERCASE | Pool::LOWERCASE;
とした時、pool(= value: 3[011])とPool::UPPERCASE(= value: 1[01])の論理積はUPPERCASE(= value: 1[01])となるためです。同様にpoolとLOWERCASEとの論理積もLOWERCASEと等しくなります。このそれぞれのif文を通過すると求めている文字プールが手に入るというわけです。
実に賢いロジックですね。
終わりに
今回解説したコードの全文です。
use std::ops::{BitOr, BitAnd};
#[derive(Debug, PartialEq, Copy, Clone)]
struct Pool {
value: u8
}
impl Pool {
const UPPERCASE: Self = Self { value: 1 };
const LOWERCASE: Self = Self { value: 2 };
const NUMBER: Self = Self { value: 4 };
const SYMBOL: Self = Self { value: 8 };
}
impl BitOr for Pool {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self {
value: self.value | rhs.value,
}
}
}
impl BitAnd for Pool {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self {
value: self.value & rhs.value
}
}
}
impl Pool {
fn to_string(pool: Pool) -> String {
let mut res: String = String::new();
if pool & Pool::UPPERCASE == Pool::UPPERCASE {
res.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
if pool & Pool::LOWERCASE == Pool::LOWERCASE {
res.push_str("abcdefghijklmnopqrstuvwxyz");
}
if pool & Pool::NUMBER == Pool::NUMBER {
res.push_str("0123456789");
}
if pool & Pool::SYMBOL == Pool::SYMBOL {
res.push_str("!@#$%^&*()-_=+[{{]}}\\|;:'\"");
}
res
}
}
又、Poolはvalue: u8を定義していますが、Pool(u8)のようにタプルを定義することでも同じ動作を望めます。
use std::ops::{BitOr, BitAnd};
#[derive(Debug, PartialEq, Copy, Clone)]
struct Pool(u8);
impl Pool {
const UPPERCASE: Self = Self(1);
const LOWERCASE: Self = Self(2);
const NUMBER: Self = Self(4);
const SYMBOL: Self = Self(8);
}
impl BitOr for Pool {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitAnd for Pool {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
}
impl Pool {
fn to_string(pool: Pool) -> String {
let mut res = String::new();
if pool & Pool::UPPERCASE == Pool::UPPERCASE {
res.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
if pool & Pool::LOWERCASE == Pool::LOWERCASE {
res.push_str("abcdefghijklmnopqrstuvwxyz");
}
if pool & Pool::NUMBER == Pool::NUMBER {
res.push_str("0123456789");
}
if pool & Pool::SYMBOL == Pool::SYMBOL {
res.push_str("!@#$%^&*()-_=+[{{]}}\\|;:'\"");
}
res
}
}
この記事をお読みいただきありがとうございました!これからも備忘録などをアップロードしていきたいと思います。