【问题标题】:Immutability of structs [duplicate]结构的不变性[重复]
【发布时间】:2009-03-03 22:42:02
【问题描述】:

可能重复:
Why are mutable structs evil?

我在很多地方都读到过,包括这里,最好将结构设为不可变。

这背后的原因是什么?我看到很多微软创建的可变结构,比如 xna 中的结构。 BCL 中可能还有更多。

不遵守本指南的利弊是什么?

【问题讨论】:

    标签: c# .net struct immutability


    【解决方案1】:

    结构应该代表。值不会改变。数字 12 是永恒的。

    但是,请考虑:

    Foo foo = new Foo(); // a mutable struct
    foo.Bar = 27;
    Foo foo2 = foo;
    foo2.Bar = 55;
    

    现在 foo.Bar 和 foo2.Bar 是不同的,这通常是出乎意料的。特别是在属性等场景中(幸运的是编译器检测到了这一点)。还有收藏品等;你如何明智地改变它们?

    使用可变结构很容易丢失数据。

    【讨论】:

    • 我怀疑你会注意到标准应用程序的不同之处。您可能会注意到一些非常专业的案例,但作为一般指导,不可变会胜出。
    • 无论答案如何(我不知道),你永远不应该让性能问题胜过良好的设计除非存在瓶颈并且分析器告诉你这是问题的根源。
    • 谢谢 Michael,我之所以谈论速度是因为对我来说,更改结构上的值看起来很明智,可能是因为我使用的其他语言或者它看起来像类。
    • @Henk = 那是不同的。我没有*重新分配 foo2;我更新了它。比如我不说 i.Sign = !i.Sign to update i;我用 i = -i; 重新分配它...重新分配它与更新属性非常不同。
    • 我真的不明白可变结构有多糟糕,如果我修改副本,我不希望修改原始结构,这就是我使用类的目的。我同意@Henk Holterman 的评估,但我不明白为什么您首先希望结构是不可变的。
    【解决方案2】:

    最大的缺点是事物的行为与您期望的不同 - 特别是当可变性来自直接值和其中的引用类型的混合时。

    老实说,当人们使用可变结构时,我不记得我看到人们在新闻组中提出的所有奇怪问题 - 但这些原因确实存在。 可变结构会导致问题。远离。

    编辑:我刚刚找到了一封我不久前写的关于这个主题的电子邮件。它详细说明了一点:

    • 这在哲学上是错误的:结构应该代表某种基本价值。这些基本上是不可变的。您无法更改数字 5。您可以将变量的值从 5 更改为 6,但逻辑上您不会更改值本身。

    • 这实际上是一个问题:它会产生很多奇怪的情况。如果它通过接口是可变的,那就特别糟糕了。然后您可以开始更改装箱值。伊克。我看过很多新闻组帖子,这些帖子是由于人们试图使用可变结构并遇到问题。我看到了一个非常奇怪的 LINQ 示例,它失败了,例如,List<T>.Enumerator 是一个结构。

    【讨论】:

    • 谢谢乔恩。速度方面,您认为更改结构中值的值比创建新结构更快吗?
    • @Joan Venge:我会回答:不。或者更确切地说,构造函数有一个方法调用,但它很可能是内联的,除非你的构造函数特别重。
    • @Henk;您永远不会更改 int;你创建一个新的。这就是不变性。
    • 你睡过吗?? XDDD
    • 真正的程序员不会睡觉,他们只是不时放弃剩余的时间段。 ;)
    【解决方案3】:

    我经常在我的(性能关键的)项目中使用可变结构 - 我没有遇到问题,因为我了解复制语义的含义。据我所知,人们提倡不可变结构的主要原因是,了解其含义的人不会陷入麻烦。

    这并不是一件可怕的事情——但我们现在面临着它成为“福音真理”的危险,而事实上,有时它是使结构可变的最佳选择。与所有事物一样,规则也有例外。

    【讨论】:

    • 请记住,大多数建议来自“框架设计指南”。这意味着他们的目标受众是编写 API 供其他人使用的人。在这种情况下,编写无操作“myClass.SomeValue.AProperty = 5”的能力只会招致失败。
    • @JonathanAllen:对此的补救措施是:(1)允许用属性标记属性和方法,指示它们是否应该在只读上下文中的结构上使用; (2) 让结构暴露字段而不是自变异属性。我想不出任何暴露自变异属性的框架结构,这些属性使用暴露的字段不会更好地实现。请注意,编译器不必猜测myClass.SomeValue.AField = 5 是否会尝试改变myClass.SomeValue。它可以说它会。
    • @JonathanAllen:我真的很恼火,没有办法覆盖编译器的“猜测”,即属性设置器将修改结构(并且应该在只读上下文中被禁止),或者其他方法不会(因此应该被允许)——特别是因为在某些情况下,结构体可以包装对可变类的不可变引用,并让属性设置器作用于类项,但最终会不得不通过使用类来创建不必要的 GC 压力,以允许属性访问器工作。
    【解决方案4】:

    没有什么比可变结构更便宜的了,这就是为什么您经常在图形处理例程等高性能代码中看到它。

    不幸的是,可变结构体不能很好地处理对象和属性,修改一个结构体的副本而不是结构体本身太容易了。因此它们不适用于您的大多数代码。

    附:为了避免复制可变结构的成本,它们通常以数组的形式存储和传递。

    【讨论】:

    • 恕我直言,属性不能很好地与结构一起工作的事实是属性工作方式的错误,并且缺乏任何标准方法来返回对值类型数组元素的引用.如果系统有一个标准的 ArrayIndexer(Of T) 类型,其中包含对数组的引用和对它的索引,则值类型属性可以工作得更好。
    • 行不通,您仍然面临编译器或 JIT 优化器将整个结构复制到堆栈中的风险。
    • 要使这个想法真正有用,编译器和 JITter 必须了解并遵守 ArrayIndexer(Of T) 类型的语义,它应该提供一种访问数组适当元素的方法不暴露数组本身。这也许可以通过一个 AccessElement(int index, ref PT1 p1, ref PT2 p2, delegate(ref T item, ref PT1 p1, ref PT2 p2)) 方法来完成,该方法将接受一个回调函数应该传递特定的数组项。如果没有编译器支持,代码会很难看,但如果有支持,应该没问题。
    • @supercat 花了一些时间,但现在已修复。如果您将属性设置为只读并使用ref 返回而不是正常返回,则可以按照您期望的方式改变基础结构字段。
    【解决方案5】:

    技术原因是可变结构似乎能够做他们实际上不做的事情。由于设计时语义与引用类型相同,这让开发人员感到困惑。这段代码:

    public void DoSomething(MySomething something)
    {
        something.Property = 10;
    }
    

    根据MySomethingstruct 还是class,行为完全不同。对我来说,这是一个令人信服的理由,但不是最令人信服的理由。如果您查看DDDValue Object,您可以看到与应该如何处理结构的联系。 DDD 中的值对象可以最好地表示为 .Net 中的值类型(因此是结构)。因为没有身份,所以无法改变。

    根据您的地址来考虑这一点。您可以“更改”您的地址,但地址本身并没有改变。事实上,您已经分配了一个新地址。从概念上讲,这是可行的,因为如果你真的更改了你的地址,你的室友也必须搬家。

    【讨论】:

      【解决方案6】:

      你已经询问了遵循结构应该是不可变的准则的利弊。

      缺点:缺点已在现有答案中得到很好的说明,并且描述的大多数问题都是由于相同的原因 - 由于结构的值语义导致的意外行为。。 p>

      优点:使用可变结构的主要优点是性能。显然,这个建议附带了关于优化的所有常见警告:确保您的代码部分需要进行优化,并确保任何更改确实通过分析优化了代码的性能。

      有关讨论何时可能需要使用可变结构的优秀文章,请参阅 Rico Mariani 的 Performance Quiz on Value-Based Programming(或更具体地说,the answers)。

      【讨论】:

        【解决方案7】:

        一个结构通常应该代表某种单一的统一体。因此,更改值的其中一个属性没有多大意义,如果您想要一个不同的值,则创建一个全新的值更有意义。

        使用不可变结构时语义会变得更简单,并且可以避免这样的陷阱:

        // a struct
        struct Interval {
           int From { get; set; }
           int To { get; set; }
        }
        
        // create a list of structs
        List<Interval> intervals = new List<Interval>();
        
        // add a struct to the list
        intervals.Add(new Interval());
        
        // try to set the values of the struct
        intervals[0].From = 10;
        intervals[0].To = 20;
        

        结果是列表中的结构根本没有改变。表达式 Interval[0] 从列表中复制结构的值,然后更改临时值的属性,但该值永远不会放回列表中。

        编辑:将示例更改为使用列表而不是数组。

        【讨论】:

        • 这在处理遗留代码时已经杀死了我很多次!将结构实现为不可变的值对象正好可以防止这种情况。
        • 谢谢 Guffa,但你确定吗? AFAIK 数组是唯一允许这样做的集合。
        • 是的,你是对的,我对此进行了测试,它实际上适用于数组。我编辑了示例以使用列表。
        • 谢谢 Guffa,一切都好 :)
        • Interval 是一个结构,因此您不能为此使用索引器。我假设您的意思是 intervals[0] 而不是 Interval[0]。即使这样,编译器也不允许赋值,因为它不是变量。您需要先分配一个局部变量,如var i = intervals[0];。更改此项目不会更改列表中的项目。
        【解决方案8】:

        当您复制周围的结构时,复制其内容,因此如果您修改复制的版本,“原始”将不会更新。

        这是错误的来源,因为即使您知道自己陷入了复制结构(只需将其传递给方法)并修改副本的陷阱。

        上周刚刚发生在我身上再次,让我花了一个小时寻找错误。

        保持结构不可变可以防止...

        除此之外,您首先需要确保有充分的理由使用结构 - “优化”或“我想要在堆栈上快速分配的东西”不算作答案。封送处理或依赖于布局的东西 - 好的,但通常不应将这些结构保留很长时间,它们是值而不是对象。

        【讨论】:

        • 谢谢,我对结构的主要依赖来自于 xna 等框架几乎将它们用于所有事情,所以即使我需要自己编写,我也只使用结构。
        【解决方案9】:

        您应该使结构不可变的原因是它们是ValueTypes,这意味着每次将它们传递给方法时都会复制它们。

        因此,例如,如果您有一个返回结构的属性,那么修改该结构上字段的值将毫无价值,因为 getter 将返回该结构的 副本,而不是而不是对结构的引用。我已经在代码中看到过这种情况,而且通常很难捕捉到。

        如果您将结构设计为不可变性,您可以帮助程序员避免这些错误。

        【讨论】:

          猜你喜欢
          • 2023-03-05
          • 1970-01-01
          • 2018-10-29
          • 2021-05-06
          • 1970-01-01
          • 1970-01-01
          • 2019-05-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多