根据定义,值类型是不可变的吗?

Posted

技术标签:

【中文标题】根据定义,值类型是不可变的吗?【英文标题】:Are value types immutable by definition? 【发布时间】:2010-10-26 11:41:44 【问题描述】:

我经常读到structs 应该是不可变的——它们不是根据定义吗?

你认为int 是不可变的吗?

int i = 0;
i = i + 123;

似乎没问题 - 我们得到一个新的 int 并将其分配回 i。这个呢?

i++;

好吧,我们可以把它当成一个捷径。

i = i + 1;

structPoint 呢?

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 和一个用法示例——可能包括ref 参数和装箱——这样就不可能重写所有出现的

foo.DoStuff(whatEverArgumentsYouLike);

foo = foo.DoStuff(whatEverArgumentsYouLike);

【问题讨论】:

【参考方案1】:

您可以编写可变结构,但最佳实践是使值类型不可变。

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

回答您的问题:不,根据定义,它们不是不可变的,这取决于它们是否应该是可变的。例如,如果它们应该用作字典键,它们应该是不可变的。

【讨论】:

您可以创建一个简单的结构,例如 struct Foo public int Bar; 。问题不在于你能不能做到这一点,而在于 Foo 是否可变。 例如,System.Drawing.Point 结构不是不可变的。 好吧,不知道框架中有什么可变结构。更正我的答案,谢谢。 我可以重写point.X = 42; as point = point.SetX(42);如果我总能做到这一点,我可能会认为 Point 结构是不可变的,即使界面不能很好地显示这一点。【参考方案2】:

可变性和值类型是两个不同的东西。

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

【讨论】:

我知道这一点,但问题是作为值类型是否意味着不可变。 这并不意味着它,因为由用户来实现不变性。您可以拥有一个不可变的 Point 类,或者实现可变的类。 我仍然认为你不能。你能举个例子,用 myStruct.DoStuuf() 修改结构不能解释为 myStruct = myStruct.DoStuff()? 发生这种情况是因为您正在重用变量 myStruct。如果 DoStuff 修改了同一个实例,则分配不会做任何事情(它会复制相同的实例)。如果 DoStuff 产生另一个修改后的实例,则分配它,并用它覆盖旧的 myStruct 内存空间。 这就是我的意思——你可以解释修改一个结构,用一个方法返回的另一个结构覆盖一个结构。因此,您可以认为结构是不可变的。以上显然不适用于引用类型。【参考方案3】:

我不想让推理复杂化 关于这个考虑ref 参数和装箱。我也知道 p = p.Offset(3, 4); 表示 不变性比 p.Offset(3, 4); 确实如此。但是 问题仍然存在 - 不是值类型 根据定义是不可变的?

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

【讨论】:

当然,这是一个相当理论性的问题,还有裁判和拳击的问题——我还没有完全弄清楚。我倾向于说 ref 没问题,因为您获得了对变量的引用,而不是对包含值的引用。拳击似乎有点难,我还在考虑。 您对 ref 的论点没有意义。是的,您获得了对它的引用,但您正在修改的值仍然是值类型。 我也不太明白你在谈论什么 ref 。这是对象:我已经获得了对它的引用。我可以更改它,这会更改与内存中相同对象关联的值。从任何意义上说,这个“不可变”是怎么回事?此时它的行为就像任何引用类型一样。 给定方法 static void Bar(ref int arg) arg = 42; 和 int foo = 1;酒吧(富);。这将修改 foo,但它不应该表明 int 是可变的。这是因为您获得了对变量 foo 的引用,而不是包含的 int 值。 @daniel:如果您不真正了解引用,那么您就不会了解可变性的真正问题。对低级语言(C)有一点经验,你就会发现问题。然后检查一个理智的高级语言(Scheme 和 Lua 非常适合这个),你会看到不变性是如何帮助的。【参考方案4】:

值类型在定义上不是不可变的吗?

不,它们不是:例如,如果您查看 System.Drawing.Point 结构体,它的 X 属性上有一个 setter 和一个 getter。

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

【讨论】:

