Why are mutable structs “evil”?

2018-12-30 23:25发布

Following the discussions here on SO I already read several times the remark that mutable structs are “evil” (like in the answer to this question).

What's the actual problem with mutability and structs in C#?

16条回答
临风纵饮
2楼-- · 2018-12-30 23:37

Structs with public mutable fields or properties are not evil.

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.

Despite the problems with the handling of self-mutating methods in read-only contexts, however, mutable structs often offer semantics far superior to mutable class types. Consider the following three method signatures:

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);

For each method, answer the following questions:

  1. Assuming the method doesn't use any "unsafe" code, might it modify foo?
  2. If no outside references to 'foo' exist before the method is called, could an outside reference exist after?

Answers:

Question 1:
Method1(): no (clear intent)
Method2(): yes (clear intent)
Method3(): yes (uncertain intent)
Question 2:
Method1(): no
Method2(): no (unless unsafe)
Method3(): yes

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.

Arrays of structures offer wonderful semantics. Given RectArray[500] of type Rectangle, it's clear and obvious how to e.g. copy element 123 to element 456 and then some time later set the width of element 123 to 555, without disturbing element 456. "RectArray[432] = RectArray[321]; ...; RectArray[123].Width = 555;". Knowing that Rectangle is a struct with an integer field called Width will tell one all one needs to know about the above statements.

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).

Personally, I'm peeved at the hatred Eric Lippert et al. spew regarding mutable value types. They offer much cleaner semantics than the promiscuous reference types that are used all over the place. Despite some of the limitations with .net's support for value types, there are many cases where mutable value types are a better fit than any other kind of entity.

查看更多
千与千寻千般痛.
3楼-- · 2018-12-30 23:40

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.

As Konrad mentions it doesn't make sense to change a date either, as the value represents that unique point in time and not an instance of a time object which has any state or context-dependency.

Hopes this makes any sense to you. It is more about the concept you try to capture with value types than practical details, to be sure.

查看更多
冷夜・残月
4楼-- · 2018-12-30 23:41

There are several issues with Mr. Eric Lippert's example. It is contrived to illustrate the point that structs are copied and how that could be a problem if you are not careful. Looking at the example I see it as a result of a bad programming habit and not really a problem with either struct or the class.

  1. A struct is supposed to have only public members and should not require any encapsulation. If it does then it really should be a type/class. You really do not need two constructs to say the same thing.

  2. If you have class enclosing a struct, you would call a method in the class to mutate the member struct. This is what I would do as a good programming habit.

A proper implementation would be as follows.

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());
    }

It looks like it is an issue with programming habit as opposed to an issue with struct itself. Structs are supposed to be mutable, that is the idea and intent.

The result of the changes voila behaves as expected:

1 2 3 Press any key to continue . . .

查看更多
忆尘夕之涩
5楼-- · 2018-12-30 23:44

Where to start ;-p

Eric Lippert's blog is always good for a quote:

This is yet another reason why mutable value types are evil. Try to always make value types immutable.

First, you tend to lose changes quite easily... for example, getting things out of a list:

Foo foo = list[0];
foo.Name = "abc";

what did that change? Nothing useful...

The same with properties:

myObj.SomeProperty.Size = 22; // the compiler spots this one

forcing you to do:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

less critically, there is a size issue; mutable objects tend to have multiple properties; yet if you have a struct with two ints, a string, a DateTime and a bool, you can very quickly burn through a lot of memory. With a class, multiple callers can share a reference to the same instance (references are small).

查看更多
梦寄多情
6楼-- · 2018-12-30 23:48

Structs are value types which means they are copied when they are passed around.

So if you change a copy you are changing only that copy, not the original and not any other copies which might be around.

If your struct is immutable then all automatic copies resulting from being passed by value will be the same.

If you want to change it you have to consciously do it by creating a new instance of the struct with the modified data. (not a copy)

查看更多
无与为乐者.
7楼-- · 2018-12-30 23:51

I wouldn't say evil but mutability is often a sign of overeagerness on the part of the programmer to provide a maximum of functionality. In reality, this is often not needed and that, in turn, makes the interface smaller, easier to use and harder to use wrong (= more robust).

One example of this is read/write and write/write conflicts in race conditions. These simply can't occur in immutable structures, since a write is not a valid operation.

Also, I claim that mutability is almost never actually needed, the programmer just thinks that it might be in the future. For example, it simply doesn't make sense to change a date. Rather, create a new date based off the old one. This is a cheap operation, so performance is not a consideration.

查看更多
登录 后发表回答