今回は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
にも継承が実装されると良いですね。