今回はRustの言語仕様について書いていきます。
言語仕様について全てを書くとなると大変なことになるため、個人的に重要だと思う箇所をピックアップして書いていきます。
もっと詳しい仕様を知りたいという方は下記ページを参照してください。
https://doc.rust-jp.rs/book-ja/
因みに、今回書きたかった内容に関してはtrait以降の部分ですので、仕様に関して理解しているという方はtrait以降の部分を読んで頂けるとありがたいです。
構造体
Rustにはクラスが存在しません。
他言語のクラスと同様のことをしたい場合は、構造体を使用します。
例として、C#と比較すると下記の様になります。
class Program {
static void Main() {
A a_class = new A();
a_class.set_i(9);
Console.WriteLine(a_class.get_i());
}
}
class A {
private int i;
public int get_i() {
return i;
}
public void set_i(int a1) {
i = a1;
}
}
fn main() {
let mut a = A::new();
// ↑ let mut a = A { i: 0 };
a.set_i(9);
println!("{}", a.get_i());
}
struct A {
i: i32,
}
impl A {
pub fn new() -> Self {
Self { i: 0 }
}
pub fn get_i(&self) -> i32 {
self.i
}
pub fn set_i(&mut self, a1: i32) {
self.i = a1;
}
}
C# → Rustで比較すると、
class→struct+impl
int→i32
public→pub
private→ (無記入)
void→fn
int型の関数→fn 関数名()->i32
となります。
Rustでインスタンスを生成する場合、new()関数を自身で作成するか、構造体の生成時に内部変数(Rustではフィールドといいます)に対して初期値を入れる必要があります。
コメントアウトされているlet a = A { i: 0 };が構造体生成の一般的な方法です。
(#[derive(Default)]を構造体に付与してA::default()で生成することもできます。)
implという他言語では見慣れない文言がありますが、implを使用することで構造体に対して関数、メソッドを付与することができます。
構造体に関数、メソッドを付与する場合は、structの名称でimplを作成し、その中に関数、メソッドを記入します。
因みに、関数かメソッドかの判別は、引数に(&self)が存在するかどうかです。
&selfは自身の構造体を指しているため、メソッド内でフィールドの値を参照したい場合にはself.~と書く必要があります。
この場合、引数に&selfを含まない関数と呼ばれるものはstaticと同様の扱いになるため、構造体を生成せずとも構造体名::関数名で呼び出すことができます。(::new()はそれと同様の意味になります。)
また、構造体に定数を持たせたい場合も、impl内に書き込みます。
Rustの関数では、戻り値にreturnを付ける必要がありません。returnを省略する場合は、;も省略して書きます。
Rustのコードでmutの文言が書かれているかと思いますが、mutはミュータブル(可変な変数)の意味になります。
Rustで宣言される変数はイミュータブル(不変な変数)になっているため、変数を上書きしたという場合は、ミュータブルで宣言する必要があります。
Rustで変数を宣言するときは、基本的に暗黙的な型変換を使用します。
明示的にしたい場合はlet a:i32;とすればaがi32になります。
型を明記しなくとも、ビルド時に変数の型が暗黙的に固定になるため、一つの変数に対して複数の型の値を入れようとするとコンパイルエラーになります。
(Visual Studio Codeでコーディングする場合は、rust-analyzerを使用するとエラーがわかりやすくなります。)
変数の所有権(借用・参照・move)
Rustは変数に対して変数を代入すると、変数が移動(move)します。
Rustの仕様で、メモリの使用を抑えるための仕組みといえば良いでしょうか?
ヒープとスタックが関係してくるのですが、ここで全て書くとなると、キリがないため、気になる方は下記ページを参照してください。
https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html
変数の移動でどのような事態が発生するかというと、下記の様なコードを書いて実行しようとするとエラーが発生します。
fn main() {
let a = A { i: 7. };
let b = a;
println!("{}", a.i);
println!("{}", b.i);
}
struct A {
i: f64,
}
ビルドをかけた時のエラーは下記の通りです。(ビルドのためのコマンドはcargo build)
error[E0382]: borrow of moved value: `a`
--> src\main.rs:6:20
|
4 | let a = A { i: 7. };
| - move occurs because `a` has type `A`, which does not implement the `Copy` trait
5 | let b = a;
| - value moved here
6 | println!("{}", a.i);
| ^^^ value borrowed here after move
エラーには、a.iはb = aを行ったタイミングでmoveしているため、a.iを借用することができなかったと出ます。
これが変数の移動(move)です。
このエラーを回避するためにはどのようにすればよいのか?という話になるのですが、そのためには参照を行います。
変数に対して&を付与することで、変数を移動させずに参照するという扱いになり、a.iの値を借用することが可能になります。
下記が参照を使った書き方です。
fn main() {
let a = A { i: 7. };
let b:&A = &a;
println!("{}", a.i);
println!("{}", b.i);
}
参照を行った場合の注意点ですが、上記コードで明記した通り、参照時の変数の型が&Aとなり、Aとは別の型という扱いになります。
また、参照値にはライフタイムというものが仕組まれており、スコープの外に出るとそのライフタイムが切れて変数を参照することができなくなります。(ライフタイムが切れない&’staticというものもあります)
上記の問題が解消できないという場合は.clone()を使用してコピーを作成します。
当然ですが、コピーを作成しているだけなので、元の値とは別の値として扱うことになります。
この辺りは、他の言語と比較するとかなり厄介かもしれません。
(ガベージコレクションを放棄してメモリ管理を行うことを考えると当然の仕様かとも思えますが)
trait
この辺りから、今回書きたかった内容です。(たどり着くまでが長い笑)
traitとは何なのかという話ですが、他言語で言うところのインターフェースです。
traitを使用することで、構造体に対してメソッドの実装を強制することができます。
サンプルは下記の通りです。
trait TraitA {
fn b(&self) -> f64;
}
struct StructA {
i: i32,
}
impl StructA {
fn get_i(&self) -> i32 {
self.i
}
}
impl TraitA for StructA {
fn b(&self) -> f64 {
// iをf64に変換して、9.9と足す
self.i as f64 + 9.9
}
}
上記の書き方をすると、StructAに対してTraitA のfn b()を定義することになります。
impl TraitA for StructA {} に対しては TraitAで定義されているfn b()しか定義できないため、StructAに対してメソッドを追加したい場合は別でimpl StructAを作成する必要があります。
継承(のようなもの)
Rustには継承は存在しません。
ですが、先ほどのtraitを使用することで、継承の様なものを実装することはできます。
trait TraitA {
fn b(&self) -> i32 {
self.get_a()
}
fn get_a(&self) -> i32;
}
struct StructA {
a: i32,
}
impl TraitA for StructA {
fn get_a(&self) -> i32 {
self.a
}
}
struct StructB {
a: i32,
}
impl TraitA for StructB {
fn get_a(&self) -> i32 {
self.a
}
}
TraitAに対してaのgetterを定義することで、StructAとStructBに対してfn b()の共通のメソッドを付与することができます。
ただ、この書き方だと大してコードの量が減らないので、この形で実装するメリットはあまりないです。
Getter
ここからはあまり言語仕様と関係がないです。
Rustにはcrateというものがあります。
他言語いうところのライブラリのようなもので、crateを追加することで機能を拡張することができます。
crateを追加するためには、Cargo.tomlファイルに対して追加する必要があります。
(以前、wasm の記事で同様のことを書いたかと思うので、説明は省きます)
因みに、lib.rs にコードを書くとcrateとしての扱いにすることができます。
crateでderive-gettersというcrateがあるのですが、derive-gettersを使用すると構造体のフィールドのgetterを自動で生成することができます。
サンプルのRust側のコードは下記の通りです。
use derive_getters::Getters;
fn main() {
let a = A { i: 7. };
print!("{}", a.i())
}
#[derive(Getters, Default, Clone)]
struct A {
i: f64,
}
上記のコードでは、#[derive(Getters)]をstruct Aに対して付与することで、フィールドのiのgetterを自動生成しています。(getterのi()の戻り値は&f64になります)
因みに、話の中で時々出ていた#[derive(~)]の書き方は上記のサンプルと同様に書きます。
おわりに
今回、長々と書いて、何を書きたかったかというと、getterを自動生成すればtraitを経由して継承できるのでは?と思ったということです。
冷静に考えれば不可能ですが(笑)
Gettersで自動生成されるgetterメソッドと、同様のメソッドをtaritに対して定義したとしても、自動生成されるgetterはimpl 構造体名{}に対してのメソッドでしかなく、impl trait名 for 構造体名{}のメソッドには成りえないためです。
なぜ、継承はできないのでしょうか…
まあ、実装すると不都合が発生するからだと思われますが、いずれRustにも継承が実装されると良いですね。