【问题标题】:Implementing IEnumerator<T> for Fixed Arrays为固定数组实现 IEnumerator<T>
【发布时间】:2020-06-23 22:21:18
【问题描述】:

我需要实现一个可变多边形,它的行为类似于一个结构,它是按值复制的,对副本的更改对原件没有副作用。

考虑一下我为这种类型编写struct 的尝试:

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

我要求Polygon 必须按值复制,因此它是struct
理由:修改副本不应对原件产生副作用。

我也希望它实现IEnumerable&lt;PointF&gt;
理由:能写for (PointF p in poly)

据我所知,C# 不允许您覆盖值类型的复制/赋值行为。如果这可能的,那么有一个“低悬的果实”可以回答我的问题。

我实现 Polygon 的按值复制行为的方法是使用 unsafe 和固定数组以允许多边形在结构本身中存储多达 64 个点,从而防止多边形被间接修改通过它的副本。

我在执行PolygonEnumerator : IEnumerator&lt;PointF&gt; 时遇到了问题。
另一个要求(一厢情愿)是枚举器将返回与Polygon 的固定数组匹配的PointF 值,即使这些点在迭代期间被修改。
理由:迭代数组的工作方式是这样的,所以这个多边形的行为应该符合用户的期望。

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

}

如何根据自己的要求实现PolygonEnumerator 类?

在我看来,我无法存储对原始多边形的引用,因此我必须将其点复制到枚举器本身;但这意味着枚举器无法访问对原始多边形的更改!

我完全可以接受“不可能”的回答。

也许我在这里为自己挖了一个洞,却错过了一个有用的语言功能或原始问题的常规解决方案。

【问题讨论】:

  • 我怀疑你想要一个不可变的类,而不是一个结构。
  • Polygon 不应该是 struct,因为 ( 64 + 64 ) * sizeof(float)512 字节。这意味着每个值复制操作都需要 512 个字节的副本。 x64 上的 .NET Framework 不保证大型对象副本将是高效的(例如,使用 AVX 操作)。
  • 这似乎是一种非常低效的方法,并且过度设计,为什么不使用点数组?
  • @Dai,对不起,为了简洁起见,我把它省略了。 setter 实际上是必要的,但直到现在我才意识到它与问题的相关性。
  • 这在很多方面都是个坏主意。

标签: c# fixed unsafe ienumerator


【解决方案1】:

您的Polygon 类型不应是struct,因为( 64 + 64 ) * sizeof(float) == 512 bytes。这意味着每个值复制操作都需要 512 字节的副本 - 这是非常低效的(尤其是因为 locality-of-reference 强烈支持存在于内存中单个位置的使用对象)。

我要求 Polygon 必须按值复制,因此它是一个结构。 (理由:修改副本不应对原件产生副作用。)

你的“要求”是错误的。而是使用显式复制操作定义一个不可变类 - 和/或使用可变“构建器”对象来有效地构造大型对象。

我也希望它实现IEnumerable&lt;PointF&gt;。 (理由:能够为 (PointF p in poly) 写)

这很好 - 但您几乎不需要自己直接实现 IEnumerator&lt;T&gt;,因为 C# 可以在使用 yield return 时为您完成(并且生成的 CIL 非常优化!)。

我实现 Polygon 的按值复制行为的方法是使用不安全和固定的数组来允许多边形在结构本身中存储多达 64 个点,从而防止多边形通过其副本被间接修改。

这不是 C# 的编写方式。应尽可能避免使用unsafe(因为它破坏了 CLR 的内置保证和保护措施)。

另一个要求(一厢情愿)是枚举器将返回与 Polygon 的固定数组匹配的 PointF 值,即使这些点在迭代期间被修改。 (基本原理:遍历数组的工作方式是这样的,所以这个多边形的行为应该符合用户的期望。)

在这种情况下,您的用户/消费者是谁?如果您非常担心不会打破用户的期望,那么您不应该使用unsafe

请考虑这种方法:

(更新:我刚刚意识到我在下面定义的class Polygon 本质上只是ImmutableList&lt;T&gt; 的一个简单的包装——所以你甚至不需要class Polygon,所以只需使用@ 987654333@ 代替)

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

【讨论】:

  • 我没有看到创建此类的充分理由,索引不应该在 c# 中创建新对象,因为这不是预期的。一个简单的数组甚至更好的跨度会好很多。
  • @FilipCordas OP 正在使用 System.Drawing.PointF,这强烈暗示他们不是针对 .NET Core,因此无法有效使用 Span&lt;T&gt;
  • 感谢您挑战我的“要求”,但我不能放弃的一个实际要求是多边形类型必须是可变的。您应该能够添加和删除点,并通过索引器覆盖点。
  • @Romen 可变性和结构不能一起工作(因为结构的值复制语义),这就是为什么 ValueTuple 不是不可变的。听起来您还没有完全弄清楚您的 Polygon 课程实际上应该做什么。我认为您确实需要两种类型:可变的class PolygonBuilderclass ImmutablePolygon
猜你喜欢
  • 2018-02-15
  • 1970-01-01
  • 1970-01-01
  • 2016-06-21
  • 1970-01-01
  • 1970-01-01
  • 2014-01-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多