我经常读到struct
s应该是不可变的 - 根据定义它们不是吗?
您认为int
是不可变的吗?
int i = 0;
i = i + 123;
似乎没问题 - 我们收到一个新int
并将其分配回i
。那怎么样?
i++;
好的,我们可以将其视为捷径。
i = i + 1;
struct
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
的完整版本和使用示例 - 可能包含ref
参数和装箱 - 这样就无法重写所有出现的
foo.DoStuff(whatEverArgumentsYouLike);
与
foo = foo.DoStuff(whatEverArgumentsYouLike);
答案 0 :(得分:43)
如果一个对象的状态是对象是不可变的 一旦对象有,就不会改变 已创建。
简答:不,根据定义,值类型不是不可变的。 结构和类都可以是可变的或不可变的。所有四种组合都是可能的。如果结构或类具有非只读公共字段,具有setter的公共属性或设置私有字段的方法,则它是可变的,因为您可以在不创建该类型的新实例的情况下更改其状态。
长答案:首先,不变性问题仅适用于具有字段或属性的结构或类。最基本的类型(数字,字符串和null)本质上是不可变的,因为没有任何东西(字段/属性)可以改变它们。 A 5是5是5.对5的任何操作只返回另一个不可变值。
您可以创建可变结构,例如System.Drawing.Point
。 X
和Y
都有修改结构字段的setter:
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,为什么他认为这是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在内存中所做的事情。 (这就是闪存的工作原理。你不能只编辑几个字节,你需要将整块KB读入内存,修改你想要的几个,然后再写回整个块。)但是,即使它们是“内部不可变的” “,这是一个实现细节,对于我们的开发人员作为结构的用户(他们的界面或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行的结果不一样......为什么?因为foo
和otherFoo
引用了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
不会影响foo2
或foo3
。修改foo2
将反映在foo3
中,但不反映在foo1
中。修改foo3
将反映在foo2
中,但不会反映在foo1
中。
混淆?坚持使用不可变值类型,并消除修改其中任何一种的冲动。
更新:修复了第一个代码示例中的拼写错误
答案 1 :(得分:9)
可变性和值类型是两个不同的东西。
将类型定义为值类型,表示运行时将复制值而不是对运行时的引用。另一方面,可变性取决于实现,每个类都可以根据需要实现它。
答案 2 :(得分:6)
您可以编写可变的结构,但最佳做法是使值类型不可变。
例如,DateTime总是在执行任何操作时创建新实例。 Point是可变的,可以更改。
回答你的问题:不,根据定义它们不是不可变的,它取决于它们是否应该是可变的情况。例如,如果它们应该作为字典键,它们应该是不可变的。
答案 3 :(得分:5)
如果你的逻辑足够远,那么所有类型都是不可变的。当您修改引用类型时,您可能会认为您实际上是在将新对象写入同一地址,而不是修改任何内容。
或者你可以说任何语言中的一切都是可变的,因为偶尔以前用过一件事的记忆会被另一个人覆盖。
有了足够的抽象,忽略了足够的语言功能,你可以得到任何你喜欢的结论。
这就错过了重点。根据.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中,情况并非如此,没有什么可以阻止我在声明后修改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()
修改当前对象。编译器不能这样做,因为它会改变程序的行为(它会打印到控制台)
因此,对于所有结构都是不可变的,编译器必须作弊并破坏语言规则。当然,如果我们愿意违反规则,我们可以证明任何事情。我可以证明所有整数也是相同的,或者定义一个新类将导致你的计算机着火。 只要我们遵守语言规则,结构就是可变的。
答案 4 :(得分:4)
我不想让推理复杂化 通过考虑
ref
来解决这个问题 参数和拳击。我也知道 那个p = p.Offset(3, 4);
表达了 不变性要好得多p.Offset(3, 4);
。但是 问题仍然存在 - 不是价值类型 根据定义不可变?
那么,你真的不是在现实世界中经营,不是吗?在实践中,值函数在函数之间移动时复制自身的倾向与不变性很好地融合,但它们实际上并不是不可变的,除非你使它们不可变,因为正如你所指出的,你可以使用对它们的引用像其他任何东西一样。
答案 5 :(得分:4)
根据定义,不是值不可变的值吗?
不,他们不是:例如,如果你看一下System.Drawing.Point
结构,它的X
属性就有一个setter和一个getter。
然而,可以说所有值类型应使用不可变API定义。
答案 6 :(得分:2)
我认为令人困惑的是,如果你的引用类型应该像值类型一样,那么使它成为不可变的是一个好主意。值类型和引用类型之间的主要区别之一是,通过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类是此类行为的主要示例。
答案 7 :(得分:2)
去年我写了一篇关于你可以通过不构造结构而遇到的问题的博客文章 不可变的。
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()方法适用于数组项本身。这意味着数组中的结构本身已更新。
在第二个示例中,我们使用了通用List<>
。当我们访问特定元素时会发生什么?好吧,调用indexer属性,这是一种方法。当方法返回时,值类型总是被复制,所以这正是发生的事情:列表的索引器方法从内部数组中检索结构并将其返回给调用者。因为它涉及值类型,所以将创建一个副本,并且将在副本上调用Update()方法,这当然不会对列表的原始项目产生影响。
换句话说,始终确保您的结构是不可变的,因为您永远不确定何时会创建副本。大部分时间都是显而易见的,但在某些情况下,它真的让你大吃一惊......
答案 8 :(得分:1)
不,他们不是。例如:
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
也将是一个完整的对象。就像垃圾收集一样,它将是程序稳定性的一大进步,在最初几年引起很多悲痛,但它将允许编写可靠的软件。今天,计算机还不够快。
答案 9 :(得分:1)
不,根据定义,值类型不不可变。
首先,我最好问一个问题“值类型的行为类似于不可变类型吗?”而不是问他们是否是不可变的 - 我认为这引起了很多混乱。
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);
}
}
[继续......]
答案 10 :(得分:1)
要定义类型是可变的还是不可变的,必须定义“类型”所指的内容。当声明引用类型的存储位置时,声明仅分配空间来保存对存储在别处的对象的引用;声明不会创建有问题的实际对象。尽管如此,在大多数情况下,人们谈论特定的引用类型时,不会讨论一个存储引用的存储位置,而是该引用所标识的对象。事实上,人们可以写入一个存储位置来保存对象的引用,这意味着该对象本身并不可变。
相反,当声明值类型的存储位置时,系统将在该存储位置内为该值类型所拥有的每个公共或私有字段分配嵌套存储位置。关于值类型的所有内容都保存在该存储位置中。如果定义类型为foo
的变量Point
及其两个字段X
和Y
,则分别按住3和6。如果将Point
中的foo
的“实例”定义为字段,则当且仅当foo
可变时,该实例才是可变的。如果将Point
的实例定义为在这些字段中保存的值(例如“3,6”),则根据定义这样的实例是不可变的,因为更改其中一个字段会导致Point
持有不同的实例。
我认为将值类型“instance”视为字段而不是它们持有的值更有帮助。根据该定义,存储在可变存储位置中并且存在任何非默认值的任何值类型将总是是可变的,无论它是如何声明的。声明MyPoint = new Point(5,8)
构建Point
的新实例,其中包含字段X=5
和Y=8
,然后通过将其字段中的值替换为字段MyPoint
来变更Point
新创建的myPoints[]
。即使struct无法修改其构造函数之外的任何字段,结构类型也无法保护实例不会被其他实例的内容覆盖其所有字段。
顺便提一下,一个简单的例子,其中一个可变结构可以实现无法通过其他方式实现的语义:假设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
,则增量的结果将丢失(结果{{1}}可能随意结束1到20之间的任何值。
答案 11 :(得分:0)
对象/结构在以无法更改数据的方式传递给函数时是不可变的,并且返回的结构是new
结构。经典的例子是
String s = "abc";
s.toLower();
如果编写了toLower
函数,那么它们会返回一个替换“s”的新字符串,它是不可变的,但是如果函数逐个字母替换“s”中的字母并且从不声明“new”字符串“,它是可变的。