Reference types, Value types, and Patterns |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
Values - in value types - are not accessed via references. In the safe part of C# it is not possible to access such values via references. Variables of value types contain their values (and not references to their values). This implies that values are allocated on the method stack, and the creation and deletion of such values are easier for the programmer to deal with than objects on the heap.
The numeric types, char, boolean and enumeration types are value types in C#. In addition, structs are value types in C#. (The numeric types, char, and boolean are - in fact - defined as structs in C#).
We will normally use the word "object" with the meaning "instance of a class". With this meaning, objects are accessed by references. But in some sense, values (of value types) are also objects in C#. Both value types and reference types inherit from the class Object. Thus, class Object is the common superclass of both reference types and value types. See Section 28.2 for additional clarification of this issue.
In order to avoid unnecessary confusion, we will - unless stated explicitly - devote the word "object" to instances of classes.
14.1. Value types
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this section we introduce the term value semantics.
A variable of value type contains its value The values are allocated on the method stack or within objects on the heap Variables of value types are dealt with by use of so-called value semantics Use of value types simplifies the management of short-lived data |
|
Data on the method stack corresponds to variables of storage class auto in C programming.
14.2. Illustration of variables of value types
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Assume that the type Point is a value type |
We will now demonstrate how value semantics works in relation to assignments.
We will assume that the type Point is a value type. In C# it will be programmed as a struct. We show Point defined as a struct in Section 14.3.
In Figure 14.1 we show two variables, p1 and p2, that contain Point values. The situation in Figure 14.1 can, for instance, be established by the initializers associated with the declarations of p1 and p2 in Program 14.1 . The assignment p1 = p2, also shown in Program 14.1, establishes the situation in Figure 14.2.
Figure 14.1 Variables of value types. The situation before the assignment p1 = p2. |
1 2 3 4 | Point p1 = new Point(1.0, 2.0), p2 = new Point(3.0, 4.0); p1 = p2; | |||
|
Figure 14.2 Variables of value types. The situation after the assignment p1 = p2. |
The thing to notice is that the assignment p1 = p2 copies the value contained in p2 into the variable p1. The coping process can be implemented as a bitwise copy, and therefore it is relatively efficient.
The equality operator p1 == p2 compares the values in p1 and p2 (bitwise comparison). Let us also observe that p1.Equals(p2) has the same boolean value as p1 == p2 when the type of p1 and p2 is a value type.
The observations about assignments from above can also be used directly on call-by-value parameter passing. Call-by-value parameter passing is - in reality - assignment of the actual parameter value to the corresponding formal parameter.
As a contrast to the description of value assignment, please see Section 13.2 where we showed what happens if p1 and p2 are declared as classes (of reference types). Notice that p1 = p2, in case p1 and p2 contain references, is likely to be even more efficient than the value assignment discussed above.
14.3. Structs in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this section we will study two C# types, which we program as structs. The two types become value types. The first, Point, is already well-known. See Program 11.2. The other, Card, is also one of our recurring examples. In Program 12.7 we programmed Card as a class.
In Program 14.2 we show a simple Point struct. In this version the data representation is private. Notice also the constructor. The constructor is used to initialize a new point. In addition there are three methods GetX, GetY, and Move. When we learn more about C# we will most likely program GetX and GetY as properties, see Section 18.1. We may also chose to program Move in a functional style, such that the struct Point becomes immutable. Immutable types are discussed in Section 14.7.
Like in classes, it is always recommended that you program one or more constructors in a struct. It cannot be a parameterless constructor, however. See Section 14.4 for details on structure initialization.
The usage of struct Point has already been illustrated above, see Program 14.1 in Section 14.2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; public struct Point { private double x, y; public Point(double x, double y){ this.x = x; this.y = y; } public double Getx (){ return x; } public double Gety (){ return y; } public void Move(double dx, double dy){ x += dx; y += dy; } public override string ToString(){ return "Point: " + "(" + x + "," + y + ")" + "."; } } | |||
|
In Program 14.3 we show the struct Card. Struct Card represents a playing card. It uses enumeration types for card suites and card values. The playing card has private fields in line 11 and 12, as we will expect. The struct is well-equipped with constructors for flexible initialization of new playing cards. The method Color calculates a card color from its suite and value. The method returns a value of the pre-existing type System.Drawing.Color. Interesting enough in this context, System.Drawing.Color is also a struct. We use the fully qualified name of class Color in the namespace System.Drawing in order not to get a conflict with the Color member in struct Card.
Finally, the usual ToString (overridden from class Object) allows us to print playing cards. This is, of course, very convenient when we write small programs that uses struct Card.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | using System; public enum CardSuite:byte {Spades, Hearts, Clubs, Diamonds }; public enum CardValue: byte {Ace = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6, Seven = 7, Eight = 8, Nine = 9, Ten = 10, Jack = 11, Queen = 12, King = 13}; public struct Card { private CardSuite suite; private CardValue value; public Card(CardSuite suite, CardValue value){ this.suite = suite; this.value = value; } public Card(CardSuite suite, int value){ this.suite = suite; this.value = (CardValue)value; } public CardSuite Suite(){ return this.suite; } public CardValue Value (){ return this.value; } public System.Drawing.Color Color (){ System.Drawing.Color result; if (suite == CardSuite.Spades || suite == CardSuite.Clubs) result = System.Drawing.Color.Black; else result = System.Drawing.Color.Red; return result; } public override String ToString(){ return String.Format("Suite:{0}, Value:{1}, Color:{2}", suite, value, Color().ToString()); } } | |||
|
A simple client of Card, which declares and constructs three playing cards, is shown in Program 14.4. The card in c1 is copied to c4. Finally, all cards are printed with WriteLine, which internally uses the programmed ToString method in struct Card.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using System; public class PlayingCardClient{ public static void Main(){ Card c1 = new Card(CardSuite.Spades, CardValue.King), c2 = new Card(CardSuite.Hearts, 1), c3 = new Card(CardSuite.Diamonds, 13), c4; c4 = c1; // Copies c1 into c4 Console.WriteLine(c1); Console.WriteLine(c2); Console.WriteLine(c3); Console.WriteLine(c4); } } | |||
|
Structs are typically used for aggregation and encapsulation of a few values, which we want to treat as a value itself, and for which we wish to apply value semantics In the System namespace, the types DateTime and TimeSpan are programmed as structs |
Very large structs, which encapsulates many data members, are not often seen. It is most attractive to use structs for small bundles of data, because structs are copied back and forth when we operate on them.
It is instructive to study the interfaces of System.DateTime and System.TimeSpan, which both are programmed as structs in the C# standard library.
14.4. Structs and Initialization
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
There are some peculiar rules about initialization of struct values, at least if compared to initialization of class instances. We will review these peculiarities in this section.
Program 14.5 shows that initializers, such as '= 5' and '= 6.6' cannot be used with structs. The designers of C# insist that the default value of a struct is predictable, as formed by the default values of the types of the instance variables a and b.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /* Right, Wrong */ using System; // Error: // Cannot have instance field initializers in structs. public struct StructOne{ int a = 5; double b = 6.6; } // OK: // Fields in structs are initialized to default values. public struct StructTwo{ int a; double b; } | |||
|
Program 14.6 shows that we cannot program parameterless constructors in a struct. This would overwrite the preexisting default constructor, which initializes all fields to their default values. The designers of C# wish to control the default constructor of structs. The default constructor of a struct therefore always initializes instance variables to their default values. Our own struct constructors should all have at least one parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /* Right, Wrong */ using System; // Error: // Structs cannot contain explicit parameterless constructors. public struct StructThree{ int a; double b; public StructThree(){ a = 1; b = 2.2; } } // OK: // We can program a constructor with parameters. // The implicit parameterless constructor is still available. public struct StructFour{ int a; double b; public StructFour(int a, double b){ this.a = a; this.b = b; } } | |||
|
14.5. Structs versus classes
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In order to summarize structs in relation to classes we provide the following comparison:
Classes | Structs | |
Reference type | Value type | |
Used with dynamic instantiation | Used with static instantiation | |
Ancestors of class Object | Ancestors of class Object | |
Can be extended by inheritance | Cannot be extended by inheritance | |
Can implement one or more interfaces | Can implement one or more interfaces | |
Can initialize fields with initializers | Cannot initialize fields with initializers | |
Can have a parameterless constructor | Cannot have a parameterless constructor |
14.6. Examples of mutable structs in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Structs are often used for immutable objects. (Here we use 'object' in a loose sense, covering both struct values and class instances). An object is immutable if its state cannot be changed once the object has been initialized. Recall that strings in C# are immutable.
We start by studying mutable structs, and hereby we seek motivation for dealing with immutable structs.
Please take a new look at struct Point in Program 14.2 from Section 14.3. In particular, focus your attention on the Move method. A call such as p.Move(7.0, 8.0) will change the state of point p. We say that the point p has been mutated.
In Program 14.7, which is a client of struct Point from Program 14.2, the point p1 is moved twice. The program output in Listing 14.8 (only on web) is as expected.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using System; public class Application{ public static void Main(){ Point p1 = new Point(1.0, 2.0); p1.Move(3.0, 4.0); // p1 has moved to (4.0, 6.0) p1.Move(5.0, 6.0); // p1 has moved to (9.0, 12.0) Console.WriteLine("{0}", p1); } } | |||
|
1 | Point: (9,12). | |||
|
The struct in Program 14.9 is similar to Program 14.2. The difference is that Move in Program 14.9 returns a point, namely the current point, denoted by this. But - as shown in Program 14.10 this causes troubles in some situations. Following the program we will explain the reason.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System; public struct Point { private double x, y; public Point(double x, double y){ this.x = x; this.y = y; } public double Getx (){ return x; } public double Gety (){ return y; } public Point Move(double dx, double dy){ x += dx; y += dy; return this; // returns a copy of the current object } public override string ToString(){ return "Point: " + "(" + x + "," + y + ")" + "."; } } | |||
|
In Program 14.10 the expression p1.Move(3.0, 4.0).Move(5.0, 6.0) is parsed as (p1.Move(3.0, 4.0)).Move(5.0, 6.0) due the left associativity of the dot operator. So p1 is first moved by 3.0 and 4.0 to (4, 6). Move returns a new copy of the point (4, 6). (This observation is important). This new copy of the point is an anonymous point, because it it is not contained in any variable. The anonymous point is then moved to (9.0, 12.0). In line 9 of Program 14.10 we print p1, which - as argued - is located at (4, 6). The program output shown in Listing 14.11 confirms our observations.
1 2 3 4 5 6 7 8 9 10 11 | using System; public class Application{ public static void Main(){ Point p1 = new Point(1.0, 2.0); p1.Move(3.0, 4.0).Move(5.0, 6.0); Console.WriteLine("{0}", p1); // Where is p1 located? } } | |||
|
1 | Point: (4,6). | |||
|
The state of affairs in Program 14.10 is not satisfactory. We have mixed imperative and functional programming in an unfortunate way. In the following section we will make another version of Move that works as expected when used in the cascading manner, such as in the expression p1.Move(3.0, 4.0).Move(5.0, 6.0). The new version will be programmed in a functional way, and it will illustrate use of immutable structs.
14.7. Examples of immutable structs in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
As an alternative to Move in Program 14.9 we can program Move in such a way that an expression like p.Move(7.0, 8.0) returns a new point, different from the point in p. The new point is displaced 7.0 in the x direction and 8.0 in the y direction relative to the point in p. The state of p is not changed by Move. We typically want to get hold on the new point in an assignment, such as in
q = p.Move(7.0, 8.0);
Program 14.12 shows yet another version of struct Point, in which Move constructs and returns a new point. In this version Point is immutable. Once constructed we never change the coordinates of a point. This is signalled by making the instance variables x and y readonly, see line 4.
Notice the difference between Move in Program 14.12 and Move in Program 14.9.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; public struct Point { private readonly double x, y; public Point(double x, double y){ this.x = x; this.y = y; } public double Getx (){ return x; } public double Gety (){ return y; } public Point Move(double dx, double dy){ return new Point(x+dx, y+dy); } public override string ToString(){ return "Point: " + "(" + x + "," + y + ")" + "."; } } | |||
|
In Program 14.13 we show the counterpart to Program 14.10 and Program 14.7.
The expression p1.Move(3.0, 4.0).Move(5.0, 6.0) now does the following:
The 'yet another point' is finally copied into the variable p2.
1 2 3 4 5 6 7 8 9 10 11 12 13 | using System; public class Application{ public static void Main(){ Point p1 = new Point(1.0, 2.0), p2; p2 = p1.Move(3.0, 4.0).Move(5.0, 6.0); Console.WriteLine("{0} {1}", p1, p2); } } | |||
|
As shown in Listing 14.14 the original point in p1 is not altered. The point, which finally is copied into p2, is located as expected.
1 | Point: (1,2). Point: (9,12). | |||
|
There is a misfit between mutable datatypes and use of value semantics It is recommended to use structs in C# together with a functional programming style |
The deep insight of all this is that we should strive for a functional programming style when we deal with structs. Structs are born to obey value semantics. This does not fit with the 'imperative point mutation' idea, as exemplified in Program 14.9 and Program 14.10. Use the style in Program 14.12 and Program 14.13 instead.
In this and the previous section I have benefited from Sestoft's and Hansen's explanations and examples from the book C# Precisely.
Exercise 4.2. Are playing cards and dice immutable? Evaluate and discuss the classes Die and Card with respect to mutability. Make sure that you understand what mutability means relative to the concrete code. Explain it to your fellow programmers! More specific, can you argue for or against the claim that a Die instance/value should be mutable? And similarly, can you argue for or against the claim that a Card instance/value should be mutable? Why is it natural to use structs for immutable objects and classes for mutable objects? Please compare your findings with the remarks in 'the solution' when it is released. Solution |
14.8. Boxing and Unboxing
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
C# has a uniform type system in the sense that both values types and reference types are compatible. Conceptually, the compatibility is ensured by the fact that both value types and reference types are derived from the class Object. See Section 28.2. Operationally, the compatibility is ensured by the boxing of value types. This will be the theme in this section.
|
|
Boxing takes place when a simple value or a struct is bound to a variable or a parameter of reference type. This is, for instance, the case if an integer value is passed to a parameter of type Object in a method.
When a value is boxed it is embedded in an object on the heap, together with information about the type of the value. If the boxed value (an object) is unboxed it can therefore be checked if the unboxing makes sense.
In Program 14.15 we first illustrate boxing of an integer i and a boolean b in line 8 and 9. The boxing is done implicitly. Next follows unboxing of the already boxed values in line 11 and 12. Unboxing must be done explicitly. Unboxing is accomplished by casts, both in the assignments to j and c, respectively, and in the context of the arithmetic and logical expressions. Line 14 and 15 illustrate attempts to do unboxing without casts. This is illegal, and the compiler finds out.
We are able to print both objects and values in the final WriteLine of Program 14.15. This is because the method ToString uses the type information of a boxed value to provide for natural generation of a printable string.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System; public class BoxingTest{ public static void Main(){ int i = 15, j, k; bool b = false, c, d; Object obj1 = i, // boxing of the value of i obj2 = b; // boxing of the value of b j = (int) obj1; // unboxing obj1 c = (bool) obj2; // unboxing obj2 // k = i + obj1; // Compilation error // d = b && obj2; // Compilation error k = i + (int)obj1; d = b && (bool)obj2; Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}", i, obj1, b, obj2, j, c, k, d); } } | |||
|
The output of the program is shown in Listing 14.16.
1 | 15, 15, False, False, 15, False, 30, False | |||
|
14.9. Nullable types
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
A variable of reference type can be null A variable of value type cannot be null A variable of nullable value type can be null |
Nullable types provide a solution to the following desire:
All values of a value type t (such as int) 'are in use'. In some programs we wish to have a distinguished value in t which stands for 'no value'.
When we use reference types we use the distinguished null value for such purposes. However, when we program with value types this is not possible. Therefore the concept of nullable types has been invented. It allows a variable of a value type to have the (distinguished) null value.
Before we see how nullable types are expressed in C# we will take a look at a motivating example, programmed without use of nullable types. The full details of the example are available in the web-version of the material. In Program 14.17 (only on web) we program a simple integer sequence class, which represents an ordered sequence of integer values. We provide this type with Min and Max operations. The problem is which value to return from Min and Max in case the sequence is empty. In Program 14.17 we return -1, but this is a bad solution because -1 may very well be the minimum or the maximum number in the sequence. Please make sure that you understand the problem in Program 14.17 before you proceed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public class IntSequence { private int[] sequence; public IntSequence(params int[] elements){ sequence = new int[elements.Length]; for(int i = 0; i < elements.Length; i++){ sequence[i] = elements[i]; } } public int Min(){ int theMinimum; if (sequence.Length == 0) return -1; else { theMinimum = sequence[0]; foreach(int e in sequence) if (e < theMinimum) theMinimum = e; } return theMinimum; } public int Max(){ int theMaximum; if (sequence.Length == 0) return -1; else { theMaximum = sequence[0]; foreach(int e in sequence) if (e > theMaximum) theMaximum = e; } return theMaximum; } // Other useful sequence methods } | |||
|
In Program 14.18 we show another version of class IntSequence. In this solution, the methods Min and Max return a value of type int?. int? means a nullable integer type. Thus, the value null is a legal value in int?. This is exactly what we need because Min and Max are now able to signal that there is no minimum/maximum value in an empty sequence.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public class IntSequence { private int[] sequence; public IntSequence(params int[] elements){ sequence = new int[elements.Length]; for(int i = 0; i < elements.Length; i++){ sequence[i] = elements[i]; } } public int? Min(){ int theMinimum; if (sequence.Length == 0) return null; else { theMinimum = sequence[0]; foreach(int e in sequence) if (e < theMinimum) theMinimum = e; } return theMinimum; } public int? Max(){ int theMaximum; if (sequence.Length == 0) return null; else { theMaximum = sequence[0]; foreach(int e in sequence) if (e > theMaximum) theMaximum = e; } return theMaximum; } // Other useful sequence methods } | |||
|
In Program 14.19 we show an application of class IntSequence, where we illustrate both empty and non-empty sequences. Notice the use of the property HasValue in line 14. The property HasValue can be applied on a value of a nullable type. The output of the program is shown in Listing 14.20 (only on web).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System; class IntSequenceClient{ public static void Main(){ IntSequence is1 = new IntSequence(-5, -1, 7, -8, 13), is2 = new IntSequence(); ReportMinMax(is1); ReportMinMax(is2); } public static void ReportMinMax(IntSequence iseq){ if (iseq.Min().HasValue && iseq.Max().HasValue) Console.WriteLine("Min: {0}. Max: {1}", iseq.Min(), iseq.Max()); else Console.WriteLine("Int sequence is empty"); } } | |||
|
1 2 | Min: -8. Max: 13 Int sequence is empty | |||
|
Let us now summarize the important properties of nullable types in C#:
|
The observations about implicit conversion from a non-nullable type t to its nullable type t? is as expected. A value in a narrow type can be converted to a value in a broader type. The other way around requires an explicit cast.
Only value types can be nullable. It is therefore possible to have nullable struct types. It is only possible to built a nullable type on a non-nullable type. Therefore, the types t??, t???, etc. are undefined in C#.
A nullable type t? is itself a value type. It might be tempting to consider a value v of t? as a boxing of v (see Section 14.8). This is, however, not a correct interpretation. A boxed value belongs to a reference type. A value in t? belongs to a value type.
The nullable type t? is syntactic sugar for the type Nullable<t> for some given value type t. Nullable<t> is a generic struct, which we discuss briefly in Section 42.7.
The type bool? has three values: true, false, and null. The operators & and | have been lifted to deal with the null value. In addition, conditional and iterative control structures allow control expressions of type bool?. In these control structures null counts as false.
The null-coalescing C# operator ?? is convenient when we work with nullable types. The expression x ?? y is a shortcut of x != null ? x : y. The ?? operator can be used to provide default values of variables of nullable types. In the context of
int? i = null, j = 7;
the expression i ?? 5 returns 5, but j ?? 5 returns 7. The ?? operator can also be used on reference types!