自己紹介
出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。
Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。
環境
新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.38.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell
前回
前回は所有権と移動について学びました。
Rust勉強中 - その11
参照と借用
参照は以前型の学習の際に出てきたポインタ型の一種です。参照は、所有権を持ちません。ある値を参照を作成することをRustでは借用と言います。
なぜ借用が必要か
fn boring(v1: Vec<i32>) -> Vec<i32> {
v1
}
fn main() {
let mut v1 = vec![0, 1, 2];
v1 = boring(v1); // move and return ownership
println!("v1 = {:?}", v1);
}
例えばもし、借用がないとした場合、上記のコードのように変数v1の値の所有権を関数boringへ移動させ、戻り値に再びその値の所有権を返さなくてはなりません。このような関数すべてに、所有権の移動と返却は面倒です。
そこで、ある値の参照を借用する仕組みがRustにあります。
共有参照と可変参照
借用には以下の2種類あります。
- 共有参照(shared reference)
Copy型の読込み専用の参照ポインタ。その値に対する共有参照は複数生成可能です。 - 可変参照(mutable reference)
Copy型ではない読書き専用の参照ポインタ。その値に対する可変参照や共有参照は同時に作成できません。
fn shared_reference(rv1: &Vec<i32>) { // shared reference1
println!("[shared reference1] rv1 = {:?}", rv1);
let v2 = vec![0, 1, 2];
let rv2 = &v2; // shared reference2
}
fn mutable_reference(rv1: &mut Vec<i32>) { // mutable reference1
println!("[mutable reference] rv1 = {:?}", rv1);
let mut v2 = vec![0, 1, 2];
let rv2 = &mut v2; // mutable reference2
rv1[0] = 3;
}
fn main() {
let mut v1 = vec![0, 1, 2];
v1 = boring(v1); // move and return ownership
println!("v1 = {:?}", v1);
shared_reference(&v1);
println!("[after shared_reference] v1 = {:?}", v1);
mutable_reference(&mut v1);
println!("[after mutable_reference] v1 = {:?}", v1);
}
v1 = [0, 1, 2]
[shared reference1] rv1 = [0, 1, 2]
[after shared_reference] v1 = [0, 1, 2]
[mutable reference] rv1 = [0, 1, 2]
[after mutable_reference] v1 = [3, 1, 2]
shared_reference関数は、引数にベクタ型の共有参照を要求します。また、関数内では共有参照の引数を出力し、新たなベクタ型の共有参照を作っています。引数の渡し方は、&v1
という形式で渡します。
mutable_reference関数は、引数にベクタ型の可変参照を要求します。また、関数内では可変参照の引数を出力し、新たなベクタ型の可変参照を作っています。最後には、可変参照の引数の値を変更しています。すると、呼び出し側の変数v1のベクタの値も同じように変更されています。引数の渡し方は、&mut v1
という形式で渡します。
値の所有権を移動させて引数を渡す方法を値渡し。参照を引数として渡すことを参照渡しと言います。
参照解決
Rustでは明示的に参照を作成する際は、&演算子を、参照解決(dereference)をするときは、*演算子を付けます。
fn dereference() {
let x = 0;
println!("x = {} {}", x, get_type(x));
let rx = &x;
println!("rx = {} {}", rx, get_type(rx));
let dx = *rx;
println!("dx = {} {}", dx, get_type(dx));
let mut y = 0;
let dy;
println!("y = {} {}", y, get_type(y));
{
let ry = &mut y;
*ry += 10;
dy = *ry;
}
println!("dy = {} {}", dy, get_type(dy));
}
x = 0 i32
rx = 0 &i32
dx = 0 i32
y = 0 i32
dy = 10 i32
参照の置き換え
Rustではある変数に格納された参照を違う参照に置き換えることもできます。
fn change_reference() {
let x = 0;
let y = 1;
let mut r = &x;
println!("r = {} {:p}", r, r);
r = &y;
println!("r = {} {:p}", r, r);
}
r = 0 0x9055f8f708
r = 1 0x9055f8f70c
参照同士の比較
参照同士の比較も行えます。
fn compare_reference() {
let x = 0;
let y = 1;
let rx = &x;
let ry = &y;
let rrx = ℞
let rry = &ry;
println!("rrx<=rry (0<=1) = {}", rrx<=rry);
println!("rrx==rry (0==1) = {}", rrx==rry);
// println!("rrx<=1 (0==1) = {}", rrx<=1); // error
}
ただし例えば、参照と数値の比較は行えないようです。
この時、比較の際には参照を順にたどっていき、値同士の比較を行っています。つまり、暗黙の参照解決が行われています。
Option<&T>
Rustの参照はnullにはなりません。
もし、CやC++のようにnullポインタを表現する場合は、Option<&T>を使います。
fn option_reference(ri: &i32) -> Option<&i32> {
if *ri>0 {
Some(ri)
} else {
None
}
}
fn main() {
...
let i = 0;
println!("option_reference(&i) = {:?}", option_reference(&i));
}
option_reference(&i) = None
このようにすれば、Option型を使う際に確認が必要なので、チェック漏れを防ぐことができます。
ライフタイム(lifetime)
※(注意): 以降は私の理解した「ライフタイムとは」なので、ご注意ください。
ライフタイムは、ある参照を安全に利用できる期間をいいます。
なぜ、ライフタイムが必要か
大前提として、ライフタイムの存在理由は、参照をダングリングポインタ(dangling pointer)にさせないためです。ダングリングポインタとは、既にドロップされたメモリ領域を指すポインタのことです。ダングリングポインタを参照してしまうと予期せぬ動作を引き起こしてしまいます。
このダングリングポインタを作らないために、ライフタイムは存在します。
fn reference_error1() {
let r;
{
let x = 0;
r = &x;
} // x dropped here
println!("r = {}", r); // r is dangling pointer
}
上記コードをコンパイルすると、
error[E0597]: `x` does not live long enough
--> .\src\main.rs:80:9
|
80 | r = &x;
| ^^^^^^ borrowed value does not live long enough
81 | } // x dropped here
| - `x` dropped here while still borrowed
82 | println!("r = {}", r); // r is dangling pointer
| - borrow later used here
error: aborting due to previous error
エラーを吐きます。これは、変数rがダングリングポインタを指すためです。
ライフタイムによる制約ルール
- ある変数の参照は、その変数よりも長生きできない。なぜなら、参照先の変数が先にドロップした場合、参照はダングリングポインタを指すため
- ある変数に格納した参照は、その格納した変数と少なくとも同じだけ生きなければいけない。なぜなら、参照が先にドロップすれば変数がダングリングポインタを指すため
(ここで私は大混乱)
制約ルール1
ある変数の参照は、その変数よりも長生きできないとは、どういうことかといいますと、
fn reference_error1() {
let r;
{
let x = 0; // ある変数x
r = &x; // ある変数xの参照のライフタイム開始 ->
} // <- ある変数xをドロップで、ある変数xの参照のライフタイム終了
println!("r = {}", r); // ある変数xの参照はここまで長生きしてはいけない
}
- ある変数xを初期化
- 変数rにある変数xの参照を格納 (ある変数xの参照のライフタイム開始)
- ある変数xがスコープから外れるためドロップ。(ある変数xの参照のライフタイム終了)
- 変数rが指す参照先を出力(ダングリングポインタを参照)
2.で参照のライフタイムが開始し、3.で終了します。この時点で参照の役目を終わらなければなりません。なぜなら、4.のようにある変数xが先にドロップしてダングリングポインタを指してしまうためです。
制約ルール2
ある変数に格納した参照は、その格納した変数と少なくとも同じだけ生きなければいけないとは、どういうことかというと、
fn reference_error1() {
let r; // 変数rの初期化
{
let x = 0;
r = &x; // ある変数xの参照 // ここから ->
}
println!("r = {}", r);
} // <- 変数rをドロップで、ある変数xの参照は少なくともここまで生きていなければいけない
- 変数rを初期化
- ある変数xの参照を変数rに格納
- 変数rがスコープから外れるためドロップ
ある変数xの参照は2.の時点から、3.の変数rがドロップされるまで少なくとも生きていなければいけません。なぜなら、参照が先にドロップしてしまうと、変数rが指す先はダングリングポインタとなるためです。
まとめると、変数xの参照を変数rに格納した場合、変数xの参照は変数rがドロップするまで生き、変数xがドロップするまでに役目を終えなければいけないということですね。
繰り返しますが、これらはすべてダングリングポインタとなるのを防ぐためです。
ライフタイムパラメータ
ライフタイムパラメータは、明示的にライフタイムを指定するパラメータのことです。
static mut S: &i32 = &0;
fn reference_error2<'a>(p: &'a i32) {
unsafe {
S = p;
}
}
fn main() {
...
reference_error2(&1);
}
'a
がライフタイムパラメータでtick Aやlifetime Aと呼ぶそうです。「任意のライフタイム'a」という意味だそうです。今まで、実はこのライフタイムパラメータは省略されており、明示的に示すと上記のようになります。上記の引数の意味は「任意のライフタイムaを持つi32型への参照の引数p」ということですね。また、コンパイラは任意のライフタイムに対しては、可能な限り短いライフタイムを想定します。上記の`aの場合は、reference_error2関数内ということになります。
上記の例ではmutableなstatic変数Sを初期化し、referrence_error2関数内で変数Sに引数pを代入しています。
これをコンパイルすると、
error[E0312]: lifetime of reference outlives lifetime of borrowed content...
--> .\src\main.rs:89:13
|
89 | S = p;
| ^
|
= note: ...the reference is valid for the static lifetime...
note: ...but the borrowed content is only valid for the lifetime 'a as defined on the function body at 87:21
--> .\src\main.rs:87:21
|
87 | fn reference_error2<'a>(p: &'a i32) {
| ^^
エラーを吐きます。これは、先に示した制約ルール2に反しています。つまり、参照pは変数Sと少なくとも同じだけ生きなければいけません。しかし、参照pのライフタイムは関数内までです。関数を抜けた後は変数Sはダングリングポインタとなります。
変数Sはstaticなので、プログラム全体をライフタイムとします。このようなライフタイムを「'staticライフタイム」といいます。
では、引数をstaticなライフタイムにすれば良いということになります。
static mut S: &i32 = &0;
fn reference_error2(p: &'static i32) {
unsafe {
S = p;
}
}
fn main() {
...
reference_error2(&1);
}
あるいは、static変数を引数に指定しても良いです。
static mut S: &i32 = &0;
static P: &i32 = &10;
fn reference_error2(p: &'static i32) {
unsafe {
S = p;
}
}
fn main() {
...
reference_error2(P);
}
しかし、先ほどのreference_error2関数にmain関数内で定義した変数を引数に入れると、エラーになります。
static mut S: &i32 = &0;
static P: &i32 = &10;
fn reference_error2(p: &'static i32) {
unsafe {
S = p;
}
}
fn main() {
...
let i = 0;
reference_error2(&i);
}
error[E0597]: `i` does not live long enough
--> .\src\main.rs:109:22
|
109 | reference_error2(&i); // error
| -----------------^^-
| | |
| | borrowed value does not live long enough
| argument requires that `i` is borrowed for `'static`
110 | }
| - `i` dropped here while still borrowed
error: aborting due to previous error
これは、制約ルール2に違反します。つまり、変数iは先にドロップしてしまうため、変数Sに格納した変数iの参照はダングリングポインタとなります。
戻り値の参照
fn ret0<'a>(v: &'a Vec<i32>) -> &'a i32 {
&v[0]
}
fn reference_error3() {
let r;
{
let v = vec![0, 1, 2];
r = ret0(&v);
}
println!("r = {}", r);
}
fn main() {
...
reference_error3();
}
error[E0597]: `v` does not live long enough
--> .\src\main.rs:101:18
|
101 | r = ret0(&v);
| ^^ borrowed value does not live long enough
102 | }
| - `v` dropped here while still borrowed
103 | println!("r = {}", r);
| - borrow later used here
error: aborting due to previous error
これも制約ルール2に関するエラーです。これは最初に示した例と同じで、変数vが先にドロップするため、戻り値の参照はダングリングポインタとなります。
reference_error3関数の定義から、引数と戻り値は同じライフタイムaを持っているということに注意です。
複数のライフタイムパラメータ
以下のように複数の引数の参照、複数の戻り値の参照のような状況を考えてみます。
fn multi_life(v1: &Vec<i32>, v2: &Vec<i32>) -> (&i32, &i32) {
(&v1[0], &v2[0])
}
fn reference_error4() {
let v1 = vec![0, 1, 2];
let r1;
{
let v2 = vec![3, 4, 5];
let r2;
{
let r = multi_life(&v1, &v2);
r1 = r.0;
r2 = r.1;
}
println!("r2 = {:?}", r2);
}
println!("r1 = {:?}", r1);
}
これをコンパイルすると、以下のようなエラーが発生します。
error[E0106]: missing lifetime specifier
--> .\src\main.rs:106:49
|
106 | fn multi_life(v1: &Vec<i32>, v2: &Vec<i32>) -> (&i32, &i32) {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed
from `v1` or `v2`
error[E0106]: missing lifetime specifier
--> .\src\main.rs:106:55
|
106 | fn multi_life(v1: &Vec<i32>, v2: &Vec<i32>) -> (&i32, &i32) {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed
from `v1` or `v2`
error: aborting due to 2 previous errors
2つの戻り値にライフタイムパラメータを要求しています。なぜかというと、コンパイラからすると、v1かv2のどちらのライフタイムを持った要素の参照かが分からないからです。これらを明示的に指定してあげる必要があります。
では、次に以下のように指定してみます。引数も戻り値も同じライフタイムです。
fn multi_life<'a>(v1: &'a Vec<i32>, v2: &'a Vec<i32>) -> (&'a i32, &'a i32) {
(&v1[0], &v2[0])
}
fn reference_error4() {
let v1 = vec![0, 1, 2];
let r1;
{
let v2 = vec![3, 4, 5];
let r2;
{
let r = multi_life(&v1, &v2);
r1 = r.0;
r2 = r.1;
}
println!("r2 = {:?}", r2);
}
println!("r1 = {:?}", r1);
}
これをコンパイルすると、以下のようなエラーが発生します。
error[E0597]: `v2` does not live long enough
--> .\src\main.rs:117:37
|
117 | let r = multi_life(&v1, &v2);
| ^^^ borrowed value does not live long enough
...
122 | }
| - `v2` dropped here while still borrowed
123 | println!("r1 = {:?}", r1);
| -- borrow later used here
error: aborting due to previous error
この時のライフタイムaは可能な限り短いライフタイムとして変数v1のライフタイムを想定します。このような想定で考えると、変数v2の参照は変数v1がドロップするまで長生きしなければいけません。しかし、先に変数v2とその参照はドロップしてしまうため、エラーを吐いてしまいます。
よって変数v1と変数v2はそれぞれ違うライフタイムを持つので、以下のようにライフタイムパラメータを指定してあげます。
fn multi_life<'a, 'b>(v1: &'a Vec<i32>, v2: &'b Vec<i32>) -> (&'a i32, &'b i32) {
(&v1[0], &v2[0])
}
fn reference_error4() {
let v1 = vec![0, 1, 2];
let r1;
{
let v2 = vec![3, 4, 5];
let r2;
{
let r = multi_life(&v1, &v2);
r1 = r.0;
r2 = r.1;
}
println!("r2 = {:?}", r2);
}
println!("r1 = {:?}", r1);
}
このようにすれば、それぞれ違うライフタイムだとコンパイラに示せるので、コンパイルも通り実行されます。
移動された参照
ある変数xの参照を格納した後、その変数xが別の変数へ所有権を移動させてしまった場合、変数xの参照はダングリングポインタとなります。
fn reference_error5() {
let v1 = vec![0; 3];
let r = &v1;
let v2 = v1; // この時、v1は未初期化状態になる
println!("r = {:?}", r); // rは未初期化状態のv1を参照
}
error[E0505]: cannot move out of `v1` because it is borrowed
--> .\src\main.rs:129:14
|
128 | let r = &v1;
| --- borrow of `v1` occurs here
129 | let v2 = v1;
| ^^ move out of `v1` occurs here
130 | println!("r = {:?}", r);
| - borrow later used here
error: aborting due to previous error
こういうところもチェックしてくれてエラーを吐いてくれます!
ソース
#![allow(unused)]
fn get_type<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
fn boring(v1: Vec<i32>) -> Vec<i32> {
v1
}
fn shared_reference(rv1: &Vec<i32>) { // shared reference1
println!("[shared reference1] rv1 = {:?}", rv1);
let v2 = vec![0, 1, 2];
let rv2 = &v2; // shared reference2
}
fn mutable_reference(rv1: &mut Vec<i32>) { // mutable reference1
println!("[mutable reference] rv1 = {:?}", rv1);
let mut v2 = vec![0, 1, 2];
let rv2 = &mut v2; // mutable reference2
rv1[0] = 3;
}
fn dereference() {
let x = 0;
println!("x = {} {}", x, get_type(x));
let rx = &x;
println!("rx = {} {}", rx, get_type(rx));
let dx = *rx;
println!("dx = {} {}", dx, get_type(dx));
let mut y = 0;
let dy;
println!("y = {} {}", y, get_type(y));
{
let ry = &mut y;
*ry += 10;
dy = *ry;
}
println!("dy = {} {}", dy, get_type(dy));
}
fn change_reference() {
let x = 0;
let y = 1;
let mut r = &x;
println!("r = {} {:p}", r, r);
r = &y;
println!("r = {} {:p}", r, r);
}
fn compare_reference() {
let x = 0;
let y = 1;
let rx = &x;
let ry = &y;
let rrx = ℞
let rry = &ry;
println!("rrx<=rry (0<=1) = {}", rrx<=rry);
println!("rrx==rry (0==1) = {}", rrx==rry);
// println!("rrx<=1 (0==1) = {}", rrx<=1); // error
}
fn option_reference(ri: &i32) -> Option<&i32> {
if *ri>0 {
Some(ri)
} else {
None
}
}
fn reference_error1() {
let r: &i32;
{
let x = 0;
// r = &x;
} // x dropped here
// println!("r = {}", r); // r is dangling pointer
}
static mut S: &i32 = &0;
static P: &i32 = &10;
fn reference_error2(p: &'static i32) {
unsafe {
S = p;
}
}
fn ret0<'a>(v: &'a Vec<i32>) -> &'a i32 {
&v[0]
}
fn reference_error3() {
// let r;
{
let v = vec![0, 1, 2];
// r = ret0(&v);
}
// println!("r = {}", r);
}
fn multi_life<'a, 'b>(v1: &'a Vec<i32>, v2: &'b Vec<i32>) -> (&'a i32, &'b i32) {
(&v1[0], &v2[0])
}
fn reference_error4() {
let v1 = vec![0, 1, 2];
let r1;
{
let v2 = vec![3, 4, 5];
let r2;
{
let r = multi_life(&v1, &v2);
r1 = r.0;
r2 = r.1;
}
println!("r2 = {:?}", r2);
}
println!("r1 = {:?}", r1);
}
fn reference_error5() {
let v1 = vec![0; 3];
let r = &v1;
let v2 = v1;
println!("r = {:?}", r);
}
fn main() {
let mut v1 = vec![0, 1, 2];
v1 = boring(v1); // move and return ownership
println!("v1 = {:?}", v1);
shared_reference(&v1);
println!("[after shared_reference] v1 = {:?}", v1);
mutable_reference(&mut v1);
println!("[after mutable_reference] v1 = {:?}", v1);
dereference();
change_reference();
compare_reference();
let i = 0;
println!("option_reference(&i) = {:?}", option_reference(&i));
reference_error1();
reference_error2(&1);
reference_error2(P);
// reference_error2(&i); // error
reference_error3();
reference_error4();
reference_error5();
}
今回はここまで~!
参照とライフタイムを学習しました。私にとって非常に理解が難しいです。そしてこれから慣れるまでしばらくライフタイムと戦うことになると思います。ただ、これらは全て参照の安全を守るためです。これに慣れることで安全なコードの書き方にも慣れると思えば苦しくありませんね!
慣れるために書いて書いて書きまくるのみです!