2022/09/23
はじめに
Rust の勉強を目的とし,Matrix クラスを作る.
それを通して得た知見を残す.
コード全体はこちら.
入門コース目次
- 入門 0 概要
- 入門 1 Struct とメソッド
- 入門 2 Unit Test
- 入門 3 演算子オーバーロードによるインデックスアクセス
- 入門 4 インスタンス生成用の自作マクロ
- 入門 5 module
- 入門 6 自作 Module を利用する
このページで作るもの
Struct の定義と,メソッドの実装
コード全体
use std::{
fmt::{Debug, Display},
ops::{AddAssign, SubAssign},
};
#[derive(Debug, PartialEq, Clone)]
pub struct Matrix<T> {
pub(super) width: u8,
pub(super) height: u8,
pub(super) data: Vec<T>,
}
impl<T> Matrix<T>
where
T: Display + AddAssign + SubAssign + Copy,
{
pub fn new(v_2d: Vec<Vec<T>>) -> Self {
let width = v_2d[0].len() as u8;
let height = v_2d.len() as u8;
let mut data = Vec::with_capacity(width as usize * height as usize);
v_2d.into_iter().for_each(|mut v_1d| {
data.append(&mut v_1d);
});
Self {
width,
height,
data,
}
}
#[inline(always)]
fn element_pos(&self, x: u8, y: u8) -> usize {
(x as u64 * self.width as u64 + y as u64) as usize
}
pub fn at(&self, x: u8, y: u8) -> &T {
&self.data[self.element_pos(x, y)]
}
pub fn at_mut(&mut self, x: u8, y: u8) -> &mut T {
let cursor = self.element_pos(x, y);
&mut self.data[cursor]
}
fn is_same_size(&self, dst: &Matrix<T>) -> bool {
if self.width != dst.width {
return false;
}
if self.height != dst.height {
return false;
}
true
}
fn same_size_check(&self, dst: &Matrix<T>) -> Result<(), ()> {
if self.is_same_size(dst) {
Ok(())
} else {
Err(())
}
}
pub fn add(&mut self, dst: &Matrix<T>) -> Result<(), ()> {
self.same_size_check(dst)?;
for i in 0..self.height {
for j in 0..self.width {
*self.at_mut(i, j) += *dst.at(i, j);
}
}
Ok(())
}
pub fn diff(&mut self, dst: &Matrix<T>) -> Result<(), ()> {
self.same_size_check(dst)?;
for i in 0..self.height {
for j in 0..self.width {
*self.at_mut(i, j) -= *dst.at(i, j);
}
}
Ok(())
}
pub fn flatten(self) -> Vec<T> {
self.data
}
pub fn print(&self) {
println!("-----------------");
for i in 0..self.height {
for j in 0..self.width {
print!("{} ", self.at(i, j));
}
println!();
}
// println!("\r]");
println!("-----------------");
}
}
解説
使うクレートの指定
use std::{
fmt::{Debug, Display},
ops::{AddAssign, SubAssign},
};
構造体定義
Rust では,構造体の定義はstructキーワードを用い,メソッドを実装する事で OOP が実現できる.
メソッドの実装は,C++とは異なり,Struct の定義と別に行う.トレイトに対する実装も同様.
pubキーワードは,publicを意味し,他の Module から見えるようにする.pub(...)のようにすることで,その視認性(visibility)を指定できる.
#[derive()]は,定義する構造体に基本的な実装を施してくれる.例えば,Debugは{:?}によるフォーマットを提供してくれるし,Cloneは.clone()によるコピーを可能にしてくれる.
Generics の定義は他言語と同様で,<>で囲む.<T>とすることで,Tは未知の型を扱うことが出来る.UpperCamelCase が推奨となる.慣例的には,名前をもたせる必要がなければ大文字アルファベット一文字にすることが多い.
#[derive(Debug, PartialEq, Clone)]
pub struct Matrix<T> {
pub(super) width: u8,
pub(super) height: u8,
pub(super) data: Vec<T>,
}
Struct へのメソッド実装
メソッドは Struct 定義とは別で,implキーワードを用いて行う.
Generics も以下のように記述する.
ここで,見慣れないwhere句があると思うが,これは,未知の型Tに対する制約(bound)を与えてくれる.
AddAssignによって+=を,SubAssignによって-=を,Tが計算できることを制約する.
これにより,コンパイラが Generics に対するエラー表示をより簡潔に行うことができるとのこと.
impl<T> Matrix<T>
where
T: Display + AddAssign + SubAssign + Copy,
{
whereの代わりにimpl<T: Display + AddAssign>のようにしてもよいが,冗長になるのでここでは敢えてwhere句を使っている.
インスタンス生成用関数
関数の視認性もpubによって定義できる.
慣例としてnew()という名前でインスタンス生成を行うことが多いので,それを踏襲した.
new()という名前に対しては制約は特に無いので,場合によってはVec::with_capacity()のように,他の初期化関数を作ることもある.
ちなみに,new()はメソッドではない.これは型関連関数(associated functions)と呼ばれるもので,他の言語でクラス内に実装するstatic関数と同じ使い方が出来る.
pub fn new(v_2d: Vec<Vec<T>>) -> Self {
let width = v_2d[0].len() as u8;
let height = v_2d.len() as u8;
let mut data = Vec::with_capacity(width as usize * height as usize);
v_2d.into_iter().for_each(|mut v_1d| {
data.append(&mut v_1d);
});
Self {
width,
height,
data,
}
}
要素の位置を算出するメソッド
クラス内でのみ用いるプライベートメソッドは,pubをつけなければ良い.
element_pos()は,計算に名前をつけたかったし,同じロジックを複数ヶ所で用いるために関数にしたが,関数呼び出しコストをかけてほしくない.
そこで,コンパイラが勝手にインライン展開してくれるかもしれないが,念の為inline属性をつけた.
Rust は C++とは違い,暗黙的な型変換が行われないので,asによってu8をu64に,u64をusizeに変換した.
#[inline(always)]
fn element_pos(&self, x: u8, y: u8) -> usize {
(x as u64 * self.width as u64 + y as u64) as usize
}
要素の参照取得メソッド
上記で定義したelement_pos()によって算出したインデックス番号の要素への参照を返す.
注意したいのは,これはイミュータブルな参照なので,受け取った際には値を変更することは出来ない.
pub fn at(&self, x: u8, y: u8) -> &T {
&self.data[self.element_pos(x, y)]
}
要素の mut 参照取得メソッド
呼び出し時にメソッド名から「これはミュータブルなんだなぁ」と分かるように要素の参照取得メソッドat()にmutをつけたat_mut()として定義した.
mutな参照を返すためには,&selfを&mut selfにする必要がある.
pub fn at_mut(&mut self, x: u8, y: u8) -> &mut T {
let cursor = self.element_pos(x, y);
&mut self.data[cursor]
}
Matrix のサイズチェックメソッド(bool)
後述のadd()およびdiff()のために,別のMatrixとサイズが等しいか確認するメソッドを書いた.
fn is_same_size(&self, dst: &Matrix<T>) -> bool {
if self.width != dst.width {
return false;
}
if self.height != dst.height {
return false;
}
true
}
Matrix のサイズチェックメソッド(Result)
add()/ diff()内でのサイズチェックを簡潔に記述するため,Result型で返すメソッドを準備した.
fn same_size_check(&self, dst: &Matrix<T>) -> Result<(), ()> {
if self.is_same_size(dst) {
Ok(())
} else {
Err(())
}
}
Matrix の加算メソッド
他のMatrixのインスタンスの各要素の値を,自身に加算するメソッド.
戻り値がResultなのは,失敗する可能性があるためで,行列のサイズが異なると失敗するようにした.
same_size_check()は,Resultを返すので,?でエラー処理を行う.
行列のサイズが異なる場合に早期リターンと同じ役割を果たす.
0..self.heightは,Range<u8>を返す.0..self.widthも同様.
処理速度の面で言うと,i in 0..self.data.len()でself.data[i]に直接加算したほうが良いが,ここではこのように記述した.
pub fn add(&mut self, dst: &Matrix<T>) -> Result<(), ()> {
self.same_size_check(dst)?;
for i in 0..self.height {
for j in 0..self.width {
*self.at_mut(i, j) += *dst.at(i, j);
}
}
Ok(())
}
また,入力のdst: &Matrix<T>だが,ここは参照渡しにしてあり,呼び出し側の入力行列を移動しないようにした.
クラスの用途によっては,dst: Matrix<T>のようにするほうが適切なこともある.こうした場合,入力したdstは呼び出し側では再利用できない.
Matrix の減算メソッド
add()と同様
pub fn diff(&mut self, dst: &Matrix<T>) -> Result<(), ()> {
self.same_size_check(dst)?;
for i in 0..self.height {
for j in 0..self.width {
*self.at_mut(i, j) -= *dst.at(i, j);
}
}
Ok(())
}
行列を一つの配列に変換
flatten(&self)ではなくflatten(self)にしてあるが,これにより,メソッド呼び出し時にデータを移動することが出来る.
つまり,Matrix内で使っていたdataをそのまま取り出してVec<T>として受け取る.
clone()なしでこのメソッドを呼び出したら,そのインスタンスは移動済みになる.(抜け殻のような状態)
まだ使いたいインスタンスでこのメソッドを呼び出したい場合はmat.clone().flatten()のようにしたら良いが,インスタンスをコピーするコストがかかる.
pub fn flatten(self) -> Vec<T> {
self.data
}
行列を表示する
内容をキレイに表示できると嬉しいので,このようなものを作った.
pub fn print(&self) {
println!("-----------------");
for i in 0..self.height {
for j in 0..self.width {
print!("{} ", self.at(i, j));
}
println!();
}
println!("-----------------");
}
}
おわりに
ここまでだけでも Rust の洗練された言語仕様を体感できますねぇ