Search code examples
c#fixedunsafeienumerator

Implementing IEnumerator<T> for Fixed Arrays


I need to implement a mutable polygon that behaves like a struct, that it is copied by value and changes to the copy have no side effects on the original.

Consider my attempt at writing a struct for such a type:

public unsafe struct Polygon : IEnumerable<System.Drawing.PointF>
{
    private int points;
    private fixed float xPoints[64];
    private fixed float yPoints[64];

    public PointF this[int i]
    {
        get => new PointF(xPoints[i], yPoints[i]);
        set
        {
            xPoints[i] = value.X;
            yPoints[i] = value.Y;
        }
    }

    public IEnumerator<PointF> GetEnumerator()
    {
        return new PolygonEnumerator(ref this);
    }
}

I have a requirement that a Polygon must be copied by value so it is a struct.
(Rationale: Modifying a copy shouldn't have side effects on the original.)

I would also like it to implement IEnumerable<PointF>.
(Rationale: Being able to write for (PointF p in poly))

As far as I am aware, C# does not allow you to override the copy/assignment behaviour for value types. If that is possible then there's the "low hanging fruit" that would answer my question.

My approach to implementing the copy-by-value behaviour of Polygon is to use unsafe and fixed arrays to allow a polygon to store up to 64 points in the struct itself, which prevents the polygon from being indirectly modified through its copies.

I am running into a problem when I go to implement PolygonEnumerator : IEnumerator<PointF> though.
Another requirement (wishful thinking) is that the enumerator will return PointF values that match the Polygon's fixed arrays, even if those points are modified during iteration.
(Rationale: Iterating over arrays works like this, so this polygon should behave in line with the user's expectations.)

public class PolygonEnumerator : IEnumerator<PointF>
{
    private int position = -1;

    private ??? poly;

    public PolygonEnumerator(ref Polygon p)
    {
        // I know I need the ref keyword to ensure that the Polygon
        // passed into the constructor is not a copy
        // However, the class can't have a struct reference as a field

        poly = ???;
    }

    public PointF Current => poly[position]; 

    // the rest of the IEnumerator implementation seems straightforward to me

}

What can I do to implement the PolygonEnumerator class according to my requirements?

It seems to me that I can't store a reference to the original polygon, so I have to make a copy of its points into the enumerator itself; But that means changes to the original polygon can't be visited by the enumerator!

I am completely OK with an answer that says "It's impossible".

Maybe I've dug a hole for myself here while missing a useful language feature or conventional solution to the original problem.


Solution

  • Your Polygon type should not be a struct because ( 64 + 64 ) * sizeof(float) == 512 bytes. That means every value-copy operation will require a copy of 512 bytes - which is very inefficient (not least because of locality-of-reference which strongly favours the use objects that exist in a single location in memory).

    I have a requirement that a Polygon must be copied by value so it is a struct. (Rationale: Modifying a copy shouldn't have side effects on the original.)

    Your "requirement" is wrong. Instead define an immutable class with an explicit copy operation - and/or use a mutable "builder" object for efficient construction of large objects.

    I would also like it to implement IEnumerable<PointF>. (Rationale: Being able to write for (PointF p in poly))

    That's fine - but you hardly ever need to implement IEnumerator<T> directly yourself because C# can do it for you when using yield return (and the generated CIL is very optimized!).

    My approach to implementing the copy-by-value behaviour of Polygon is to use unsafe and fixed arrays to allow a polygon to store up to 64 points in the struct itself, which prevents the polygon from being indirectly modified through its copies.

    This is not how C# should be written. unsafe should be avoided wherever possible (because it breaks the CLR's built-in guarantees and safeguards).

    Another requirement (wishful thinking) is that the enumerator will return PointF values that match the Polygon's fixed arrays, even if those points are modified during iteration. (Rationale: Iterating over arrays works like this, so this polygon should behave in line with the user's expectations.)

    Who are your users/consumers in this case? If you're so concerned about not breaking user's expectations then you shouldn't use unsafe!

    Consider this approach instead:

    (Update: I just realised that the class Polygon I defined below is essentially just a trivial wrapper around ImmutableList<T> - so you don't even need class Polygon, so just use ImmutableList<Point> instead)

    public struct Point
    {
        public Point( Single x, Single y )
        {
            this.X = x;
            this.Y = y;
        }
    
        public Single X { get; }
        public Single Y { get; }
    
        // TODO: Implement IEquatable<Point>
    }
    
    public class Polygon : IEnumerable<Point>
    {
        private readonly ImmutableList<Point> points;
    
        public Point this[int i] => this.points[i];
    
        public Int32 Count => this.points[i];
    
        public Polygon()
        {
            this.points = new ImmutableList<Point>();
        }
    
        private Polygon( ImmutableList<Point> points )
        {
            this.points = points;
        }
    
        public IEnumerator<PointF> GetEnumerator()
        {
            //return Enumerable.Range( 0, this.points ).Select( i => this[i] );
            return this.points.GetEnumerator();
        }
    
        public Polygon AddPoint( Single x, Single y ) => this.AddPoint( new Point( x, y ) );
    
        public Polygon AddPoint( Point p )
        {
            ImmutableList<Point> nextList = this.points.Add( p );
            return new Polygon( points: nextList );
        }
    }