iTranslated by AI
Implementing Rust-style Ownership in C#
First, understand Rust's Ownership
If Rust isn't the first programming language you've touched, it's better to completely forget the ownership system. Since the fundamentals of it as a programming language are no different from others, you only need to grasp Rust's specificities.
- Ownership: Refers to the instance (entity)
- Borrowing: Refers to the pointer
- Rust's specificities
- ✅ All operations are "move-by-transfer = moving the entity" (implicit/semantic
std::move)- ✅ Exchanges between local variables are not copies either
- ❌ "Because it's a struct, it's passed by value (copy) and only the ownership is moved!" (Wrong)
- ❌ "Does 'move' mean copying and then deleting the original, like a Dokodemo Door (Anywhere Door)?" (Not a technical discussion)
- ✅ Mutable pointers (
&mut==T*T* const) can only exist one at a time.- ✅ Why? ← Because it's Rust.
- Extra: Rust does not have the concept of classes (reference types).
- ✅ All operations are "move-by-transfer = moving the entity" (implicit/semantic
std::move is "copying and then setting the original data to nullptr (0)," which prevents unexpected data rewriting and reading at "runtime."
Rust is revolutionary because it can check this "statically" at "compile-time" rather than at runtime using the borrow checker (≈ linter/analyzer).
(So, if you built a borrow checker for C#, you should be able to achieve the same memory safety. Though you can't make it GC-less!)
Example) It doesn't matter if it's a struct or not using the heap
Copying between local variables is also impossible. (As stated in the error, you can do it by implementing the Copy trait)
Execution environment: 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
Example) Recursive move checks are performed
- Note: If
valueimplements theCopytrait likei32, the latter will succeed.
let foo = Foo { value: ... };
// Error: Attempting to access an instance that has been moved
baz(foo, foo.value);
~~~~~~~~~
// 👇 If you change the order...
// Error: Operating on 'foo', which has been partially moved
bar(foo.value, foo);
~~~
Other bullet points
- Regarding "reference / pointer / and others," descriptions are not strictly precise across all programming languages and their contexts, but I will skip that here as it deviates from the main topic.[1]
--
- Ownership always accompanies the entity / Ownership cannot be stripped from the entity.
- In other words, Ownership = Instance (Entity).
- Borrowing is a pointer.
- Naturally, the instance (= ownership) itself does not move even if you exchange pointers (= memory address of the instance).
- In short, we're talking about normal things.
- Rust has no concept of classes / No concept of reference types.
- The concepts of pass-by-reference / pass-by-value (pass-by-copy) have also disappeared.
- This is a logical discussion and unrelated to technical matters (memory operations, etc.).
- All exchanges between methods involve "move-by-transfer," which accompanies the movement of the instance.
- 👉 This is the specific part that causes confusion, like "Ownership moves even though I'm passing a
structby value!? (Since it's a struct, shouldn't it be a copy?)".- The default is an implicit
std::move. - Languages other than Rust have pass-by-value as the default for value types.
- In Rust, everything is move-by-transfer.
- By implementing the Copy trait, traditional pass-by-value (pass-by-copy = copy then move) becomes possible.
- Since the instance is duplicated (= ownership is duplicated), it becomes possible to pass it.
- Since it's not just the ownership being duplicated, editing the value won't automatically apply the value to another instance.
- In short, we're talking about normal things.
- The default is an implicit
- 👉 This is the specific part that causes confusion, like "Ownership moves even though I'm passing a
- The concepts of pass-by-reference / pass-by-value (pass-by-copy) have also disappeared.
- "Borrowing," where ownership does not move, simply means pass-by-reference (pass-by-value of a reference).
- As seen from the use of the
&operator, it is nothing more than a simple pointer.- 👉 What's special about Rust is the constraint that only one pointer with write permission (
T*/T* const) can exist at a time. - In Rust, strong restrictions are placed on various things/actions by default (pointers are
const T* constby default).
- 👉 What's special about Rust is the constraint that only one pointer with write permission (
- Even if you move-transfer a pointer (reference to an entity), the instance itself does not move.
- Ownership (= instance) does not move.
- In short, we're talking about normal things.
- As seen from the use of the
- Lifetime = (mostly) scope of a variable.
- When a variable holding ownership goes out of scope, it is automatically destroyed (
Drop::dropis called). - Even if you get an error saying "Lifetime annotation...", changing it to
&'staticwon't "extend the life of a local variable."- In other words, a lifetime is nothing more than metadata for the compiler.
- Lifetime = scope of a variable.
- In short, we're talking about normal things.
-
- Strictly speaking, it's "the maximum coincides with the variable's scope," and while it cannot be extended, it can be set shorter, so they are not perfectly equal.
-
- When a variable holding ownership goes out of scope, it is automatically destroyed (
Rust and std::move
Up to this point, we have looked at the semantic aspects of the programming language, but here we will delve into the machine-language side of Rust.
- Q: I'm not interested in the coding concept of ownership.
- Q: Is the process of clearing the original value performed by LLVM? Or is it skipped because it's guaranteed at compile-time?
The Rust compiler generates Intermediate Representation (IR) for LLVM. [2] This is similar to the relationship between TypeScript and JavaScript.
- None of the type system information from TypeScript remains in the transcoded JavaScript.
- Similarly, in Rust, information such as analysis by the borrow checker completely disappears from the compilation results.
Doing Rust in C#
Unfortunately, you cannot achieve perfect Rust.
AbsoluteOwnership and Borrow
Usage
Declare AbsoluteOwnership as a read-only field.
readonly AbsoluteOwnership<int[]> _arrayOwnership;
When foo acquires ownership, bar's ownership becomes invalid. (Attempting to access bar's contents results in a NullReferenceException.)
foo._arrayOwnership.Take(bar._arrayOwnership);
If you want to treat it as an int[], release the ownership once.
var rawData = foo._arrayOwnership.Move(); // Move ownership!
var result = DoSomething(rawData);
Once the process is finished, acquire ownership again.
foo._arrayOwnership.Take(ref result); // result becomes null
Implicit conversion to Borrow is possible, but since it's a ref struct, there's a constraint that it cannot be held for a long period.
DoSomething(foo._arrayOwnership);
void DoSomething(Borrow<T> array)
{
foreach (var value in array.Read) { }
}
Loopholes
As a prerequisite, the following must be met:
- Keep
AbsoluteOwnership<T>in areadonlyfield -
AbsoluteOwnership<T>must not be used as a method parameter
Cannot prevent copying
In Rust, exchanges between local variables are also move-transfers, but in C#, there is no way to prevent implicit copy creation.
var array = foo._arrayOwnership.Move();
var violation = array;
// Re-acquire the moved ownership, while leaking the secretly created copy
foo._arrayOwnership.Take(ref array);
DoSomething(violation);
Cannot prevent copying 2
Even when ownership is received as a method parameter, it results in a duplicate.
So, I gave it the name
AbsoluteOwnership<T>, which feels a bit heavy to use, and rely on operational conventions to handle it.
DoSomething(foo._arrayOwnership);
void DoSomething(AbsoluteOwnership<T> implicitCopy)
{
//...
}
Cannot prevent copying 3
Since Read and Clone in Borrow<T> are purely symbolic, you can copy as much as you want.
void DoSomething(Borrow<T> array)
{
// Both Read and Clone are only meant to add semantic meaning to the source code
fooInstance.stolenArray = array.Read;
barInstance.stolenArray = array.Clone(); // This one is at least as intended
}
Conclusion
Typing this article took longer than writing RustSharp.cs.
That's all. Thank you for your time.
Discussion
それは参照渡しではなくて 参照型の話ではないでしょうか?
参照渡しは変数共有、参照の値渡しは値共有(オブジェクト共有)します。
C# は ref, in, out をつければ参照渡し、つけなければ値渡し(参照の値渡しを含む)です。
なので、C# の参照渡しは参照の値渡しではありません。
確かに C# では~と前置きした上で間違ってますね。直します!
参照渡し、参照の値渡し、ポインタ渡しはそれぞれ違う動作なので、適切に使い分けませんか?
参照渡し:値型にもポインタ型にも参照型にも使える(Rustにはない)
参照の値渡し:参照型に使う(オブジェクトは参照型、Rustにはない)
ポインタ渡し:ポインタ型に使う(ポインタ型は値型の一種)
その表現だと括弧の方は値渡しでは……?
C# の
refに対応するのは C のT&(エイリアス)だと思うんですが、その概念は Rust にはなく(多分)、&こそ同じですが Rust の&&mutmut foo: &mutはスマートポインター的で C のソレとは完全に互換性のある表現ではないという別の問題もアリ、、、(C の&もコードの文脈によってエイリアスまたはポインターと意味が変わってくる)Rust はこの辺りの似ているようでちょっと違う、を全て
&(Unsafe は*)にまとめたことで分かりやすくなっているのが Linux のコアに近い部分を Rust で書くぜ! まで普及した理由かも?それなら 参照渡しというよりもまだ参照型では……?
参照渡しと参照の値渡し(参照型変数の値渡し)は別物だということは認識していますでしょうか?

参照の参照渡し(参照型変数の参照渡し)もあります。
(C++の参照型は「変数参照=変数共有=エイリアス」なので混乱の元)
NLL(非レキシカルライフタイム)により「参照の有効期間は字句スコープ完全一致ではない」ことも強調したいです。なのでここは厳密には誤りかと。