【问题标题】:Updating a property of another property in a class更新类中另一个属性的属性
【发布时间】:2020-03-05 12:39:31
【问题描述】:

我有一个非常初学者的问题:我确定我在这里遗漏了一些非常基本的东西

我有这个代码的简化版本(使用数组等。仅用于说明目的):

DataObject dataObj = new DataObject(new byte[7]{1,1,1,1,1,1,1));

dataObj.Meta.Prop0 = 8;
dataObj.Property1 = new byte[5]{8,8,8,8,8};

CollectionAssert.AreEqual(new byte[7]{8,1,8,8,8,8,8}, dataObj.GetData()); //<--- Fails.  array[0] is still 1.
Assert.AreEqual(8, dataObj.Meta.Prop0); //<--- Fails.  Prop0 never updated (still 1)
Assert.AreEqual(new byte[5]{8,8,8,8,8}, dataObj.Property1); //<--- OK.

为什么dataObj.Meta.Prop0 没有返回正确的值?我正在更新DataObject.Meta setter 中的_data,但是,如果我在Metadata.GetData() 方法中放置一个断点(从DataObject.Meta setter 内部调用),它永远不会被触发..

以下是DataObject 类实现的详细信息(根据要求更新了GetSubrray()UpdateData() 的实现):

    public class DataObject
    {
        private byte[] _data;

        public DataObject(byte[] objectData)
        {
            _data = objectData;
        }

        public byte[] GetData()
        {
            return _data;
        }

        public Metadata Meta
        {
            get
            {
                return new Metadata(GetSubarray(0, 2));
            }
            set
            {
                UpdateData(0, value.GetData());
            }
        }

        public byte[] Property1
        {
            get
            {
                return GetSubarray(2, 5);
            }
            set
            {
                UpdateData(2, value);
            }
        }

        private void UpdateData(int start, byte[] data)
        {
            Array.ConstrainedCopy(data, 0, _data, start, data.Length);
        }

        private byte[] GetSubarray(int start, int length)
        {
            byte[] arr = new byte[length];
            Array.ConstrainedCopy(_data, start, arr, 0, length);
            return arr;
        }
    }

    public class Metadata
    {
        private byte[] _metadata;

        public Metadata(byte[] metadata)
        {
            _metadata = metadata;
        }

        public byte[] GetData()
        {
            return _metadata;
        }

        public byte Prop0
        {
            get
            {
                return _metadata[0];
            }
            set
            {
                _metadata[0] = value;
            }
        }

        public byte Prop1
        {
            get
            {
                return _metadata[1];
            }
            set
            {
                _metadata[1] = value;
            }
        }
    }

【问题讨论】:

  • Subarray 方法从何而来?这不是我熟悉的标准方法。我的猜测是它正在复制数组的一部分 - 因此更改 copy 不会更改原始数据。
  • Update 方法相同。请给我们看。
  • @Sach 它们不存在。我编造它们是为了表明我正在更新数组(并且我知道该功能有效)。 Subarray(m,n) 做类似byte[] arr = new byte[n]; Array.ConstrainedCopy(_data, m, arr, 0, n); return arr; 的事情,而Update(m,n) 做相反的事情。
  • 您能否在 .NetFiddle 中获得一个工作示例,谢谢。
  • @Sach:如果我在Metadata.GetData() 方法中的return _metadata; 行上设置断点,则根本不会触发。

标签: c# class oop object properties


【解决方案1】:

很抱歉,设计是错误的! 您有 byte[] 主要数据的副本。例如当你说

return new Metadata(_data.Subarray(0,2));

你有相同的数据,但主要数据的副本。 这是副本。不引用主对象!

private byte[] GetSubarray(int start, int length)
        {
            byte[] arr = new byte[length];
            Array.ConstrainedCopy(_data, start, arr, 0, length);
            return arr;
        }

因此,当您在 Meta byte[] 上更新 Prop0 Prop1 等时,您认为主数据将被更新。对不起,不行。它是数据字节的副本.. 不是参考。

如果你想更新主对象,你应该把这个属性移到主对象里面。不是复制的东西。

