Rustの基本的な型表現
組み込み型
Rustにおける組み込み型の表現はあまり一般的ではありません。
システムプログラミング言語という性質に起因するものとは思いますが、少しとっつきにくさはあります。
型エイリアスを使ってよくある表現に直すという手もなくはないですが、Rust的な書き方に慣れてしまう方が早いと思うので、少しずつ慣れていきましょう。
| 型名 | 扱える値 |
|---|---|
| i8 | -128 〜 127 |
| i16 | -32,768 〜 32,767 |
| i32 | -2,147,483,648 〜 2,147,483,647 |
| i64 | -9,223,372,036,854,775,808 〜 9,223,372,036,854,775,807 |
| i128 | ±170,141,183,460,469,231,731,687,303,715,884,105,728 |
| u8 | 0 〜 255 |
| u16 | 0 〜 65,535 |
| u32 | 0 〜 4,294,967,295 |
| u64 | 0 〜 18,446,744,073,709,551,615 |
| u128 | 0 〜 340,282,366,920,938,463,463,374,607,431,768,211,455 |
| isize | アーキテクチャ(32bit/64bit)依存の符号付き整数 |
| usize | アーキテクチャ(32bit/64bit)依存の符号なし整数 |
| f32 | 浮動小数点(単精度) |
| f64 | 浮動小数点(倍精度) |
| bool | true / false |
| char | Unicode文字 |
配列
コンパイル時にサイズが固定される同じ型の要素の集合。スタックメモリ上に配置されます。
fn main() {
// 宣言
let array = [1, 2, 3, 4, 5];
// 個別アクセス
println!("{}", array[0]);
// ループ処理
for element in array.iter() {
println!("{}", element);
}
}
ベクタ
いわゆるリストです。ヒープメモリ上に配置され、実行時にサイズ変更ができます。
fn main() {
// 作成
let mut v = Vec::new(); // 動的なサイズ変更により参照アドレスが変更になる恐れがあるため、mutが必要です
// 末尾に追加
v.push(1);
v.push(2);
v.push(3);
// 末尾から削除
let last = v.pop();
println!("{}", last.unwrap()); // このunwarap()は後で説明します
// インデックス1に10を挿入
v.insert(1, 10);
// インデックス0を削除
v.remove(0);
// 個別アクセス
println!("{}", v[0]);
// ループ処理
for i in v {
println!("{}", i);
}
}
なお、vec!を使うとより簡潔に定義できます(vec!の正体はもっと後で説明します)。
fn main() {
// vec!で簡潔に定義
let mut v = vec![1, 2, 3];
// 末尾から削除
let last = v.pop();
println!("{}", last.unwrap()); // このunwarap()は後で説明します
// インデックス1に10を挿入
v.insert(1, 10);
// インデックス0を削除
v.remove(0);
// 個別アクセス
println!("{}", v[0]);
// ループ処理
for i in v {
println!("{}", i);
}
}
構造体
他言語で言うところのクラスに近いですが、クラスと異なり、宣言内でメソッドを持つことができません(宣言内というのがポイント。詳細は後述)。
なので、C言語の構造体と同じ位置付けです。
struct [StructName] {
[field_name1]: field_type1,
[field_name2]: field_type2,
...フィールドの数だけ定義...
}
列挙型
特定の組み合わせ・有効値の範囲を定義するものです。
Rustの列挙型は非常に自由度が高く、異なる型表現や入れ子構造が可能です。
// 組み合わせの定義のみを行うシンプルな列挙型
enum Direction {
North,
South,
East,
West,
}
// 値を持つ列挙型
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
トレイト
概要
Rustのトレイトはインターフェースによく似た概念です。
共通して言えるのは「特定の振る舞いを持つことを約束する」機能だということ。
ただし、他言語では「振る舞いはクラス内で定義する」のに対して、Rustでは「振る舞いは別枠で追加定義する」という点で大きく捉え方が異なります。
一般的なプログラミング言語におけるメソッド実装方法
一般的なプログラミング言語では、クラスでまとめた値とそれに対する処理をひとまとめにして記述します。
class Person {
name: string;
greet() {
return `Hello, ${this.name}!`;
}
}
Rustにおけるメソッド実装方法
Rustでは前述の通り、構造体自体に直接メソッドを実装することはできません。
struct Person {
name: String,
fn greet() -> String { // これはエラーになる
format!("Hello, {}!", name)
}
}
代わりに、対象となる構造体に対してメソッドを追加定義します。
struct Person {
name: String,
}
// impl [構造体名]でメソッドを追加定義する
impl Person {
// インスタンス参照したい場合、第一引数に &self が必要
fn greet(&self) -> String {
format!("Hello, {}!", self.name)
}
}
Rust式のメリット
Rustのメソッド実装方法は一見するとただ面倒なだけに感じるかもしれません。
インスタンス参照する際に&selfをいちいち記載しなければいけないのは実際のところ面倒ではあります。
しかしながら、このRust式にはひとつ大きなメリットがあります。
それは構造体(Rustにおけるクラスの位置付け)がそのメソッドを実装していなくても、外部から追加することが可能ということです。
例えば他言語の場合、ライブラリなどで公開されているクラスに対して新しくメソッドを追加することは基本的にできません。
しかしながらRustでは、必要に応じて後からいくらでもメソッドを追加することができます。組み込み型でさえ例外ではありません。
これにより柔軟にプログラムの再利用が可能となる、というのがこの方式のメリットです。
が、1点だけ、守らなければいけないルールがあります。
Orphan Rule
異なるライブラリ間で、同じ構造体に対して同一名称のメソッドをそれぞれ追加実装していた場合、プログラムはどちらの振る舞いを取れば良いのかわからなくなってしまいます。
この問題を防ぐため、RustではOrphan Ruleというものが定義されています。
Orphan Rule:型かトレイトのどちらかが自分のクレートで定義されている場合のみ、impl可能
自身のクレート(他言語でいうところのライブラリのことです)で型かトレイトが定義されている場合のみ可能であるため、外部の構造体に対してメソッド実装するには自クレート内のトレイト定義が必須となります。
トレイトの基本的な利用方法
前述の考え方およびルールを踏まえ、例えば組み込み型であるi32に特定の振る舞いを追加したい場合は下記の通りとなります。
trait Greetable {
fn greet(&self) -> String;
}
impl Greetable for i32 {
fn greet(&self) -> String {
format!("Hello, {}!", self.to_string())
}
}
fn main() {
println!("{}", 5.greet());
}
基本構文は下記の通りとなります。
trait [TraitName] {
fn [method_name](argument: Type) -> ReturnType;
}
impl [TraitName] for TargetType {
fn [method_name](argument: Type) -> ReturnType {
...トレイト実装...
}
}
引数や戻り値としての利用方法
前述の通り、「特定の振る舞いを持つことを約束する」機能であることは他言語と変わりません。
そのためimpl [TraitName]と記述することで、引数や戻り値においてポリモーフィズム(多態性)として活用することが可能です。
trait Greetable {
fn greet(&self) -> String;
}
impl Greetable for i32 {
fn greet(&self) -> String {
format!("Hello, {}!", self.to_string())
}
}
fn main() {
let greetable = test_trait1();
println!("{}", test_trait2(greetable));
}
fn test_trait1() -> impl Greetable {
5
}
fn test_trait2(greetable: impl Greetable) -> String {
greetable.greet()
}
Rustの命名規則
Rustには公式スタイルガイドがあり、原則としてスネークケースによる命名規則が採用されています。
ただし、構造体名・列挙型名・トレイトについてはキャメルケースが採用されています。
守っていない場合はちゃんと警告表示が出るので、適宜修正しながら開発しましょう。
ジェネリクス
基本的な使い方
Rustでもジェネリック型は定義・利用可能です。
fn test<T>(item: T) -> T {
item
}
fn main() {
println!("{}", test(42));
println!("{}", test("hoge"));
println!("{}", test(3.14));
}
トレイト境界
単純なジェネリック型だと何でもありになってしまうため、関数やメソッド内で実行できる処理がほぼない形になってしまいます。
試しに先程の関数でitemの後に「.」を入れて何らかのメソッドを実行してみようとしてみてください。利用できるメソッドとしてはintoとtry_intoしか出てこないのがわかるかと思います。
こういった時に、特定の振る舞いを持つかどうかによって境界線を引くことをトレイト境界と言います。
何だか格好つけた分かりづらい言い回しですが、ようはジェネリック型に特定のトレイト実装していることを条件として付与することを意味します。
use std::fmt::Display;
fn test<T: Display>(item: T) {
println!("{}", item); // impl Displayが約束されているのでprintlnが利用可能
}
fn main() {
test(42);
test("hoge");
test(3.14);
}
基本構文は下記の通りです。
fn function_name<T: TraitType>(argument: T) -> ReturnType {
...処理...
}
// 複数のトレイト境界を持たせたい場合は下記のように記述します
fn function_name<T: FirstTraitType + SecondTraitType>(argument: T) -> ReturnType {
...処理...
}
お疲れ様でした!
お疲れ様でした。
Rustが他言語と大きく異なる点のひとつ、トレイトの考え方や利用方法は理解できたでしょうか?
次回は、Rustの最も重要な概念である所有権・ライフタイム・借用チェッカーについて学びます。
ここが非常に難関なので、頑張って突破しましょう!
Rust入門講座③所有権・ライフタイム・借用チェッカー・参照外し
Hope you enjoy it!