Skip to content

'foreach' does not handle 'ref' and 'ref readonly' correctly #33201

@aalmada

Description

@aalmada

Version Used:

master (28 Oct 2018) using SharpLab

Steps to Reproduce:

  1. Create an enumerator where Current returns a ref readonly:
public static partial class Array
{
    public static WhereArray<TSource> Where<TSource>(this TSource[] source, Func<TSource, bool> predicate) 
    {
        if (source == null) ThrowSourceNull();
        if (predicate is null) ThrowPredicateNull();

        return new WhereArray<TSource>(source, predicate);

        void ThrowSourceNull() => throw new ArgumentNullException(nameof(source));
        void ThrowPredicateNull() => throw new ArgumentNullException(nameof(predicate));
    }

    public readonly struct WhereArray<TSource> 
        : IEnumerable<TSource>
    {
        readonly TSource[] source;
        readonly Func<TSource, bool> predicate;

        internal WhereArray(TSource[] source, Func<TSource, bool> predicate)
        {
            this.source = source;
            this.predicate = predicate;
        }

        public Enumerator GetEnumerator() => new Enumerator(in this);
        IEnumerator<TSource> IEnumerable<TSource>.GetEnumerator() => new Enumerator(in this);
        IEnumerator IEnumerable.GetEnumerator() => new Enumerator(in this);

        public struct Enumerator : IEnumerator<TSource>
        {
            readonly TSource[] source;
            readonly Func<TSource, bool> predicate;
            readonly int count;
            int index;

            internal Enumerator(in WhereArray<TSource> enumerable)
            {
                source = enumerable.source;
                predicate = enumerable.predicate;
                count = enumerable.source.Length;
                index = -1;
            }

            public ref readonly TSource Current => ref source[index];
            TSource IEnumerator<TSource>.Current => source[index];
            object IEnumerator.Current => source[index];

            public bool MoveNext()
            {
                index++;
                while (index < count)
                {
                    if (predicate(source[index]))
                        return true;

                    index++;
                }
                return false;
            }

            public void Reset() => index = -1;

            public void Dispose() { }
        }
    }
}
  1. When the iteration variable is not used:
public static class Program 
{
    public static void Main() 
    {
        var a = new int[] { 0, 1, 2, 3, 4 };
        foreach(var i in a.Where(_ => true))
          Console.WriteLine("🌄");
    }
}

The generated code is correct, where the reference variable is of type ref int:

NOTE: The readonly is missing but that is expected.

Array.WhereArray<int>.Enumerator enumerator = obj.Where(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = <>c.<>9.<Main>b__0_0)).GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        ref int reference = ref enumerator.Current;
        Console.WriteLine("\ud83c\udf04");
    }
}
finally
{
    ((IDisposable)enumerator).Dispose();
}
  1. When the iteration variable is used:
public static class Program 
{
    public static void Main() 
    {
        var a = new int[] { 0, 1, 2, 3, 4 };
        foreach(var i in a.Where(_ => true))
            DoSomething(i);
    }
    
    public static void DoSomething(in int value) 
        => Console.WriteLine(value);
}

The generated code is the following, where the value variable now is of type int. This causes a copy of the value type.

Array.WhereArray<int>.Enumerator enumerator = obj.Where(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = <>c.<>9.<Main>b__0_0)).GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
      int value = enumerator.Current;
      DoSomething(ref value);
    }
}
finally
{
    ((IDisposable)enumerator).Dispose();
}
  1. When 'foreach' is not used:
public class Program 
{
    static readonly int[] values = new int[] { 1 };
    
    static ref readonly int GetValue() => ref values[0];
    static void SetValue(in int value) => values[0] = value;
    
    public static void Main() 
    {
        ref readonly int a = ref GetValue();
        SetValue(a);
    }
}

The generated code if correct:

public static void Main()
{
  SetValue(ref GetValue());
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-CompilersuntriagedIssues and PRs which have not yet been triaged by a lead

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions