-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
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.

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.