ufcpp/UfcppSample

ref fields

Closed this issue · 11 comments

ufcpp commented

https://ufcpp.net/blog/2022/2/ref-field/
csharplang/pull/6338
csharplang/issues/6337

  • 記法としてはフィールドの型に ref 付けるだけ
  • ref struct にだけ持てる
  • これの安全性を保障するために、ref struct に対する escape analysis にちょっと手が入ってる
  • scoped (↑ の修正前と同じ挙動を得るために使う)
    • 引数
    • ローカル変数に付けれる
    • 構造体の this と out 引数はデフォルトで scoped だっけ
    • foreach と using の中の変数にも
  • readonly ref readonly T
  • オブジェクト初期化子で ref 代入
  • 実例
    • (BCL 内部の話だけど) Span<T> が普通の ref struct になった
    • TypedReference
    • 多値 ref 戻り値
  • TypedReference の通常 ref struct 化、 __makeref 退役
    • ほんとに .NET 7 でやる?それとも 8?
  • UnscopedRef 属性
    • FixedArray みたいなの
  • C# 10 ルールで解析するか、11 ルールで解析するかをアセンブリ単位で属性で指定
  • ref struct の ref field は持てなくなっちゃった…
  • 内部的には scoped は ScopedRef 属性っぽい
  • unsafe コンテキストではエラーじゃなくて警告になるっぽい
ufcpp commented

ref 構造体 のページに話足そうかな。

参照渡しSpan のページでも軽く触れてリンク?

fast Spanのとこと、参照戻り値で返せるもののとこ。

ufcpp commented

↓は結局いままでのまま。

TypedReferenc m() => default;
ufcpp commented

今 scoped ref しかない(11ではそれしか対応しない判断した)けど、今後 ref scoped もあり得るらしい。

もしや…
readonly scoped ref readonly scoped Span ある?

ufcpp commented

ref 解析、unsafe コンテキストではエラーじゃなくて警告だけにするってやつ、ちゃんと入ってた。

static ref int SafeRef()
{
    int i = 1;
    return ref i; // エラー
}

unsafe static ref int UnsafeRef()
{
    int i = 1;
    return ref i; // 警告だけに緩和。この例はマジでダメなやつ。
}
ufcpp commented

これの作業の一環でやったらしいんだけど、参照型のアドレスを & で取れるようになってるっぽい?

unsafe
{
    object o = new();
    object* ptr = &o;
}

いつの間に…
なぜ unsafe ref 解析に混ぜてやってる?

csharplang/issues/6476

.NET 8 時点、scoped が付いてる BCL のメソッド、5個しかなかった。

MemoryMarshal CreateSpan reference
MemoryMarshal CreateReadOnlySpan reference
DefaultInterpolatedStringHandler AppendFormatted value
DefaultInterpolatedStringHandler AppendFormatted value
Unsafe AsRef source

しかも MemoryMarshal と Unsafe のは「本来 scoped が付いてたらまずい」「unsafe な手段でコンパイラーに嘘ついてる」メソッド。
実質 DefaultInterpolatedStringHandler AppendFormatted だけが scoped。

static class Util
{
    // こっちは OK
    public static void AppendAbc(this ref DefaultInterpolatedStringHandler x)
    {
        x.AppendFormatted(stackalloc char[3] { 'a', 'b', 'c' });
    }

    // こっちはダメ。
    // stackalloc したものが x に伝搬する可能性を懸念。
    public static void AppendAbc(this ref Handler x)
    {
        x.AppendFormatted(stackalloc char[3] { 'a', 'b', 'c' });
    }
}

// DefaultInterpolatedStringHandler と同じシグネチャで、scoped だけ取る。
ref struct Handler
{
    public void AppendFormatted(ReadOnlySpan<char> span) { }
}
using System.Diagnostics.CodeAnalysis;

ref struct RefInt(ref int target)
{
    public ref int Target = ref target;

    // 参照がメソッドの外に漏れる想定。
    public void Reassign([UnscopedRef] ref int newTarget)
    {
        Target = ref newTarget;
    }

    // 参照がメソッドの外に漏れない想定。
    public void AddTo(ref int target)
    {
        target += Target;
    }
}

class RefIntExample
{
    public static RefInt M1()
    {
        int x = 0;
        return new RefInt(ref x); // ダメ。x の参照が RefInt.Target 越しで外に漏れる。
    }

    public static RefInt M2()
    {
        RefInt r = default; // default だと何も参照しないので、
        return r; // 外に渡せる。
    }

    public static RefInt M3()
    {
        int x = 0;
        RefInt r = default;
        r.Reassign(ref x); // ダメ
        return r; // ここは OK
    }

    public static RefInt M4()
    {
        int x = 0;
        RefInt r = default;
        r.AddTo(ref x);
        return r;
    }
}

ref T はデフォルトが scoped だけど、Span (ref 構造体)はデフォルトが unscoped。

(ref T のデフォルトは return-only みたい。特殊。)

ref struct RefSpan(Span<int> target)
{
    public Span<int> Target = target;

    // Span (実質は参照)がメソッドの外に漏れる想定。
    public void Reassign(Span<int> newTarget)
    {
        Target = newTarget;
    }

    // Span (実質は参照)がメソッドの外に漏れない想定。
    public void AddTo(scoped Span<int> target)
    {
        var len = int.Min(target.Length, Target.Length);
        for (var i = 0; i < len; i++) target[i] += Target[i];
    }
}

class RefIntExample
{
    public static RefSpan M1()
    {
        Span<int> x = stackalloc int[1];
        return new RefSpan(x); // ダメ。x (が参照してる stackalloc)が RefInt.Target 越しで外に漏れる。
    }

    public static RefSpan M2()
    {
        RefSpan r = default; // default だと何も参照しないので、
        return r; // 外に渡せる。
    }

    public static RefSpan M3()
    {
        Span<int> x = stackalloc int[1];
        RefSpan r = default;
        r.Reassign(x); // ダメ
        return r; // ここは OK
    }

    public static RefSpan M4()
    {
        Span<int> x = stackalloc int[1];
        RefSpan r = default;
        r.AddTo(x);
        return r;
    }
}

ref T のコンストラクター引数は変。

ref struct A
{
    public ref int X;

    // コンストラクター引数は Unscoped 扱い(特殊)。
    // コンストラクターとその他のメソッドでそろってない。
    public A(ref int x) => X = ref x;
    public void M([UnscopedRef] ref int x) => X = ref x;
}

ref struct B
{
    public Span<int> X;

    // コンストラクターとその他のメソッドでそろってる。
    public B(Span<int> x) => X = x;
    public void M(Span<int> x) => X = x;
}