ys memos

Blog

Rust入門 Matrixクラス 1 Structとメソッド


rust

2022/09/23


Rust の勉強を目的とし,Matrix クラスを作る.

それを通して得た知見を残す.

コード全体はこちら



Struct の定義と,メソッドの実装


src/matrix/matrix.rs
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 定義とは別で,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によってu8u64に,u64usizeに変換した.

    #[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)]
    }

呼び出し時にメソッド名から「これはミュータブルなんだなぁ」と分かるように要素の参照取得メソッド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]
    }

後述の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
    }

add()/ diff()内でのサイズチェックを簡潔に記述するため,Result型で返すメソッドを準備した.

    fn same_size_check(&self, dst: &Matrix<T>) -> Result<(), ()> {
        if self.is_same_size(dst) {
            Ok(())
        } else {
            Err(())
        }
    }

他の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は呼び出し側では再利用できない.


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 の洗練された言語仕様を体感できますねぇ


関連タグを探す