Closed8

Rust学習メモ1

Kentaro AzumaKentaro Azuma

Rustのシステムについて

The Rust Programming Language 日本語版を見ながらRustの重要な点をまとめていきます。

概要レベルの特徴

  • 低レイヤのプログラミングにおいて、プログラマの努力(メモリリークや排他制御など)をコンパイラが肩代わりする。

インストール(Linux)

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Hello,World

$ cargo new hello_world
$ cd hello_world
$ cargo run

cargo

  • Rustのビルドシステム兼パッケージマネージャ
  • cargo checkでコンパイルが通るか確認
  • cargo buildでビルド、デフォルトではdebugビルド
  • cargo build --releaseでreleaseビルド
  • cargo runでビルドから実行までワンストップ
  • Cargo.tomlに依存パッケージ(クレート)を記述すると、ビルド時に依存関係を再帰的に解決する
Kentaro AzumaKentaro Azuma

Rustプログラムの特徴(変数)

変数

イミュータブルとミュータブル

デフォルトでは変数はイミュータブル(不変)であるため、再代入はできない

let x = 5;

変数をミュータブル(可変)にしたい場合は変数名の前にmutをつける

let mut y = 6;
y = 8;

ただし、変数の型は変えることができない

シャドーイング

同じ名前の変数名に異なる値をバインドすることで、先にバインドされていた値を覆い隠す

fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {}", x);
    }
    println!("The value of x is: {}", x);
}

データ型

  • 整数、浮動小数点数、論理値、文字の4つはスカラー型であり、単独の値を表す
  • タプルと配列の2つは複合型であり、複数の値を一つの型にまとめて表す

整数

大きさ 符号付き 符号なし
8 bit i8 u8
16 bit i16 u16
32 bit i32 u32
64 bit i64 u64
数値リテラル
10進数 240
16進数 0xF0
8進数 0o360
2進数 0b1111_0000
_は見た目の区切り記号

浮動小数点

  • f32 : 単精度浮動小数点数
  • f64(デフォルト) : 倍精度浮動小数点数

論理値

  • true
  • false

文字

  • 文字型はUnicodeのスカラー値となる
  • リテラルはシングルクォートで囲む(文字列型はダブルクォーテーション)

タプル

  • タプルの位置ごとに型がある
  • 個々の値を取り出すにはパターンマッチングか、アクセスしたい値の番号をピリオド(.)に続けて書く
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
let five_hundred = tup.0;
let six_point_four = tup.1;
let one = tup.2;

配列

  • 配列の全要素は、 同じ型でなければならない
  • 配列のサイズは固定長で、一度宣言したら変更できない
let a: [i32; 5] = [1, 2, 3, 4, 5];

以下の2つは等価(シンタックスシュガー?)

let a = [3; 5];
let a = [3, 3, 3, 3, 3];
Kentaro AzumaKentaro Azuma

Rustプログラムの特徴(関数)

関数定義の仕方

  • fn function_name(x: i64, y: i32) -> i64 { x + y }
  • 関数と変数の命名規則は、スネークケース(全て小文字で単語の区切りにはアンダースコア)
  • 関数を定義する場所は、呼び出す場所より前でも後でも構わない
  • 各仮引数の型を必ず宣言しなければならない
  • 関数本体には文が並び、最後には式または文を置く
  • 関数本体ブロックの最後の式の値が戻り値になる(return キーワードを使用することもできる)

式と文

  • 文は何らかの動作をして値を返さない命令
  • 文は値を返さないので変数に代入することはできない
  • Rustではletによる代入は文となり、値を返さない
  • 式は結果値に評価される

要注意ポイント

fn plus_one(x: i32) -> i32 {
    x + 1 //OK
}

fn plus_one(x: i32) -> i32 {
    x + 1; //Error
}
Kentaro AzumaKentaro Azuma

Rustプログラムの特徴(制御フロー)

if式

if 条件式 { ... }の形を取る
式なのでlet文の右辺に置くことができる
ただし、評価した結果は同じ型である必要がある

 let condition = true;
 let number = if condition { 5 } else { 6 };

条件式はbool型でなければならないため、他の言語で見る以下の記述はできない

let number = 6;
if number {
    println!("number is not zero");
}

if文?

if式の例で紹介されているコードは文なのでは?

let number = 6;
if number % 4 == 0 {
    println!("number is divisible by 4");
} else if number % 3 == 0 {
    println!("number is divisible by 3");
} else if number % 2 == 0 {
    println!("number is divisible by 2");
} else {
    println!("number is not divisible by 4, 3, or 2");
}

loop(無限ループ)

let mut number = 3;
loop {
    println!("{}!", number);
    number -= 1;
    if number != 0 {
        break;
    }
}

breakで脱出しない限りループする
loopにも式と文の両方がある

breakとcontinue

デフォルトではbreakとcountinueは最も内側のループに適用される
指定したラベルに対してbreakとcontinueを適用することもできる

let mut number = 3;
'outer_loop: loop {
    'inner_loop: loop {
        number -= 1;
        if number != 0 {
            break 'outer_loop;
        }
    }
}

while(条件付きループ)

let mut number = 3;
while number != 0 {
    println!("{}!", number);
    number -= 1;
}

loopifelsebreakを使った同じ処理よりもネストが1段少ない

for(イテレーション)

インデックスを使わないため、範囲外アクセスを回避できる

let a = [10, 20, 30, 40, 50];
for element in a {
    println!("the value is: {}", element);
}

カウントアップとしての使い方もできる

