Understanding C# Types - C# Fundamentals - Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Part I: C# Fundamentals

Hour 3. Understanding C# Types


What You’ll Learn in This Hour

An overview of types

The C# predefined types

Other commonly used types

Working with operators

Default values

Null and nullable types

Casting and conversion


In Hour 1, “The .NET Framework and C#,” you were introduced to the fundamentals of the .NET Framework and C#, including the framework class library, the common language runtime, and the idea of automatic memory management. You briefly learned about namespaces and types and then moved on to statements, expressions, variables, constants, identifiers, and keywords. From those humble beginnings, you then built a simple C# application and learned about debugging in Hour 2, “Introducing Visual Studio.”

Building on what you have already learned, this hour introduces you to the predefined types offered by C# and the different operations that you can perform using them. You then learn about value and reference types. After that, you see nullable types and learn about type conversion.

At the end of this hour, you should have a thorough understanding of the C# types, including the difference between value, reference, and nullable types. You will also have written some more advanced applications that can store and manipulate simple data.

An Overview of Types

C# is both a type-safe and statically typed language. Being statically typed requires you to inform the compiler of the data type for any variable you create. In return, the compiler guarantees that you can only store a compatible data type in that variable, making it type safe. This combination helps prevent common programming errors, leading to a more stable and secure application.

Types are divided into three main categories:

• Value types

• Reference types

• Type parameters


Go To

HOUR 12, “UNDERSTANDING GENERICS,” for more information on type parameters.



Tip: Pointers

There is actually a fourth category of type, called a pointer, which is not part of the core C# language. A pointer type contains the actual location (called an address) of an item in memory. Pointers also allow arithmetic operations as if the value were a number. Although pointers are powerful, they can also be difficult to use correctly and safely.

There are times, however, when using pointers might be required. Fortunately, almost all those times are situations that are more advanced and not something that we need to worry about on a regular basis. Some of those situations can include directly interacting with the underlying operating system or implementing an extremely time-critical algorithm.

To allow the flexibility (and danger) of pointers, C# enables you to write unsafe code in which it is possible to create and operate on pointers. When using unsafe code and pointers, be aware that the garbage collector does not track pointers, so you must handle the memory allocation and deletion yourself. In a way, it’s like writing C code in a C# program.

By disallowing pointer types except in explicit unsafe code blocks, C# can eliminate an entire category of common errors, making it a much safer language.


Put simply, a value type is completely self-contained and copied “by value.” This means that variables of a value type directly contain their data, and it is not possible for operations on one to affect the other. Value types are further categorized into structures, enumerated types, and nullable types.

A reference type contains a reference to the actual data, meaning it is possible for two variables to reference the same object, allowing the possibility that operations on one will affect the other. Reference types are further categorized into classes, arrays, interfaces, and delegates.


Note: Unified Type System

Despite this division between types, C# has a unified type system, enabling the value of any nonpointer type to be treated as an object. This gives value types the benefits a reference type has without introducing unnecessary overhead and makes it possible to call object methods on any value, even predefined value types.


The C# Predefined Types

The C# language predefines a set of types that map to types in the common type system. If you are familiar with another programming language, the names of these types might be different, but you can easily see the correlation. All the predefined types are value types except for object andstring. The predefined types are shown in Table 3.1.

Table 3.1. Predefined C# Types

Image

By including a type to directly represent Boolean values (values that are either true or false), there is no ambiguity that the value is intended to be a Boolean value as opposed to an integer value. This helps eliminate several common programming errors, making it easier to write self-documenting code.


Tip: Boolean Values

In C, Boolean values are represented as an integer value, and it is left up to the programmer to decide if 0 means true or false. Typically, C programs define named constants representing the integer values of 0 and 1 to help eliminate this ambiguity, but this still allows any integer value to be used.


The decimal type provides at least 28 significant digits and is designed to have no representation error over a wide range of values frequently used in financial calculations. The range of values the double type can represent with no representation error is a set used primarily in physical calculations.

The object type is the underlying base type for all the other reference and value types. The string type represents a sequence of Unicode code units and cannot be changed once given a value. As a result, values of type string are immutable.

C# also has some special types, the most common being the void type. The void type indicates the absence of a type. The dynamic type is similar to object, with the primary difference being all operations on that type will be resolved at runtime rather than compile time.


