Skip to content

Lifetime of collection expressions seems to be longer than it should be in some cases #80107

@hamarb123

Description

@hamarb123

Version Used: sharplab.io

Steps to Reproduce:

The lifetime of a collection expression of type ReadOnlySpan<T> (when not of constants of a primitive type only) or Span<T> is meant to be declaration-scope as per https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions.

Image

But it does not appear to be enforced in a number of cases:

1:

using System;
public class C
{
    public static void Main()
    {
        scoped SpanStorage value = new();
        for (int i = 0; i < 2; i++)
        {
            ReadOnlySpan<int> s = [i, i + 1];
            value.Receive(s); // Span escapes declaration scope here
        }
        //Console.WriteLine(string.Join(", ", value.Span1.ToArray())); // Prints [1, 2]
        //Console.WriteLine(string.Join(", ", value.Span2.ToArray())); // Prints [1, 2]
    }
}
ref struct SpanStorage()
{
    public ReadOnlySpan<int> Span1, Span2;
    private bool gotFirst = false;
    public void Receive(ReadOnlySpan<int> span)
    {
        if (gotFirst) Span2 = span;
        else Span1 = span;
        gotFirst = true;
    }
}

(link)

2:

using System;
public class C
{
    public static void Main()
    {
        scoped SpanStorage value = new();
        for (int i = 0; i < 2; i++)
        {
            value.Receive([i, i + 1]); // Inline array escapes declaration scope here
        }
        //Console.WriteLine(string.Join(", ", value.Span1.ToArray())); // Prints [1, 2]
        //Console.WriteLine(string.Join(", ", value.Span2.ToArray())); // Prints [1, 2]
    }
}
ref struct SpanStorage()
{
    public ReadOnlySpan<int> Span1, Span2;
    private bool gotFirst = false;
    public void Receive(ReadOnlySpan<int> span)
    {
        if (gotFirst) Span2 = span;
        else Span1 = span;
        gotFirst = true;
    }
}

(link)

3:

using System;
using System.Runtime.CompilerServices;
public class C
{
    public static void Main()
    {
        scoped SpanStorage value = new();
        for (int i = 0; i < 2; i++)
        {
            SpanHolder s = [i, i + 1];
            value.Receive(s); // Our struct escapes declaration scope here
        }
        //Console.WriteLine(string.Join(", ", value.Span1.ToArray())); // Prints [1, 2]
        //Console.WriteLine(string.Join(", ", value.Span2.ToArray())); // Prints [1, 2]
    }
}
ref struct SpanStorage()
{
    public ReadOnlySpan<int> Span1, Span2;
    private bool gotFirst = false;
    public void Receive(SpanHolder span)
    {
        if (gotFirst) Span2 = span.Span;
        else Span1 = span.Span;
        gotFirst = true;
    }
}
[CollectionBuilder(typeof(SpanHolder), "Create")]
ref struct SpanHolder(ReadOnlySpan<int> span)
{
    public ReadOnlySpan<int> Span = span;
    public static SpanHolder Create(ReadOnlySpan<int> span) => new(span);
    public readonly ReadOnlySpan<int>.Enumerator GetEnumerator() => Span.GetEnumerator();
}

(link)

Note in all of the above code snippets, that the collection expression ends up being scoped to the current method, not the declaration block (which is what they're meant to be).

Here's an example of how it's meant to work, via explicit ref rather than collection expressions.

ref version that's equivalent to the span version above:

using System;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
public class C
{
    public static void Main()
    {
        scoped RefStorage value = new();
        for (int i = 0; i < 2; i++)
        {
            int j = i;
            value.Receive(ref j); // ERROR HERE
        }
    }
}
ref struct RefStorage()
{
    public ref int ByRef1, ByRef2;
    private bool gotFirst = false;
    public void Receive([UnscopedRef] ref int byref)
    {
        if (gotFirst) ByRef1 = ref byref;
        else ByRef2 = ref byref;
        gotFirst = true;
    }
}

(link)

OR

ref version that's equivalent to the span version above (if you added scoped to the ROS parameter - note: this one works as expected):

using System;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
public class C
{
    public static void Main()
    {
        scoped RefStorage value = new();
        for (int i = 0; i < 2; i++)
        {
            int j = i;
            value.Receive(ref j);
        }
    }
}
ref struct RefStorage()
{
    public ref int ByRef1, ByRef2;
    private bool gotFirst = false;
    public void Receive(ref int byref)
    {
        if (gotFirst) ByRef1 = ref byref; // ERROR HERE
        else ByRef2 = ref byref; // ERROR HERE
        gotFirst = true;
    }
}

(link)

Expected Behavior:

Error (or warning in unsafe) about lifetimes.

Actual Behavior:

Silently overwrites buffer as a result of re-using it when it's meant to be unused, but it's re-used since the lifetimes aren't set up correctly to make it complain properly.

Metadata

Metadata

Assignees

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions