首页 > 解决方案 > 使用副本更改对象仍会导致源对象更改

问题描述

我有一个点云对象

public interface IPointCloud
{
    void ShiftToOrigin();

    Vertex[] Vertices { get; set; }

    // More here...
}

public class PointCloud : IEquatable<PointCloud>, IPointCloud
{
    public PointCloud()
    {
        Id = new TId();
    }

    public PointCloud(IPointCloud pointCloud) 
        : this(pointCloud.Vertices, pointCloud.Index, pointCloud.Source) { }

    public PointCloud(Vertex[] vertices) : this()
    {
        Vertices = vertices;
    }

    public PointCloud(Vertex[] vertices, int? index, string source = default)
        : this(vertices)
    {
        Index = index;
        Source = source;
    }

    #region Methods.
    public Vector3 GetCentroid()
    {
        var centroid = new Vector3();
        if (IsEmpty)
            return centroid;

        foreach (var vertex in Vertices)
            centroid += vertex.Point;

        return centroid / Vertices.Length;
    }

    public int ReorientateNormals()
    {
        if (!ContainsNormals)
            return 0;

        int counter = 0;
        var centroid = GetCentroid();
        for (int i = 0; i < Vertices.Length; ++i)
        {
            var normal = Vertices[i].Normal;
            Vertices[i].Normal = FlipNormalTowardCentroid(Vertices[i], centroid);

            if (normal != Vertices[i].Normal)
                counter++;
        }
        return counter;
    }

    private Vector3 FlipNormalTowardCentroid(Vertex vertex, Vector3 centroid)
    {
        var normal = vertex.Normal.ToVector();
        var viewPointVector = (centroid - vertex.Point).ToVector();

        if (viewPointVector.DotProduct(normal) < 0)
            normal *= -1;

        return normal.ToVector3();
    }

    /// <summary>
    /// Translate point cloud so that center = origin total shift applied to this point cloud is stored in this->shift.
    /// </summary>
    public void ShiftToOrigin()
    {
        var (minimumBound, maximumBound) = GetBoundingBoxAsPoints();
        Vector3 newshift = Vector3.Divide(minimumBound + maximumBound, 2.0f);

        for (int i = 0; i < Vertices.Length; ++i)
            Vertices[i].Point = Vertices[i].Point - newshift;

        GlobalShiftVector += newshift;
    }

    public virtual void RemovePoints(IPointCloud pointCloudToRemove)
    {
        if (pointCloudToRemove.Vertices == null || pointCloudToRemove.Vertices.Length == 0)
            return;

        var verticesToKeep = new List<Vertex>();
        var verticesToRemove = pointCloudToRemove.Vertices.ToList();

        foreach(var vertex in Vertices)
        {
            if (!verticesToRemove.Contains(vertex))
                verticesToKeep.Add(vertex);
        }
        Debug.Assert(verticesToKeep.Count == Vertices.Length - verticesToRemove.Count);

        Array.Clear(Vertices, 0, Vertices.Length);
        Vertices = verticesToKeep.ToArray();
    }

    public IPointCloud Merge(IPointCloud pointCloud)
    {
        if (pointCloud == null || pointCloud.Vertices.Length == 0)
            return this;

        List<Vertex> vertices = pointCloud.Vertices.ToList();
        if (Vertices == null || Vertices.Length == 0)
            return new PointCloud(vertices.ToArray(), pointCloud.Index, pointCloud.Source);

        vertices.AddRange(Vertices);
        return new PointCloud(vertices.ToArray(), Index, Source);
    }

    public (Vector3 MinimumBound, Vector3 MaximumBound) GetBoundingBoxAsPoints()
    {
        var min = new Vector3();
        var max = new Vector3();

        if (Vertices.Length > 0)
        {
            foreach (var point in Vertices.Select(v => v.Point))
            {
                if (min.X > point.X)
                    min.X = point.X;

                if (min.Y > point.Y)
                    min.Y = point.Y;

                if (min.Z > point.Z)
                    min.Z = point.Z;

                if (max.X < point.X)
                    max.X = point.X;

                if (max.Y < point.Y)
                    max.Y = point.Y;

                if (max.Z < point.Z)
                    max.Z = point.Z;
            }
        }
        return (min, max);
    }