Note: System.Object

All the value types and the class, array, and delegate reference types derive from object. Interface types can derive only from other interface types, but they are convertible to object.

Type parameter types actually do not derive from anything, but they are still convertible to object.

Unsafe pointer types neither derive from nor are convertible to object because they are outside the normal type rules for C#.

All this actually means that every nonpointer type in C# is convertible to, but might not derive from, object.



Note: Predefined Types and CLS Compliance

All the predefined types are CLS-compliant except the unsigned integer types and the sbyte type. You can use these types and still be CLS-compliant as long as they are not publicly accessible. If you do need to make one of these types publicly accessible, they can safely map to a CLS-compliant type:

• sbyte maps to short.

• uint normally maps to long but can be mapped to int when the original value is less than 2,147,483,647.5.

• ulong normally maps to decimal but can be mapped to long when the original value is less than 9,223,372,036,854,775,807.5.

• ushort normally maps to int but can be mapped to short when the original value is less than 32,767.5.


Although void and dynamic are types, var represents an implicitly typed variable and tells the compiler to determine the real type based on the assigned data.


Caution: Var Is Not Short for Variant

When the var type was first introduced, many people thought it was equivalent to the Visual Basic Variant type. A Variant is a type that can be used to represent any other data type and is not strongly typed. A var type is still strongly typed because it is replaced with a real data type during compilation. Even so, overusing it can decrease the understandability of your code, so use it carefully.



Try It Yourself: Working with the Predefined Types

Now that you are familiar with the predefined types, let’s see how to use them. By following these steps, you write an application that creates some local variables and displays their values. Then you create an implicitly typed variable and verify that it actually creates a strongly typed variable:

1. Create a new console application.

2. In the Main method of the Program.cs file, enter the following code:

int i = 20;
float f = 20.2f;
string s = "Hello, world...again";

Console.WriteLine("This is an {0} value: {1}", i.GetTypeCode(), i);
Console.WriteLine("This is a {0} value: {1}", f.GetTypeCode(), f);
Console.WriteLine("This is a {0} value: {1}", s.GetTypeCode(), s);

3. Run the application by pressing Ctrl+F5; you should see the following in the console window, as shown in Figure 3.1.

Image

Figure 3.1. Output of working with predefined types.

4. Press any key to close the console and return to Visual Studio.

5. Enter the following code in the Main method, just after the previous code:

var v = 20;
Console.WriteLine("This is also an {0} value: {1}", v.GetTypeCode(), v);

6. Hover the mouse cursor over the var keyword until the ToolTip is displayed, which confirms that v is actually an int, as shown in Figure 3.2.

Image

Figure 3.2. ToolTip showing a var as an int.

7. Press Ctrl+F5 again to run the application, and you should now see an additional line appear:

This is also an Int32 value: 20

8. Press any key to close the console and return to Visual Studio.

9. Enter the following line in the Main method:

v = "hello";

10. You should immediately notice a red “squiggly” line under the statement you just entered and an error message stating that you Cannot implicitly convert type 'string' to 'int'. This error occurs because the compiler has already assigned v to be of type int and the strong-typing capabilities of C# prevent you from assigning a string value to the same variable, which is an incompatible type.

11. Remove the line you entered from step 9 so your program compiles again.


Other Commonly Used Types

In addition to the standard predefined types, the .NET Framework provides types for other commonly used values. These types do not have aliases in C# like the predefined types but allow the same operations.

Date and Time

Working with date and time values is done with the DateTime structure, which enables you to create values that represent a date and a time, just a date, or just a time value. The two most common ways to create a new DateTime value are to use one of the various constructor overloads or one of the four static parse methods: Parse, ParseExact, TryParse, or TryParseExact.

The DateTime structure provides several properties; the most common are shown in Table 3.2.

Table 3.2. Common DateTime Properties

Image

When adding or subtracting date or time values, you can use instance methods, which return a new DateTime value rather than modifying the original one. The common DateTime arithmetic methods are shown in Table 3.3.

Table 3.3. Common DateTime Arithmetic Methods

Image

It is also possible to subtract two DateTime values using the subtraction operator, which results in a TimeSpan instance. A TimeSpan represents an interval of time measured as a positive or negative number of days, hours, minutes, seconds, and fractions of a second. To ensure consistency, time intervals are measured in days. You can also add a TimeSpan to or subtract a TimeSpan from a DateTime, both of which result in a new DateTime value.

The common methods and properties of TimeSpan are shown in Table 3.4.

Table 3.4. Common TimeSpan Members

Image

Image

Globally Unique Identifiers (GUIDs)

A GUID is a 128-bit integer value that can be used whenever a unique identifier is required that has a low probability of being duplicated. The System.Guid structure enables you to create and compare GUID values. The common members are shown in Table 3.5.

Table 3.5. Common Guid Members

Image

Uniform Resource Identifiers (URIs)

A uniform resource identifier (URI) is a compact representation of a resource available on the intranet or the Internet and can be an absolute URI (like a web page address) or a relative URI that must be expanded with respect to a base URI.

The Uri class enables you to create new URI values and access the parts of a URI, and provides methods for working with URIs, such as parsing, comparing, and combining. Some of the common members are shown in Table 3.6.

Table 3.6. Common Uri Members

Image

An instance of the Uri class is immutable. To create a modifiable URI, use the UriBuilder class. The UriBuilder class enables you to easily change the properties of a URI without creating a new instance for each modification. All the properties shown in Table 3.7 are common to both Uri(where they are read-only) and UriBuilder except for the Uri property, which is only available on UriBuilder.

Table 3.7. Common Uri and UriBuilder Properties

Image

Listing 3.1 shows how to use the UriBuilder class.

Listing 3.1. Working with UriBuilder


Uri immutableUri = new Uri("http://www.example.com");
Console.WriteLine(immutableUri);

UriBuilder mutableUri = new UriBuilder(immutableUri);
Console.WriteLine(mutableUri);

mutableUri.Scheme = "https";
mutableUri.Host = "www.example.com";
mutableUri.Path = "exampleFile.html";
Console.WriteLine(mutableUri);


Big Integers and Complex Numbers

The System.Numerics.BigInteger type represents an arbitrarily large integer value that has no theoretical upper or lower bound. When a BigInteger instance has been created, you can use it just as you would any of the other integer types, enabling you to perform basic mathematical operations and comparisons. Listing 3.2 shows some of the ways you can use the BigInteger type.

Listing 3.2. Working with BigInteger


BigInteger b1 = new BigInteger(987321.5401);
BigInteger b2 = (BigInteger)435623411897L;
BigInteger b3 = BigInteger.Parse("435623411897");

Console.WriteLine(BigInteger.Pow(Int32.MaxValue, 2));
Console.WriteLine(b2 == b3);
Console.WriteLine(BigInteger.GreatestCommonDivisor(b1, b2));


The System.Numerics.Complex type represents a complex number, in the form of a + bi, where a is the real part, and b is the imaginary part and allows you to

• Compare two complex numbers to determine equality.

• Perform arithmetic operations, such as addition, subtraction, multiplication, and division; other numerical operations, such as raising a complex number to a specific power; finding the square root or getting the absolute value; and trigonometric operations, such as calculating the cosine of an angle represented by a complex number.

Listing 3.3 shows some of the ways you can use the Complex type.

Listing 3.3. Working with Complex


Complex c1 = new Complex(10, 2);
Complex c2 = 3.14;
Complex c3 = Complex.FromPolarCoordinates(5, 0.25);
Complex c4 = (Complex)10.2m;

Console.WriteLine(c1);
Console.WriteLine(c2);
Console.WriteLine(Complex.Sqrt(c3));
Console.WriteLine(Complex.Exp(c4));


Working with Operators

C# supports a wide variety of operators, but we only cover the more commonly used ones. An operator is a special symbol that indicates which operation to perform in an expression. All the C# predefined types support operators, although not all types support the same operators.

Table 3.8 shows all the C# operators in order of precedence. Within each category, operators have equal precedence.

Table 3.8. Operators and Operator Precedence in C#

Image

Arithmetic and Assignment Operators

You have already seen the assignment operator (=) in many of the previous examples. This operator simply stores the value of the right operand in the variable indicated by its left operand. Both operands must be the same type or the right operand must be implicitly convertible to the type of the left operand.

