© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
A. Troelsen, P. JapiksePro C# 9 with .NET 5https://doi.org/10.1007/978-1-4842-6939-8_4

4. Core C# Programming Constructs, Part 2

Andrew Troelsen1   and Phillip Japikse2
(1)
Minneapolis, MN, USA
(2)
West Chester, OH, USA
 

This chapter picks up where Chapter 3 left off and completes your investigation of the core aspects of the C# programming language. You will start with an investigation of the details behind manipulating arrays using the syntax of C# and get to know the functionality contained within the related System.Array class type.

Next, you will examine various details regarding the construction of C# methods, exploring the out, ref, and params keywords. Along the way, you will also examine the role of optional and named parameters. I finish the discussion on methods with a look at method overloading .

Next, this chapter discusses the construction of enumeration and structure types, including a detailed examination of the distinction between a value type and a reference type . This chapter wraps up by examining the role of nullable data types and the related operators.

After you have completed this chapter, you will be in a perfect position to learn the object-oriented capabilities of C#, beginning in Chapter 5.

Understanding C# Arrays

As I would guess you are already aware, an array is a set of data items, accessed using a numerical index. More specifically, an array is a set of contiguous data points of the same type (an array of ints, an array of strings, an array of SportsCars, etc.). Declaring, filling, and accessing an array with C# are all quite straightforward. To illustrate, create a new Console Application project named FunWithArrays that contains a helper method named SimpleArrays(); as follows:
Console.WriteLine("***** Fun with Arrays *****");
SimpleArrays();
Console.ReadLine();
static void SimpleArrays()
{
  Console.WriteLine("=> Simple Array Creation.");
  // Create and fill an array of 3 integers
  int[] myInts = new int[3];
  // Create a 100 item string array, indexed 0 - 99
  string[] booksOnDotNet = new string[100];
  Console.WriteLine();
}

Look closely at the previous code comments. When declaring a C# array using this syntax, the number used in the array declaration represents the total number of items, not the upper bound. Also note that the lower bound of an array always begins at 0. Thus, when you write int[] myInts = new int[3], you end up with an array holding three elements, indexed at positions 0, 1, and 2.

After you have defined an array variable, you are then able to fill the elements index by index, as shown here in the updated SimpleArrays() method :
static void SimpleArrays()
{
  Console.WriteLine("=> Simple Array Creation.");
  // Create and fill an array of 3 Integers
  int[] myInts = new int[3];
  myInts[0] = 100;
  myInts[1] = 200;
  myInts[2] = 300;
  // Now print each value.
  foreach(int i in myInts)
  {
    Console.WriteLine(i);
  }
  Console.WriteLine();
}
Note

Do be aware that if you declare an array but do not explicitly fill each index, each item will be set to the default value of the data type (e.g., an array of bools will be set to false or an array of ints will be set to 0).

Looking at the C# Array Initialization Syntax

In addition to filling an array element by element, you can fill the items of an array using C# array initialization syntax. To do so, specify each array item within the scope of curly brackets ({}). This syntax can be helpful when you are creating an array of a known size and want to quickly specify the initial values. For example, consider the following alternative array declarations:
static void ArrayInitialization()
{
  Console.WriteLine("=> Array Initialization.");
  // Array initialization syntax using the new keyword.
  string[] stringArray = new string[]
    { "one", "two", "three" };
  Console.WriteLine("stringArray has {0} elements", stringArray.Length);
  // Array initialization syntax without using the new keyword.
  bool[] boolArray = { false, false, true };
  Console.WriteLine("boolArray has {0} elements", boolArray.Length);
  // Array initialization with new keyword and size.
  int[] intArray = new int[4] { 20, 22, 23, 0 };
  Console.WriteLine("intArray has {0} elements", intArray.Length);
  Console.WriteLine();
}

Notice that when you make use of this “curly-bracket” syntax, you do not need to specify the size of the array (seen when constructing the stringArray variable), given that this will be inferred by the number of items within the scope of the curly brackets. Also notice that the use of the new keyword is optional (shown when constructing the boolArray type).

In the case of the intArray declaration , again recall the numeric value specified represents the number of elements in the array, not the value of the upper bound. If there is a mismatch between the declared size and the number of initializers (whether you have too many or too few initializers), you are issued a compile-time error. The following is an example:
// OOPS! Mismatch of size and elements!
int[] intArray = new int[2] { 20, 22, 23, 0 };

Understanding Implicitly Typed Local Arrays

In Chapter 3, you learned about the topic of implicitly typed local variables. Recall that the var keyword allows you to define a variable, whose underlying type is determined by the compiler. In a similar vein, the var keyword can be used to define implicitly typed local arrays. Using this technique, you can allocate a new array variable without specifying the type contained within the array itself (note you must use the new keyword when using this approach).
static void DeclareImplicitArrays()
{
  Console.WriteLine("=> Implicit Array Initialization.");
  // a is really int[].
  var a = new[] { 1, 10, 100, 1000 };
  Console.WriteLine("a is a: {0}", a.ToString());
  // b is really double[].
  var b = new[] { 1, 1.5, 2, 2.5 };
  Console.WriteLine("b is a: {0}", b.ToString());
  // c is really string[].
  var c = new[] { "hello", null, "world" };
  Console.WriteLine("c is a: {0}", c.ToString());
  Console.WriteLine();
}
Of course, just as when you allocate an array using explicit C# syntax, the items in the array’s initialization list must be of the same underlying type (e.g., all ints, all strings, or all SportsCars). Unlike what you might be expecting, an implicitly typed local array does not default to System.Object; thus, the following generates a compile-time error:
// Error! Mixed types!
var d = new[] { 1, "one", 2, "two", false };

Defining an Array of Objects

In most cases, when you define an array, you do so by specifying the explicit type of item that can be within the array variable. While this seems quite straightforward, there is one notable twist. As you will come to understand in Chapter 6, System.Object is the ultimate base class to every type (including fundamental data types) in the .NET Core type system. Given this fact, if you were to define an array of System.Object data types, the subitems could be anything at all. Consider the following ArrayOfObjects() method :
static void ArrayOfObjects()
{
  Console.WriteLine("=> Array of Objects.");
  // An array of objects can be anything at all.
  object[] myObjects = new object[4];
  myObjects[0] = 10;
  myObjects[1] = false;
  myObjects[2] = new DateTime(1969, 3, 24);
  myObjects[3] = "Form & Void";
  foreach (object obj in myObjects)
  {
    // Print the type and value for each item in array.
    Console.WriteLine("Type: {0}, Value: {1}", obj.GetType(), obj);
  }
  Console.WriteLine();
}
Here, as you are iterating over the contents of myObjects, you print the underlying type of each item using the GetType() method of System.Object, as well as the value of the current item. Without going into too much detail regarding System.Object.GetType() at this point in the text, simply understand that this method can be used to obtain the fully qualified name of the item (Chapter 17 examines the topic of type information and reflection services in detail). The following output shows the result of calling ArrayOfObjects():
=> Array of Objects.
Type: System.Int32, Value: 10
Type: System.Boolean, Value: False
Type: System.DateTime, Value: 3/24/1969 12:00:00 AM
Type: System.String, Value: Form & Void

Working with Multidimensional Arrays

In addition to the single dimension arrays you have seen thus far, C# supports two varieties of multidimensional arrays. The first of these is termed a rectangular array, which is simply an array of multiple dimensions, where each row is of the same length. To declare and fill a multidimensional rectangular array, proceed as follows:
static void RectMultidimensionalArray()
{
  Console.WriteLine("=> Rectangular multidimensional array.");
  // A rectangular MD array.
  int[,] myMatrix;
  myMatrix = new int[3,4];
  // Populate (3 * 4) array.
  for(int i = 0; i < 3; i++)
  {
    for(int j = 0; j < 4; j++)
    {
      myMatrix[i, j] = i * j;
    }
  }
  // Print (3 * 4) array.
  for(int i = 0; i < 3; i++)
  {
    for(int j = 0; j < 4; j++)
    {
      Console.Write(myMatrix[i, j] + " ");
    }
    Console.WriteLine();
  }
  Console.WriteLine();
}
The second type of multidimensional array is termed a jagged array . As the name implies, jagged arrays contain some number of inner arrays, each of which may have a different upper limit. Here is an example:
static void JaggedMultidimensionalArray()
{
  Console.WriteLine("=> Jagged multidimensional array.");
  // A jagged MD array (i.e., an array of arrays).
  // Here we have an array of 5 different arrays.
  int[][] myJagArray = new int[5][];
  // Create the jagged array.
  for (int i = 0; i < myJagArray.Length; i++)
  {
    myJagArray[i] = new int[i + 7];
  }
  // Print each row (remember, each element is defaulted to zero!).
  for(int i = 0; i < 5; i++)
  {
    for(int j = 0; j < myJagArray[i].Length; j++)
    {
      Console.Write(myJagArray[i][j] + " ");
    }
    Console.WriteLine();
  }
  Console.WriteLine();
}
The output of calling each of the RectMultidimensionalArray() and JaggedMultidimensionalArray() methods is shown next:
=> Rectangular multidimensional array:
0       0       0       0
0       1       2       3
0       2       4       6
=> Jagged multidimensional array:
0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0

Using Arrays As Arguments or Return Values

After you have created an array, you are free to pass it as an argument or receive it as a member return value. For example, the following PrintArray() method takes an incoming array of ints and prints each member to the console, while the GetStringArray() method populates an array of strings and returns it to the caller:
static void PrintArray(int[] myInts)
{
  for(int i = 0; i < myInts.Length; i++)
  {
    Console.WriteLine("Item {0} is {1}", i, myInts[i]);
  }
}
static string[] GetStringArray()
{
  string[] theStrings = {"Hello", "from", "GetStringArray"};
  return theStrings;
}
These methods can be invoked as you would expect.
static void PassAndReceiveArrays()
{
  Console.WriteLine("=> Arrays as params and return values.");
  // Pass array as parameter.
  int[] ages = {20, 22, 23, 0} ;
  PrintArray(ages);
  // Get array as return value.
  string[] strs = GetStringArray();
  foreach(string s in strs)
  {
    Console.WriteLine(s);
  }
  Console.WriteLine();
}

At this point, you should feel comfortable with the process of defining, filling, and examining the contents of a C# array variable. To complete the picture, let’s now examine the role of the System.Array class .

Using the System.Array Base Class

Every array you create gathers much of its functionality from the System.Array class. Using these common members, you can operate on an array using a consistent object model. Table 4-1 gives a rundown of some of the more interesting members (be sure to check the documentation for full details).
Table 4-1.

Select Members of System.Array

Member of Array Class

Meaning in Life

Clear()

This static method sets a range of elements in the array to empty values (0 for numbers, null for object references, false for Booleans).

CopyTo()

This method is used to copy elements from the source array into the destination array.

Length

This property returns the number of items within the array.

Rank

This property returns the number of dimensions of the current array.

Reverse()

This static method reverses the contents of a one-dimensional array.

Sort()

This static method sorts a one-dimensional array of intrinsic types. If the elements in the array implement the IComparer interface, you can also sort your custom types (see Chapters 8 and 10).

Let’s see some of these members in action. The following helper method makes use of the static Reverse() and Clear() methods to pump out information about an array of string types to the console:
static void SystemArrayFunctionality()
{
  Console.WriteLine("=> Working with System.Array.");
  // Initialize items at startup.
  string[] gothicBands = {"Tones on Tail", "Bauhaus", "Sisters of Mercy"};
  // Print out names in declared order.
  Console.WriteLine("-> Here is the array:");
  for (int i = 0; i < gothicBands.Length; i++)
  {
    // Print a name.
    Console.Write(gothicBands[i] + ", ");
  }
  Console.WriteLine(" ");
  // Reverse them...
  Array.Reverse(gothicBands);
  Console.WriteLine("-> The reversed array");
  // ... and print them.
  for (int i = 0; i < gothicBands.Length; i++)
  {
    // Print a name.
    Console.Write(gothicBands[i] + ", ");
  }
  Console.WriteLine(" ");
  // Clear out all but the first member.
  Console.WriteLine("-> Cleared out all but one...");
  Array.Clear(gothicBands, 1, 2);
  for (int i = 0; i < gothicBands.Length; i++)
  {
    // Print a name.
    Console.Write(gothicBands[i] + ", ");
  }
  Console.WriteLine();
}
If you invoke this method, you will get the output shown here:
=> Working with System.Array.
-> Here is the array:
Tones on Tail, Bauhaus, Sisters of Mercy,
-> The reversed array
Sisters of Mercy, Bauhaus, Tones on Tail,
-> Cleared out all but one...
Sisters of Mercy, , ,

