从外部代码的角度来看,一个不可变的可变结构怎么样?

发布于 2024-09-24 19:36:07 字数 2992 浏览 9 评论 0原文

更新:在发布这个问题后,我想到这个想法的主要缺点就是这种类型很容易被不当使用。也就是说,必须以非常特定的方式使用该类型才能获得任何好处。我最初想到的是像这样使用的东西(为了保持一致性,坚持使用原始问题中的 SquareRootStruct 示例):

class SomeClass
{
    SquareRootStruct _key;

    public SomeClass(int value)
    {
        _key = new SquareRootStruct(value);
    }

    public double SquareRoot
    {
        // External code would have to access THIS property for caching
        // to provide any benefit.
        get { return _key.SquareRoot; }
    }

    public SquareRootStruct GetCopyOfKey()
    {
        // If _key has cached its calculation, then its copy will carry
        // the cached results with it.
        return _key;
    }
}

// elsewhere in the code...
var myObject = new SomeClass();

// If I do THIS, only a COPY of myObject's struct is caching a calculation,
// which buys me nothing.
double x = myObject.GetCopyOfKey().SquareRoot;

// So I would need to do this in order to get the benefit (which is,
// admittedly, confusing)...
double y = myObject.SquareRoot;

因此,考虑到这很容易出错,我'我倾向于认为里德(在他的评论中)也许是对的,这作为一个班级更有意义。


假设我有一个struct,我希望它具有以下特征:

  1. 外部的有利位置来看是不可变的
  2. 快速初始化
  3. 某些属性的惰性计算(和缓存)

显然是第三个特征意味着可变性,我们都知道这是不好的(假设“不要创建可变值类型!”这句格言已经深入我们的头脑)。但在我看来,只要可变部分仅在类型本身内部可见,并且从外部代码的角度来看,该值始终是相同的,这是可以接受的。

这是我正在谈论的一个例子:

struct SquareRootStruct : IEquatable<SquareRootStruct>
{
    readonly int m_value;  // This will never change.

    double m_sqrt;         // This will be calculated on the first property
                           // access, and thereafter never change (so it will
                           // appear immutable to external code).

    bool m_sqrtCalculated; // This flag will never be visible
                           // to external code.

    public SquareRootStruct(int value) : this()
    {
        m_value = value;
    }

    public int Value
    {
        get { return m_value; }
    }

    public double SquareRoot
    {
        if (!m_sqrtCalculated)
        {
            m_sqrt = Math.Sqrt((double)m_value);
            m_sqrtCalculated = true;
        }

        return m_sqrt;
    }

    public bool Equals(SquareRootStruct other)
    {
        return m_value == other.m_value;
    }

    public override bool Equals(object obj)
    {
        return obj is SquareRootStruct && Equals((SquareRootStruct)obj);
    }

    public override int GetHashCode()
    {
        return m_value;
    }
}

现在,显然这是一个简单的例子,因为 Math.Sqrt 几乎肯定不会花费足够的成本来考虑这种方法值得这个案例。这只是出于说明目的的示例。

但我的想法是,这实现了我的三个目标,而最明显的替代方法却无法实现。具体来说:

  • 可以在类型的构造函数中执行计算;但这可能达不到上面的第二个目标(快速初始化)。
  • 可以对每个属性访问执行计算;但这可能达不到上面的第三个目标(缓存计算结果以供将来访问)。

所以是的,这个想法将有效地导致内部可变的值类型。然而,就任何外部代码而言(据我所知),它看起来是不可变的,同时带来一些性能优势(同样,我意识到上面的示例不会< /em> 是这个想法的适当使用;我所说的“性能优势”将取决于计算实际上是否足够昂贵以保证缓存)。

我是否错过了什么,或者这实际上是一个值得的想法?

Update: It occurred to me after posting this question that the main downside of this idea would simply be that such a type would be easy to use improperly. That is, the type would have to be used in a very specific way to draw any benefits. What I originally had in mind was something that would be used like this (sticking with the SquareRootStruct example from the original question just for consistency):

class SomeClass
{
    SquareRootStruct _key;

    public SomeClass(int value)
    {
        _key = new SquareRootStruct(value);
    }

    public double SquareRoot
    {
        // External code would have to access THIS property for caching
        // to provide any benefit.
        get { return _key.SquareRoot; }
    }

    public SquareRootStruct GetCopyOfKey()
    {
        // If _key has cached its calculation, then its copy will carry
        // the cached results with it.
        return _key;
    }
}

// elsewhere in the code...
var myObject = new SomeClass();

