C# で Rust する

に公開4

まずは Rust の所有権について理解する

Rust が初めて触るプログラミング言語でない場合、所有権システムは完全に忘れたほうが良いです。プログラミング言語としての根本は他の言語と変わらないので Rust の特殊性のみ抑えればおkです。

  • 所有権: インスタンス(実体)のこと
  • 借用: ポインターのこと
  • Rust の特殊性
    1. ✅ 全ての操作が「移動渡し(実体を移動する)」(std::move
      • ✅ 値渡し(コピー渡し)/参照渡しという概念が無い(C# の参照渡しは参照の値渡し)
      • ❌ 構造体だから値渡し(コピー)して所有権だけを移動しているって訳よ!(違う)
      • ❌ 移動ってどこでもドアと同じでコピーしてから元を消すってことだよね?(技術的詳細の話じゃない)
    2. ✅ 可変ポインター(&mut == T* T* const)は同時に一つしか存在できない
      • ✅ どうして? ← Rust だから
    3. 番外: Rust にクラス(参照型)の概念はない

std::move は「コピーしてから元のデータを nullptr(0)にする」もので、これにより「実行時に」予期せぬデータの書き換えと読み取りを防ぐことが出来ます。

Rust はボローチェッカー(≒リンター/アナライザー)によってコレを実行時ではなく「コンパイル時に」「静的に」完全にチェックできるのが革新的。

(なので C# でもボローチェッカーを作れば同じメモリ安全性を手に入れられるハズ。GC レスにはできないが!)

ローカル変数間のコピーも不可能。(エラーに書いてある通り Copy トレイトを実装すればできる)

実行環境: https://play.rust-lang.org/

struct Test {
}

fn main() {
    let foo = Test { };
    let _bar = foo;
    let _baz = foo;
}
error[E0382]: use of moved value: `foo`
 --> src/main.rs:7:16
  |
5 |     let foo = Test { };
  |         --- move occurs because `foo` has type `Test`, which does not implement the `Copy` trait
6 |     let _bar = foo;
  |                --- value moved here
7 |     let _baz = foo;
  |                ^^^ value used here after move
  |
note: if `Test` implemented `Clone`, you could clone the value
例)再帰的に移動チェックが行われる
let foo = Foo { value: ... };

// エラー: 一部が移動された 'foo' を操作しています
bar(foo.value, foo);
               ~~~

// 👇 順番を変えると、、、

// エラー: 移動したインスタンスへのアクセスを試みています
baz(foo, foo.value);
         ~~~~~~~~~
その他箇条書き
  • 実体には常に所有権が付きまとう/実体から所有権を剥がすことは出来ない
    • つまり所有権=インスタンス(実体)のこと
  • 借用はポインターのこと
    • ポインター(=インスタンスのメモリアドレス)をやり取りしても当然インスタンス(=所有権)そのものは移動しない
    • 要するに普通の話をしている
  • Rust にはクラスの概念が無い/参照型の概念が無い
    • 参照渡し/値渡し(コピー渡し)の概念もまた無くなっている
      • 論理的な話で技術的な話(メモリー操作等)とは関係ない
    • メソッド間でのやり取りは全てインスタンスの移動を伴う「移動渡し」になっている
      • 👉 コレが特殊で「struct(構造体)を値渡ししてるのに所有権が移るの!?(構造体だからコピーでしょ?)」で分けわからんくなる
        • 既定が std::move
        • Rust 以外の言語は値型は値渡しがデフォルト
        • Rust は全てが移動渡し
          • Copy トレイトを実装すると従来通りの値渡し(コピー渡し=コピーして移動)が可能
          • インスタンスが複製される(=所有権が複製される)ので渡せるようになる
            • 所有権のみ複製しているわけではないので、値を編集したら別のインスタンスに自動的に値が適用されるといったことは無い
        • 要するに普通の話をしている
  • 所有権が移らない「借用」とはつまりは参照渡し(参照の値渡し)を意味している
    • & 演算子を使っていることから分かるように単なるポインターでしかない
      • 👉 Rust が特殊なのは、書き込み権限アリのポインター(T*T* const)を同時に一つしか作れない制約があること
      • Rust では様々なモノ/コトに既定で強い制限がかかっている(ポインターは const T* const がデフォ)
    • ポインター(実体の参照)を移動渡ししてもインスタンスそのものは移動しない
      • 所有権(=インスタンス)は移動しない
    • 要するに普通の話をしている
  • ライフタイム=変数のスコープ
    • 所有権を持つ変数がスコープを抜けると自動的に破棄(Drop::drop が呼ばれる)
    • 「ライフタイム注釈が~~」というエラーが出たからと言って &'static に変えたら「ローカル変数の延命が出来る」とかは無い
      • つまりライフタイムは単なるコンパイラー向けのメタデータでしかない
      • ライフタイム=変数のスコープ
    • 要するに普通の話をしている

Rust と std::move

  • Q: 所有権というコーディング上の概念に興味は無いです
  • Q: 元の値をクリアするという処理は LLVM で行われている? それともコンパイル時に保証できているからスキップしている?

https://zenn.dev/sator_imaging/scraps/c91c377005c257

C# で Rust する

残念ながら完全には Rust 出来ません。

AbsoluteOwnershipBorrow

https://github.com/sator-imaging/Unity-Fundamentals/blob/a17e1e529f6d14f99e83e43a0a51aab79991728c/Runtime/FancyStuff/RustSharp.cs#L42-L107

つかいかた

読み取り専用フィールドとして AbsoluteOwnership を宣言。

readonly AbsoluteOwnership<int[]> _arrayOwnership;

foo が所有権を取得すると bar の所有権は無効になる。(bar の中身にアクセスしようとするとぬるぽ)

foo._arrayOwnership.Take(bar._arrayOwnership);

int[] として扱いたい場合は一度所有権を手放す。

var rawData = foo._arrayOwnership.Move();  // 所有権を移動!
var result = DoSomething(rawData);

処理が終わったら再度所有権を取得する。

foo._arrayOwnership.Take(ref result);  // result はヌルになる

Borrow へは暗黙的に変換可能だけど ref 構造体なので長い期間保持できない制約がある。

DoSomething(foo._arrayOwnership);

void DoSomething(Borrow<T> array)
{
    foreach (var value in array.Read) { }
}

抜け穴

前提として以下を満たす必要があります。

  • AbsoluteOwnership<T>readonly フィールドに保持する
  • AbsoluteOwnership<T> をメソッドパラメーターにしてはならない

コピーを防げない

Rust はローカル変数間でのやり取りも移動渡しですが、C# ではどうやっても防げません。

var array = foo._arrayOwnership.Move();
var violation = array;

// コピーをメソッドに渡す
foo._arrayOwnership.Take(ref array);
DoSomething(violation);

コピーを防げない2

メソッドパラメーターとして所有権を受け取った場合も複製になってしまう。

なので使い辛そうな AbsoluteOwnership<T> という名前にして運用でカバー。

DoSomething(foo._arrayOwnership);

void DoSomething(AbsoluteOwnership<T> implicitCopy)
{
    //...
}

コピーを防げない3

Borrow<T>ReadClone は完全に雰囲気モノなのでコピーし放題。

void DoSomething(Borrow<T> array)
{
    // Read と Clone どちらもソースコードに意味付けする為だけのモノ
    fooInstance.stolenArray = array.Read;
    barInstance.stolenArray = array.Clone();
}

おわりに

RustSharp.cs よりこの記事のタイピングの方が時間かかりました。

以上です。お疲れ様でした。

Discussion

junerjuner

(C# の参照渡しは参照の値渡し)

それは参照渡しではなくて 参照型の話ではないでしょうか?

shiracamusshiracamus

参照渡しは変数共有、参照の値渡しは値共有(オブジェクト共有)します。
C# は ref, in, out をつければ参照渡し、つけなければ値渡し(参照の値渡しを含む)です。
なので、C# の参照渡しは参照の値渡しではありません。

  1. 値型変数の値渡し: 値コピー
  2. 値型変数の参照渡し: 変数共有
  3. 参照型変数の値渡し(参照の値渡し): 値共有(オブジェクト共有)
  4. 参照型変数の参照渡し: 変数共有

https://zenn.dev/shiracamus/articles/7d0b9e65e92e67

shiracamusshiracamus

参照渡し、参照の値渡し、ポインタ渡しはそれぞれ違う動作なので、適切に使い分けませんか?
参照渡し:値型にもポインタ型にも参照型にも使える(Rustにはない)
参照の値渡し:参照型に使う(オブジェクトは参照型、Rustにはない)
ポインタ渡し:ポインタ型に使う(ポインタ型は値型の一種)
https://ja.wikipedia.org/wiki/引数

C#
using System;

class Program
{
    unsafe static void call_by_value(int x, int *y) {
        *y = 2;         // 呼出し元に影響する
        x = 3;          // 呼出し元に影響しない
        int four = 4;
        y = &four;      // 呼出し元に影響しない
    }

    unsafe static void call_by_reference(ref int x, ref int *y) {
        *y = 5;         // 呼出し元に影響する
        x = 6;          // 呼出し元に影響する
        int seven = 7;
        y = &seven;     // 呼出し元に影響する
    }

    unsafe static void Main() {
        int value = 1;                                  // 値型変数value
        int *pointer = &value;                          // ポインタ型変数pointer
        call_by_value(value, pointer);                  // 値型変数、ポインタ型変数の値渡し
        Console.WriteLine("{0} {1}", value, *pointer);  // 2 2
        call_by_reference(ref value, ref pointer);      // 値型変数、ポインタ型変数の参照渡し
        Console.WriteLine("{0} {1}", value, *pointer);  // 6 7
    }
}