Skip to content

Instantly share code, notes, and snippets.

@mgravell
Last active November 5, 2025 11:51
Show Gist options
  • Select an option

  • Save mgravell/b0797d75c1d98c0bb2815deb674c03ef to your computer and use it in GitHub Desktop.

Select an option

Save mgravell/b0797d75c1d98c0bb2815deb674c03ef to your computer and use it in GitHub Desktop.
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