问题
With the new readonly instance member features in C# 8, I try to minify unnecessary copying of struct instances in my code.
I do have some foreach
iterations over arrays of structs, and according to this answer, it means that every element is copied when iterating over the array.
I thought I can simply modify my code now to prevent the copying, like so:
// Example struct, real structs may be even bigger than 32 bytes.
struct Color
{
public int R;
public int G;
public int B;
public int A;
}
class Program
{
static void Main()
{
Color[] colors = new Color[128];
foreach (ref readonly Color color in ref colors) // note 'ref readonly' placed here
Debug.WriteLine($"Color is {color.R} {color.G} {color.B} {color.A}.");
}
}
This sadly does not compile with
CS1510 A ref or out value must be an assignable variable
However, using an indexer like this compiles:
static void Main()
{
Color[] colors = new Color[128];
for (int i = 0; i < colors.Length; i++)
{
ref readonly Color color = ref colors[i];
Debug.WriteLine($"Color is {color.R} {color.G} {color.B} {color.A}.");
}
}
Is my syntax in the foreach
alternative wrong, or is this simply not possible in C# 8 (possibly because of how the enumeration is implemented internally)?
Or is C# 8 applying some intelligence nowadays and does no longer copy the Color
instances by itself?
回答1:
foreach
works based on the target type's definitions rather than some internal blackboxes. We could make use of this to create by-ref enumeration support:
//using System;
public readonly struct ArrayEnumerableByRef<T>
{
private readonly T[] _target;
public ArrayEnumerableByRef(T[] target) => _target = target;
public Enumerator GetEnumerator() => new Enumerator(_target);
public struct Enumerator
{
private readonly T[] _target;
private int _index;
public Enumerator(T[] target)
{
_target = target;
_index = -1;
}
public readonly ref T Current
{
get
{
if (_target is null || _index < 0 || _index > _target.Length)
{
throw new InvalidOperationException();
}
return ref _target[_index];
}
}
public bool MoveNext() => ++_index < _target.Length;
public void Reset() => _index = -1;
}
}
public static class ArrayExtensions
{
public static ArrayEnumerableByRef<T> ToEnumerableByRef<T>(this T[] array) => new ArrayEnumerableByRef<T>(array);
}
Then we could enumerate an array with foreach
loop by reference:
static void Main()
{
var colors = new Color[128];
foreach (ref readonly var color in colors.ToEnumerableByRef())
{
Debug.WriteLine($"Color is {color.R} {color.G} {color.B} {color.A}.");
}
}
回答2:
Inspried by Alsein's answer, I realized that I can simply retrieve a Span
of an array with the AsSpan()
extension method (available in the System
namespace), and use the span capability of ref-enumerating it:
static void Main()
{
Color[] colors = new Color[128];
foreach (ref readonly Color color in colors.AsSpan())
Debug.WriteLine($"Color is {color.R} {color.G} {color.B} {color.A}.");
}
Keep in mind that this only works for arrays, not List<T>
instances, I did not find a simple solution for ref-enumerating lists of structs yet.
I was worried about performance, so I measured the time it takes iterating over 10 and 1000 Color
instances in the following ways:
for
with copyingfor
withref readonly
for
with copying and caching the array lengthfor
withref readonly
and caching the array lengthforeach
with copyingforeach
withref readonly
andAsSpan()
ref foreach
and foreach
seems to perform the best (even at a longer 10000 instance run):
| Method | ColorCount | Mean | Error | StdDev | Rank |
|------------------ |----------- |------------:|-----------:|-----------:|-----:|
| For | 10 | 76.76 ns | 0.3310 ns | 0.3096 ns | 4 |
| ForRef | 10 | 77.31 ns | 0.4397 ns | 0.3898 ns | 4 |
| ForCacheLength | 10 | 69.39 ns | 0.1923 ns | 0.1605 ns | 3 |
| ForCacheLengthRef | 10 | 69.46 ns | 0.4859 ns | 0.4545 ns | 3 |
| ForEach | 10 | 68.28 ns | 0.7367 ns | 0.6152 ns | 2 |
| ForEachRef | 10 | 64.76 ns | 0.6355 ns | 0.5944 ns | 1 |
| For | 1000 | 6,912.80 ns | 49.9517 ns | 44.2808 ns | 7 |
| ForRef | 1000 | 6,882.85 ns | 44.9467 ns | 39.8441 ns | 7 |
| ForCacheLength | 1000 | 6,874.55 ns | 59.6360 ns | 55.7835 ns | 7 |
| ForCacheLengthRef | 1000 | 6,871.79 ns | 42.3081 ns | 39.5750 ns | 7 |
| ForEach | 1000 | 6,701.68 ns | 31.3103 ns | 27.7558 ns | 6 |
| ForEachRef | 1000 | 6,341.90 ns | 80.8536 ns | 75.6305 ns | 5 |
来源:https://stackoverflow.com/questions/58069669/can-i-foreach-over-an-array-of-structs-without-copying-the-elements-in-c-sharp-8