C# provides arithmetic operators that support the standard mathematical operations of addition (+), subtraction (-), multiplication (*), and division (/). Subtle differences exist between the behavior of the C# arithmetic operators and the arithmetic rules you learned in school. In particular, integer division behaves a bit differently depending on the data types you are dividing. When dividing one integer by another, the result is an integer. Any remainder is discarded, and the result is rounded toward zero. To obtain the remainder of an integer division, you must use the modulus operator (%).

C# also supports a compound assignment operator, which combines an arithmetic operation and an assignment in a single operator. A corresponding operation (+=, -=, *=, /=) exists for each of the standard arithmetic operators and the modulus operator (%=), which combine addition, subtraction, multiplication, division, and modulus division with assignment.

For example, suppose you need to increment a variable by one. Using the standard arithmetic operators, such an action would typically look like this:

i = i + 1;

However, by using the addition compound assignment operator, this operation could be performed like this:

i += 1;


Note: Increment and Decrement Operators

Taking this simplification even further, you can increment or decrement a value by 1 using the increment (++) and decrement (--) operators.

These operators support two different notations: a postfix notation, where the operator occurs after the variable, and a prefix notation, where the operator occurs before the variable. For example, to increment the value of an integer i by 1 and decrement the value of an integer j by 1 using both the postfix and prefix notation you would use the following:

int i = 1;
i++;
++i;

int j = 3;
j--;
--j;

The only difference between them is the result of the operation, not when the operation occurs. Typically, the result of the postfix notation is the value of the variable before the operation while the result of the prefix operation is the result of the variable after the operation. In both cases, the variable itself has the same value after the operation.



Try It Yourself: Arithmetic Operators

To examine how the assignment, addition, compound assignment, and increment and decrement operators behave, follow these steps.

1. Create a new console application.

2. In the Main method of the Program.cs file, enter the following code:

int i = 20;
Console.WriteLine("i = {0}", i);

i = i + 1;
Console.WriteLine("i = {0}", i);

i += 2;
Console.WriteLine("i = {0}", i);

i -= 3;
Console.WriteLine("i = {0}", i);

3. Run the application by pressing Ctrl+F5; you should see the following lines in the console window, as shown in Figure 3.3.

Image

Figure 3.3. Output of working with the arithmetic operators.

4. Press any key to close the console and return to Visual Studio.


Relational Operators

The relational operators, shown in Table 3.9, are used when comparing two values and result in a Boolean value.

Table 3.9. Relational Operators in C#

Image

In many programming languages, the assignment and equality operators are easily confused because they use the same symbol. This confusion can result in accidental assignments, which remains one of the more common programming mistakes today. To help minimize the possibility for confusion, C# defines a different operator for equality (==).

Logical Operators

The logical operators, shown in Table 3.10, evaluate Boolean expressions that result in either true or false.

Table 3.10. Logical Operators in C#

Image

The rules for the logical operators can be easily summarized, assuming an x and y that are Boolean expressions, as shown in Table 3.11.

Table 3.11. Logical Operators Truth Table

Image


Note: Short-Circuit Evaluation

In C#, the conditional operator perform short-circuit evaluation, or minimal evaluation, which means that additional expressions are evaluated only if the first expression would not result in the entire expression being false. The logical operators do not perform short-circuit evaluation.

When short-circuit evaluation is in effect, if the first expression of an AND operator is false, it is not necessary to evaluate any additional expressions because the entire expression will be false. Similarly, if the first expression of an OR operator is true, it is not necessary to evaluate any additional expressions because the entire expression will be true. It is only when the first expression is not sufficient to determine the result of the entire expression that the additional expressions will be evaluated.



Try It Yourself: Relational and Logical Operators

By following these steps, you verify the expressions shown in Table 3.9 and Table 3.10:

1. Create a new console application.

2. In the Main method, declare two integer variables named x and y and initialize them to 20 and 10, respectively.

3. Using the expressions from Table 3.9 and Table 3.10, write a series of Console.WriteLine statements using the following format, where expression is replaced with the correct expression from the tables:

Console.WriteLine("expression: {0}", expression);

4. Run the application by pressing Ctrl+F5, and observe that the results match what is shown in the results column of both tables.


Conditional Operator

The conditional operator (also called a ternary operator, or ternary if, because it takes three terms) is useful for writing concise expressions and evaluates a condition returning one of two values depending on the result.

The conditional operator has the following form:

condition ? consequence : alternative

When condition is true, the consequence is evaluated and becomes the result. However, when condition is false, the alternative is evaluated and becomes the result instead.


Caution: Common Problems with the Ternary Operator

This operator is right-associative, different than most of the other operators, which are left-associative. This means an expression like

a ? b : c ? d : e

is evaluated as

a ? b : ( c ? d : e )

The type of the conditional expression is determined only from the types of the consequence and alternative, not from the type to which it is being assigned.

Ultimately, this requires that the consequence and alternative be of the same type, which means an expression like

object x = b ? 0 : "hello";

won’t compile because the types of the consequence and alternative are int and string.

Although this code isn’t practical and should probably never be used outside of this example, the correct way to write this would be

object x = b ? (object)0 : (object)"hello";


Default Values

You learned earlier that C# does not allow you to use an uninitialized variable, which means the variable must have a value before you use it. Although this idea of definite assignment helps reduce errors, because it is enforced by the compiler, it can be cumbersome if you have to explicitly provide a default value for every field.

To alleviate this burden, fields, or member variables, are always initially assigned with an appropriate default value. Table 3.12 shows the default value for the different predefined data types.

Table 3.12. Default Values

Image

As you can see, for the integral value types, the default value is zero. The default value for the char type is the character equivalent of zero and false for the bool type. The object and string types have a default value of null, representing a null reference that literally is one that does not refer to any object.

Null and Nullable Types

These default values mean that a value type cannot be null, which at first glance might seem reasonable. However, it presents certain limitations when you work with databases, other external data sources, or other data types that can contain elements that might not be assigned a value. A classic example of this is a numeric field in a database that can store any integer data or might be undefined.

Nullable types provide a solution to this problem. A nullable type is a value type that can represent the proper value range of its underlying type and a null value. Nullable types are represented by the syntax Nullable<T> or T? where T is a value type. The preferred syntax is T?. You assign a value to a nullable type just as you would a non-nullable type:

int x = 10;
int? x = 10;
int? x = null;

To access the value of a nullable type, you should use the GetValueOrDefault method, which returns the assigned value, or, if the value is null, the default value for the underlying type. You can also use the HasValue property, which returns true if the variable contains an actual value, and theValue property, which returns the actual value or results in an exception if the value is null.

All nullable types, including reference types, support the null-coalescing operator (??), which defines the default value to be returned when a nullable type is assigned to a non-nullable type. If the left operator is null, the right operator is returned; otherwise, the left operator is returned.Listing 3.4 shows how the null-coalescing operator can be used.

Listing 3.4. Null-Coalescing Operator


int? x = null;
Console.WriteLine(x ?? -1);

x = 3;
Console.WriteLine(x ?? -1);

string s = null;
Console.WriteLine(s ?? "Undefined");



Try It Yourself: Working with Nullable Types

To examine how to work with nullable types, follow these steps. You create a nullable int, making use of HasValue, Value, and GetValueOrDefault() and the implicit conversion between a nullable int and a non-nullable int:

1. Create a new console application.

2. In the Main method, declare an integer variable named x and initialize it to 10. Then declare a nullable integer named nx and initialize it to null.

3. Enter the following statements:

Console.WriteLine("nx has a value? {0}", nx.HasValue);
Console.WriteLine("x == nx: {0}", x == nx);
Console.WriteLine("x != nx: {0}", x != nx);

4. Now, set nx equal to 20, and enter the following statements:

Console.WriteLine("nx has a value? {0}", nx.HasValue);
Console.WriteLine("nx has the value {0}", nx.Value);
Console.WriteLine("x == nx: {0}", x == nx);
Console.WriteLine("x != nx: {0}", x != nx);

5. Set nx equal to null and enter the following statements:

Console.WriteLine("nx = {0}", nx ?? -1);
Console.WriteLine("nx = {0}", nx.GetValueOrDefault());
Console.WriteLine("nx = {0}", nx.GetValueOrDefault(-2));

6. Finally, set nx equal to 10 and enter the following statements:

Console.WriteLine("nx = {0}", nx ?? -1);
Console.WriteLine("nx = {0}", nx.GetValueOrDefault());
Console.WriteLine("nx = {0}", nx.GetValueOrDefault(-2));

