Implementing IEnumerator<T> for Fixed Arrays

144 views Asked by At

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.

1

There are 1 answers

11
Dai On BEST ANSWER

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 );
    }
}