Notice that many members of System.Array are defined as static members and are, therefore, called at the class level (e.g., the Array.Sort() and Array.Reverse() methods). Methods such as these are passed in the array you want to process. Other members of System.Array (such as the Length property) are bound at the object level; thus, you can invoke the member directly on the array .

Using Indices and Ranges (New 8.0)

To simplify working with sequences (including arrays), C# 8 introduces two new types and two new operators for use when working with arrays:
  • System.Index represents an index into a sequence.

  • System.Range represents a subrange of indices.

  • The index from end operator (^) specifies that the index is relative to the end of the sequence.

  • The range operator (...) specifies the start and end of a range as its operands.

Note

Indices and ranges can be used with arrays, strings, Span<T>, and ReadOnlySpan<T>.

As you have already seen, arrays are indexed beginning with zero (0). The end of a sequence is the length of the sequence – 1. The previous for loop that printed the gothicBands array can be updated to the following:
for (int i = 0; i < gothicBands.Length; i++)
{
  Index idx = i;
  // Print a name
  Console.Write(gothicBands[idx] + ", ");
}
The index from end operator lets you specify how many positions from the end of sequence, starting with the length. Remember that the last item in a sequence is one less than the actual length, so ^0 would cause an error. The following code prints the array in reverse:
for (int i = 1; i <= gothicBands.Length; i++)
{
  Index idx = ^i;
  // Print a name
  Console.Write(gothicBands[idx] + ", ");
}
The range operator specifies a start and end index and allows for access to a subsequence within a list. The start of the range is inclusive, and the end of the range is exclusive. For example, to pull out the first two members of the array, create ranges from 0 (the first member) to 2 (one more than the desired index position).
foreach (var itm in gothicBands[0..2])
{
  // Print a name
  Console.Write(itm + ", ");
}
Console.WriteLine(" ");
Ranges can also be passed to a sequence using the new Range data type, as shown here:
Range r = 0..2; //the end of the range is exclusive
foreach (var itm in gothicBands[r])
{
  // Print a name
  Console.Write(itm + ", ");
}
Console.WriteLine(" ");
Ranges can be defined using integers or Index variables . The same result will occur with the following code:
Index idx1 = 0;
Index idx2 = 2;
Range r = idx1..idx2; //the end of the range is exclusive
foreach (var itm in gothicBands[r])
{
  // Print a name
  Console.Write(itm + ", ");
}
Console.WriteLine(" ");
If the beginning of the range is left off, the beginning of the sequence is used. If the end of the range is left off, the length of the range is used. This does not cause an error, since the value at the end of the range is exclusive. For the previous example of three items in an array, all the ranges represent the same subset.
gothicBands[..]
gothicBands[0..^0]
gothicBands[0..3]

Understanding Methods

Let’s examine the details of defining methods. Methods are defined by an access modifier and return type (or void for no return type) and may or may not take parameters. A method that returns a value to the caller is commonly referred to as a function, while methods that do not return a value are commonly referred to as methods.

Note

Access modifiers for methods (and classes) are covered in Chapter 5. Method parameters are covered in the next section.

At this point in the text, each of your methods has the following basic format:
// Recall that static methods can be called directly
// without creating a class instance.
class Program
{
  // static returnType MethodName(parameter list) { /* Implementation */ }
  static int Add(int x, int y)
  {
    return x + y;
  }
}