7. Run the application by pressing Ctrl+F5 and observe that the output is the same, as shown in Figure 3.4.

Image

Figure 3.4. Output of working with nullable types.

8. Press any key to close the console and return to Visual Studio.


Casting and Conversion

Put simply, a conversion allows an expression to be treated as being a specific type and is typically used to treat an expression of one type as being of a different type. Conversion can be either implicit or explicit, which determines if an explicit cast is required.

All of the predefined types support implicit conversions, shown in Table 3.13, that always succeed. These implicit conversions are allowed because when converting from the original numeric type to the new numeric type, no magnitude can be lost. An explicit conversion is required when there is the possibility of precision being lost as the result of the conversion operation and it requires you to specify the type to which you are converting the original value.

Table 3.13. Implicit Conversions on the Predefined Types

Image


Note: Implicit Conversion

A loss of precision may occur when converting from int, uint, long, or ulong to float and from long or ulong to double. These conversions will, however, never lose magnitude. All of the other implicit numeric conversions never lose any information.


For example, an int value can be implicitly converted to a long while the opposite, converting a long to an int, is explicit and requires an explicit cast. The form of an explicit cast is (T)e, where T is the destination (or result) type and e is the expression or variable being cast.

int i = 36;
long j = i; // Implicit conversion from int to long.
int j = (int)j; // Explicit conversion from long to int.

Boxing and Unboxing Conversions

What happens when you need a value type to act like a reference type? Earlier you learned that, as part of the unified type system, all value types are convertible to object. When a value type variable needs to be used as a reference type, an object “box” is automatically created and the value is copied into the box. When an object box is changed back to its original value type, the value is copied out of the box and into the variable. Once boxed, operations on the boxed variable do not affect the unboxed (original) variable and vice versa.

The following code shows an example of an implicit conversion where the integer variable i is implicitly boxed to an object variable named boxed. That object variable is then explicitly converted back into an integer variable named j.

int i = 36;
object boxed = i; // Implicit boxing conversion from int to object.
int j = (int)boxed; // Explicit unboxing conversion from object to int.

One of the problems with explicit conversion is that, if you are not careful, you can end up with code that compiles but fails at runtime. An explicit conversion effectively tells the compiler that you are certain the conversion will succeed, and if it doesn’t, a runtime error is acceptable.

To reduce the possibilities of an explicit conversion failing at runtime, C# provides the as operator, which looks like e as T, where e is an expression and T must be either a reference or nullable type. The as operator tells the compiler that there is sufficient reason to believe the conversion will succeed and attempt to convert the value to the specified type. If the conversion was successful, the converted value as T is returned; otherwise a null is returned.

To take advantage of the as operator, the previous code can be rewritten like this:

int? i = 36;
object boxed = i;
int? j = boxed as int?;


Note: Boxing and Unboxing Operations

Although conversions between value types and reference types are usually called casts because they use the C# cast operator, the CIL instructions are box and unbox. As a result, these conversions are also called boxing and unboxing operations.

A boxing conversion is always implicit and converts a value type to a reference type. An unboxing conversion is always explicit and converts a boxed value type (a reference type) back to a value type.

Boxing and unboxing operations are expensive in terms of resources and overhead, so you should try to avoid them whenever possible and ensure that you use the correct type to solve your problem.



Try It Yourself: Conversions

By following these steps, you explore how to use conversions by converting a value type to a reference type. The application demonstrates how operations on value types, reference types, and boxed value types affect each other:

1. Create a new console application.

2. In the Main method, declare an integer variable named i and initialize it to 36. Then declare an object named boxed and initialize it to i.

3. Enter two Console.WriteLine statements to display the value of i and the value of boxed.

4. Increment the value of boxed by 2, making use of an explicit cast.

5. Duplicate the two Console.WriteLine statements entered from step 3 to verify that the value of i has not changed while the value of boxed has.

6. Now, increment the value of i by 1 and duplicate the two Console.WriteLine statements from step 3 to verify that the value of i has changed while the value of boxed has not.

7. Set the value of i to the new value of boxed, again using an explicit cast.

8. Finally, declare two nullable integers named h and j, initializing h to null and j to i and an object named jboxed initialized to j.

9. Enter the following code:

Console.WriteLine("h has a value? {0}", h.HasValue);
h = jboxed as int?;
Console.WriteLine("h now has the value {0}", h.Value);