是的,它有二传手,但我可以重写 point.X = 42; as point = point.SetX(42) - 问题是这是否总是可能的。如果是,您可以认为该结构是不可变的(但其接口不能很好地表达这种不可变性)。 如果一个类型的目的是封装一个固定的自变量集合(例如一个点的坐标),那么最优的实现是一个带有暴露公共字段的结构体(它的行为就像一个固定的自变量的集合)。使用不可变类可以笨拙地实现这种行为,并且可以以与不可变类一样笨拙的方式编码结构,但是如果一个人的目标是封装一组与管道类型固定在一起的固定变量,为什么不使用实现和行为完全符合预期的数据类型? @supercat 主要问题是具有 set 属性的结构,它可以让您执行类似 point.X += 3 之类的操作,但不能满足您的预期;而需要您说出point.SetX(point.X + 3) 的 API 则不太容易出错。 @ChrisW:我目前的理念是结构体应该在实用时要么尝试模拟不可变类,或者应该是公开的公共的集合字段,没有任何写入this的方法。 API 应避免写入this 的结构方法,因为当此类方法用于只读结构时,编译器会生成虚假代码。你给的API是最有问题的形式;我想你是想说point = point.WithX(point.X+3);Point2d.SetX(ref point, point.x+3); @ChrisW:基本上,在我看来,暴露字段结构并没有真正“封装”任何东西。在封装有帮助的情况下,这是一件坏事,但在某些情况下,封装是一个障碍。如果一个结构除了一组可以读取而没有副作用的值之外不包含任何状态,并且如果可以创建具有这些值的任意组合而没有副作用的实例,那么这些事实将完全定义该结构的语义——它将等效于具有这些类型字段的暴露字段结构。那么,为什么要让人们按原样使用它呢?【参考方案5】:

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

String s = "abc";

s.toLower();

如果编写了toLower 函数,因此返回一个替换“s”的新字符串,它是不可变的,但如果函数逐个字母替换“s”中的字母并且从不声明“新字符串” ,它是可变的。

【讨论】:

【参考方案6】:

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

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

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

这没有抓住重点。根据 .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 只声明一次。在此声明之后发生的任何事情都是修改。

在类似具有不可变变量的函数式语言中,这是不合法的。 ++i 是不可能的。一旦声明了一个变量,它就有一个固定的值。

在 .NET 中,情况并非如此,没有什么可以阻止我在声明 i 后对其进行修改。

再想一想,下面是另一个可能更好的例子:

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?

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

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

【讨论】:

也许是迄今为止最好的答案。但我认为这是两个概念——变量的可变性和变量值的可变性。打算考虑一下...+1 我已经重写了...我稍微修改了您的示例 - 如果我引入了您不想要的内容,请撤消此操作,但我相信您确实想将该字段设置为 43。 其实“set”函数是错误的。它显然应该只设置私有“i”字段而不是其他任何东西。没有返回值。 我再次删除了您的示例并稍微修改了我的帖子以使我的观点更清楚。也许您因为上述错误而误读了我的示例?我想展示的是,如果结构是不可变的,编译器必须实例化一个新的 S 对象来覆盖我在 Foo() 的第一行显式创建的那个。但是编译器没有办法创建一个“i”字段= 43的S实例,所以它不能随意创建新实例。它必须修改现有的,因为这是将“i”设置为 43 的唯一方法。 我忘记在我的编辑中添加一些东西 - 我不是在谈论让编译器重写代码。我只是在谈论结构及其接口的手动重新设计,因此您的示例与我的重写版本之间存在很大差异。【参考方案7】:

不,他们不是。示例:

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;

这是不同的。 i 不是一成不变的,5 是。第二个赋值不会复制对包含i 的结构的引用,而是复制i 的内容。所以在幕后,发生了完全不同的事情:你得到了变量的完整副本,而不是内存中地址(引用)的副本。

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

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

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

随着计算机变得更强大并获得更多内存,这种区别将会消失,因为它会导致大量错误和问题。在下一代中,将只有不可变对象,任何操作都将受到事务的保护,甚至int 将是一个完整的对象。就像垃圾收集一样,它将在程序稳定性方面向前迈出一大步,在最初的几年里会引起很多痛苦,但它将允许编写可靠的软件。今天,计算机的速度还不够快。

【讨论】:

你说,“现在,Point 是不可变的等等”,但这不是一个很好的例子:Point 不是不可变的。 你错了,如果Point是值类型,p.moveTo(5,7)方法调用后p2不等于p。 @Daniel:我是对的,因为 Point 在我的示例中不是值类型。 (“就地操作”) @ChrisW:这是因为没有方法可以就地修改它。 "这是不同的。i 不是不可变的,5 是。"这是个好的观点。变量本身是可变的,但不是变量的值。所以反对你回答“不,他们不是”。我仍然相信他们是。你能举一个 myStruct.DoStuff() 不能解释为 myStruct = myStruct.DoStuff() 的例子吗?【参考方案8】:

我认为令人困惑的是,如果您有一个应该像值类型一样工作的引用类型,那么让它不可变是个好主意。值类型和引用类型之间的主要区别之一是通过引用类型上的一个名称所做的更改可以显示在另一个名称中。值类型不会发生这种情况:

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 类就是这种行为的典型例子。

【讨论】:

这一点很清楚 - 具有值类型语义的引用类型必须或至少应该设计为不可变的。从您的陈述“[...] 这不会发生在值类型中:[...]”我得出结论,您倾向于同意我的结论 - 值类型根据定义是不可变的,因为您无法获得对值的引用对吧? 否 - 根据定义,值类型不是不可变的。在我上面的例子中,b.x = 2; 语句改变了b——它只是不会改变b2。这是值类型和引用类型之间的关键区别。我想您可以将其视为b 在更改时获得了具有新值的全新对象,但这不是正在发生的事情,我认为以这种方式思考它没有任何用处。 现在你明白我的意思了。我目前正在考虑 myStruct.DoStuff();重写为 myStruct = myStruct.DoStuff();因为这显然使结构的不变性。我的问题可以改写 - 你能找到一个上述转换无法完成或不起作用的例子吗? 我认为它没有技术问题。 "你能找到一个例子吗...?"是的,如果 b 和 b2 都声明为接口 IBar。【参考方案9】:

一个对象是不可变的,如果它的状态 一旦对象有就不会改变 已创建。

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


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

您可以创建可变结构,例如System.Drawing.PointXY 都有修改结构字段的设置器:

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。主要是因为可变值类型会导致各种不那么明显的错误。在上面的例子中,程序员可能在调用SetX() 之后期望p1(5, 0)。或者想象一下按一个以后可以更改的值进行排序。然后您的排序集合将不再按预期排序。字典和哈希也是如此。 Fabulous Eric Lippert (blog) 写了一个 whole series about immutability 以及为什么他认为这是 C# 的未来。 Here's one of his examples 可让您“修改”只读变量。


更新:您的示例:

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

正是 Lippert 在他的帖子中提到的关于修改只读变量的内容。 Offset(3,4) 实际上修改了一个Point,但它是readOnlyPoint副本,它从未分配给任何东西,所以它丢失了。

就是为什么可变值类型是邪恶的:它们让你认为你正在修改某些东西,而有时你实际上是在修改一个副本,这会导致意想不到的错误.如果Point 是不可变的,Offset() 将不得不返回一个新的Point,而您将无法将其分配给readOnlyPoint。然后你去“哦,对了,它是只读的是有原因的。我为什么要尝试更改它?幸好编译器现在阻止了我。”


更新:关于您改写的请求...我想我知道您的意思。在某种程度上,您可以“认为”结构是内部不可变的,修改结构与用修改后的副本替换它是一样的。据我所知,它甚至可能是 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

第 1 行和第 2 行的结果不同...为什么?因为foootherFoo 指的是Foo 的同一个盒装实例。第 1 行中 foo 中的任何更改都会反映在 otherFoo 中。第 2 行将 foo 替换为一个新值,并且对 otherFoo 没有任何作用(假设 DoStuff() 返回一个新的 IFoo 实例并且不修改 foo 本身)。

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 不会影响foo2foo3。修改foo2 将反映在foo3 中,但不会反映在foo1 中。修改 foo3 将反映在 foo2 中,但不会反映在 foo1 中。

令人困惑?坚持使用不可变的值类型,您就可以消除修改它们的冲动。


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

【讨论】:

我之前在几个答案中添加了这个评论。我可以重写 p.X = 5;如 p = p.SetX(5);.如果我总能做到这一点——值类型语义可能允许这样做,但我不确定——我可以认为该结构是不可变的或等效于不可变结构。所以我改写了这个问题——我可以一直做这种转变吗?如果是,这意味着结构是不可变的,因为我可以以一种使不变性显而易见的方式重写它们。 @Daniel:我不确定我是否关注你。如果你可以做 p.X = 5 类型是可变的。如果 p2 = p1.SetX(5) 不改变 p1,也没有办法改变 p1,那么它是不可变的。请注意,p = p.SetX(5) 是用新值替换 p 的值,而不是修改原始值。 你是绝对正确的。而且因为 p 是一个值类型并且你不能引用它,所以修改存储在 p 中的值或者用修改后的版本替换它都没有关系。如果你找到一个重要的例子——可能涉及 ref 参数、装箱或我什至没有想过的东西(目前正在考虑值类型属性)——那么我错了,结构是可变的。如果我总是可以将 myStruct.ChangeStuff() 转换为 myStruct = myStruct.ChangeStuff(),那么我可以认为结构是不可变的。 我想我终于明白你的意思了!这一切都在“p 是一个值类型,你不能引用它”,但你可以,通过装箱和接口。我已经更新了我的答案。 内部不变性:这不是一个实现细节——如果你有一个带有 myStruct.ChangeState() 的“可变”结构,你可以重新设计 myStruct = myStruct.GetCloneWithChangedState() 的接口来制作结构“不变”。两个版本使用不同的接口具有完全相同的行为 - 那么为什么我应该调用一个版本 mutabe 和一个不可变版本?装箱:我考虑装箱一个结构,然后传递对象,但所有方法都将在不同的未装箱值上操作,因此调用不会修改共享值。【参考方案10】:

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

首先,我应该问“值类型是否表现得像不可变类型?”这个问题。而不是询问它们是否是不可变的 - 我认为这会引起很多混乱。

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】:

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

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&lt;&gt;

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() 方法对数组项本身起作用。这意味着数组中的结构本身已更新。

在第二个示例中,我们使用了通用的List&lt;&gt;。当我们访问特定元素时会发生什么?好吧,indexer 属性被调用,这是一个方法。值类型在方法返回时总是被复制,所以这正是发生的情况:列表的索引器方法从内部数组中检索结构并将其返回给调用者。因为涉及到一个值类型,所以会做一个拷贝,在拷贝上调用Update()方法,当然对列表的原始项没有影响。

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

【讨论】:

问题不在于可变结构是邪恶的,而是 C# 没有办法指示哪些方法会改变结构,因此它可以禁止在只读上下文中使用它们。与混杂对象相比,可变结构通常提供非常优越的语义。如果我有一个 struct 'foo',并且我调用 bar1(foo),我可以保证 'bar' 不会更改 foo 的任何字段(如果某些字段包含类引用,当然有可能这些参考的目标可以改变)。如果我调用 bar2(ref foo),那么 bar2() 可能会改变 foo,但是... ...任何将要发生的更改都将在 bar() 返回之前发生。相比之下,如果我有一个类对象“zoo”并且我调用 bar3(zoo),bar3() 可能会立即改变 zoo,或者它可能会在某处存储对 zoo 的引用,这会导致其他线程在一些任意的未来时间。这似乎比可变结构的任何问题都要邪恶得多。可以肯定的是,.net 对可变结构的支持有一些奇怪的怪癖,但这些都是 .net 的缺陷,而不是可变结构的概念。【参考方案12】:

要定义一种类型是可变的还是不可变的,必须定义该“类型”所指的内容。当声明引用类型的存储位置时,声明只是分配空间来保存对存储在其他地方的对象的引用;该声明不会创建有问题的实际对象。尽管如此,在谈论特定引用类型的大多数上下文中,人们不会谈论包含引用的存储位置,而是由该引用标识的对象。可以写入保存对象引用的存储位置这一事实绝不意味着对象本身是可变的。

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

我认为将值类型“实例”视为字段而不是它们所持有的值更有帮助。根据该定义,存储在可变存储位置的任何值类型以及存在任何非默认值的值类型将始终是可变的,无论它是如何声明的。语句MyPoint = new Point(5,8) 构造Point 的新实例,其中包含字段X=5Y=8,然后通过将其字段中的值替换为新创建的Point 的值来改变MyPoint。即使 struct 无法在其构造函数之外修改其任何字段,但 struct 类型也无法保护实例的所有字段不被另一个实例的内容覆盖。

顺便说一下,一个可变结构可以实现通过其他方式无法实现的语义的简单示例:假设myPoints[] 是一个可被多个线程访问的单元素数组,让二十个线程同时执行代码:

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

如果myPoints[0].X 开始时等于 0,并且有 20 个线程执行上述代码,无论是否同时,myPoints[0].X 将等于 20。如果有人试图模仿上面的代码:

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

那么如果任何线程在另一个线程读取它并写回修改后的值之间读取myPoints[0].X,则增量的结果将丢失(结果myPoints[0].X可以任意结束在1之间的任何值和 20。

【讨论】:

以上是关于根据定义,值类型是不可变的吗?的主要内容,如果未能解决你的问题,请参考以下文章

原始类型变量在 PL/SQL 代码中是不可变的吗?

在Java中String类为什么要设计成final?String真的不可变吗?其他基本类型的包装类也是不可变的吗?

javaString 类型真是不可变的吗

Redshift 系统表是不可变且有序的吗?

ReadonlyCollection,对象是不可变的吗?

Elixir 变量真的是不可变的吗?