As you will see over the next several chapters, methods can be implemented within the scope of classes, structures, or (new in C# 8) interfaces.

Understanding Expression-Bodied Members

You already learned about simple methods that return values, such as the Add() method. C# 6 introduced expression-bodied members that shorten the syntax for single-line methods. For example, Add() can be rewritten using the following syntax:
static int Add(int x, int y) => x + y;

This is what is commonly referred to as syntactic sugar, meaning that the generated IL is no different. It is just another way to write the method. Some find it easier to read, and others do not, so the choice is yours (or your team’s) which style you prefer.

Note

Don’t be alarmed by the => operator. This is a lambda operation, which is covered in detail in Chapter 12. That chapter also explains exactly how expression-bodied members work. For now, just consider them a shortcut to writing single-line statements.

Understanding Local Functions (New 7.0, Updated 9.0)

A feature introduced in C# 7.0 is the ability to create methods within methods, referred to officially as local functions. A local function is a function declared inside another function, must be private, with C# 8.0 can be static (see next section), and does not support overloading. Local functions do support nesting: a local function can have a local function declared inside it.

To see how this works, create a new Console Application project named FunWithLocalFunctions. As an example, let’s say you want to extend the Add() example used previously to include validation of the inputs. There are many ways to accomplish this, and one simple way is to add the validation directly into the Add() method. Let’s go with that and update the previous example to the following (the comment representing validation logic):
static int Add(int x, int y)
{
  //Do some validation here
  return x + y;
}
As you can see, there are no big changes. There is just a comment indicating that real code should do something. What if you wanted to separate the actual reason for the method (returning the sum of the arguments) from the validation of the arguments? You could create additional methods and call them from the Add() method. But that would require creating another method just for use by one other method. Maybe that’s overkill. Local functions allow you to do the validation first and then encapsulate the real goal of the method defined inside the AddWrapper() method, as shown here:
static int AddWrapper(int x, int y)
{
  //Do some validation here
  return Add();
  int Add()
  {
    return x + y;
  }
}

The contained Add() method can be called only from the wrapping AddWrapper() method. So, the question I am sure you are thinking is, “What did this buy me?” The answer for this specific example, quite simply, is little (if anything). But what if AddWrapper() needed to execute the Add() function from multiple places? Now you should start to see the benefit of having a local function for code reuse that is not exposed outside of where it is needed. You will see even more benefit gained with local functions when we cover custom iterator methods (Chapter 8) and asynchronous methods (Chapter 15).

Note

The AddWrapper() local function is an example of local function with a nested local function. Recall that functions declared in top-level statements are created as local functions. The Add() local function is in the AddWrapper() local function. This capability typically is not used outside of teaching examples, but if you ever need to nest local functions, you know that C# supports it.

C# 9.0 updated local functions to allow for adding attributes to a local function, its parameters, and its type parameters, as in the following example (do not worry about the NotNullWhen attribute, which will be covered later in this chapter) :
#nullable enable
private static void Process(string?[] lines, string mark)
{
    foreach (var line in lines)
    {
        if (IsValid(line))
        {
            // Processing logic...
        }
    }
    bool IsValid([NotNullWhen(true)] string? line)
    {
        return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
    }
}

Understanding Static Local Functions (New 8.0)

An improvement to local functions that was introduced in C# 8 is the ability to declare a local function as static. In the previous example, the local Add() function was referencing the variables from the main function directly. This could cause unexpected side effects, since the local function can change the values of the variables.

To see this in action, create a new method called AddWrapperWithSideEffect(), as shown here:
static int AddWrapperWithSideEffect(int x, int y)
{
  //Do some validation here
  return Add();
  int Add()
  {
    x += 1;
    return x + y;
  }
}

Of course, this example is so simple, it probably would not happen in real code. To prevent this type of mistake, add the static modifier to the local function. This prevents the local function from accessing the parent method variables directly, and this causes the compiler exception CS8421, “A static local function cannot contain a reference to ‘<variable name>.’”

The improved version of the previous method is shown here :
static int AddWrapperWithStatic(int x, int y)
{
  //Do some validation here
  return Add(x,y);
  static int Add(int x, int y)
  {
    return x + y;
  }
}

Understanding Method Parameters

Method parameters are used to pass data into a method call. Over the next several sections, you will learn the details of how methods (and their callers) treat parameters.

Understanding Method Parameter Modifiers

The default way a parameter is sent into a function is by value. Simply put, if you do not mark an argument with a parameter modifier, a copy of the data is passed into the function. As explained later in this chapter, exactly what is copied will depend on whether the parameter is a value type or a reference type.

While the definition of a method in C# is quite straightforward, you can use a handful of methods to control how arguments are passed to a method, as listed in Table 4-2.
Table 4-2.

C# Parameter Modifiers

Parameter Modifier

Meaning in Life

(None)

If a value type parameter is not marked with a modifier, it is assumed to be passed by value, meaning the called method receives a copy of the original data. Reference types without a modifier are passed in by reference.

out

Output parameters must be assigned by the method being called and, therefore, are passed by reference. If the called method fails to assign output parameters, you are issued a compiler error.

ref

The value is initially assigned by the caller and may be optionally modified by the called method (as the data is also passed by reference). No compiler error is generated if the called method fails to assign a ref parameter.

in

New in C# 7.2, the in modifier indicates that a ref parameter is read-only by the called method.

params

This parameter modifier allows you to send in a variable number of arguments as a single logical parameter. A method can have only a single params modifier, and it must be the final parameter of the method. You might not need to use the params modifier all too often; however, be aware that numerous methods within the base class libraries do make use of this C# language feature.

To illustrate the use of these keywords, create a new Console Application project named FunWithMethods. Now, let’s walk through the role of each keyword.

Understanding the Default Parameter-Passing Behavior

When a parameter does not have a modifier, the behavior for value types is to pass in the parameter by value and for reference types is to pass in the parameter by reference.

Note

Value types and reference types are covered later in this chapter.

The Default Behavior for Value Types

The default way a value type parameter is sent into a function is by value. Simply put, if you do not mark the argument with a modifier, a copy of the data is passed into the function. Add the following method to the Program class that operates on two numerical data types passed by value:
// Value type arguments are passed by value by default.
static int Add(int x, int y)
{
  int ans = x + y;
  // Caller will not see these changes
  // as you are modifying a copy of the
  // original data.
  x = 10000;
  y = 88888;
  return ans;
}
Numerical data falls under the category of value types. Therefore, if you change the values of the parameters within the scope of the member, the caller is blissfully unaware, given that you are changing the values on a copy of the caller’s original data.
Console.WriteLine("***** Fun with Methods ***** ");
// Pass two variables in by value.
int x = 9, y = 10;
Console.WriteLine("Before call: X: {0}, Y: {1}", x, y);
Console.WriteLine("Answer is: {0}", Add(x, y));
Console.WriteLine("After call: X: {0}, Y: {1}", x, y);
Console.ReadLine();
As you would hope, the values of x and y remain identical before and after the call to Add(), as shown in the following output, as the data points were sent in by value. Thus, any changes on these parameters within the Add() method are not seen by the caller, as the Add() method is operating on a copy of the data.
***** Fun with Methods *****
Before call: X: 9, Y: 10
Answer is: 19
After call: X: 9, Y: 10

The Default Behavior for Reference Types

The default way a reference type parameter is sent into a function is by reference for its properties, but by value for itself. This is covered in detail later in this chapter, after the discussion of value types and reference types.

Note

Even though the string data type is technically a reference type, as discussed in Chapter 3, it’s a special case. When a string parameter does not have a modifier, it is passed in by value.

Using the out Modifier (Updated 7.0)

Next, you have the use of output parameters . Methods that have been defined to take output parameters (via the out keyword) are under obligation to assign them to an appropriate value before exiting the method scope (if you fail to do so, you will receive compiler errors).

To illustrate, here is an alternative version of the Add() method that returns the sum of two integers using the C# out modifier (note the physical return value of this method is now void):
// Output parameters must be assigned by the called method.
static void AddUsingOutParam(int x, int y, out int ans)
{
  ans = x + y;
}
Calling a method with output parameters also requires the use of the out modifier. However, the local variables that are passed as output variables are not required to be assigned before passing them in as output arguments (if you do so, the original value is lost after the call). The reason the compiler allows you to send in seemingly unassigned data is because the method being called must make an assignment. To call the updated Add method, create a variable of type int, and use the out modifier in the call, like this:
int ans;
AddUsingOutParam(90, 90, out ans);
Starting with C# 7.0, out parameters do not need to be declared before using them. In other words, they can be declared inside the method call, like this:
AddUsingOutParam(90, 90, out int ans);
The following code is an example of calling a method with an inline declaration of the out parameter:
Console.WriteLine("***** Fun with Methods *****");
// No need to assign initial value to local variables
// used as output parameters, provided the first time
// you use them is as output arguments.
// C# 7 allows for out parameters to be declared in the method call
AddUsingOutParam(90, 90, out int ans);
Console.WriteLine("90 + 90 = {0}", ans);
Console.ReadLine();
The previous example is intended to be illustrative in nature; you really have no reason to return the value of your summation using an output parameter. However, the C# out modifier does serve a useful purpose: it allows the caller to obtain multiple outputs from a single method invocation.
// Returning multiple output parameters.
static void FillTheseValues(out int , out string b, out bool c)
{
  a = 9;
  b = "Enjoy your string.";
  c = true;
}
The caller would be able to invoke the FillTheseValues() method . Remember that you must use the out modifier when you invoke the method, as well as when you implement the method.
Console.WriteLine("***** Fun with Methods *****");
FillTheseValues(out int i, out string str, out bool b);
Console.WriteLine("Int is: {0}", i);
Console.WriteLine("String is: {0}", str);
Console.WriteLine("Boolean is: {0}", b);
Console.ReadLine();
Note

C# 7 also introduced tuples, which are another way to return multiple values out of a method call. You will learn more about that later in this chapter.

Always remember that a method that defines output parameters must assign the parameter to a valid value before exiting the method scope. Therefore, the following code will result in a compiler error, as the output parameter has not been assigned within the method scope:
static void ThisWontCompile(out int a)
{
  Console.WriteLine("Error! Forgot to assign output arg!");
}

Discarding out Parameters (New 7.0)

If you do not care about the value of an out parameter, you can use a discard as a placeholder. Discards are temporary, dummy variables that are intentionally unused. They are unassigned, do not have a value, and might not even allocate any memory. This can provide a performance benefit as well as make your code more readable. Discards can be used with out parameters, with tuples (later in this chapter), with pattern matching (Chapters 6 and 8), or even as stand-alone variables.

For example, if you want to get the value for the int in the previous example but do not care about the second two parameters, you can write the following code:
//This only gets the value for a, and ignores the other two parameters
FillTheseValues(out int a, out _, out _);

Note that the called method is still doing the work setting the values for all three parameters; it is just that the last two parameters are being discarded when the method call returns.

The out Modifier in Constructors and Initializers (New 7.3)

C# 7.3 extended the allowable locations for using the out parameter. In addition to methods, parameters for constructors, field and property initializers, and query clauses can all be decorated with the out modifier. Examples of these will be examined later in this book.

Using the ref Modifier

Now consider the use of the C# ref parameter modifier. Reference parameters are necessary when you want to allow a method to operate on (and usually change the values of) various data points declared in the caller’s scope (such as a sorting or swapping routine). Note the distinction between output and reference parameters:
  • Output parameters do not need to be initialized before they are passed to the method. The reason for this is that the method must assign output parameters before exiting.

  • Reference parameters must be initialized before they are passed to the method. The reason for this is that you are passing a reference to an existing variable. If you do not assign it to an initial value, that would be the equivalent of operating on an unassigned local variable.

Let’s check out the use of the ref keyword by way of a method that swaps two string variables (of course, any two data types could be used here, including int, bool, float, etc.).
// Reference parameters.
public static void SwapStrings(ref string s1, ref string s2)
{
  string tempStr = s1;
  s1 = s2;
  s2 = tempStr;
}
This method can be called as follows:
Console.WriteLine("***** Fun with Methods *****");
string str1 = "Flip";
string str2 = "Flop";
Console.WriteLine("Before: {0}, {1} ", str1, str2);
SwapStrings(ref str1, ref str2);
Console.WriteLine("After: {0}, {1} ", str1, str2);
Console.ReadLine();
Here, the caller has assigned an initial value to local string data (str1 and str2). After the call to SwapStrings() returns, str1 now contains the value "Flop", while str2 reports the value "Flip".
Before: Flip, Flop
After: Flop, Flip

Using the in Modifier (New 7.2)

The in modifier passes a value by reference (for both value and reference types) and prevents the called method from modifying the values. This clearly states a design intent in your code, as well as potentially reducing memory pressure. When value types are passed by value, they are copied (internally) by the called method. If the object is large (such as a large struct), the extra overhead of making a copy for local use can be significant. Also, even when reference types are passed without a modifier, they can be modified by the called method. Both issues can be resolved using the in modifier.

Revisiting the Add() method from earlier, there are two lines of code that modify the parameters, but do not affect the values for the calling method. The values are not affected because the Add() method makes a copy of the variables x and y to use locally. While the calling method does not have any adverse side effects, what if the Add() method was changed to the following code?
static int Add2(int x,int y)
{
  x = 10000;
  y = 88888;
  int ans = x + y;
  return ans;
}
Running this code then returns 98888, regardless of the numbers sent into the method. This is obviously a problem. To correct this, update the method to the following:
static int AddReadOnly(in int x,in int y)
{
  //Error CS8331 Cannot assign to variable 'in int' because it is a readonly variable
  //x = 10000;
  //y = 88888;
  int ans = x + y;
  return ans;
}

When the code attempts to change the values of the parameters, the compiler raises the CS8331 error, indicating that the values cannot be modified because of the in modifier.

Using the params Modifier

C# supports the use of parameter arrays using the params keyword. The params keyword allows you to pass into a method a variable number of identically typed parameters (or classes related by inheritance) as a single logical parameter. As well, arguments marked with the params keyword can be processed if the caller sends in a strongly typed array or a comma-delimited list of items. Yes, this can be confusing! To clear things up, assume you want to create a function that allows the caller to pass in any number of arguments and return the calculated average.

If you were to prototype this method to take an array of doubles, this would force the caller to first define the array, then fill the array, and finally pass it into the method. However, if you define CalculateAverage() to take a params of double[] data types, the caller can simply pass a comma-delimited list of doubles. The list of doubles will be packaged into an array of doubles behind the scenes.
// Return average of "some number" of doubles.
static double CalculateAverage(params double[] values)
{
  Console.WriteLine("You sent me {0} doubles.", values.Length);
  double sum = 0;
  if(values.Length == 0)
  {
    return sum;
  }
  for (int i = 0; i < values.Length; i++)
  {
    sum += values[i];
  }
  return (sum / values.Length);
}
This method has been defined to take a parameter array of doubles. What this method is in fact saying is “Send me any number of doubles (including zero), and I’ll compute the average.” Given this, you can call CalculateAverage() in any of the following ways :
Console.WriteLine("***** Fun with Methods *****");
// Pass in a comma-delimited list of doubles...
double average;
average = CalculateAverage(4.0, 3.2, 5.7, 64.22, 87.2);
Console.WriteLine("Average of data is: {0}", average);
// ...or pass an array of doubles.
double[] data = { 4.0, 3.2, 5.7 };
average = CalculateAverage(data);
Console.WriteLine("Average of data is: {0}", average);
// Average of 0 is 0!
Console.WriteLine("Average of data is: {0}", CalculateAverage());
Console.ReadLine();

If you did not make use of the params modifier in the definition of CalculateAverage(), the first invocation of this method would result in a compiler error, as the compiler would be looking for a version of CalculateAverage() that took five double arguments.

Note

To avoid any ambiguity, C# demands a method support only a single params argument, which must be the final argument in the parameter list.

As you might guess, this technique is nothing more than a convenience for the caller, given that the array is created by the .NET Core Runtime as necessary. By the time the array is within the scope of the method being called, you can treat it as a full-blown .NET Core array that contains all the functionality of the System.Array base class library type. Consider the following output :
You sent me 5 doubles.
Average of data is: 32.864
You sent me 3 doubles.
Average of data is: 4.3
You sent me 0 doubles.
Average of data is: 0

Defining Optional Parameters

C# allows you to create methods that can take optional arguments . This technique allows the caller to invoke a single method while omitting arguments deemed unnecessary, provided the caller is happy with the specified defaults.

To illustrate working with optional arguments, assume you have a method named EnterLogData(), which defines a single optional parameter.
static void EnterLogData(string message, string owner = "Programmer")
{
  Console.Beep();
  Console.WriteLine("Error: {0}", message);
  Console.WriteLine("Owner of Error: {0}", owner);
}
Here, the final string argument has been assigned the default value of "Programmer" via an assignment within the parameter definition. Given this, you can call EnterLogData() in two ways.
Console.WriteLine("***** Fun with Methods *****");
...
EnterLogData("Oh no! Grid can't find data");
EnterLogData("Oh no! I can't find the payroll data", "CFO");
Console.ReadLine();

Because the first invocation of EnterLogData() did not specify a second string argument, you would find that the programmer is the one responsible for losing data for the grid, while the CFO misplaced the payroll data (as specified by the second argument in the second method call).

One important thing to be aware of is that the value assigned to an optional parameter must be known at compile time and cannot be resolved at runtime (if you attempt to do so, you will receive compile-time errors!). To illustrate, assume you want to update EnterLogData() with the following extra optional parameter:
// Error! The default value for an optional arg must be known
// at compile time!
static void EnterLogData(string message, string owner = "Programmer", DateTime timeStamp = DateTime.Now)
{
  Console.Beep();
  Console.WriteLine("Error: {0}", message);
  Console.WriteLine("Owner of Error: {0}", owner);
  Console.WriteLine("Time of Error: {0}", timeStamp);
}

This will not compile because the value of the Now property of the DateTime class is resolved at runtime, not compile time.

Note

To avoid ambiguity, optional parameters must always be packed onto the end of a method signature. It is a compiler error to have optional parameters listed before non-optional parameters.

Using Named Arguments (Updated 7.2)

Another language feature found in C# is support for named arguments . Named arguments allow you to invoke a method by specifying parameter values in any order you choose. Thus, rather than passing parameters solely by position (as you will do in most cases), you can choose to specify each argument by name using a colon operator. To illustrate the use of named arguments, assume you have added the following method to the Program class:
static void DisplayFancyMessage(ConsoleColor textColor,
  ConsoleColor backgroundColor, string message)
{
  // Store old colors to restore after message is printed.
  ConsoleColor oldTextColor = Console.ForegroundColor;
  ConsoleColor oldbackgroundColor = Console.BackgroundColor;
  // Set new colors and print message.
  Console.ForegroundColor = textColor;
  Console.BackgroundColor = backgroundColor;
  Console.WriteLine(message);
  // Restore previous colors.
  Console.ForegroundColor = oldTextColor;
  Console.BackgroundColor = oldbackgroundColor;
}
Now, the way DisplayFancyMessage() was written, you would expect the caller to invoke this method by passing two ConsoleColor variables followed by a string type. However, using named arguments, the following calls are completely fine:
Console.WriteLine("***** Fun with Methods *****");
DisplayFancyMessage(message: "Wow! Very Fancy indeed!",
  textColor: ConsoleColor.DarkRed,
  backgroundColor: ConsoleColor.White);
DisplayFancyMessage(backgroundColor: ConsoleColor.Green,
  message: "Testing...",
  textColor: ConsoleColor.DarkBlue);
Console.ReadLine();

The rules for using named arguments were updated slightly with C# 7.2. Prior to 7.2, if you begin to invoke a method using positional parameters, you must list them before any named parameters. With 7.2 and later versions of C#, named and unnamed parameters can be mingled if the parameters are in the correct position.

Note

Just because you can mix and match named arguments with positional arguments in C# 7.2 and later, it’s not considered a good idea. Just because you can does not mean you should!

The following code is an example :
// This is OK, as positional args are listed before named args.
DisplayFancyMessage(ConsoleColor.Blue,
  message: "Testing...",
  backgroundColor: ConsoleColor.White);
// This is OK, all arguments are in the correct order
DisplayFancyMessage(textColor: ConsoleColor.White, backgroundColor:ConsoleColor.Blue, "Testing...");
// This is an ERROR, as positional args are listed after named args.
DisplayFancyMessage(message: "Testing...",
  backgroundColor: ConsoleColor.White,
  ConsoleColor.Blue);

This restriction aside, you might still be wondering when you would ever want to use this language feature. After all, if you need to specify three arguments to a method, why bother flipping around their positions?

Well, as it turns out, if you have a method that defines optional arguments, this feature can be helpful. Assume DisplayFancyMessage() has been rewritten to now support optional arguments, as you have assigned fitting defaults.
static void DisplayFancyMessage(ConsoleColor textColor = ConsoleColor.Blue,
  ConsoleColor backgroundColor = ConsoleColor.White,
  string message = "Test Message")
{
   ...
}
Given that each argument has a default value, named arguments allow the caller to specify only the parameters for which they do not want to receive the defaults. Therefore, if the caller wants the value "Hello!" to appear in blue text surrounded by a white background, they can simply specify the following:
DisplayFancyMessage(message: "Hello!");
Or, if the caller wants to see “Test Message” print out with a green background containing blue text, they can invoke the following:
DisplayFancyMessage(backgroundColor: ConsoleColor.Green);

As you can see, optional arguments and named parameters tend to work hand in hand. To wrap up your examination of building C# methods, I need to address the topic of method overloading.

Understanding Method Overloading

Like other modern object-oriented languages, C# allows a method to be overloaded. Simply put, when you define a set of identically named methods that differ by the number (or type) of parameters, the method in question is said to be overloaded.

To understand why overloading is so useful, consider life as an old-school Visual Basic 6.0 (VB6) developer. Assume you are using VB6 to build a set of methods that return the sum of various incoming data types (Integers, Doubles, etc.). Given that VB6 does not support method overloading, you would be required to define a unique set of methods that essentially do the same thing (return the sum of the arguments).
' VB6 code examples.
Public Function AddInts(ByVal x As Integer, ByVal y As Integer) As Integer
  AddInts = x + y
End Function
Public Function AddDoubles(ByVal x As Double, ByVal y As Double) As Double
  AddDoubles = x + y
End Function
Public Function AddLongs(ByVal x As Long, ByVal y As Long) As Long
  AddLongs = x + y
End Function

Not only can code such as this become tough to maintain, but the caller must now be painfully aware of the name of each method. Using overloading, you can allow the caller to call a single method named Add(). Again, the key is to ensure that each version of the method has a distinct set of arguments (methods differing only by return type are not unique enough).

Note

As will be explained in Chapter 10, it is possible to build generic methods that take the concept of overloading to the next level. Using generics, you can define type placeholders for a method implementation that are specified at the time you invoke the member in question.

To check this out firsthand, create a new Console Application project named FunWithMethodOverloading. Add a new class named AddOperations.cs, and update the code to the following:
namespace FunWithMethodOverloading {
  // C# code.
  // Overloaded Add() method.
  public static class AddOperations
  {
    // Overloaded Add() method.
    public static int Add(int x, int y)
    {
      return x + y;
    }
    public static double Add(double x, double y)
    {
      return x + y;
    }
    public static long Add(long x, long y)
    {
      return x + y;
    }
  }
}
Replace the code in Program.cs with the following :
using System;
using FunWithMethodOverloading;
using static FunWithMethodOverloading.AddOperations;
Console.WriteLine("***** Fun with Method Overloading ***** ");
// Calls int version of Add()
Console.WriteLine(Add(10, 10));
// Calls long version of Add() (using the new digit separator)
Console.WriteLine(Add(900_000_000_000, 900_000_000_000));
// Calls double version of Add()
Console.WriteLine(Add(4.3, 4.4));
Console.ReadLine();
Note

The using static statement will be covered in Chapter 5. For now, consider it a keyboard shortcut for using methods containing a static class named AddOperations in the FunWithMethodOverloading namespace.

The top-level statements called three different versions of the Add method, each using a different data type.

Both Visual Studio and Visual Studio Code help when calling overloaded methods to boot. When you type in the name of an overloaded method (such as your good friend Console.WriteLine()), IntelliSense will list each version of the method in question. Note that you can cycle through each version of an overloaded method using the up and down arrow keys, as indicated in Figure 4-1.
../images/340876_10_En_4_Chapter/340876_10_En_4_Fig1_HTML.jpg
Figure 4-1.

Visual Studio IntelliSense for overloaded methods

If your overload has optional parameters, then the compiler will pick the method that is the best match for the calling code, based on named and/or positional arguments. Add the following method :
static int Add(int x, int y, int z = 0)
{
  return x + (y*z);
}

If the optional argument is not passed in by the caller, the compiler will match the first signature (the one without the optional parameter). While there is a rule set for method location, it is generally not a good idea to create methods that differ only on the optional parameters.

Finally, in, ref, and out are not considered as part of the signature for method overloading when more than one modifier is used. In other words, the following overloads will throw a compiler error:
static int Add(ref int x) { /* */ }
static int Add(out int x) { /* */ }
However, if only one method uses in, ref, or out, the compiler can distinguish between the signatures. So, this is allowed:
static int Add(ref int x) { /* */ }
static int Add(int x) { /* */ }

That wraps up the initial examination of building methods using the syntax of C#. Next, let’s check out how to build and manipulate enumerations and structures.

Understanding the enum Type

Recall from Chapter 1 that the .NET Core type system is composed of classes, structures, enumerations, interfaces, and delegates. To begin exploration of these types, let’s check out the role of the enumeration (or simply, enum) using a new Console Application project named FunWithEnums.

Note

Do not confuse the term enum with enumerator; they are completely different concepts. An enum is a custom data type of name-value pairs. An enumerator is a class or structure that implements a .NET Core interface named IEnumerable. Typically, this interface is implemented on collection classes, as well as the System.Array class. As you will see in Chapter 8, objects that support IEnumerable can work within the foreach loop.

When building a system, it is often convenient to create a set of symbolic names that map to known numerical values. For example, if you are creating a payroll system, you might want to refer to the type of employees using constants such as vice president, manager, contractor, and grunt. C# supports the notion of custom enumerations for this very reason. For example, here is an enumeration named EmpTypeEnum (you can define this in the same file as your top-level statements, if it is placed at the end of the file):
using System;
Console.WriteLine("**** Fun with Enums ***** ");
Console.ReadLine();
//local functions go here:
// A custom enumeration.
enum EmpTypeEnum
{
  Manager,      // = 0
  Grunt,        // = 1
  Contractor,   // = 2
  VicePresident // = 3
}
Note

By convention, enum types are usually suffixed with Enum. This is not necessary but makes for more readable code.

The EmpTypeEnum enumeration defines four named constants, corresponding to discrete numerical values. By default, the first element is set to the value zero (0), followed by an n+1 progression. You are free to change the initial value as you see fit. For example, if it made sense to number the members of EmpTypeEnum as 102 through 105, you could do so as follows:
// Begin with 102.
enum EmpTypeEnum
{
  Manager = 102,
  Grunt,        // = 103
  Contractor,   // = 104
  VicePresident // = 105
}
Enumerations do not necessarily need to follow a sequential ordering and do not need to have unique values. If (for some reason or another) it makes sense to establish your EmpTypeEnum as shown here, the compiler continues to be happy:
// Elements of an enumeration need not be sequential!
enum EmpType
{
  Manager = 10,
  Grunt = 1,
  Contractor = 100,
  VicePresident = 9
}

Controlling the Underlying Storage for an enum

By default, the storage type used to hold the values of an enumeration is a System.Int32 (the C# int); however, you are free to change this to your liking. C# enumerations can be defined in a similar manner for any of the core system types (byte, short, int, or long). For example, if you want to set the underlying storage value of EmpTypeEnum to be a byte rather than an int, you can write the following:
// This time, EmpTypeEnum maps to an underlying byte.
enum EmpTypeEnum : byte
{
  Manager = 10,
  Grunt = 1,
  Contractor = 100,
  VicePresident = 9
}
Changing the underlying type of an enumeration can be helpful if you are building a .NET Core application that will be deployed to a low-memory device and need to conserve memory wherever possible. Of course, if you do establish your enumeration to use a byte as storage, each value must be within its range! For example, the following version of EmpTypeEnum will result in a compiler error, as the value 999 cannot fit within the range of a byte:
// Compile-time error! 999 is too big for a byte!
enum EmpTypeEnum : byte
{
  Manager = 10,
  Grunt = 1,
  Contractor = 100,
  VicePresident = 999
}

Declaring enum Variables

Once you have established the range and storage type of your enumeration, you can use it in place of so-called magic numbers. Because enumerations are nothing more than a user-defined data type, you can use them as function return values, method parameters, local variables, and so forth. Assume you have a method named AskForBonus(), taking an EmpTypeEnum variable as the sole parameter. Based on the value of the incoming parameter, you will print out a fitting response to the pay bonus request.
Console.WriteLine("**** Fun with Enums *****");
// Make an EmpTypeEnum variable.
EmpTypeEnum emp = EmpTypeEnum.Contractor;
AskForBonus(emp);
Console.ReadLine();
// Enums as parameters.
static void AskForBonus(EmpTypeEnum e)
{
  switch (e)
  {
    case EmpType.Manager:
      Console.WriteLine("How about stock options instead?");
      break;
    case EmpType.Grunt:
      Console.WriteLine("You have got to be kidding...");
      break;
    case EmpType.Contractor:
      Console.WriteLine("You already get enough cash...");
      break;
    case EmpType.VicePresident:
      Console.WriteLine("VERY GOOD, Sir!");
      break;
  }
}
Notice that when you are assigning a value to an enum variable, you must scope the enum name (EmpTypeEnum) to the value (Grunt). Because enumerations are a fixed set of name-value pairs, it is illegal to set an enum variable to a value that is not defined directly by the enumerated type.
static void ThisMethodWillNotCompile()
{
  // Error! SalesManager is not in the EmpTypeEnum enum!
  EmpTypeEnum emp = EmpType.SalesManager;
  // Error! Forgot to scope Grunt value to EmpTypeEnum enum!
  emp = Grunt;
}

Using the System.Enum Type

The interesting thing about .NET Core enumerations is that they gain functionality from the System.Enum class type. This class defines several methods that allow you to interrogate and transform a given enumeration. One helpful method is the static Enum.GetUnderlyingType(), which, as the name implies, returns the data type used to store the values of the enumerated type (System.Byte in the case of the current EmpTypeEnum declaration).
Console.WriteLine("**** Fun with Enums *****");
...
// Print storage for the enum.
Console.WriteLine("EmpTypeEnum uses a {0} for storage",
  Enum.GetUnderlyingType(emp.GetType()));
Console.ReadLine();

The Enum.GetUnderlyingType() method requires you to pass in a System.Type as the first parameter. As fully examined in Chapter 17, Type represents the metadata description of a given .NET Core entity.

One possible way to obtain metadata (as shown previously) is to use the GetType() method, which is common to all types in the .NET Core base class libraries. Another approach is to use the C# typeof operator. One benefit of doing so is that you do not need to have a variable of the entity you want to obtain a metadata description of.
// This time use typeof to extract a Type.
Console.WriteLine("EmpTypeEnum uses a {0} for storage",
    Enum.GetUnderlyingType(typeof(EmpTypeEnum)));

Dynamically Discovering an enum’s Name-Value Pairs

Beyond the Enum.GetUnderlyingType() method , all C# enumerations support a method named ToString(), which returns the string name of the current enumeration’s value. The following code is an example:
EmpTypeEnum emp = EmpTypeEnum.Contractor;
...
// Prints out "emp is a Contractor".
Console.WriteLine("emp is a {0}.", emp.ToString());
Console.ReadLine();
If you are interested in discovering the value of a given enumeration variable, rather than its name, you can simply cast the enum variable against the underlying storage type. The following is an example:
Console.WriteLine("**** Fun with Enums *****");
EmpTypeEnum emp = EmpTypeEnum.Contractor;
...
// Prints out "Contractor = 100".
Console.WriteLine("{0} = {1}", emp.ToString(), (byte)emp);
Console.ReadLine();
Note

The static Enum.Format() method provides a finer level of formatting options by specifying a desired format flag. Consult the documentation for a full list of formatting flags.

System.Enum also defines another static method named GetValues(). This method returns an instance of System.Array. Each item in the array corresponds to a member of the specified enumeration. Consider the following method, which will print out each name-value pair within any enumeration you pass in as a parameter:
// This method will print out the details of any enum.
static void EvaluateEnum(System.Enum e)
{
  Console.WriteLine("=> Information about {0}", e.GetType().Name);
  Console.WriteLine("Underlying storage type: {0}",
    Enum.GetUnderlyingType(e.GetType()));
  // Get all name-value pairs for incoming parameter.
  Array enumData = Enum.GetValues(e.GetType());
  Console.WriteLine("This enum has {0} members.", enumData.Length);
  // Now show the string name and associated value, using the D format
  // flag (see Chapter 3).
  for(int i = 0; i < enumData.Length; i++)
  {
    Console.WriteLine("Name: {0}, Value: {0:D}",
      enumData.GetValue(i));
  }
}
To test this new method, update your code to create variables of several enumeration types declared in the System namespace (as well as an EmpTypeEnum enumeration for good measure). The following code is an example:
  Console.WriteLine("**** Fun with Enums *****");
  ...
  EmpTypeEnum e2 = EmpType.Contractor;
  // These types are enums in the System namespace.
  DayOfWeek day = DayOfWeek.Monday;
  ConsoleColor cc = ConsoleColor.Gray;
  EvaluateEnum(e2);
  EvaluateEnum(day);
  EvaluateEnum(cc);
  Console.ReadLine();
Some partial output is shown here:
=> Information about DayOfWeek
Underlying storage type: System.Int32
This enum has 7 members.
Name: Sunday, Value: 0
Name: Monday, Value: 1
Name: Tuesday, Value: 2
Name: Wednesday, Value: 3
Name: Thursday, Value: 4
Name: Friday, Value: 5
Name: Saturday, Value: 6

As you will see over the course of this text, enumerations are used extensively throughout the .NET Core base class libraries. When you make use of any enumeration, always remember that you can interact with the name-value pairs using the members of System.Enum .

Using Enums, Flags, and Bitwise Operations

Bitwise operations provide a fast mechanism for operating on binary numbers at the bit level. Table 4-3 contains the C# bitwise operators, what they do, and an example of each.
Table 4-3.

Bitwise Operations

Operator

Operation

Example

 & (AND)

Copies a bit if it exists in both operands

0110 & 0100 = 0100 (4)

 | (OR)

Copies a bit if it exists in both operands

0110 | 0100 = 0110 (6)

 ^ (XOR)

Copies a bit if it exists in one but not both operands

0110 ^ 0100 = 0010 (2)

 ~ (ones’ compliment)

Flips the bits

~0110 = -7 (due to overflow)

 << (left shift)

Shifts the bits left

0110 << 1 = 1100 (12)

 >> (right shift)

Shifts the bits right

0110 << 1 = 0011 (3)

To show these in action, create a new Console Application project named FunWithBitwiseOperations. Update the Program.cs file to the following code:
using System;
using FunWithBitwiseOperations;
Console.WriteLine("===== Fun wih Bitwise Operations");
Console.WriteLine("6 & 4 = {0} | {1}", 6 & 4, Convert.ToString((6 & 4),2));
Console.WriteLine("6 | 4 = {0} | {1}", 6 | 4, Convert.ToString((6 | 4),2));
Console.WriteLine("6 ^ 4 = {0} | {1}", 6 ^ 4, Convert.ToString((6 ^ 4),2));
Console.WriteLine("6 << 1  = {0} | {1}", 6 << 1, Convert.ToString((6 << 1),2));
Console.WriteLine("6 >> 1 = {0} | {1}", 6 >> 1, Convert.ToString((6 >> 1),2));
Console.WriteLine("~6 = {0} | {1}", ~6, Convert.ToString(~((short)6),2));
Console.WriteLine("Int.MaxValue {0}", Convert.ToString((int.MaxValue),2));
Console.readLine();
When you execute the code, you will see the following result:
===== Fun wih Bitwise Operations
6 & 4 = 4 | 100
6 | 4 = 6 | 110
6 ^ 4 = 2 | 10
6 << 1  = 12 | 1100
6 >> 1 = 3 | 11
~6 =  -7 | 11111111111111111111111111111001
Int.MaxValue 1111111111111111111111111111111
Now that you know the basics of bitwise operations, it is time to apply them to enums. Add a new file named ContactPreferenceEnum.cs and update the code to the following :
using System;
namespace FunWithBitwiseOperations
{
  [Flags]
  public enum ContactPreferenceEnum
  {
    None = 1,
    Email = 2,
    Phone = 4,
    Ponyexpress = 6
  }
}
Notice the Flags attribute. This allows multiple values from an enum to be combined into a single variable. For example, Email and Phone can be combined like this:
ContactPreferenceEnum emailAndPhone = ContactPreferenceEnum.Email | ContactPreferenceEnum.Phone;
This allows you to check if one of the values exists in the combined value. For example, if you want to check to see which ContactPreference value is in emailAndPhone variable, you can use the following code:
Console.WriteLine("None? {0}", (emailAndPhone | ContactPreferenceEnum.None) == emailAndPhone);
Console.WriteLine("Email? {0}", (emailAndPhone | ContactPreferenceEnum.Email) == emailAndPhone);
Console.WriteLine("Phone? {0}", (emailAndPhone | ContactPreferenceEnum.Phone) == emailAndPhone);
Console.WriteLine("Text? {0}", (emailAndPhone | ContactPreferenceEnum.Text) == emailAndPhone);
When executed, the following is presented to the console window:
None? False
Email? True
Phone? True
Text? False

Understanding the Structure (aka Value Type)

Now that you understand the role of enumeration types, let’s examine the use of .NET Core structures (or simply structs). Structure types are well suited for modeling mathematical, geometrical, and other “atomic” entities in your application. A structure (such as an enumeration) is a user-defined type; however, structures are not simply a collection of name-value pairs. Rather, structures are types that can contain any number of data fields and members that operate on these fields.

Note

If you have a background in OOP, you can think of a structure as a “lightweight class type,” given that structures provide a way to define a type that supports encapsulation but cannot be used to build a family of related types. When you need to build a family of related types through inheritance, you will need to make use of class types.

On the surface, the process of defining and using structures is simple, but as they say, the devil is in the details. To begin understanding the basics of structure types, create a new project named FunWithStructures. In C#, structures are defined using the struct keyword . Define a new structure named Point, which defines two member variables of type int and a set of methods to interact with said data.
struct Point
{
  // Fields of the structure.
  public int X;
  public int Y;
  // Add 1 to the (X, Y) position.
  public void Increment()
  {
    X++; Y++;
  }
  // Subtract 1 from the (X, Y) position.
  public void Decrement()
  {
    X--; Y--;
  }
  // Display the current position.
  public void Display()
  {
    Console.WriteLine("X = {0}, Y = {1}", X, Y);
  }
}

Here, you have defined your two integer fields (X and Y) using the public keyword , which is an access control modifier (Chapter 5 continues this discussion). Declaring data with the public keyword ensures the caller has direct access to the data from a given Point variable (via the dot operator).

Note

It is typically considered bad style to define public data within a class or structure. Rather, you will want to define private data, which can be accessed and changed using public properties. These details will be examined in Chapter 5.

Here is code that takes the Point type out for a test-drive:
Console.WriteLine("***** A First Look at Structures ***** ");
// Create an initial Point.
Point myPoint;
myPoint.X = 349;
myPoint.Y = 76;
myPoint.Display();
// Adjust the X and Y values.
myPoint.Increment();
myPoint.Display();
Console.ReadLine();
The output is as you would expect.
***** A First Look at Structures *****
X = 349, Y = 76
X = 350, Y = 77

Creating Structure Variables

When you want to create a structure variable, you have a variety of options. Here, you simply create a Point variable and assign each piece of public field data before invoking its members. If you do not assign each piece of public field data (X and Y in this case) before using the structure, you will receive a compiler error.
// Error! Did not assign Y value.
Point p1;
p1.X = 10;
p1.Display();
// OK! Both fields assigned before use.
Point p2;
p2.X = 10;
p2.Y = 10;
p2.Display();
As an alternative, you can create structure variables using the C# new keyword, which will invoke the structure’s default constructor . By definition, a default constructor does not take any arguments. The benefit of invoking the default constructor of a structure is that each piece of field data is automatically set to its default value.
// Set all fields to default values
// using the default constructor.
Point p1 = new Point();
// Prints X=0,Y=0.
p1.Display();
It is also possible to design a structure with a custom constructor . This allows you to specify the values of field data upon variable creation, rather than having to set each data member field by field. Chapter 5 will provide a detailed examination of constructors; however, to illustrate, update the Point structure with the following code:
struct Point
{
  // Fields of the structure.
  public int X;
  public int Y;
  // A custom constructor.
  public Point(int xPos, int yPos)
  {
    X = xPos;
    Y = yPos;
  }
...
}
With this, you could now create Point variables, as follows:
// Call custom constructor.
Point p2 = new Point(50, 60);
// Prints X=50,Y=60.
p2.Display();

Using Read-Only Structs (New 7.2)

Structs can also be marked as read-only if there is a need for them to be immutable. Immutable objects must be set up at construction and because they cannot be changed, can be more performant. When declaring a struct as read-only, all the properties must also be read-only. But you might ask, how can a property be set (as all properties must be on a struct) if it is read-only? The answer is that the value must be set during the construction of the struct.

Update the point class to the following example:
readonly struct ReadOnlyPoint
{
  // Fields of the structure.
  public int X {get; }
  public int Y { get; }
  // Display the current position and name.
  public void Display()
  {
    Console.WriteLine($"X = {X}, Y = {Y}");
  }
  public ReadOnlyPoint(int xPos, int yPos)
  {
    X = xPos;
    Y = yPos;
  }
}

The Increment and Decrement methods have been removed since the variables are read-only. Notice also the two properties, X and Y. Instead of setting them up as fields, they are created as read-only automatic properties. Automatic properties are covered in Chapter 5.

Using Read-Only Members (New 8.0)

New in C# 8.0, you can declare individual fields of a struct as readonly. This is more granular than making the entire struct read-only. The readonly modifier can be applied to methods, properties, and property accessors. Add the following struct code to your file, outside of the Program class:
struct PointWithReadOnly
{
  // Fields of the structure.
  public int X;
  public readonly int Y;
  public readonly string Name;
  // Display the current position and name.
  public readonly void Display()
  {
    Console.WriteLine($"X = {X}, Y = {Y}, Name = {Name}");
  }
  // A custom constructor.
  public PointWithReadOnly(int xPos, int yPos, string name)
  {
    X = xPos;
    Y = yPos;
    Name = name;
  }
}
To use this new struct, add the following to the top-level statements:
PointWithReadOnly p3 =
  new PointWithReadOnly(50,60,"Point w/RO");
p3.Display();

Using ref Structs (New 7.2)

Also added in C# 7.2, the ref modifier can be used when defining a struct. This requires all instances of the struct to be stack allocated and cannot be assigned as a property of another class. The technical reason for this is that ref structs cannot referenced from the heap. The difference between the stack and the heap is covered in the next section.

These are some additional limitations of ref structs:
  • They cannot be assigned to a variable of type object or dynamic, and cannot be an interface type.

  • They cannot implement interfaces.

  • They cannot be used as a property of a non-ref struct.

  • They cannot be used in async methods, iterators, lambda expressions, or local functions.

The following code, which creates a simple struct and then attempts to create a property in that struct typed to a ref struct, will not compile:
struct NormalPoint
{
  //This does not compile
  public PointWithRef PropPointer { get; set; }
}

readonly and ref modifiers can be combined to gain the benefits and restrictions of both.

Using Disposable ref Structs (New 8.0)

As covered in the previous section, ref structs (and read-only ref structs) cannot implement an interface and therefore cannot implement IDisposable. New in C# 8.0, ref structs and read-only ref structs can be made disposable by adding a public void Dispose() method.

Add the following struct definition to the main file:
ref struct DisposableRefStruct
{
  public int X;
  public readonly int Y;
  public readonly void Display()
  {
    Console.WriteLine($"X = {X}, Y = {Y}");
  }
  // A custom constructor.
  public DisposableRefStruct(int xPos, int yPos)
  {
    X = xPos;
    Y = yPos;
    Console.WriteLine("Created!");
  }
  public void Dispose()
  {
    //clean up any resources here
    Console.WriteLine("Disposed!");
  }
}
Next, add the following to the end of the top-level statements to create and dispose of the new struct:
var s = new DisposableRefStruct(50, 60);
s.Display();
s.Dispose();
Note

Object lifetime and disposing of objects are covered in depth in Chapter 9.

To deepen your understanding of stack and heap allocation, you need to explore the distinction between a .NET Core value type and a .NET Core reference type.

Understanding Value Types and Reference Types

Note

The following discussion of value types and reference types assumes that you have a background in object-oriented programming. If this is not the case, you might want to skip to the “Understanding C# Nullable Types” section of this chapter and return to this section after you have read Chapters 5 and 6.

Unlike arrays, strings, or enumerations, C# structures do not have an identically named representation in the .NET Core library (i.e., there is no System.Structure class) but are implicitly derived from System.ValueType. The role of System.ValueType is to ensure that the derived type (e.g., any structure) is allocated on the stack, rather than the garbage-collected heap. Simply put, data allocated on the stack can be created and destroyed quickly, as its lifetime is determined by the defining scope. Heap-allocated data, on the other hand, is monitored by the .NET Core garbage collector and has a lifetime that is determined by many factors, which will be examined in Chapter 9.

Functionally, the only purpose of System.ValueType is to override the virtual methods defined by System.Object to use value-based versus reference-based semantics. As you might know, overriding is the process of changing the implementation of a virtual (or possibly abstract) method defined within a base class. The base class of ValueType is System.Object. In fact, the instance methods defined by System.ValueType are identical to those of System.Object.
// Structures and enumerations implicitly extend System.ValueType.
public abstract class ValueType : object
{
  public virtual bool Equals(object obj);
  public virtual int GetHashCode();
  public Type GetType();
  public virtual string ToString();
}
Given that value types are using value-based semantics, the lifetime of a structure (which includes all numerical data types [int, float], as well as any enum or structure) is predictable. When a structure variable falls out of the defining scope, it is removed from memory immediately.
// Local structures are popped off
// the stack when a method returns.
static void LocalValueTypes()
{
  // Recall! "int" is really a System.Int32 structure.
  int i = 0;
  // Recall! Point is a structure type.
  Point p = new Point();
} // "i" and "p" popped off the stack here!

Using Value Types, Reference Types, and the Assignment Operator

When you assign one value type to another, a member-by-member copy of the field data is achieved. In the case of a simple data type such as System.Int32, the only member to copy is the numerical value. However, in the case of your Point, the X and Y values are copied into the new structure variable. To illustrate, create a new Console Application project named FunWithValueAndReferenceTypes and then copy your previous Point definition into your new namespace. Next, add the following local function to your top-level statements:
// Assigning two intrinsic value types results in
// two independent variables on the stack.
static void ValueTypeAssignment()
{
  Console.WriteLine("Assigning value types ");
  Point p1 = new Point(10, 10);
  Point p2 = p1;
  // Print both points.
  p1.Display();
  p2.Display();
  // Change p1.X and print again. p2.X is not changed.
  p1.X = 100;
  Console.WriteLine(" => Changed p1.X ");
  p1.Display();
  p2.Display();
}
Here, you have created a variable of type Point (named p1) that is then assigned to another Point (p2). Because Point is a value type, you have two copies of the Point type on the stack, each of which can be independently manipulated. Therefore, when you change the value of p1.X, the value of p2.X is unaffected.
Assigning value types
X = 10, Y = 10
X = 10, Y = 10
=> Changed p1.X
X = 100, Y = 10
X = 10, Y = 10
In stark contrast to value types, when you apply the assignment operator to reference types (meaning all class instances), you are redirecting what the reference variable points to in memory. To illustrate, create a new class type named PointRef that has the same members as the Point structures, beyond renaming the constructor to match the class name.
// Classes are always reference types.
class PointRef
{
  // Same members as the Point structure...
  // Be sure to change your constructor name to PointRef!
  public PointRef(int xPos, int yPos)
  {
    X = xPos;
    Y = yPos;
  }
}
Now, use your PointRef type within the following new method. Note that beyond using the PointRef class, rather than the Point structure, the code is identical to the ValueTypeAssignment() method .
static void ReferenceTypeAssignment()
{
  Console.WriteLine("Assigning reference types ");
  PointRef p1 = new PointRef(10, 10);
  PointRef p2 = p1;
  // Print both point refs.
  p1.Display();
  p2.Display();
  // Change p1.X and print again.
  p1.X = 100;
  Console.WriteLine(" => Changed p1.X ");
  p1.Display();
  p2.Display();
}
In this case, you have two references pointing to the same object on the managed heap. Therefore, when you change the value of X using the p1 reference, p2.X reports the same value. Assuming you have called this new method, your output should look like the following:
Assigning reference types
X = 10, Y = 10
X = 10, Y = 10
=> Changed p1.X
X = 100, Y = 10
X = 100, Y = 10

Using Value Types Containing Reference Types

Now that you have a better feeling for the basic differences between value types and reference types, let’s examine a more complex example. Assume you have the following reference (class) type that maintains an informational string that can be set using a custom constructor:
class ShapeInfo
{
  public string InfoString;
  public ShapeInfo(string info)
  {
    InfoString = info;
  }
}
Now assume that you want to contain a variable of this class type within a value type named Rectangle. To allow the caller to set the value of the inner ShapeInfo member variable, you also provide a custom constructor. Here is the complete definition of the Rectangle type:
struct Rectangle
{
  // The Rectangle structure contains a reference type member.
  public ShapeInfo RectInfo;
  public int RectTop, RectLeft, RectBottom, RectRight;
  public Rectangle(string info, int top, int left, int bottom, int right)
  {
    RectInfo = new ShapeInfo(info);
    RectTop = top; RectBottom = bottom;
    RectLeft = left; RectRight = right;
  }
  public void Display()
  {
    Console.WriteLine("String = {0}, Top = {1}, Bottom = {2}, " +
      "Left = {3}, Right = {4}",
      RectInfo.InfoString, RectTop, RectBottom, RectLeft, RectRight);
  }
}
At this point, you have contained a reference type within a value type. The million-dollar question now becomes “What happens if you assign one Rectangle variable to another?” Given what you already know about value types, you would be correct in assuming that the integer data (which is indeed a structure, System.Int32) should be an independent entity for each Rectangle variable. But what about the internal reference type? Will the object’s state be fully copied, or will the reference to that object be copied? To answer this question, define the following method and invoke it:
static void ValueTypeContainingRefType()
{
  // Create the first Rectangle.
  Console.WriteLine("-> Creating r1");
  Rectangle r1 = new Rectangle("First Rect", 10, 10, 50, 50);
  // Now assign a new Rectangle to r1.
  Console.WriteLine("-> Assigning r2 to r1");
  Rectangle r2 = r1;
  // Change some values of r2.
  Console.WriteLine("-> Changing values of r2");
  r2.RectInfo.InfoString = "This is new info!";
  r2.RectBottom = 4444;
  // Print values of both rectangles.
  r1.Display();
  r2.Display();
}
The output is shown here:
-> Creating r1
-> Assigning r2 to r1
-> Changing values of r2
String = This is new info!, Top = 10, Bottom = 50, Left = 10, Right = 50
String = This is new info!, Top = 10, Bottom = 4444, Left = 10, Right = 50

As you can see, when you change the value of the informational string using the r2 reference, the r1 reference displays the same value. By default, when a value type contains other reference types, assignment results in a copy of the references. In this way, you have two independent structures, each of which contains a reference pointing to the same object in memory (i.e., a shallow copy). When you want to perform a deep copy, where the state of internal references is fully copied into a new object, one approach is to implement the ICloneable interface (as you will do in Chapter 8).

Passing Reference Types by Value

As covered earlier in the chapter, reference types or value types can be passed as parameters to methods. However, passing a reference type (e.g., a class) by reference is quite different from passing it by value. To understand the distinction, assume you have a simple Person class defined in a new Console Application project named FunWithRefTypeValTypeParams, defined as follows:
class Person
{
  public string personName;
  public int personAge;
  // Constructors.
  public Person(string name, int age)
  {
    personName = name;
    personAge = age;
  }
  public Person(){}
  public void Display()
  {
    Console.WriteLine("Name: {0}, Age: {1}", personName, personAge);
  }
}
Now, what if you create a method that allows the caller to send in the Person object by value (note the lack of parameter modifiers, such as out or ref)?
static void SendAPersonByValue(Person p)
{
  // Change the age of "p"?
  p.personAge = 99;
  // Will the caller see this reassignment?
  p = new Person("Nikki", 99);
}
Notice how the SendAPersonByValue() method attempts to reassign the incoming Person reference to a new Person object, as well as change some state data. Now let’s test this method using the following code:
// Passing ref-types by value.
Console.WriteLine("***** Passing Person object by value *****");
Person fred = new Person("Fred", 12);
Console.WriteLine(" Before by value call, Person is:");
fred.Display();
SendAPersonByValue(fred);
Console.WriteLine(" After by value call, Person is:");
fred.Display();
Console.ReadLine();
The following is the output of this call:
***** Passing Person object by value *****
Before by value call, Person is:
Name: Fred, Age: 12
After by value call, Person is:
Name: Fred, Age: 99

As you can see, the value of personAge has been modified. This behavior, discussed earlier, should make more sense now that you understand the way reference types work. Given that you were able to change the state of the incoming Person, what was copied? The answer: a copy of the reference to the caller’s object. Therefore, as the SendAPersonByValue() method is pointing to the same object as the caller, it is possible to alter the object’s state data. What is not possible is to reassign what the reference is pointing to.

Passing Reference Types by Reference

Now assume you have a SendAPersonByReference() method, which passes a reference type by reference (note the ref parameter modifier).
static void SendAPersonByReference(ref Person p)
{
  // Change some data of "p".
  p.personAge = 555;
  // "p" is now pointing to a new object on the heap!
  p = new Person("Nikki", 999);
}
As you might expect, this allows complete flexibility of how the callee is able to manipulate the incoming parameter. Not only can the callee change the state of the object, but if it so chooses, it may also reassign the reference to a new Person object. Now ponder the following updated code:
// Passing ref-types by ref.
Console.WriteLine("***** Passing Person object by reference *****");
...
Person mel = new Person("Mel", 23);
Console.WriteLine("Before by ref call, Person is:");
mel.Display();
SendAPersonByReference(ref mel);
Console.WriteLine("After by ref call, Person is:");
mel.Display();
Console.ReadLine();
Notice the following output:
***** Passing Person object by reference *****
Before by ref call, Person is:
Name: Mel, Age: 23
After by ref call, Person is:
Name: Nikki, Age: 999
As you can see, an object named Mel returns after the call as an object named Nikki, as the method was able to change what the incoming reference pointed to in memory. The golden rule to keep in mind when passing reference types is the following:
  • If a reference type is passed by reference, the callee may change the values of the object’s state data, as well as the object it is referencing.

  • If a reference type is passed by value, the callee may change the values of the object’s state data but not the object it is referencing.

Final Details Regarding Value Types and Reference Types

To wrap up this topic, consider the information in Table 4-4, which summarizes the core distinctions between value types and reference types.
Table 4-4.

Value Types and Reference Types Comparison

Intriguing Question

Value Type

Reference Type

Where are objects allocated?

Allocated on the stack.

Allocated on the managed heap.

How is a variable represented?

Value type variables are local copies.

Reference type variables are pointing to the memory occupied by the allocated instance.

What is the base type?

Implicitly extends System.ValueType.

Can derive from any other type (except System.ValueType), if that type is not “sealed” (more details on this in Chapter 6).

Can this type function as a base to other types?

No. Value types are always sealed and cannot be inherited from.

Yes. If the type is not sealed, it may function as a base to other types.

What is the default parameter-passing behavior?

Variables are passed by value (i.e., a copy of the variable is passed into the called function).

For reference types, the reference is copied by value.

Can this type override System.Object.Finalize()?

No.

Yes, indirectly (more details on this in Chapter 9).

Can I define constructors for this type?

Yes, but the default constructor is reserved (i.e., your custom constructors must all have arguments).

But of course!

When do variables of this type die?

When they fall out of the defining scope.

When the object is garbage collected (see Chapter 9).

Despite their differences, value types and reference types both can implement interfaces and may support any number of fields, methods, overloaded operators, constants, properties, and events.

Understanding C# Nullable Types

Let’s examine the role of the nullable data type using a Console Application project named FunWithNullableValueTypes. As you know, C# data types have a fixed range and are represented as a type in the System namespace. For example, the System.Boolean data type can be assigned a value from the set {true, false}. Now, recall that all the numerical data types (as well as the Boolean data type) are value types. Value types can never be assigned the value of null, as that is used to establish an empty object reference.
// Compiler errors!
// Value types cannot be set to null!
bool myBool = null;
int myInt = null;

C# supports the concept of nullable data types. Simply put, a nullable type can represent all the values of its underlying type, plus the value null. Thus, if you declare a nullable bool, it could be assigned a value from the set {true, false, null}. This can be extremely helpful when working with relational databases, given that it is quite common to encounter undefined columns in database tables. Without the concept of a nullable data type, there is no convenient manner in C# to represent a numerical data point with no value.

To define a nullable variable type, the question mark symbol (?) is suffixed to the underlying data type. Prior to C# 8.0, this syntax was legal only when applied to value types (more on this in the next section, “Nullable Reference Types”). Like a non-nullable variable, local nullable variables must be assigned an initial value before you can use them.
static void LocalNullableVariables()
{
  // Define some local nullable variables.
  int? nullableInt = 10;
  double? nullableDouble = 3.14;
  bool? nullableBool = null;
  char? nullableChar = 'a';
  int?[] arrayOfNullableInts = new int?[10];
}

Using Nullable Value Types

In C#, the ? suffix notation is a shorthand for creating an instance of the generic System.Nullable<T> structure type. It is also used for creating nullable reference types (covered in the next section), although the behavior is a bit different. While you will not examine generics until Chapter 10, it is important to understand that the System.Nullable<T> type provides a set of members that all nullable types can make use of.

For example, you can programmatically discover whether the nullable variable indeed has been assigned a null value using the HasValue property or the != operator. The assigned value of a nullable type may be obtained directly or via the Value property. In fact, given that the ? suffix is just a shorthand for using Nullable<T>, you could implement your LocalNullableVariables() method as follows:
static void LocalNullableVariablesUsingNullable()
{
  // Define some local nullable types using Nullable<T>.
  Nullable<int> nullableInt = 10;
  Nullable<double> nullableDouble = 3.14;
  Nullable<bool> nullableBool = null;
  Nullable<char> nullableChar = 'a';
  Nullable<int>[] arrayOfNullableInts = new Nullable<int>[10];
}
As stated, nullable data types can be particularly useful when you are interacting with databases, given that columns in a data table may be intentionally empty (e.g., undefined). To illustrate, assume the following class, which simulates the process of accessing a database that has a table containing two columns that may be null. Note that the GetIntFromDatabase() method is not assigning a value to the nullable integer member variable, while GetBoolFromDatabase() is assigning a valid value to the bool? member.
class DatabaseReader
{
  // Nullable data field.
  public int? numericValue = null;
  public bool? boolValue = true;
  // Note the nullable return type.
  public int? GetIntFromDatabase()
  { return numericValue; }
  // Note the nullable return type.
  public bool? GetBoolFromDatabase()
  { return boolValue; }
}
Now, examine the following code, which invokes each member of the DatabaseReader class and discovers the assigned values using the HasValue and Value members, as well as using the C# equality operator (not equal, to be exact):
Console.WriteLine("***** Fun with Nullable Value Types ***** ");
DatabaseReader dr = new DatabaseReader();
// Get int from "database".
int? i = dr.GetIntFromDatabase();
if (i.HasValue)
{
  Console.WriteLine("Value of 'i' is: {0}", i.Value);
}
else
{
  Console.WriteLine("Value of 'i' is undefined.");
}
// Get bool from "database".
bool? b = dr.GetBoolFromDatabase();
if (b != null)
{
  Console.WriteLine("Value of 'b' is: {0}", b.Value);
}
else
{
  Console.WriteLine("Value of 'b' is undefined.");
}
Console.ReadLine();

Using Nullable Reference Types (New 8.0)

A significant feature added with C# 8 is support for nullable reference types. In fact, the change is so significant that the .NET Framework could not be updated to support this new feature. Hence, the decision to only support C# 8 in .NET Core 3.0 and later and the decision to make nullable reference type support disabled by default. When you create a new project in .NET Core 3.0/3.1 or .NET 5, reference types work the same way that they did with C# 7. This is to prevent breaking billions of lines of code that exist in the pre–C# 8 ecosystem. Developers must opt-in to enable nullable reference types in their applications.

Nullable reference types follow many of the same rules as nullable value types. Non-nullable reference types must be assigned a non-null value at initialization and cannot later be changed to a null value. Nullable reference types can be null, but still must be assigned something before first use (either an actual instance of something or the value of null).

Nullable reference types use the same symbol (?) to indicate that they are nullable. However, this is not a shorthand for using System.Nullable<T>, as only value types can be used in place of T. As a reminder, generics and constraints are covered in Chapter 10.

Opting in for Nullable Reference Types

Support for nullable reference types is controlled by setting a nullable context. This can be as big as an entire project (by updating the project file) or as small as a few lines (by using compiler directives). There are also two contexts that can be set:
  • Nullable annotation context: This enables/disables the nullable annotation (?) for nullable reference types.

  • Nullable warning context: This enables/disables the compiler warnings for nullable reference types.

To see these in action, create a new console application named FunWithNullableReferenceTypes. Open the project file (if you are using Visual Studio, double-clicking the project name in Solution Explorer or right-clicking the project name and selecting Edit Project File). Update the project file to support nullable reference types by adding the <Nullable> node (all the available options are shown in Table 4-5).
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
Table 4-5.

Values for Nullable in Project Files

Value

Meaning in Life

Enable

Nullable annotations are enabled, and nullable warnings are enabled.

Warnings

Nullable annotations are disabled, and nullable warnings are enabled.

Annotations

Nullable annotations are enabled, and nullable warnings are disabled.

Disable

Nullable annotations are disabled, and nullable warnings are disabled.

The <Nullable> element effects the entire project. To control smaller parts of the project, use the compiler directives shown in Table 4-6.
Table 4-6.

Values for #nullable Compiler Directive

Value

Meaning in Life

Enable

Annotations are enabled, and warnings are enabled.

Disable

Annotations are disabled, and warnings are disabled.

Restore

Restores all settings to the project settings.

disable warnings

Warnings are disabled, and annotations are unaffected.

enable warnings

Warnings are enabled, and annotations are unaffected.

restore warnings

Warnings reset to project settings; annotations are unaffected.

disable annotations

Annotations are disabled, and warnings are unaffected.

enable annotations

Annotations are enabled, and warnings are unaffected.

restore annotations

Annotations are reset to project settings; warnings are unaffected.

Nullable Reference Types in Action

Largely because of the significance of the change, nullable types only throw errors when used improperly. Add the following class to the Program.cs file:
public class TestClass
{
  public string Name { get; set; }
  public int Age { get; set; }
}
As you can see, this is just a normal class. The nullability comes in when you use this class in your code. Take the following declarations:
string? nullableString = null;
TestClass? myNullableClass = null;
The project file setting makes the entire project a nullable context. The nullable context allows the declarations of the string and TestClass types to use the nullable annotation (?). The following line of code generates a warning (CS8600) due to the assignment of a null to a non-nullable type in a nullable context:
//Warning CS8600 Converting null literal or possible null value to non-nullable type
TestClass myNonNullableClass = myNullableClass;
For finer control of where the nullable contexts are in your project, you can use compiler directives (as discussed earlier) to enable or disable the context. The following code turns off the nullable context (set at the project level) and then reenables it by restoring the project settings:
#nullable disable
TestClass anotherNullableClass = null;
//Warning CS8632 The annotation for nullable reference types
//should only be used in code within a '#nullable' annotations
TestClass? badDefinition = null;
//Warning CS8632 The annotation for nullable reference types
//should only be used in code within a '#nullable' annotations
string? anotherNullableString = null;
#nullable restore

As a final note, the nullable reference types do not have the HasValue and Value properties, as those are supplied by System.Nullable<T>.

Migration Considerations

When migrating your code from C# 7 to C# 8 or C# 9 and you want to make use of nullable reference types, you can use a combination of the project setting and compiler directives to work through your code. A common practice is to start by enabling warnings and disabling nullable annotations for the entire project. Then, as you clean up areas of code, use the compiler directives to gradually enable the annotations.

Operating on Nullable Types

C# provides several operators for working with nullable types. The next sessions code the null-coalescing operator, the null-coalescing assignment operator, and the null conditional operator. For these examples, go back to the FunWithNullableValueTypes project.

The Null-Coalescing Operator

The next aspect to be aware of is any variable that might have a null value can make use of the C# ?? operator, which is formally termed the null-coalescing operator. This operator allows you to assign a value to a nullable type if the retrieved value is in fact null. For this example, assume you want to assign a local nullable integer to 100 if the value returned from GetIntFromDatabase() is null (of course, this method is programmed to always return null, but I am sure you get the general idea). Move back to the NullableValueTypes project (and set it as the startup project), and enter the following code:
//omitted for brevity
Console.WriteLine("***** Fun with Nullable Data ***** ");
DatabaseReader dr = new DatabaseReader();
// If the value from GetIntFromDatabase() is null,
// assign local variable to 100.
int myData = dr.GetIntFromDatabase() ?? 100;
Console.WriteLine("Value of myData: {0}", myData);
Console.ReadLine();
The benefit of using the ?? operator is that it provides a more compact version of a traditional if/else condition. However, if you want, you could have authored the following functionally equivalent code to ensure that if a value comes back as null, it will indeed be set to the value 100 :
// Longhand notation not using ?? syntax.
int? moreData = dr.GetIntFromDatabase();
if (!moreData.HasValue)
{
  moreData = 100;
}
Console.WriteLine("Value of moreData: {0}", moreData);

The Null-Coalescing Assignment Operator (New 8.0)

Building on the null-coalescing operator, C# 8 introduced the null-coalescing assignment operator (??=). This operator assigns the left-hand side to the right-hand side only if the left-hand side is null. For example, enter the following code:
//Null-coalescing assignment operator
int? nullableInt = null;
nullableInt ??= 12;
nullableInt ??= 14;
Console.WriteLine(nullableInt);

The nullableInt variable is initialized to null. The next line assigns the value of 12 to the variable since the left-hand side is indeed null. The next line does not assign 14 to the variable since it is not null.

The Null Conditional Operator

When you are writing software, it is common to check incoming parameters, which are values returned from type members (methods, properties, indexers), against the value null. For example, let’s assume you have a method that takes a string array as a single parameter. To be safe, you might want to test for null before proceeding. In that way, you will not get a runtime error if the array is empty. The following would be a traditional way to perform such a check:
static void TesterMethod(string[] args)
{
  // We should check for null before accessing the array data!
  if (args != null)
  {
    Console.WriteLine($"You sent me {args.Length} arguments.");
  }
}
Here, you use a conditional scope to ensure that the Length property of the string array will not be accessed if the array is null. If the caller failed to make an array of data and called your method like so, you are still safe and will not trigger a runtime error:
TesterMethod(null);
C# includes the null conditional operator token (a question mark placed after a variable type but before an access operator) to simplify the previous error checking. Rather than explicitly building a conditional statement to check for null, you can now write the following:
static void TesterMethod(string[] args)
{
  // We should check for null before accessing the array data!
  Console.WriteLine($"You sent me {args?.Length} arguments.");
}
In this case, you are not using a conditional statement. Rather, you are suffixing the ? operator directly after the string array variable. If the variable is null, its call to the Length property will not throw a runtime error. If you want to print an actual value, you could leverage the null-coalescing operator to assign a default value as so:
Console.WriteLine($"You sent me {args?.Length ?? 0} arguments.");

There are some additional areas of coding where the C# 6.0 null conditional operator will be quite handy, especially when working with delegates and events. Those topics are addressed later in the book (see Chapter 12), and you will see many more examples .

Understanding Tuples (New/Updated 7.0)

To wrap up this chapter, let’s examine the role of tuples using a Console Application project named FunWithTuples. As mentioned earlier in this chapter, one way to use out parameters is to retrieve more than one value from a method call. Another way is to use a light construct called a tuple.

Tuples are lightweight data structures that contain multiple fields. They were added to the language in C# 6, but in an extremely limited way. There was also a potentially significant problem with the C# 6 implementation: each field is implemented as a reference type, potentially creating memory and/or performance problems (from boxing/unboxing).

In C# 7, tuples use the new ValueTuple data type instead of reference types, potentially saving significant memory. The ValueTuple data type creates different structs based on the number of properties for a tuple. An additional feature added in C# 7 is that each property in a tuple can be assigned a specific name (just like variables), greatly enhancing the usability.

These are two important considerations for tuples:
  • The fields are not validated.

  • You cannot define your own methods.

They are really designed to just be a lightweight data transport mechanism.

Getting Started with Tuples

Enough theory. Let’s write some code! To create a tuple, simply enclose the values to be assigned to the tuple in parentheses, as follows:
("a", 5, "c")
Notice that they do not all have to be the same data type. The parenthetical construct is also used to assign the tuple to a variable (or you can use the var keyword and the compiler will assign the data types for you). To assign the previous example to a variable, the following two lines achieve the same thing. The values variable will be a tuple with two string properties and an int property sandwiched in between.
(string, int, string) values = ("a", 5, "c");
var values = ("a", 5, "c");
By default, the compiler assigns each property the name ItemX, where X represents the one-based position in the tuple. For the previous example, the property names are Item1, Item2, and Item3. Accessing them is done as follows:
Console.WriteLine($"First item: {values.Item1}");
Console.WriteLine($"Second item: {values.Item2}");
Console.WriteLine($"Third item: {values.Item3}");
Specific names can also be added to each property in the tuple on either the right side or the left side of the statement. While it is not a compiler error to assign names on both sides of the statement, if you do, the right side will be ignored, and only the left-side names are used. The following two lines of code show setting the names on the left and the right to achieve the same end:
(string FirstLetter, int TheNumber, string SecondLetter) valuesWithNames = ("a", 5, "c");
var valuesWithNames2 = (FirstLetter: "a", TheNumber: 5, SecondLetter: "c");
Now the properties on the tuple can be accessed using the field names as well as the ItemX notation, as shown in the following code:
Console.WriteLine($"First item: {valuesWithNames.FirstLetter}");
Console.WriteLine($"Second item: {valuesWithNames.TheNumber}");
Console.WriteLine($"Third item: {valuesWithNames.SecondLetter}");
//Using the item notation still works!
Console.WriteLine($"First item: {valuesWithNames.Item1}");
Console.WriteLine($"Second item: {valuesWithNames.Item2}");
Console.WriteLine($"Third item: {valuesWithNames.Item3}");
Note that when setting the names on the right, you must use the keyword var to declare the variable. Setting the data types specifically (even without custom names) triggers the compiler to use the left side, assign the properties using the ItemX notation, and ignore any of the custom names set on the right. The following two examples ignore the Custom1 and Custom2 names:
(int, int) example = (Custom1:5, Custom2:7);
(int Field1, int Field2) example = (Custom1:5, Custom2:7);

It is also important to call out that the custom field names exist only at compile time and are not available when inspecting the tuple at runtime using reflection (reflection is covered in Chapter 17).

Tuples can also be nested as tuples inside of tuples. Since each property in a tuple is a data type, and a tuple is a data type, the following code is perfectly legitimate:
Console.WriteLine("=> Nested Tuples");
var nt = (5, 4, ("a", "b"));

Using Inferred Variable Names (Updated 7.1)

An update to tuples in C# 7.1 is the ability for C# to infer the variable names of tuples, as shown here:
Console.WriteLine("=> Inferred Tuple Names");
var foo = new {Prop1 = "first", Prop2 = "second"};
var bar = (foo.Prop1, foo.Prop2);
Console.WriteLine($"{bar.Prop1};{bar.Prop2}");

Understanding Tuple Equality/Inequality (New 7.3)

An added feature in C# 7.1 is the tuple equality (==) and inequality (!=). When testing for inequality, the comparison operators will perform implicit conversions on data types within the tuples, including comparing nullable and non-nullable tuples and/or properties. That means the following tests work perfectly, despite the difference between int/long:
Console.WriteLine("=> Tuples Equality/Inequality");
// lifted conversions
var left = (a: 5, b: 10);
(int? a, int? b) nullableMembers = (5, 10);
Console.WriteLine(left == nullableMembers); // Also true
// converted type of left is (long, long)
(long a, long b) longTuple = (5, 10);
Console.WriteLine(left == longTuple); // Also true
// comparisons performed on (long, long) tuples
(long a, int b) longFirst = (5, 10);
(int a, long b) longSecond = (5, 10);
Console.WriteLine(longFirst == longSecond); // Also true

Tuples that contain tuples can also be compared, but only if they have the same shape. You cannot compare one tuple of three int properties with another tuple of two ints and a tuple.

Understanding Tuples as Method Return Values

Earlier in this chapter, out parameters were used to return more than one value from a method call. There are additional ways to do this, such as creating a class or structure specifically to return the values. But if this class or struct is only to be used as a data transport for one method, that is extra work and extra code that does not need to be developed. Tuples are perfectly suited for this task, are lightweight, and are easy to declare and use.

This is one of the examples from the out parameter section. It returns three values but requires three parameters passed in as transport mechanisms for the calling code.
static void FillTheseValues(out int a, out string b, out bool c)
{
  a = 9;
  b = "Enjoy your string.";
  c = true;
}
By using a tuple, you can remove the parameters and still get the three values back.
static (int a,string b,bool c) FillTheseValues()
{
  return (9,"Enjoy your string.",true);
}
Calling this method is as simple as calling any other method.
var samples = FillTheseValues();
Console.WriteLine($"Int is: {samples.a}");
Console.WriteLine($"String is: {samples.b}");
Console.WriteLine($"Boolean is: {samples.c}");
Perhaps a better example is deconstructing a full name into its individual parts (first, middle, last). The following code takes in a full name and returns a tuple with the different parts:
static (string first, string middle, string last) SplitNames(string fullName)
{
  //do what is needed to split the name apart
  return ("Philip", "F", "Japikse");
}

Understanding Discards with Tuples

Following up on the SplitNames() example, suppose you know that you need only the first and last names and do not care about the middle. By providing variable names for the values you want returned and filling in the unneeded values with an underscore (_) placeholder, you can refine the return value like this:
var (first, _, last) = SplitNames("Philip F Japikse");
Console.WriteLine($"{first}:{last}");

The middle name value of the tuple is discarded.

Understanding Tuple Pattern Matching switch Expressions (New 8.0)

Now that you have a thorough understanding of tuples, it is time to revisit the switch expression with tuples from Chapter 3. Here is the example again:
//Switch expression with Tuples
static string RockPaperScissors(string first, string second)
{
  return (first, second) switch
  {
    ("rock", "paper") => "Paper wins.",
    ("rock", "scissors") => "Rock wins.",
    ("paper", "rock") => "Paper wins.",
    ("paper", "scissors") => "Scissors wins.",
    ("scissors", "rock") => "Rock wins.",
    ("scissors", "paper") => "Scissors wins.",
    (_, _) => "Tie.",
  };
}

In this example, the two parameters are converted into a tuple as they are passed into the switch expression. The relevant values are represented in the switch expression, and any other cases are handled by the final tuple, which is composed of two discards.

The RockPaperScissors() method signature could also be written to take in a tuple, like this:
static string RockPaperScissors(
  (string first, string second) value)
{
  return value switch
  {
    //omitted for brevity
  };
}

Deconstructing Tuples

Deconstructing is the term given when separating out the properties of a tuple to be used individually. FillTheseValues() did just that. But there is another use for this pattern that can be helpful, and that is deconstructing custom types.

Take a shorter version of the Point structure used earlier in this chapter. A new method named Deconstruct() has been added to return the individual properties of the Point instance as a tuple with properties named XPos and YPos.
struct Point
{
  // Fields of the structure.
  public int X;
  public int Y;
  // A custom constructor.
  public Point(int XPos, int YPos)
  {
    X = XPos;
    Y = YPos;
  }
  public (int XPos, int YPos) Deconstruct() => (X, Y);
}
Notice the new Deconstruct() method, shown in bold in the previous code listing. This method can be named anything, but by convention it is typically named Deconstruct(). This allows a single method call to get the individual values of the structure by returning a tuple.
Point p = new Point(7,5);
var pointValues = p.Deconstruct();
Console.WriteLine($"X is: {pointValues.XPos}");
Console.WriteLine($"Y is: {pointValues.YPos}");

Deconstructing Tuples with Positional Pattern Matching (New 8.0)

When tuples have an accessible Deconstruct() method, the deconstruction can be used in a tuple-based switch expression. Using the Point example, the following code uses the generated tuple and uses those values for the when clause of each expression:
static string GetQuadrant1(Point p)
{
  return p.Deconstruct() switch
  {
    (0, 0) => "Origin",
    var (x, y) when x > 0 && y > 0 => "One",
    var (x, y) when x < 0 && y > 0 => "Two",
    var (x, y) when x < 0 && y < 0 => "Three",
    var (x, y) when x > 0 && y < 0 => "Four",
    var (_, _) => "Border",
  };
}
If the Deconstruct() method is defined with two out parameters, then the switch expression will automatically deconstruct the point. Add another Deconstruct method to the Point as follows:
public void Deconstruct(out int XPos, out int YPos)
  => (XPos,YPos)=(X, Y);
Now you can update (or add a new) GetQuadrant() method to this:
static string GetQuadrant2(Point p)
{
  return p switch
  {
    (0, 0) => "Origin",
    var (x, y) when x > 0 && y > 0 => "One",
    var (x, y) when x < 0 && y > 0 => "Two",
    var (x, y) when x < 0 && y < 0 => "Three",
    var (x, y) when x > 0 && y < 0 => "Four",
    var (_, _) => "Border",
  };
}

The change is very subtle (and is highlighted in bold). Instead of calling p.Deconstruct(), just the Point variable is used in the switch expression.

Summary

This chapter began with an examination of arrays. Then, we discussed the C# keywords that allow you to build custom methods. Recall that by default parameters are passed by value; however, you may pass a parameter by reference if you mark it with ref or out. You also learned about the role of optional or named parameters and how to define and invoke methods taking parameter arrays.

After you investigated the topic of method overloading, the bulk of this chapter examined several details regarding how enumerations and structures are defined in C# and represented within the .NET Core base class libraries. Along the way, you examined several details regarding value types and reference types, including how they respond when passing them as parameters to methods and how to interact with nullable data types and variables that might be null (e.g., reference type variables and nullable value type variables) using the ?, ??, and ??= operators.

The final section of the chapter investigated a long-anticipated feature in C#, tuples. After getting an understanding of what they are and how they work, you used them to return multiple values from methods as well as to deconstruct custom types.

In Chapter 5, you will begin to dig into the details of object-oriented development.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.227.194