10. Run the application using Ctrl+F5 and observe that the output matches what is shown in Figure 3.5.

Image

Figure 3.5. Output of boxing, unboxing, and casts.

11. Press any key to close the console and return to Visual Studio.


Summary

Continuing to build your C# foundation, you have explored the predefined types provided by C# and learned about the rich set of operations you can perform on them. You then learned about value and reference types, including how you can treat a value type as a reference type and how to create a nullable type.

You have written a few more simple C# applications to explore how these concepts work. Although these applications might not be glamorous, they help to complete your foundation, enabling you to build applications that are more advanced. As your foundation in C# grows, the examples and exercises expect you to do more work.

Q&A

Q. What does being statically typed mean?

A. C# is a statically typed language, so you must always inform the compiler of the data type for any variable you create. In return, the compiler guarantees that you can store only compatible data in that variable.

Q. Does C# have pointers?

A. C# does have pointer types, although they are not part of the core language. Pointers are available only in the context of unsafe code.

Q. Why is the unified type system in C# important?

A. By providing a unified type system, C# enables the value of any type to be treated as an object without unnecessary overhead.

Q. Are all the predefined types CLS-compliant?

A. No, the unsigned integer types and the sbyte type are not CLS-compliant. There are CLS-compliant types that can be used in place of these types, if necessary.

Q. Is a variable declared using var strongly typed?

A. Yes, a variable declared using var is still strongly typed because you let the compiler fill in the real type during compilation. var is not equivalent to the Visual Basic Variant type.

Q. What is the difference between a value type and a reference type?

A. Value types directly contain their data, whereas reference types contain a reference to their data.

Q. Can value types be null?

A. All value types are either nullable or non-nullable. A nullable value type can be either null or a value of its underlying non-nullable type. A non-nullable value type cannot be null.

Q. Why should you avoid boxing and unboxing operations when possible?

A. You should avoid boxing and unboxing operations when possible because they are expensive in terms of resources and overhead.

Workshop

Quiz

1. What are the three primary groups C# types are divided into?

2. Which predefined type is useful for financial calculations and why?

3. What is a base type for all the predefined types?

4. Why is the inclusion of a distinct bool type important?

5. Is all string and character data stored as Unicode?

6. What are the implications of strings being immutable?

7. What is the difference between a prefix increment and a postfix increment operation?

8. Can the null-coalescing operator (??) be used with reference types and nullable value types?

9. Explain what happens during a boxing operation.

10. Can a long be implicitly converted to an int?

Answers

1. Types in C# are divided into reference types, value types, and type parameter types.

2. The decimal type is useful for financial calculations because it eliminates many representation errors commonly found with other floating-point types.

3. All the predefined types and everything in C# ultimately derive from the object type.

4. By including a distinct bool type, C# helps eliminate several common programming errors by eliminating the ambiguity that can arise when using an integer 0 or 1 value.

5. Yes, all strings and characters in C# are stored as Unicode code units, allowing them to be localized.

6. Because strings are immutable, they cannot be changed after given a value. This means that any string concatenation operations result in creating an entirely new string object to hold the new value. Performing a large number of these operations in a repetitive fashion over a short period of time can lead to significantly increased memory usage and should be done using a StringBuilder instead.

7. In a prefix increment operation, the result is the value of the variable before the increment; in a postfix increment operation, the result is the incremented value assigned back to the variable.

8. Yes, the null-coalescing operator can be used with any type that can contain a null, including objects.

9. A boxing operation occurs when a value type is used as a reference type and involves creating a new instance to hold the boxed value. Operations on a boxed object do not affect the original value.

10. No, a long cannot be implicitly converted to an int because it would lose precision; it can, however, be explicitly converted.

Exercises

1. Write a console application that generates the truth table shown in Table 2.12.

2. Write a console application that demonstrates the difference between value types and reference types. The application should declare two integer variables and two object variables of type LightHouse. For the object variables, create a new class file named LightHouse.cs and replace the generated code for LightHouse with the following code:

public class LightHouse
{
public int NumberOfLights = 1;
public int RevolutionsPerMinute = 30;
}

3. Expand the console application you wrote in the Arithmetic Operators Try It Yourself exercise to explore how the postfix and prefix increment and decrement operators behave.