    public BoundingBox GetBoundingBox()
    {
        var (MinimumBound, MaximumBound) = GetBoundingBoxAsPoints();
        return new BoundingBox(
            new Range(MinimumBound.X, MaximumBound.X),
            new Range(MinimumBound.Y, MaximumBound.Y),
            new Range(MinimumBound.Z, MaximumBound.Z));
    }

    public double GetPointDensity()
    {
        var boundingBox = GetBoundingBox();
        var volume = boundingBox.XRange.Length * boundingBox.YRange.Length * boundingBox.ZRange.Length;
        return Vertices.Length / volume;
    }
    #endregion // Methods.

    #region Operator Overrides.
    public bool Equals([AllowNull] PointCloud other)
    {
        return other.Id == Id;
    }

    public static bool operator ==(PointCloud left, PointCloud right)
    {
        if (ReferenceEquals(left, null))
            return ReferenceEquals(right, null);

        return left.Equals(right);
    }

    public static bool operator !=(PointCloud left, PointCloud right)
    {
        if (ReferenceEquals(left, null))
            return !ReferenceEquals(right, null);

        return !left.Equals(right);
    }
    #endregion // Operator Overrides.

    #region Object Overrides.
    /// <summary>
    /// Determines whether the specified <see cref="Range"/> is equal to the current one.
    /// </summary>
    /// <param name="obj">The <see cref="Range"/> object to compare with the current one.</param>
    /// <returns><c>true</c> if the specified <see cref="Range"/> is equal to the current one;
    /// otherwise <c>false</c>.</returns>
    public override bool Equals(object obj)
    {
        return this.Equals(obj as PointCloud);
    }

    /// <summary>
    /// Serves as a hash function for a particular type.
    /// </summary>
    /// <returns>A hash code for the current <see cref="Range"/>.</returns>
    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 29 + Id.GetHashCode();
            return hash;
        }
    }

    public override string ToString()
    {
        StringBuilder builder = new StringBuilder($"Point Cloud {{{Id}}}, Index {Index ?? -1:N0} (\"{Source}\"), ");
        builder.Append($"\tVertex count {Vertices.Length:N0}, Contains Normals = {ContainsNormals.ToString()}");
        return builder.ToString();
    }
    #endregion // Object Overrides.

    public TId Id { get; private set; }

    public Vertex[] Vertices { get; set; }

    public int? Index { get; set; }

    public string Source { get; set; }

    public bool IsEmpty => Vertices == null || Vertices.Length == 0;

    public bool ContainsNormals => Vertices == null ? false : Vertices.Any(v => v.Normal.X != 0.0f || v.Normal.Y != 0.0f || v.Normal.Z != 0.0f);

    public Vector3 GlobalShiftVector { get; private set; }
}

在哪里

public class Vertex : IEquatable<Vertex>, ICloneable
{
    public Vertex() { }
    public Vertex(Vector3 point, Vector3 normal)
    {
        Point = point;
        Normal = normal;
    }
    public Vertex(Vector3 point) : this(point, new Vector3()) { }

    #region IClonable.
    public object Clone()
    {
        return (Vertex)MemberwiseClone();
    }
    #endregion // IClonable.