将您的 Prop0 Prop1 等移动到主对象内。直接更新 main _data。不是对象的副本。


public class DataObject
    {
        private byte[] _data;

        public DataObject(byte[] objectData)
        {
            _data = objectData;
        }

        public byte[] GetData()
        {
            return _data;
        }

        public Metadata Meta {
            get {
                return new Metadata(GetSubarray(0, 2));
            }
            set {
                UpdateData(0, value.GetData());
            }
        }

        public byte[] Property1 {
            get {
                return GetSubarray(2, 5);
            }
            set {
                UpdateData(2, value);
            }
        }

        public byte Prop0 {
            get {
                return Meta.GetData()[0];
            }
            set {
                UpdateData(0, new byte[]{ value});
            }
        }

        public byte Prop1 {
            get {
                return Meta.GetData()[1];
            }
            set {
                UpdateData(1, new byte[] { value });
            }
        }

        private void UpdateData(int start, byte[] data)
        {
            Array.ConstrainedCopy(data, 0, _data, start, data.Length);
        }

        private byte[] GetSubarray(int start, int length)
        {
            byte[] arr = new byte[length];
            Array.ConstrainedCopy(_data, start, arr, 0, length);
            return arr;
        }
    }

【讨论】:

  • 对不起,我没有真正关注:您想将Metadata 对象的属性移动到DataObject 中吗?这不是我想象的那样:MetadataDataObject 的子对象。关于在我尝试访问Meta 时处理数据副本:虽然这可能是真的,但我也(理论上)在Meta (UpdateData(0, value.GetData());) 的设置器中写回对该副本所做的任何修改
【解决方案2】:

正如 cmets 和发布的其他答案中所述,您的代码的根本问题在这里:

public Metadata Meta
{
    get { return new Metadata(GetSubarray(0, 2)); }
    set { UpdateData(0, value.GetData()); }
}

您正在创建一个新的Metadata 对象这一事实使此语句不会按照您的想法执行:

dataObj.Meta.Prop0 = 8;

Meta 属性获取器正在创建一个新的Meta 对象,然后用于设置Prop0 属性。但是这个Meta 对象与DataObject 对象没有任何联系。调用对象的Prop0 设置器,然后立即忘记Meta 对象。没有对它的引用,最终会被垃圾回收器回收,对DataObject对象没有影响。

鉴于您当前的DataObject 实现,修复代码的一种方法是将Meta 对象检索到变量中,设置Prop0 属性,然后再次设置Meta 属性:

Meta meta = dataObj.Meta;

meta.Prop0 = 8;
dataObj.Meta = meta;

现在,坦率地说,我认为这个整体设计完全是坏掉了。将这些值存储在数组中,然后尝试将数组元素映射到各个属性的想法,只是自找麻烦。它将导致您在此处遇到的那种错误,并使理解和使用这些对象变得非常困难。如果你确实真的想使用一个数组来支持属性,那么你真的应该至少确保你只分配一次数组和对象,这样对于调用者来说,属性就像普通属性而不是这种奇怪的“你必须保存对象,修改它,然后再次设置属性值”语义。例如,像这样:

public class DataObject
{
    private struct ByteSpan : IList<byte>
    {
        private readonly byte[] _data;
        private readonly int _start;
        private readonly int _count;

        public ByteSpan(byte[] data, int start, int count)
        {
            _data = data;
            _start = start;
            _count = count;
        }

        public byte this[int index]
        {
            get
            {
                if (index < 0 || index >= _count) throw new IndexOutOfRangeException();
                return _data[_start + index];
            }
            set
            {
                if (index < 0 || index >= _count) throw new IndexOutOfRangeException();
                _data[_start + index] = value;
            }
        }

        public int Count => _count;
        public bool IsReadOnly => false;
        public void Add(byte item) => throw new NotImplementedException();
        public void Clear() => throw new NotImplementedException();
        public bool Contains(byte item) => this.Any(b => b == item);
        public void CopyTo(byte[] array, int arrayIndex) => Array.Copy(_data, _start, array, arrayIndex, _count);
        public IEnumerator<byte> GetEnumerator() => _data.Skip(_start).Take(_count).GetEnumerator();