// If I do THIS, only a COPY of myObject's struct is caching a calculation,
// which buys me nothing.
double x = myObject.GetCopyOfKey().SquareRoot;

// So I would need to do this in order to get the benefit (which is,
// admittedly, confusing)...
double y = myObject.SquareRoot;

So, considering how easy this would be to get wrong, I'm inclined to think that maybe Reed's right (in his comment) that this would make more sense as a class.


Suppose I have a struct that I want to have the following characteristics:

  1. Immutable from an external vantage point
  2. Fast to initialize
  3. Lazy calculation (and caching) of certain properties

Obviously the third characteristic implies mutability, which we all know is bad (assuming the mantra "Don't make mutable value types!" has been drilled into our heads sufficiently). But it seems to me that this would be acceptable as long as the mutable part is visible only internally to the type itself, and from outside code's perspective the value would always be the same.

Here's an example of what I'm talking about:

struct SquareRootStruct : IEquatable<SquareRootStruct>
{
    readonly int m_value;  // This will never change.

    double m_sqrt;         // This will be calculated on the first property
                           // access, and thereafter never change (so it will
                           // appear immutable to external code).

    bool m_sqrtCalculated; // This flag will never be visible
                           // to external code.

    public SquareRootStruct(int value) : this()
    {
        m_value = value;
    }

    public int Value
    {
        get { return m_value; }
    }

    public double SquareRoot
    {
        if (!m_sqrtCalculated)
        {
            m_sqrt = Math.Sqrt((double)m_value);
            m_sqrtCalculated = true;
        }

        return m_sqrt;
    }

    public bool Equals(SquareRootStruct other)
    {
        return m_value == other.m_value;
    }

    public override bool Equals(object obj)
    {
        return obj is SquareRootStruct && Equals((SquareRootStruct)obj);
    }

    public override int GetHashCode()
    {
        return m_value;
    }
}

Now, obviously this is a trivial example, as Math.Sqrt is almost certainly not costly enough to consider this approach worthwhile in this case. It's only an example for illustration purposes.

But my thinking is that this accomplishes my three objectives where the most obvious alternative approaches would not. Specifically:

  • I could perform the calculation in the type's constructor; but this would potentially fall short of the 2nd objective above (fast to initialize).
  • I could perform the calculation on every property access; but this would potentially fall short of the 3rd objective above (caching of calculated result for future accesses).

So yes, this idea would effectively lead to a value type that is internally mutable. However, as far as any external code could tell (as I see it), it would appear immutable, while bringing with it some performance benefits (again, I realize the above example would not be an appropriate use of this idea; the "performance benefits" I'm talking about would be contingent on the calculation actually being sufficiently costly to warrant caching).

Am I missing something, or is this in fact a worthwhile idea?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

不即不离 2024-10-01 19:36:07

拥有内部使用突变的不可变数据结构并没有什么问题(只要线程安全得到很好的宣传)。只是结构总是被复制,所以你不能对突变做太多事情。这在 C/C++ 中不是问题,因为结构通常通过引用传递,而在 C# 中很少通过引用传递结构。由于很难推理按值传递的结构,因此在 C# 中不鼓励使用可变结构。

There's nothing wrong with having immutable data structures that internally use mutation (so long as the thread safety is well advertised). It's just that structs get copied all the time, so you can't do much with mutation. This isn't a problem in C/C++ because structs are usually passed by reference, whereas in C# it is rare to pass structs by reference. Since it's hard to reason about structs that are passed by value, mutable structs are discouraged in C#.

请叫√我孤独 2024-10-01 19:36:07

您所描述的内容可能会有所帮助,但有一个重要的警告:缓慢计算的结果在计算时可能会也可能不会被存储。例如,如果对枚举返回的结构执行计算,结果将存储在“临时”结构中,并将被丢弃,而不是传播回存储在数据结构中的结构。

What you describe could probably work somewhat, but with an important caveat: the result of the slow calculation may or may not get stored when it is computed. For example, if the calculation is performed on the structs returned by an enumeration, the results will get stored in 'temporary' structs and will be discarded rather than propagated back into the ones stored in the data structure.

素手挽清风 2024-10-01 19:36:07

这看起来类似于 Future。有人提到 C# 4.0 并行扩展中对 Future 的更好支持。 (就像在另一个核心/线程上与正常工作并行计算它们一样)。

This looks similar to a Future. There was some mention of better support for Futures in C# 4.0 parallel extensions. (Like computing them on another core/thread in parallel to your normal work).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文