    #region Object Overrides.
    public override bool Equals(object obj)
    {
        return this.Equals(obj as Vertex);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 29 + Point.GetHashCode();
            hash = hash * 29 + Normal.GetHashCode();
            return hash;
        }
    }
    #endregion //  Object Overrides.

    #region Comparison Operators.
    public static bool operator ==(Vertex left, Vertex right)
    {
        if (ReferenceEquals(left, null))
            return ReferenceEquals(right, null);

        return left.Equals(right);
    }

    public static bool operator !=(Vertex left, Vertex right)
    {
        if (ReferenceEquals(left, null))
            return !ReferenceEquals(right, null);

        return !left.Equals(right);
    }
    #endregion // Comparison Operators.

    #region IEquatable<Vertex>.
    public bool Equals(Vertex other)
    {
        if (ReferenceEquals(other, null))
            return false;

        if (ReferenceEquals(other, this))
            return true;

        return Point.IsEqual(other.Point) &&
               Normal.IsEqual(other.Normal);
    }
    #endregion // IEquatable<Vertex>.

    public Vector3 Point;

    public Vector3 Normal;
}

在这个对象上,我希望执行一些计算,但我不想修改原来的. 所以,在下面的方法中

public List<LineDescriptor> HoughTansform(IPointCloud pointCloud)
{
    _dX = _settingsProvider.LfdSolverSettings.HoughSpaceStepSize.Value;
    _minimumVotes = _settingsProvider.LfdSolverSettings.HoughMinimumVotesForLineSelection.Value;

    if (pointCloud.Vertices.Length < 2)
        throw new PointCloudException("Point cloud has less than two points");

    // Add more checks once we expose for parameterization. 
    if (_dX < 0)
        throw new ArgumentException("dX cannot be negative");

    if (_minimumVotes < 2)
        _minimumVotes = 2;

    // Ensure we get a shallow copy. 
    var basePointCloud = new PointCloud(pointCloud.Vertices.Select(v => (Vertex)v.Clone()).ToArray());
    
    var (minimumBound, maximumBound) = basePointCloud.GetBoundingBoxAsPoints();
    double d = (maximumBound - minimumBound).Norm();

    if (d == 0.0)
        throw new PointCloudException("All points in point cloud are identical");

    // ... More stuff here
    

我正在尝试使用数组创建一个新副本,Vertex这样我就不会修改原始点云对象pointCloud。我首先尝试使用简单的

var basePointCloud = new PointCloud(pointCloud.Vertices);

但这仍然出于某种原因修改了原件。按照下面的建议,我更新了这个,使用下面的“浅拷贝方法”

    var basePointCloud = new PointCloud(pointCloud.Vertices.Select(v => (Vertex)v.Clone()).ToArray());

这在附加调试器时工作正常,但令人惊讶的是它在发布模式下运行时不起作用,边界框被计算为 d = 0.0 - 这很讨厌。

如何确保制作正确的干净副本,以便我的更改basePointCloud不会影响原始pointCloud对象?

请注意,由于使用了Vector3结构,我无法使用任何形式的序列化创建深层副本,我的IPointCloud对象不可序列化。

更新

它实际上是

var (minimumBound, maximumBound) = basePointCloud.GetBoundingBoxAsPoints();

这一行,为什么要返回一个元组(0.0f, 0.0f)?我用了一些Console.WriteLines来看看发生了什么

元组的返回和设置失败。我试过使用

(Vector3 minimumBound, Vector3 maximumBound) = basePointCloud.GetBoundingBoxAsPoints();

但这也给出了上述结果。我很困惑。

标签: c#copy

解决方案


Vertex是引用类型。制作Vertex数组的副本不会复制其中包含的项目。原始数组和副本都将保存对相同Vertex对象的引用。

一种解决方案是转换Vertex为值类型。查看类定义,如果使其不可变是一种选择,它似乎不是一个糟糕的选择。可变结构总是一个坏主意。

另一种可能的选择(糟糕!)是创建一个复制机制Vertex,返回一个副本,然后将其投影到一个新数组:

new PointCloud(pointCloud.Vertices.Select(v => v.Copy()).ToArray());

但如果可能的话,我会推荐第一个选项。如果您需要按值复制语义,请使用类型系统提供的工具;值类型。


推荐阅读