返回一个新的 ref 结构实例,该实例保存对另一个结构的返回实例的引用

问题描述 投票:0回答:1

我最近想编写代码,其中结构体 S1 的实例 A 返回结构体 S2 的某个实例 B,该实例 B 需要实例 A 中的一些值。结构体 S2 的简单实现是将结构体 S1 所需值的副本存储在 S2 内。显然,根据具体情况,它可能会产生相当大的结构。我的想法是在 S2 中存储对 S1 的引用。为此,S2 需要是一个 ref 结构,但我对此表示同意。

考虑以下代码:

public readonly record struct TopLevelStruct(int A, int B) {
    //This works. In IL this does get passed by ref into the implicit operator
    public RefStruct Ref => this;
    public RefStruct RefByMethod() => this;
    //Compiler error:
    //Cannot use a result of 'RefStruct.RefStruct(ref readonly TopLevelStruct)' in this context 
    //because it may expose variables referenced by parameter 'tls' outside of their declaration scope
    public RefStruct RefWithConstructor => new(in this);
    //Cannot use a result of 'RefStruct.RefStruct(ref readonly TopLevelStruct)' in this context 
    //because it may expose variables referenced by parameter 'tls' outside of their declaration scope
    public RefStruct RefWithConstructorByMethod() => new(in this);
}
public readonly ref struct RefStruct(ref readonly TopLevelStruct tls) {
    private readonly ref readonly TopLevelStruct _tls = ref tls;
    public int Sum => _tls.A + _tls.B;
    public static implicit operator RefStruct(in TopLevelStruct tls) => new(in tls);
}
public static class TopLevelStructExtensions {
    //This works, even though RefWithConstructorByMethod doesn't while both are arguably the same
    //(i.e. both functions will have a TopLevelStruct Reference as parameter. 
    //One implicit because of being a member function. The other one explicit because of being a extension method)
    public static RefStruct GetRef(this ref readonly TopLevelStruct tls) => new(in tls);
    public static RefStruct GetRefByOperator(this ref readonly TopLevelStruct tls) => tls;
}

由于某种原因,返回使用 this 通过引用构造的 ref 结构会出现编译器错误(请参阅代码)。我想知道这个错误的确切原因是什么。我好像没完全明白。

现在,尽管上面的方法不能作为成员方法使用,但它可以作为扩展方法使用。为什么编译器认为成员方法实现是错误的,而扩展方法是有效的?

更有趣的是,成员方法能够通过隐式转换从该引用构造 ref 结构。为什么这个“[...] 不能将参数‘tls’引用的变量暴露在其声明范围之外”。

话虽这么说,我知道通过上述实现,以下内容成立:

TopLevelStruct tls = new(1, 2);
var refStruct = tls.Ref;
Console.WriteLine(refStruct.Sum); //Output: 3
tls = new(3, 4);
Console.WriteLine(refStruct.Sum); //Output: 7

最后我想知道编译器错误是否应该在不发生的情况下发生,或者是否不应该发生在发生的情况下。否则为什么它们会在发生的地方发生,而不会发生在不发生的地方?


编辑1: 自从我发布了这个问题以来,我意识到使用转换运算符来构造 ref 结构可能是编译器应该禁止或至少警告我们的事情,但是,我不知道编译器是否能够检测到它合理的努力。

现在考虑以下代码:

ReadOnlySpan<int> span1 = GetSpan();
Span<int> buffer = stackalloc int[20];
for(int i = 0; i < buffer.Length; i++) buffer[i] = 420;
System.Console.WriteLine(span1[0]); // Output: 420 (because the stack unrolled after GetSpan() call)

Top top = new(42);
ReadOnlySpan<int> span2 = GetSpanSafe(in top);
buffer = stackalloc int[20];
for(int i = 0; i < buffer.Length; i++) buffer[i] = 420;
System.Console.WriteLine(span2[0]); // Output: 42 (top is still on the stack)

ReadOnlySpan<int> GetSpan() => new Top(42).Span;
ReadOnlySpan<int> GetSpanSafe(ref readonly Top top) => Top.ToSpan(in top);

//Similar struct to the one in the above code
readonly record struct Top(int I)
{
    private readonly int _i = I;
    // public ReadOnlySpan<int> Span => new(in _i); 
    public ReadOnlySpan<int> Span => (ReadOnlySpan<int>)this; // 
    public static explicit operator ReadOnlySpan<int>(in Top top) => new(in top._i);
    public static ReadOnlySpan<int> ToSpan(ref readonly Top i) => new(in i._i);
}

在第一部分中,我们得到了错误的输出,因为我们的 ref 结构指向一个临时变量,该临时变量已从堆栈中弹出。在第二部分中,我们使用 getter 的静态版本,一切都按预期工作。

据我了解,在静态版本中使用

ref readonly
参数限制该参数不是临时的。我不确定如果 Top 结构驻留在堆上然后被垃圾收集,它会如何表现。我也不知道引用局部变量和字段是托管还是非托管,也不知道如何验证给定的堆内存是否已释放。

不幸的是,这并没有为我们提供更具可读性的属性语法。我想知道在语言中引入一些限制在实际变量上调用成员方法的东西是否有意义(禁止在临时变量上调用它们)。也许范围修饰符会合适。我不确定在这种情况下接收变量是否需要是局部作用域。

c# struct reference pass-by-reference ref-struct
1个回答
1
投票

正如消息所暗示的,这里存在一些关于生命周期和转义分析的巨大问题 - 编译器正在努力确保您不会得到无效的指针(托管或非托管),并且在一般情况下 :有一些有问题的场景 - 例如:

new RefStruct(new TopLevelStruct(1, 2));

有效(尽管它确实会生成警告);如果我们存储这个托管指针:它去了哪里?。这个例子相当简单(编译器生成一个隐藏的局部变量,所以……嗯)——但概括起来:它……很复杂。

语言确实允许使用

scoped
关键字,但它并不能完全满足您的要求;它允许传入托管指针进行访问,但不允许该托管指针转义 - 意思是:如果你有

public readonly ref struct RefStruct(scoped ref readonly TopLevelStruct tls)

您可以从 tls

读取值
,但无法存储 引用。这意味着你的
_tls
初始化器失败了,但其他一切都变得愉快。

从根本上讲,

ref struct
和托管指针规则很复杂。有些事情就是不可能的,而证明它们是否应该(即它们是否不会导致问题)是难以置信困难。

© www.soinside.com 2019 - 2024. All rights reserved.