代码之家  ›  专栏  ›  技术社区  ›  Daniel Brückner

值类型是定义不变的吗?

  •  35
  • Daniel Brückner  · 技术社区  · 15 年前

    我经常读到 struct s应该是不可变的——它们不是定义上的吗?

    你认为 int 不可改变?

    int i = 0;
    i = i + 123;
    

    看起来不错-我们有一个新的 int i . 这个怎么样?

    i++;
    

    好吧,我们可以把它看作是一条捷径。

    i = i + 1;
    

    那这个呢 结构 Point

    Point p = new Point(1, 2);
    p.Offset(3, 4);
    

    这真的改变了这一点吗 (1, 2) ? 难道我们不应该把它看作是一条捷径,让下面的事情发生吗 Point.Offset() 返回一个新点?

    p = p.Offset(3, 4);
    

    这种思想的背景是这样的——没有标识的值类型怎么可能是可变的?你必须至少看两次才能确定它是否改变了。但是没有身份你怎么能做到这一点呢?

    我不想因为考虑到这一点而使推理复杂化 ref 参数和装箱。我也知道 p = p.Offset(3, 4); p.Offset(3, 4);

    使现代化

    我认为至少涉及两个概念——变量或字段的易变性和变量值的易变性。

    public class Foo
    {
        private Point point;
        private readonly Point readOnlyPoint;
    
        public Foo()
        {
            this.point = new Point(1, 2);
            this.readOnlyPoint = new Point(1, 2);
        }
    
        public void Bar()
        {
            this.point = new Point(1, 2);
            this.readOnlyPoint = new Point(1, 2); // Does not compile.
    
            this.point.Offset(3, 4); // Is now (4, 6).
            this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
        }
    }
    

    在本例中,我们必须定义字段-可变字段和不可变字段。因为值类型字段包含整个值,所以存储在不可变字段中的值类型也必须是不可变的。我仍然对这个结果感到惊讶——我没有检查readonly字段是否保持不变。

    变量(除常量外)总是可变的,因此它们对值类型的可变性没有限制。


    答案似乎不是那么直截了当,所以我将重新措辞这个问题。

    public struct Foo
    {
        public void DoStuff(whatEverArgumentsYouLike)
        {
            // Do what ever you like to do.
        }
    
        // Put in everything you like - fields, constants, methods, properties ...
    }
    

    你能给我一个完整的版本吗 Foo 和一个使用示例-可能包括 裁判 参数和装箱-因此不可能重写

    foo.DoStuff(whatEverArgumentsYouLike);
    

    具有

    foo = foo.DoStuff(whatEverArgumentsYouLike);
    
    12 回复  |  直到 15 年前
        1
  •  53
  •   Asik    4 年前

    如果对象的状态为 一旦对象发生更改,则不会更改 已经创建了。

    简短回答:不,值类型不是定义不变的。 结构和类都可以是可变的或不可变的。 所有四种组合都是可能的。如果结构或类具有非只读公共字段、带setter的公共属性或设置私有字段的方法,则该结构或类是可变的,因为您可以更改其状态,而无需创建该类型的新实例。


    长答案:首先,不变性问题只适用于具有字段或属性的结构或类。最基本的类型(数字、字符串和null)本质上是不可变的,因为它们没有任何(字段/属性)可更改。5是5是5。5上的任何操作只返回另一个不可变值。

    System.Drawing.Point . 二者都 X Y 具有修改结构字段的设置器:

    Point p = new Point(0, 0);
    p.X = 5;
    // we modify the struct through property setter X
    // still the same Point instance, but its state has changed
    // it's property X is now 5
    

    有些人似乎混淆了不可变性和值类型是通过值(因此它们的名称)而不是通过引用传递的事实。

    void Main()
    {
        Point p1 = new Point(0, 0);
        SetX(p1, 5);
        Console.WriteLine(p1.ToString());
    }
    
    void SetX(Point p2, int value)
    {
        p2.X = value;
    }
    

    在这种情况下 Console.WriteLine() 写下“ {X=0,Y=0} ”“给你 p1 未修改,因为 SetX() 被改进的 p2 哪一个是 复制 属于 p1 . 这是因为 p1 是一个 值类型 不是因为它是 不变的 (事实并非如此)。

    为什么? 应该 值类型是不可变的吗?有很多原因。。。看见 this question . 主要是因为可变值类型会导致各种不太明显的错误。在上面的例子中,程序员可能已经预料到了 p1 成为 (5, 0) 打完电话 SetX() . 或者想象一下,按照一个以后可能更改的值进行排序。然后,已排序的集合将不再按预期排序。字典和散列也是如此。这个 Fabulous Eric Lippert ( blog )他写了一封信 whole series about immutability Here's one of his examples 它允许您“修改”只读变量。


    更新:您的示例包括:

    this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
    

    Offset(3,4) 实际上修改了 Point ,但这是一个 复制 属于 readOnlyPoint ,而且它从未分配给任何东西,所以它丢失了。

    那个 认为 您正在修改某些内容,而有时您实际上正在修改副本,这会导致意外的错误。如果 指向 是一成不变的,, Offset() 指向 ,而您将无法将其分配给 . 然后你就走了


    更新:关于您重新措辞的请求。。。我想我知道你的意思。在某种程度上,您可以“认为”结构是 内部 不可变,即修改结构等同于用修改后的副本替换它。据我所知,它甚至可能是CLR在内存中内部执行的操作。(闪存就是这样工作的。你不能只编辑几个字节,你需要将一整块千字节读入内存,修改你想要的几个字节,然后写回整个块。)然而,即使它们是“内部不可变的”,这也是一个实现细节,对于作为结构用户的美国开发人员(如果你愿意的话,他们的接口或API),他们 可以 可以改变。我们不能忽视这一事实而“认为它们是不变的”。

    在一条评论中,您说“您不能引用字段或变量的值”。假设每个结构变量都有不同的副本,这样修改一个副本不会影响其他副本。这并不完全正确。如果出现以下情况,则不可更换下面标记的线路:。。。

    interface IFoo { DoStuff(); }
    struct Foo : IFoo { /* ... */ }
    
    IFoo otherFoo = new Foo();
    IFoo foo = otherFoo;
    foo.DoStuff(whatEverArgumentsYouLike); // line #1
    foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2
    

    foo otherFoo 参考 相同的装箱实例 福的。无论发生了什么变化 第#1行反映在 其他食物 . 第2行替换 其他食物 DoStuff() 返回一个新的 IFoo 实例,不进行修改 本身)。

    Foo foo1 = new Foo(); // creates first instance
    Foo foo2 = foo1; // create a copy (2nd instance)
    IFoo foo3 = foo2; // no copy here! foo2 and foo3 refer to same instance
    

    foo1 不会影响 foo2 foo3 . 修改 食物2 foo3 ,但不是在 foo1 . 修改 将反映在 食物2 但不是在

    令人困惑坚持使用不可变的值类型,就可以消除修改其中任何一种类型的冲动。


    更新:修复了第一个代码示例中的打字错误

        2
  •  11
  •   pgb    15 年前

    将类型定义为值类型表示运行时将复制值,而不是对运行时的引用。另一方面,可变性取决于实现,每个类都可以根据自己的需要实现它。

        3
  •  8
  •   Stefan Steinegger    15 年前

    您可以编写可变的结构,但最好的做法是使值类型不可变。

    例如,在执行任何操作时,instance DateTime始终创建新实例。点是可变的,可以更改。

    回答你的问题:不,它们在定义上不是不变的,这取决于它们是否应该是可变的。例如,如果它们应该用作字典键,那么它们应该是不可变的。

        4
  •  5
  •   Stack Overflow is garbage    15 年前

    如果你的逻辑足够深入,那么 全部的 类型是不可变的。当您修改引用类型时,您可能会认为您实际上是在将新对象写入同一地址,而不是修改任何内容。

    或者你可以争辩说,在任何语言中,任何东西都是可变的,因为有时以前用于一件事的内存会被另一件事覆盖。

    有了足够的抽象,忽略了足够的语言特征,你可以得出任何你喜欢的结论。

    这没有抓住重点。根据.NET规范,值类型是可变的。你可以修改它。

    int i = 0;
    Console.WriteLine(i); // will print 0, so here, i is 0
    ++i;
    Console.WriteLine(i); // will print 1, so here, i is 1
    

    但我还是一样。变量 i 只声明一次。在这个声明之后发生的任何事情都是一个修改。

    在类似于具有不可变变量的函数式语言的情况下,这是不合法的。那我就不可能了。一旦变量被声明,它就有一个固定的值。

    在.NET中,情况并非如此,没有什么可以阻止我修改 在它被宣布之后。

    再仔细考虑一下,下面是另一个可能更好的例子:

    struct S {
      public S(int i) { this.i = i == 43 ? 0 : i; }
      private int i;
      public void set(int i) { 
        Console.WriteLine("Hello World");
        this.i = i;
      }
    }
    
    void Foo {
      var s = new S(42); // Create an instance of S, internally storing the value 42
      s.set(43); // What happens here?
    }
    

    在最后一行,根据您的逻辑,我们可以说我们实际上构造了一个新对象,并用该值覆盖旧对象。 但那是不可能的!要构造新对象,编译器必须设置 变量为42。但这是私人的!它只能通过用户定义的构造函数访问,该构造函数明确禁止值43(改为设置为0),然后通过我们的 set 方法,它有一个令人讨厌的副作用。编译器无法进行编译 只是 使用喜欢的值创建新对象。唯一的方法 s.i 可设置为43是由 修改 通过调用 set() . 编译器不能这样做,因为它会改变程序的行为(它会打印到控制台)

    因此,要使所有结构都是不可变的,编译器必须欺骗并打破语言的规则。当然,如果我们愿意打破规则,我们可以证明一切。我可以证明所有的整数也是相等的,或者定义一个新的类会导致你的计算机着火。 只要我们遵守语言的规则,结构是可变的。

        5
  •  4
  •   mqp    15 年前

    我不想把推理复杂化 ref 参数和装箱。我也知道 p = p.Offset(3, 4); 快件 不变性比 p.Offset(3, 4); 做但是 问题仍然存在——难道不是值类型吗 定义不变?

    那你不是在现实世界里工作吧?在实践中,值类型在函数之间移动时复制自身的倾向与不变性很好地结合在一起,但除非将它们设置为不可变,否则它们实际上不是不可变的,因为正如您所指出的,您可以像其他任何东西一样使用对它们的引用。

        6
  •  4
  •   ChrisW    15 年前

    不,他们不是:如果你看 System.Drawing.Point 例如,它有一个setter和一个getter X 所有物

    然而,可以说所有的值类型 应该 可以使用不可变的API定义。

        7
  •  2
  •   Michael Burr    15 年前

    我认为混淆之处在于,如果您有一个应该像值类型一样工作的引用类型,那么最好使其不可变。值类型和引用类型之间的关键区别之一是,通过ref类型上的一个名称所做的更改可以显示在另一个名称中。值类型不会发生这种情况:

    public class foo
    {
        public int x;
    }
    
    public struct bar
    {
        public int x;
    }
    
    
    public class MyClass
    {
        public static void Main()
        {
            foo a = new foo();
            bar b = new bar();
    
            a.x = 1;
            b.x = 1;
    
            foo a2 = a;
            bar b2 = b;
    
            a.x = 2;
            b.x = 2;
    
            Console.WriteLine( "a2.x == {0}", a2.x);
            Console.WriteLine( "b2.x == {0}", b2.x);
        }
    }
    

    生产:

    a2.x == 2
    b2.x == 1
    

    现在,如果你有一个你想要值语义的类型,但是不想让它成为一个值类型——也许是因为它需要的存储太多或者什么,你应该认为不可变是设计的一部分。对于不可变的ref类型,对现有引用所做的任何更改都会生成一个新对象,而不是更改现有对象,因此您可以获得值类型的行为,即您持有的任何值都不能通过其他名称更改。

    当然,System.String类就是此类行为的一个主要示例。

        8
  •  2
  •   Philippe Leybaert    15 年前

    去年,我写了一篇关于不制作结构可能遇到的问题的博文 不变的

    The full post can be read here

    这是一个例子,说明了事情会变得多么糟糕:

    //Struct declaration:
    
    struct MyStruct
    {
      public int Value = 0;
    
      public void Update(int i) { Value = i; }
    }
    

    代码示例:

    MyStruct[] list = new MyStruct[5];
    
    for (int i=0;i<5;i++)
      Console.Write(list[i].Value + " ");
    Console.WriteLine();
    
    for (int i=0;i<5;i++)
      list[i].Update(i+1);
    
    for (int i=0;i<5;i++)
      Console.Write(list[i].Value + " ");
    Console.WriteLine();
    

    此代码的输出为:

    0 0 0 0 0
    1 2 3 4 5
    

    现在让我们做同样的事情,但是用数组替换泛型 List<>

    List<MyStruct> list = new List<MyStruct>(new MyStruct[5]); 
    
    for (int i=0;i<5;i++)
      Console.Write(list[i].Value + " ");
    Console.WriteLine();
    
    for (int i=0;i<5;i++)
      list[i].Update(i+1);
    
    for (int i=0;i<5;i++)
      Console.Write(list[i].Value + " ");
    Console.WriteLine();
    

    输出为:

    0 0 0 0 0
    0 0 0 0 0
    

    从数组中访问元素时,运行时将直接获取数组元素,因此Update()方法对数组项本身有效。这意味着数组中的结构本身将被更新。

    在第二个示例中,我们使用了泛型 列表<&燃气轮机; . 当我们访问特定元素时会发生什么?嗯,索引器属性被调用,这是一个方法。值类型在方法返回时总是被复制的,所以这就是实际情况:列表的indexer方法从内部数组检索结构并将其返回给调用方。因为它涉及一个值类型,所以将创建一个副本,并对副本调用Update()方法,这当然不会影响列表的原始项。

    换句话说,始终确保您的结构是不可变的,因为您永远无法确定何时将创建副本。大多数情况下,这是显而易见的,但在某些情况下,它真的会让你大吃一惊。。。

        9
  •  1
  •   Aaron Digulla    15 年前

    不,他们不是。例子:

    Point p = new Point (3,4);
    Point p2 = p;
    p.moveTo (5,7);
    

    在这个例子中 moveTo() 到位 活动它会更改隐藏在引用后面的结构 p p2 :它的位置也将发生变化。有着一成不变的结构,, moveTo() 必须返回一个新结构:

    p = p.moveTo (5,7);
    

    现在 Point 是不可变的,当您在代码中的任何地方创建对它的引用时,您不会得到任何惊喜。让我们看看 i :

    int i = 5;
    int j = i;
    i = 1;
    

    这是不同的。 不是一成不变的,, 5 是第二个赋值不会将引用复制到包含 但它复制了 . 因此,在幕后,发生了完全不同的事情:您获得了变量的完整副本,而不是内存中地址(引用)的副本。

    与对象等效的是复制构造函数:

    Point p = new Point (3,4);
    Point p2 = new Point (p);
    

    这里是 复制到新对象/结构中,并 p2 将包含对它的引用。但这是一个非常昂贵的操作(与上面的整数赋值不同),这就是为什么大多数编程语言会做出区分。

    int 将是一个完整的对象。就像垃圾收集一样,它将在程序稳定性方面向前迈出一大步,在最初的几年里会造成很多痛苦,但它将允许编写可靠的软件。今天,计算机的速度还不够快。

        10
  •  1
  •   Daniel Brückner    15 年前

    不,值类型是 根据定义是不变的。

    首先,我应该问一个问题“值类型的行为是否像不可变类型?”而不是问它们是否是不可变的——我想这会引起很多混乱。

    struct MutableStruct
    {
        private int state;
    
        public MutableStruct(int state) { this.state = state; }
    
        public void ChangeState() { this.state++; }
    }
    
    struct ImmutableStruct
    {
        private readonly int state;
    
        public MutableStruct(int state) { this.state = state; }
    
        public ImmutableStruct ChangeState()
        {
            return new ImmutableStruct(this.state + 1);
        }
    }
    

    [待续……]

        11
  •  1
  •   nawfal Donny V.    11 年前

    要定义类型是可变的还是不可变的,必须定义“类型”所指的内容。当声明引用类型的存储位置时,该声明仅分配空间来保存对存储在别处的对象的引用;该声明不会创建所讨论的实际对象。尽管如此,在大多数讨论特定引用类型的上下文中,都不会讨论特定的引用类型 保存引用的存储位置 ,而是 由该引用标识的对象 . 可以写入包含对象引用的存储位置这一事实并不意味着对象本身是可变的。

    相反,当声明值类型的存储位置时,系统将在该存储位置内为该值类型持有的每个公共或私有字段分配嵌套的存储位置。值类型的所有内容都保存在该存储位置。如果定义了一个变量 foo 类型 Point 以及它的两个领域,, X Y 指向 作为一对 领域 ,该实例将是可变的当且仅当 是可变的。如果定义了 作为 保存在这些字段中(例如“3,6”),则根据定义,这样的实例是不可变的,因为更改其中一个字段会导致 指向 持有不同的实例。

    我认为将值类型“instance”视为字段比它们所持有的值更有帮助。根据该定义,存储在可变存储位置且存在任何非默认值的任何值类型都将 总是 无论它是如何声明的,都是可变的。一个声明 MyPoint = new Point(5,8) 构造的新实例 指向 ,带字段 X=5 Y=8 ,然后变异 MyPoint 通过将其字段中的值替换为新创建的 指向 . 即使一个结构不能提供修改其构造函数之外的任何字段的方法,结构类型也无法保护一个实例,使其所有字段不被另一个实例的内容覆盖。

    myPoints[] 是一个单元素数组,可供多个线程访问,有20个线程同时执行代码:

    Threading.Interlocked.Increment(myPoints[0].X);
    

    如果 myPoints[0].X myPoints[0].X 等于二十。如果有人试图用以下方法模拟上述代码:

    myPoints[0] = new Point(myPoints[0].X + 1, myPoints[0].Y);
    

    如果有线程读取 myPoints[0].X 在另一个线程读取并写回修改后的值之间,增量的结果将丢失(其结果是 myPoints[0].X

        12
  •  0
  •   David Cooper    15 年前

    当对象/结构以无法更改数据的方式传递到函数中时,它们是不可变的,并且返回的结构是 new 结构。经典的例子是

    String s = "abc";

    s.toLower();

    toLower 函数被写入后,返回一个替换“s”的新字符串,该字符串是不可变的,但如果函数逐字替换“s”中的字母,并且从不声明“新字符串”,则该字符串是可变的。

    推荐文章