在这里的SO讨论之后,我已经多次看到这样的评论,即可变结构体是“邪恶的”(就像在这个问题的答案中)。
c#中的可变性和结构的实际问题是什么?
在这里的SO讨论之后,我已经多次看到这样的评论,即可变结构体是“邪恶的”(就像在这个问题的答案中)。
c#中的可变性和结构的实际问题是什么?
从哪里开始;-p
埃里克·利珀特的博客总是很适合引用:
这是可变的另一个原因 值类型是邪恶的。试着总是 使值类型不可变。
首先,您很容易丢失更改……例如,从列表中获取内容:
Foo foo = list[0];
foo.Name = "abc";
这改变了什么?没有什么有用的…
属性也是一样:
myObj.SomeProperty.Size = 22; // the compiler spots this one
强迫你做:
Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;
不那么关键的是规模问题;可变对象往往有多个属性;然而,如果你有一个包含两个int型,一个string型,一个DateTime型和一个bool型的结构体,你会很快消耗大量内存。使用类,多个调用方可以共享对同一个实例的引用(引用很小)。
它与结构无关(也与c#无关),但在Java中,当可变对象是哈希映射中的键时,你可能会遇到问题。如果你在将它们添加到映射后更改它们,它也更改了哈希代码,可能会发生糟糕的事情。
我不会说可变性是邪恶的,但可变性通常是程序员急于提供最大功能的标志。在现实中,这通常是不需要的,反过来,使界面更小,更容易使用,更难使用错误(=更健壮)。
其中一个例子就是竞态条件中的读/写和写/写冲突。这些在不可变结构中根本不可能发生,因为写操作不是有效的操作。
另外,我认为可变性几乎从来都不是真正需要的,程序员只是认为它在未来可能会出现。例如,改变日期是没有意义的。相反,在旧日期的基础上创建一个新的日期。这是一个廉价的操作,所以性能不是一个考虑因素。
结构是值类型,这意味着它们在传递时被复制。
所以如果你改变了一份副本,你只是改变了那份副本,而不是原件,也不是周围可能存在的其他副本。
如果你的struct是不可变的,那么所有通过值传递的自动副本都是相同的。
如果你想要改变它,你必须有意识地用修改过的数据创建一个结构的新实例。(非副本)
可变数据有许多优点和缺点。最大的缺点就是别名。如果相同的值在多个地方使用,其中一个地方更改了它,那么它将神奇地更改到正在使用它的其他地方。这与竞态条件有关,但并不完全相同。
有时候,价值百万美元的优势是模块化。可变状态允许您向代码隐藏更改的信息,而代码不需要知道这些信息。
《解释器的艺术》详细讨论了这些权衡,并给出了一些例子。
Value types basically represents immutable concepts. Fx, it makes no sense to have a mathematical value such as an integer, vector etc. and then be able to modify it. That would be like redefining the meaning of a value. Instead of changing a value type, it makes more sense to assign another unique value. Think about the fact that value types are compared by comparing all the values of its properties. The point is that if the properties are the same then it is the same universal representation of that value.
正如Konrad所提到的,更改日期也没有意义,因为值代表的是唯一的时间点,而不是具有任何状态或上下文依赖关系的时间对象的实例。
希望这能让你明白。可以肯定的是,它更多的是关于您试图用值类型捕获的概念,而不是实际的细节。
假设您有一个包含1,000,000个结构体的数组。每个结构体都用bid_price、offer_price(可能是小数)等表示权益,这是由c# /VB创建的。
假设数组是在非托管堆中分配的内存块中创建的,以便其他一些本地代码线程能够并发地访问该数组(可能是一些高性能代码进行数学运算)。
想象一下c# /VB代码正在监听价格变化的市场反馈,该代码可能必须访问数组的某些元素(用于任何安全性),然后修改一些价格字段。
想象一下,这个过程以每秒数万次甚至数十万次的速度进行。
让我们面对现实吧,在这种情况下,我们确实希望这些结构体是可变的,它们必须是可变的,因为它们被其他本地代码共享所以创建副本是没有用的;他们需要这样做,因为以这样的速率复制大约120字节的结构是疯狂的,特别是当一个更新实际上可能只影响一两个字节时。
Hugo
就我个人而言,当我看代码时,下面的代码看起来相当笨拙:
data.value.set ( data.value.get () + 1 ) ;
而不是简单地
数据.值++ ;或数据值 = 数据值 + 1 ;
数据封装在传递类时非常有用,并且您希望确保以受控的方式修改值。然而,当你拥有公共的set和get函数,它们所做的仅仅是将值设置为传递进来的值时,这比简单地传递公共数据结构有什么改进呢?
当我在类中创建私有结构时,我创建了该结构来将一组变量组织到一个组中。我希望能够在类范围内修改该结构,而不是获得该结构的副本并创建新实例。
对我来说,这阻止了有效使用用于组织公共变量的结构,如果我想要访问控制,我会使用类。
从程序员的角度来看,还有一些其他的极端情况可能导致不可预测的行为。
不可变值类型和只读字段
// Simple mutable structure.
// Method IncrementI mutates current state.
struct Mutable
{
public Mutable(int i) : this()
{
I = i;
}
public void IncrementI() { I++; }
public int I { get; private set; }
}
// Simple class that contains Mutable structure
// as readonly field
class SomeClass
{
public readonly Mutable mutable = new Mutable(5);
}
// Simple class that contains Mutable structure
// as ordinary (non-readonly) field
class AnotherClass
{
public Mutable mutable = new Mutable(5);
}
class Program
{
void Main()
{
// Case 1. Mutable readonly field
var someClass = new SomeClass();
someClass.mutable.IncrementI();
// still 5, not 6, because SomeClass.mutable field is readonly
// and compiler creates temporary copy every time when you trying to
// access this field
Console.WriteLine(someClass.mutable.I);
// Case 2. Mutable ordinary field
var anotherClass = new AnotherClass();
anotherClass.mutable.IncrementI();
// Prints 6, because AnotherClass.mutable field is not readonly
Console.WriteLine(anotherClass.mutable.I);
}
}
可变值类型和数组
假设我们有一个可变结构的数组,我们为该数组的第一个元素调用IncrementI方法。你希望从这个电话中得到什么行为?它应该改变数组的值还是只改变一个副本?
Mutable[] arrayOfMutables = new Mutable[1];
arrayOfMutables[0] = new Mutable(5);
// Now we actually accessing reference to the first element
// without making any additional copy
arrayOfMutables[0].IncrementI();
// Prints 6!!
Console.WriteLine(arrayOfMutables[0].I);
// Every array implements IList<T> interface
IList<Mutable> listOfMutables = arrayOfMutables;
// But accessing values through this interface lead
// to different behavior: IList indexer returns a copy
// instead of an managed reference
listOfMutables[0].IncrementI(); // Should change I to 7
// Nope! we still have 6, because previous line of code
// mutate a copy instead of a list value
Console.WriteLine(listOfMutables[0].I);
因此,只要您和团队的其他成员清楚地了解您在做什么,可变结构就不是邪恶的。但是有太多的极端情况,当程序行为与预期不同时,这可能会导致微妙的难以产生和难以理解的错误。
具有公共可变字段或属性的结构并不邪恶。
Struct methods (as distinct from property setters) which mutate "this" are somewhat evil, only because .net doesn't provide a means of distinguishing them from methods which do not. Struct methods that do not mutate "this" should be invokable even on read-only structs without any need for defensive copying. Methods which do mutate "this" should not be invokable at all on read-only structs. Since .net doesn't want to forbid struct methods that don't modify "this" from being invoked on read-only structs, but doesn't want to allow read-only structs to be mutated, it defensively copies structs in read-only contexts, arguably getting the worst of both worlds.
尽管在只读上下文中处理自突变方法存在问题,但是,可变结构通常提供的语义要比可变类类型优越得多。考虑以下三个方法签名:
struct PointyStruct {public int x,y,z;}; class PointyClass {public int x,y,z;}; void Method1(PointyStruct foo); void Method2(ref PointyStruct foo); void Method3(PointyClass foo);
对于每种方法,请回答以下问题:
假设该方法没有使用任何“不安全”代码,它会修改foo吗? 如果在调用方法之前没有对'foo'的外部引用,那么在调用方法之后是否可以存在外部引用?
答案:
问题1: Method1(): no(意图明确) Method2(): yes(明确的意图) Method3(): yes(不确定意图) 问题2: Method1():没有 Method2(): no(除非不安全) Method3():是的
Method1 can't modify foo, and never gets a reference. Method2 gets a short-lived reference to foo, which it can use modify the fields of foo any number of times, in any order, until it returns, but it can't persist that reference. Before Method2 returns, unless it uses unsafe code, any and all copies that might have been made of its 'foo' reference will have disappeared. Method3, unlike Method2, gets a promiscuously-sharable reference to foo, and there's no telling what it might do with it. It might not change foo at all, it might change foo and then return, or it might give a reference to foo to another thread which might mutate it in some arbitrary way at some arbitrary future time. The only way to limit what Method3 might do to a mutable class object passed into it would be to encapsulate the mutable object into a read-only wrapper, which is ugly and cumbersome.
结构数组提供了美妙的语义。给定矩形类型的RectArray[500],如何将元素123复制到元素456,然后在不影响元素456的情况下,将元素123的宽度设置为555是显而易见的。"RectArray[432] = RectArray[321];…;RectArray[123]。宽度= 555;"。知道Rectangle是一个具有名为Width的整数字段的结构体,就可以知道关于上述语句的所有信息。
Now suppose RectClass was a class with the same fields as Rectangle and one wanted to do the same operations on a RectClassArray[500] of type RectClass. Perhaps the array is supposed to hold 500 pre-initialized immutable references to mutable RectClass objects. in that case, the proper code would be something like "RectClassArray[321].SetBounds(RectClassArray[456]); ...; RectClassArray[321].X = 555;". Perhaps the array is assumed to hold instances that aren't going to change, so the proper code would be more like "RectClassArray[321] = RectClassArray[456]; ...; RectClassArray[321] = New RectClass(RectClassArray[321]); RectClassArray[321].X = 555;" To know what one is supposed to do, one would have to know a lot more both about RectClass (e.g. does it support a copy constructor, a copy-from method, etc.) and the intended usage of the array. Nowhere near as clean as using a struct.
To be sure, there is unfortunately no nice way for any container class other than an array to offer the clean semantics of a struct array. The best one could do, if one wanted a collection to be indexed with e.g. a string, would probably be to offer a generic "ActOnItem" method which would accept a string for the index, a generic parameter, and a delegate which would be passed by reference both the generic parameter and the collection item. That would allow nearly the same semantics as struct arrays, but unless the vb.net and C# people can be pursuaded to offer a nice syntax, the code is going to be clunky-looking even if it is reasonably performance (passing a generic parameter would allow for use of a static delegate and would avoid any need to create any temporary class instances).
就我个人而言,我对Eric Lippert等人对可变值类型的憎恨感到恼火。它们提供了比到处使用的混杂引用类型清晰得多的语义。尽管.net对值类型的支持有一些限制,但在许多情况下,可变值类型比任何其他类型的实体都更适合。
可变结构体并不邪恶。
在高绩效环境下,它们是绝对必要的。例如,当缓存线和垃圾收集成为瓶颈时。
我不认为在这些完全有效的用例中使用不可变结构体是“邪恶的”。
我可以看到c#的语法没有帮助区分值类型或引用类型的成员的访问,所以我完全赞成使用强制不变性的不可变结构,而不是可变结构。
然而,与其简单地给不可变结构贴上“邪恶”的标签,我建议接受这种语言,提倡更有帮助和建设性的经验法则。
例如:"struct是默认复制的值类型。如果你不想复制他们,你需要一份推荐信。 “首先尝试使用只读结构体”。
当某种东西可以变异时,它就获得了一种认同感。
struct Person {
public string name; // mutable
public Point position = new Point(0, 0); // mutable
public Person(string name, Point position) { ... }
}
Person eric = new Person("Eric Lippert", new Point(4, 2));
Because Person is mutable, it's more natural to think about changing Eric's position than cloning Eric, moving the clone, and destroying the original. Both operations would succeed in changing the contents of eric.position, but one is more intuitive than the other. Likewise, it's more intuitive to pass Eric around (as a reference) for methods to modify him. Giving a method a clone of Eric is almost always going to be surprising. Anyone wanting to mutate Person must remember to ask for a reference to Person or they'll be doing the wrong thing.
如果你让类型是不可变的,这个问题就消失了;如果我不能修改eric,无论我收到eric还是eric的克隆对我来说都没有区别。更一般地说,如果类型的所有可观察状态都保存在以下成员中,则按值传递是安全的:
不可变的 引用类型 安全通过价值
如果满足这些条件,那么可变值类型的行为就像引用类型一样,因为浅拷贝仍然允许接收方修改原始数据。
The intuitiveness of an immutable Person depends on what you're trying to do though. If Person just represents a set of data about a person, there's nothing unintuitive about it; Person variables truly represent abstract values, not objects. (In that case, it'd probably be more appropriate to rename it to PersonData.) If Person is actually modeling a person itself, the idea of constantly creating and moving clones is silly even if you've avoided the pitfall of thinking you're modifying the original. In that case it'd probably be more natural to simply make Person a reference type (that is, a class.)
诚然,函数式编程已经告诉我们,使所有东西都不可变是有好处的(没有人可以秘密地保留对eric的引用并改变他),但由于这在OOP中不是惯用的,因此对于使用您的代码的其他人来说仍然是不直观的。
如果你曾经用C/ c++这样的语言编程,结构体可以作为可变的。只要把球传给裁判,没有什么会出错的。我发现的唯一问题是c#编译器的限制,在某些情况下,我无法强迫这个愚蠢的东西使用对结构的引用,而不是Copy(比如当结构是c#类的一部分时)。
所以,可变结构体不是邪恶的,是c#把它们变成了邪恶的。我一直在c++中使用可变结构体,它们非常方便和直观。相比之下,c#让我完全放弃了作为类成员的结构体,因为它们处理对象的方式。他们的便利让我们付出了代价。
如果使用得当,我不相信它们是邪恶的。我不会把它放在我的生产代码中,但我会把它放在像结构化单元测试模拟这样的东西中,其中结构的生命周期相对较小。
使用Eric示例,也许您想要创建该Eric的第二个实例,但是要进行调整,因为这是您的测试的性质(即复制,然后修改)。如果我们只是在测试脚本的剩余部分中使用Eric2,那么Eric的第一个实例发生什么并不重要,除非您计划使用Eric2作为测试比较。
这对于测试或修改那些浅层定义特定对象(结构体的重点)的遗留代码非常有用,但是通过使用不可变的结构体,可以避免令人讨厌的使用。
李柏特先生举的例子有几个问题。它是为了说明结构是复制的,以及如果你不小心的话,这可能是一个问题。看看这个例子,我认为这是一个坏的编程习惯的结果,而不是结构或类的问题。
结构应该只有公共成员,不需要任何封装。如果是这样,那么它真的应该是一个类型/类。你真的不需要两个构念来表达同一件事。 如果有一个类包含一个结构,则可以调用该类中的一个方法来更改成员结构。我认为这是一个良好的编程习惯。
正确的实现如下所示。
struct Mutable {
public int x;
}
class Test {
private Mutable m = new Mutable();
public int mutate()
{
m.x = m.x + 1;
return m.x;
}
}
static void Main(string[] args) {
Test t = new Test();
System.Console.WriteLine(t.mutate());
System.Console.WriteLine(t.mutate());
System.Console.WriteLine(t.mutate());
}
看起来这是编程习惯的问题,而不是结构本身的问题。结构体应该是可变的,这是它的思想和意图。
更改的结果voila表现如预期:
1 2 3. 按任意键继续…
如果你坚持结构体的用途(在c#、Visual Basic 6、Pascal/Delphi、c++结构类型(或类)中,当它们不用作指针时),你会发现结构体只不过是一个复合变量。这意味着:您将把它们视为一个通用名称(您引用成员的记录变量)下的一组变量。
我知道这会让很多习惯于面向对象编程的人感到困惑,但如果使用得当,这并不是说这些东西本质上是邪恶的理由。有些结构按照它们的意图是不可变的(Python的namedtuple就是这种情况),但这是另一种需要考虑的范例。
是的:结构体涉及大量内存,但它不会精确地通过执行以下操作来增加内存:
point.x = point.x + 1
相比:
point = Point(point.x + 1, point.y)
在不可变的情况下,内存消耗至少是相同的,甚至更多(尽管这种情况对于当前堆栈来说是临时的,这取决于语言)。
But, finally, structures are structures, not objects. In POO, the main property of an object is their identity, which most of the times is not more than its memory address. Struct stands for data structure (not a proper object, and so they don't have identity anyhow), and data can be modified. In other languages, record (instead of struct, as is the case for Pascal) is the word and holds the same purpose: just a data record variable, intended to be read from files, modified, and dumped into files (that is the main use and, in many languages, you can even define data alignment in the record, while that's not necessarily the case for properly called Objects).
Want a good example? Structs are used to read files easily. Python has this library because, since it is object-oriented and has no support for structs, it had to implement it in another way, which is somewhat ugly. Languages implementing structs have that feature... built-in. Try reading a bitmap header with an appropriate struct in languages like Pascal or C. It will be easy (if the struct is properly built and aligned; in Pascal you would not use a record-based access but functions to read arbitrary binary data). So, for files and direct (local) memory access, structs are better than objects. As for today, we're used to JSON and XML, and so we forget the use of binary files (and as a side effect, the use of structs). But yes: they exist, and have a purpose.
他们并不邪恶。只要把它们用在正确的地方。
如果你从锤子的角度思考,你会想把螺丝当作钉子,发现螺丝更难扎进墙里,这将是螺丝的错,它们将是邪恶的。