【问题标题】:Modification of a field in copied structure change original structure field修改复制结构中的字段更改原始结构字段
【发布时间】:2017-06-23 20:18:15
【问题描述】:

在我的研究中,我读到结构是值类型,因此复制结构的更改不会改变原始结构,而不是类。这是我想要和期望的行为。但是,在下面的最小代码示例中,很明显,复制的结构中的更改会反映回原始结构 - 对于列表和数组都是如此。这是一个简单的WFA,一键供您测试。

为什么会这样?我read 说这可能发生在将结构作为数组成员传递时,我没有这样做,我在类似主题上发现的其他问题没有显示这种行为。如果有任何重复或相关问题,我没有找到它们,并且将不胜感激日后如何找到它们的提示。

namespace TestingStructs
{
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    struct SingleLevel
    {
        public int Level { get; }
        public int Property1 { get; }
        public decimal Property2 { get; }

        public SingleLevel(
            int level,
            int prop1,
            decimal prop2)
        {
            Property1 = prop1;
            Property2 = prop2;
            Level = level;
        }
    }

    struct Levels
    {
        public int NumberOfLevels { get; }
        public readonly List<SingleLevel> RightLevels;
        public readonly SingleLevel[] RightLevelsArray;
        public Levels(int numberofLevels)
        {
            NumberOfLevels = numberofLevels;

            RightLevels = new List<SingleLevel>();
            RightLevelsArray = new SingleLevel[numberofLevels + 1];
            for (int i = 1; i <= 3; i++)
            {
                SingleLevel rightToAdd = new SingleLevel(i, i * 100, 10 + i);
                RightLevels.Add(rightToAdd);
                RightLevelsArray[i - 1] = rightToAdd;
            }
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Levels lastLevels = new Levels(3);
        Levels temporaryLevels = lastLevels;
        MessageBox.Show("temp");
        MessageBox.Show(string.Join(Environment.NewLine, temporaryLevels.RightLevels.Select(right => right.Property2)));
        MessageBox.Show(string.Join(Environment.NewLine, temporaryLevels.RightLevelsArray.Select(right => right.Property2)));
        MessageBox.Show("last");
        MessageBox.Show(string.Join(Environment.NewLine, lastLevels.RightLevels.Select(right => right.Property2)));
        MessageBox.Show(string.Join(Environment.NewLine, lastLevels.RightLevelsArray.Select(right => right.Property2)));

        SingleLevel rightLevelToAdd = new SingleLevel(1, 50, 9.5m);
        temporaryLevels.RightLevels.Insert(0, rightLevelToAdd);
        temporaryLevels.RightLevelsArray[0] = rightLevelToAdd;

        MessageBox.Show("temp after insert");
        MessageBox.Show(string.Join(Environment.NewLine, temporaryLevels.RightLevels.Select(right => right.Property2)));
        MessageBox.Show(string.Join(Environment.NewLine, temporaryLevels.RightLevelsArray.Select(right => right.Property2)));
        MessageBox.Show("last after insert");
        MessageBox.Show(string.Join(Environment.NewLine, lastLevels.RightLevels.Select(right => right.Property2)));
        MessageBox.Show(string.Join(Environment.NewLine, lastLevels.RightLevelsArray.Select(right => right.Property2)));


    }
  }
}

输出:插入后的 temp 和 last 包含 Property2 的完全相同的值。

背景:在我的代码中,我使用了一个在运行时可能会或可能不会被修改的对象实例。因此,我想复制它,修改副本,然后可能将其恢复为原始对象。如果不是,我不想更改原件,因此不会将任何阶段的修改应用于原件。设计可能有缺陷,问题是关于结构问题。不过,如果您对此也有任何建议,我将不胜感激。

