Last active
November 5, 2025 11:51
-
-
Save mgravell/b0797d75c1d98c0bb2815deb674c03ef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Runtime.CompilerServices; | |
| Foo foo = new Foo(5,6,7,8); | |
| // scenario (why we're here): the GetRef method doesn't compile :( | |
| // you cannot conveniently use by-ref return from struct methods | |
| // or properties, because "this" is has scoped-like semantics; | |
| // ("scoped" can be paraphrazed as "this reference is not captured | |
| // or returned", i.e. it doesn't risk exposing the reference later) | |
| /* | |
| ref readonly int x = ref foo.GetRef(2); | |
| //Console.WriteLine(x); // 7 | |
| */ | |
| // however! extension blocks do not have the same restrictions! | |
| ref readonly int x = ref foo.GetRefExt(2); // legal! | |
| Console.WriteLine(x); // 7 | |
| x = ref foo.GetRefPureEvil(3); // legal! | |
| Console.WriteLine(x); // 8 | |
| static ref readonly int StackEscapeDangerous() | |
| { | |
| // this method shows what "scoped" (or scoped-equivalent) | |
| // intends to avoid; "val" is in the local stack-frame - we | |
| // should never be able to pass this value *upwards* (to the caller) | |
| var val = new Foo(42, 42, 42, 42); | |
| ref readonly int r = ref val.IncorrectUnsafeDoNotUse(1); | |
| Console.WriteLine(r); // totally fine here | |
| return ref r; // eek! we just leaked an invalid ref | |
| } | |
| static ref readonly int InvariantPreservedCorrectly() | |
| { | |
| var val = new Foo(42, 42, 42, 42); | |
| ref readonly int r = ref val.GetRefExt(1); | |
| Console.WriteLine(r); // totally fine here | |
| return ref r; // error CS8157 - the compiler prevents us doing bad things, yay! | |
| } | |
| readonly struct Foo(int a, int b, int c, int d) | |
| { | |
| internal readonly int _a = a, _b = b, _c = c, _d = d; | |
| // not legal, for context only; "this" is similar to | |
| // "scoped in Foo this" (or "scoped ref Foo this" if not "readonly") | |
| public ref readonly int GetRef(int index) | |
| { | |
| switch (index) | |
| { | |
| case 0: return ref this._a; // error CS8170 | |
| case 1: return ref this._b; // error CS8170 | |
| case 2: return ref this._c; // error CS8170 | |
| case 3: return ref this._d; // error CS8170 | |
| default: throw new IndexOutOfRangeException(); | |
| } | |
| } | |
| public ref readonly int IncorrectUnsafeDoNotUse(int index) | |
| { | |
| // this casts away the scoped-equivalent on arg0, which | |
| // means the caller might not enforce lifetime correctly | |
| ref Foo unscoped = ref Unsafe.AsRef(in this); | |
| switch (index) | |
| { | |
| case 0: return ref unscoped._a; | |
| case 1: return ref unscoped._b; | |
| case 2: return ref unscoped._c; | |
| case 3: return ref unscoped._d; | |
| default: throw new IndexOutOfRangeException(); | |
| } | |
| } | |
| } | |
| static class FooExtensions | |
| { | |
| extension(in Foo source) | |
| { | |
| public ref readonly int GetRefExt(int index) | |
| { | |
| // show that an extension block allows us to return | |
| // by-ref access to struct members | |
| switch (index) | |
| { | |
| case 0: return ref source._a; | |
| case 1: return ref source._b; | |
| case 2: return ref source._c; | |
| case 3: return ref source._d; | |
| default: throw new IndexOutOfRangeException(); | |
| } | |
| } | |
| public ref readonly int GetRefPureEvil(int index) | |
| { | |
| // if the logic of GetRefExt is semantically valid, | |
| // then we can also do some more ... "exotic" things | |
| // *without* breaking any rules | |
| if ((index & 0b11) != index) Throw(); // bounds check | |
| // JIT should see right through all of this, effectively | |
| // similar to "((int*)source)+index" if source was Foo* | |
| return ref Unsafe.Add(ref Unsafe.As<Foo, int>( | |
| ref Unsafe.AsRef(in source)), index); | |
| static void Throw() => throw new IndexOutOfRangeException(); | |
| } | |
| } | |
| extension(scoped in Foo source) | |
| { | |
| public ref readonly int GetScopedRefExt(int index) | |
| { | |
| // show that "scoped" preserves the same escape | |
| // logic as a regular instance method (albeit | |
| // with a different CS error numbmer) | |
| switch (index) | |
| { | |
| case 0: return ref source._a; // error CS9076 | |
| case 1: return ref source._b; // error CS9076 | |
| case 2: return ref source._c; // error CS9076 | |
| case 3: return ref source._d; // error CS9076 | |
| default: throw new IndexOutOfRangeException(); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment