Page 24 - MSDN Magazine, June 2018
P. 24
differing getter/setter accessibility, either. This all raises another question—should the tuple type be mutable?
Do Not Define Mutable Value Types
The next guideline to consider is that of the mutable value type. Once again, the Arc example (shown in the code in Figure 2) violates the guideline. It’s obvious if you think about it—a value type passes a copy, so changing the copy will not be observable from the caller. However, while the code in Figure 2 demonstrates the con- cept of only modifying the copy, the readability of the code does not. From a readability perspective, it would seem the arc changes.
What’s confusing is that in order for a developer to expect value copy behavior, they would have to know that Arc was a value type. However, there’s nothing obvious from the source code that indi- cates the value type behavior (though to be fair, the Visual Studio IDE will show a value type as a struct if you hover over the data type). You could perhaps argue that C# programmers should know value type versus reference type semantics, such that the behavior in Figure 2 is expected. However, consider the scenario in Figure 3 when the copy behavior is not so obvious.
Notice that, in spite of invocation Arc’s Rotate function, the Arc, in fact, never rotates. Why? This confusing behavior is due to the combination of two factors. First, Arc is a value type that causes it to be passed by value rather than by reference. As a result, invoking pie.Arc returns a copy of Arc, rather than returning the same instance of Arc that was instantiated in the constructor. This wouldn’t be a problem, if it wasn’t for the second factor. The invo-
Figure 2 Value Types Are Copied So The Caller Doesn’t Observe the Change
Figure 3 Mutable Value Types Behave Unexpectedly
cation of Rotate is intended to modify the instance of Arc stored within pie, but in actuality, it modifies the copy returned from the Arc property. And that’s why we have the guideline, “Do Not define mutable value types.”
As before, tuples in C# 7.0 ignore this guideline and exposes public fields that, by definition, make ValueTuple<...> mutable. Despite this violation, ValueTuple<...> doesn’t suffer the same drawbacks as Arc. The reason is that the only way to modify the tuple is via the Item field. However, the C# compiler doesn’t allow the modification of a field (or property) returned from a containing type (whether the containing type is a reference type, value type or even an array or other type of collection). For example, the fol- lowing code will not compile:
pie.Arc.Radius = 0;
Nor will this code:
pie.Arc.Radius++;
The tuple was introduced as a language feature to enable multiple return values without the complex syntaxrequired by out or ref parameters.
These statements fail with the message, “Error CS1612: Cannot modify the return value of ‘PieShape.Arc’ because it is not a vari- able.” In other words, the guideline is not necessarily accurate. Rather than avoiding all mutable value types, the key is to avoid mutating functions (read/write properties are allowable). That wisdom, of course, assumes that the value semantics shown in Figure 2 are obvious enough such that the intrinsic value type behavior is expected.
Do Not Create Value Types Larger Than 16 Bytes This guideline is needed because of how often the value type is copied. In fact, with the exception of a ref or out parameter, value types are copied virtually every time they’re accessed. This is true whether assigning one value type instance to another (such as Arc = arc in Figure 3) or a method invocation (such as Modify(arc) shown in Figure 2). For performance reasons, the guideline is to keep value type size small.
The reality is that the size of a ValueTuple<...> can often be larger than 128 bits (16 bytes) because a ValueTuple<...> may contain seven individual items (and even more if you specify another tuple for the eighth type parameter). Why, then, is the C# 7.0 tuple defined as a value type?
As mentioned earlier, the tuple was introduced as a language feature to enable multiple return values without the complex syn- tax required by out or ref parameters. The general pattern, then, was to construct and return a tuple and then deconstruct it back at the caller. In fact, passing a tuple down the stack via a return
[TestMethod]
public void PassByValue_Modify_ChangeIsLost() {
void Modify(Arc paramameter) { paramameter.Radius++; } Arc arc = new Arc(42, 0, 90);
Modify(arc);
Assert.AreEqual<double>(42, arc.Radius);
}
public class PieShape {
public Point Center { get; } public Arc Arc { get; }
public PieShape(Arc arc, Point center = default) {
Arc = arc;
Center = center; }
}
public class PieShapeTests {
[TestMethod]
public void Rotate_GivenArcOnPie_Fails() {
PieShape pie = new PieShape(new Arc(42, 0, 90)); Assert.AreEqual<double>(90, pie.Arc.SweepAngle); pie.Arc.Rotate(42);
Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
} }
18 msdn magazine
.NET Framework