问题
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:
forwith copyingforwithref readonlyforwith copying and caching the array lengthforwithref readonlyand caching the array lengthforeachwith copyingforeachwithref readonlyandAsSpan()
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