【问题讨论】:

    标签: c#


    【解决方案1】:

    虽然temporaryLevels 实际上是lastLevels 的副本,但属性RightLevels 和RightLevelsArray 仍然引用相同的数据,因为数组和列表是类类型。

    在作业中

           Levels temporaryLevels = lastLevels;
    

    您创建了对列表和数组的引用的副本,而不是对象本身的副本。

    我不知道有什么内置方法可以做到这一点,但你可以在 Levels 中定义一个复制构造函数:

    public Levels(Levels source)
    {
        NumberOfLevels = source.NumberOfLevels;
        RightLevelsArray = new SingleLevel[NumberOfLevels + 1];
        source.RightLevels.CopyTo(RightLevelsArray);
        RightLevels = new List<SingleLevel>();
        RightLevels.AddRange(source.RightLevels);
    }
    

    并且,您可以像这样调用新的构造函数,而不是赋值:

            Levels temporaryLevels = new Levels(lastLevels);
    

    现在temporaryLevels 是最后一个Levels 的深层副本,修改一个结构的列表或数组不会改变另一个。

    【讨论】:

    • 好的.. 我该如何改变呢?我没有看到自己使用与 Lists 不同的集合来实现我的目的,我需要能够单独复制和修改它们。你有什么建议吗。
    • 我已经编辑了我的原始答案并添加了一个可能的解决方案。
    【解决方案2】:

    我读到结构是值类型,因此复制的更改 struct不改变原来的

    这并不完全正确。结构确实是值类型并存储为一个,如果它包含结构 - 它仍然会内联堆栈中的字段,是的。

    struct Foo
    {
        int A;
        Bar bar;
    }
    
    struct Bar
    {
        int B;
        int C;
    }
    
    sizeof(Foo) == sizeof(int)+sizeof(Bar)
    

    但是,如果您的 struct 包含引用(例如,对于您的类),它将仅内联引用自身(地址值),而不是引用指向的类。

    struct Foo
    {
        int A;
        Bar bar;
    }
    
    class Bar
    {
        int B;
        int C;
    }
    
    sizeof(Foo) == sizeof(int)+sizeof(IntPtr)
    

    您的列表是一个。并且您的结构仅包含对此类型实例的引用。不是实例本身。

    【讨论】:

    • 您的回答似乎很有见地,但是我不知道这是在做什么诚实.. 或者它对我有什么帮助:) 但我很想理解,你能详细说明一下吗?顺便说一句,目前的代码不在 C# 中运行 - 'Program.Foo' 没有预定义的大小,因此 sizeof 只能在不安全的上下文中使用(考虑使用 System.Runtime.InteropServices.Marshal.大小
    【解决方案3】:

    您的问题似乎在于您对代码的工作方式所做的一些假设。

    当你这样做时:

    Levels temporaryLevels = lastLevels;
    

    它将创建一个新的Levels 结构,其中包含原始值的副本。如果被复制的属性是一个结构,那么它将创建一个新版本,如果它是一个类,那么它将引用同一个对象。这意味着虽然你的两个对象不同,但它们中的数组和列表对象是两者都引用相同的类,因此更改一个会更改另一个。

    要做你想做的事,你需要自己创建代码来克隆/复制结构。当您来到 List 和数组时,您需要创建新的并用旧的数据填充它们,确保在数据不是结构时创建副本(尽管在这种情况下它们是结构,所以您没有那个问题)。

    【讨论】:

    • 好的,您的解释有助于理解。至于“列表”的问题 - 尽管我明白你的观点,但我没有看到任何其他选项用于我的目的,在编写自己的克隆/复制方法时我将如何解决这个问题?
    • 我不清楚为什么你需要列表和数组(感觉就像一个奇怪的设计选择)但处理它很容易。在您的复制代码中,只需为您的副本创建一个新的List&lt;SingleLevel&gt;,其中包含每个 SingleLevel 项目的复制/克隆版本。
    • 啊,Array 只是为了测试行为是否相同。在我的真实代码中,我不使用数组。谢谢你的建议。这似乎符合 eocron 的回答:Your List 是一个类。并且您的结构仅包含对此类型实例的引用。不是实例本身。。如果我说出以下陈述,我是否正确理解了问题? “如果我通过将其等同于现有的List&lt;SingleLevel&gt; 创建一个新的List&lt;SingleLevel&gt;,我通过引用(并且修改将更改原始),但如果我逐项创建一个新的List&lt;SingleLevel&gt;,问题就解决了” ?
    • 我意识到我原来的答案实际上是错误的,我自己对结构的工作方式有一些误解。我已更新我的答案以使其更正确。请务必重读,对于第一次读错了,我们深表歉意。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-31
    • 1970-01-01
    • 2017-03-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多