for number in (1..4) {
    println!("{}", number);
}
Kentaro AzumaKentaro Azuma

所有権(Ownership)

Rustの最も大きな特徴の1つで、Rustプログラムがメモリを管理する規則の集合である。

  • Rustの各値は、所有者と呼ばれる変数と対応している(バインドとは別?)
  • ある時点において所有者は1つのみ
  • 所有者がスコープから外れたら、値は破棄される(メモリは自動的に返還される)

変数がスコープを抜ける時、Rustは特別な関数であるdropを呼び出してメモリを返還する。

Move

以下のコードはエラーになる

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

s1とs2はそれぞれスタック上に存在し、ヒープ上の同じメモリを参照する。
shallow copyに近いが、コンパイラがs1を無効化する点が異なる。
(所有権がs1からs2に移動した?)

Clone

deepcopyをする場合は明示的にclone()メソッドを呼び出す。

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

(s1とs2がそれぞれヒープ上の異なる領域を参照し、独立する所有権を持つ?)

Copy

コンパイル時にサイズが分かっている型はスタック上でのみ保持されるため、
変数の無効化が必要ない(つまり所有権はヒープ上のメモリを解放するためのもの?)
また、deepcopyとshallowcopyに違いがない。

所有権と関数

関数に変数を渡す際には代入と同様にムーブが行われるため、
関数呼び出し時に所有権が移動して渡した変数が無効化される。
また、関数が値を返す際も同様に所有権が戻り値に移動する。

参照と借用

データへの参照の場合は所有権を持たずにデータにアクセスできる
関数の引数に参照を取ることを借用と呼ぶ

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

参照にもイミュータブルな参照とミュータブルな参照がある。
ただし、以下の制約がある。

  • あるデータに対するミュータブルな参照は特定のスコープにおいて1つまで
  • あるデータに対してイミュータブルな参照がある場合にはミュータブルな参照はできない

スライス型

  • コレクション全体ではない、連続した要素を参照することができる
  • スライスは参照の一種なので所有権を持たない
  • 最初の数値を省略すると0が、最後の数値を省略すると末尾を指定した場合と等価
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let hello_world = &s[..];
Kentaro AzumaKentaro Azuma

ユーザ定義型(構造体)

Rustではユーザ定義型として構造体と列挙型がある

構造体

各フィールドを定義する

構造体を定義するにはstructキーワードを使って、構造体名、フィールド名とその型を定義する。
(最後に;がないから式?)

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

構造体のインスタンスを生成するには、各フィールドの値を指定する。
値を指定する順番は定義と同じである必要はない。
値を指定していないフィールドがあるとエラーになる。

let user1 = User {
    username: String::from("someusername123"),
    email: String::from("someone@example.com"),
    active: true,
    sign_in_count: 1,
};

特定のフィールドにアクセスする際にはドットを使用する。
ドットで指定したフィールドへの代入によって、フィールドの値を変更する。
このとき、インスタンス全体がミュータブルである必要がある。
一部のフィールドのみをミュータブルにすることはできない。

let mut user2 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};
user2.email = String::from("anotheremail@example.com");

Rustにはコンストラクタはない。
構造体の新規インスタンスを関数本体の最後の式として生成してそのインスタンスを返す形になる。

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

仮引数と構造体のフィールド名が同じ場合は、フィールド初期化省略記法を使用できる。

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

他のインスタンスの値を使用しつつ、一部を変更する形で新しいインスタンスを生成することもできる。

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

この場合には、構造体更新記法を使用できる。

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

メソッドを定義する

構造体のメソッドを定義するにはキーワードimplを使う。
そしてブロック内に関数を定義する。(最後に;がないから式?)
implブロックは複数定義することもできる。
メソッドの最初の引数は必ずselfになり、所有権を奪う/奪わない、
参照をミュータブル/イミュータブルにするなどの変更ができる。

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

関連関数

implブロック内にselfを引数に取らない関数を定義することもでき、その場合は関連関数と呼ぶ。

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}
Kentaro AzumaKentaro Azuma

ユーザ定義型(列挙型)

Rustではユーザ定義型として構造体と列挙型がある

列挙型

列挙型はenumとも呼ばれる。
enumは取りうる値を列挙することで型を定義する。

定義

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

V4とV6は列挙子と呼ばれる。

enumもimplを使ってメソッドを定義できる。

impl IpAddr {
    fn display(&self) {
        // メソッド本体はここに定義される
    }
}

let home = IpAddr::V4(127, 0, 0, 1);
home.display();
Kentaro AzumaKentaro Azuma

match式

マッチしたパターンに応じてコードを実行する。
パターンと、そのパターンに対応するコードを合わせてアームと呼ぶ。
各アームはカンマで区切られる。
すべてのありうるパターンを記述しなければコンパイルエラーとなる。
マッチのアームで複数行のコードを実行したい場合は、ブロックで囲む。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}


fn main() {
    let coin = Coin::Penny;
    let cent: u32 = value_in_cents(coin);
    println!("{} cents", cent);
}

実行結果は以下のようになる。

Lucky penny!
1 cents

マッチのアームでは、パターンにマッチした値の一部を取り出せる。

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

どんな値にもマッチする_というパターンを最後のアームに記述することで、
その前に指定したパターンのいずれにもマッチしない場合を処理することができる。

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

if let

1つのパターンにマッチした場合にだけ処理を実行したい場合にはif letによる記述が便利だ。

let mut count = 0;
match coin {
    // {:?}州のクォーターコイン
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

これは以下のコードと等価になる。

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}
このスクラップは1日前にクローズされました