        public int IndexOf(byte item)
        {
            int indexOf = ((IList<byte>)_data).IndexOf(item) - _start;

            if (indexOf < 0) return -1;
            if (indexOf >= _count) return -1;
            return indexOf;
        }

        public void Insert(int index, byte item) => throw new NotImplementedException();
        public bool Remove(byte item) => throw new NotImplementedException();
        public void RemoveAt(int index) => throw new NotImplementedException();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

    private readonly byte[] _data;
    private readonly IList<byte> _property1;

    public DataObject(byte[] objectData)
    {
        _data = objectData;
        Meta = new Metadata(new ByteSpan(_data, 0, 2));
        _property1 = new ByteSpan(_data, 2, 5);
    }

    public byte[] GetData()
    {
        return _data;
    }

    public Metadata Meta { get; }

    public IList<byte> Property1
    {
        get => _property1;
        set => CopyIList(value, _property1);
    }

    private void CopyIList<T>(IList<T> source, IList<T> destination)
    {
        if (source.Count != destination.Count) throw new ArgumentException("source and destination must be the same length");

        for (int i = 0; i < source.Count; i++)
        {
            destination[i] = source[i];
        }
    }
}

public class Metadata
{
    private IList<byte> _metadata;

    public Metadata(IList<byte> metadata)
    {
        _metadata = metadata;
    }

    public IList<byte> GetData()
    {
        return _metadata;
    }

    public byte Prop0
    {
        get => _metadata[0];
        set => _metadata[0] = value;
    }

    public byte Prop1
    {
        get => _metadata[1];
        set => _metadata[1] = value;
    }
}

要做到以上,需要对DataObject接口稍作修改。具体来说,它必须返回 IList&lt;T&gt; 对象,而不是返回数组。这允许将同一个数组用作所有公开的类数组属性的底层存储。

(旁白:上面使用了一个自定义的ByteSpan 类型,它实现了代表底层数据数组子集的IList&lt;T&gt;。如果你可以使用.NET Core,你会找到等效的@987654343 @ struct 可以用来代替。)

如果您坚持返回数组值而不是IList&lt;byte&gt;,那么您将不得不稍微修改上面的内容,以便属性设置器将提供的数组中的值复制到底层数据数组中。同样,getter 必须将值复制出来。不幸的是,这将导致与您已经处理的非常相似的事情,即属性返回的数组不能直接修改底层数据数组。如果调用者只检索数组值并修改它而不再次将属性设置为同一个数组,则底层数据数组将不会被修改,您将遇到同样的问题。

即使在上面的版本中,属性使用IList&lt;byte&gt; 而不是byte[],setter 仍然需要将给定的列表复制到属性自己的列表中,因为传递给setter 的列表可能是也可能不是原来的。如果不是,那么将属性的列表引用分配给它会断开该属性与底层存储的连接。只有通过将值从一个复制到另一个,属性的列表才能保持不变,并继续引用原始底层数据数组。

但至少通过使用IList&lt;byte&gt; 而不是byte[],调用者不需要在修改列表后再次设置属性值。修改列表本身将修改底层数据数组。

我强烈建议不要使用数据数组作为底层存储,但如果你这样做,我同样强烈建议你这样做使用类似ByteSpanSpan&lt;T&gt; 类型的东西来包装对象中的底层数据数组,调用者可以在直接影响底层存储的同时对其进行操作。

【讨论】:

  • 谢谢。我确实理解您的解释,即Meta 属性get 创建了与DataObject 无关的新对象。在我的脑海中,我认为我在 set { UpdateData(0, value.GetData()); } 中处理了这个问题,我不应该从这个不相关的元数据对象中获取数据并将其保存到 DataObject 中的数组中。我认为这是我没有完全到达这里的差距:为什么在更新新对象时不会调用set
  • “为什么在更新新对象时不调用 set” -- 仅在分配属性本身时才调用属性设置器。属性返回的 value 与此无关。见docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-21
  • 2017-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多