Chapter 3. C# Language Fundamentals

Chapter 2 demonstrated a very simple C# program. Nonetheless, that little program was complex enough that I had to skip some of the pertinent details. This chapter illuminates these details by delving more deeply into the syntax and structure of the C# language itself.

This chapter discusses the type system in C#, drawing a distinction between built-in types (int, bool, etc.) versus user-defined types (types you create as classes and interfaces). The chapter also covers programming fundamentals such as how to create and use variables and constants. It then goes on to introduce enumerations, strings, identifiers, expressions, and statements.

The second part of the chapter explains and demonstrates the use of flow control statements, using the if, switch, while, do...while, for, and foreach statements. Also discussed are operators, including the assignment, logical, relational, and mathematical operators. This is followed by an introduction to namespaces and a short tutorial on the C# precompiler.

Although C# is principally concerned with the creation and manipulation of objects, it is best to start with the fundamental building blocks: the elements from which objects are created. These include the built-in types that are an intrinsic part of the C# language as well as the syntactic elements of C#.

Types

C# is a strongly typed language. In a strongly typed language you must declare the type of each object you create (e.g., integers, floats, strings, windows, buttons, etc.), and the compiler will help you prevent bugs by enforcing that only data of the right type is assigned to those objects. The type of an object signals to the compiler the size of that object (e.g., int indicates an object of 4 bytes) and its capabilities (e.g., buttons can be drawn, pressed, and so forth).

Tip

C# 1.1 programmers take note: until Version 2, .NET was strongly typed in everything except collections. With the addition of generics, however, it is now easy to create strongly typed collection classes, as shown in Chapter 9.

Like C++ and Java, C# divides types into two sets: intrinsic (built-in) types that the language offers and user-defined types that the programmer defines.

C# also divides the set of types into two other categories: value types and reference types.[1] The principal difference between value and reference types is the manner in which their values are stored in memory. A value type holds its actual value in memory allocated on the stack (or it is allocated as part of a larger reference type object). The address of a reference type variable sits on the stack, but the actual object is stored on the heap.

Tip

C and C++ programmers take note: in C#, there is no explicit indication that an object is a reference type (i.e., no use of the & operator). Also, pointers aren’t normally used (but see Chapter 22 for the exception to this rule).

If you have a very large object, putting it on the heap has many advantages. Chapter 4 discusses the various advantages and disadvantages of working with reference types; the current chapter focuses on the intrinsic value types available in C#.

Tip

In C#, the size and format of the storage for different intrinsic types (e.g., int) are platform-independent and consistent across all .NET languages.

C# also supports C++ style pointer types, but these are used only when working with unmanaged code. Unmanaged code is created outside of the .NET platform (for example, COM objects; working with COM objects is discussed in Chapter 22).

Working with Built-in Types

The C# language offers the usual cornucopia of intrinsic (built-in) types one expects in a modern language, each of which maps to an underlying type supported by the .NET CLS. Mapping the C# primitive types to the underlying .NET type ensures that objects created in C# can be used interchangeably with objects created in any other language compliant with the .NET CLS, such as VB.NET.

Tip

Java programmers take note: C# has a broader range of basic types than Java. The C# decimal type is notable, and is useful for financial calculations.

Each type has a specific and unchanging size. Unlike with C++, a C# int is always 4 bytes because it maps to an Int32 in the .NET CLS. Table 3-1 lists the built-in value types offered by C#.

Table 3-1. C# built-in value types

Type

Size (in bytes)

.NET type

Description

byte
1
Byte

Unsigned (values 0-255).

char
2
Char

Unicode characters.

bool
1
Boolean

True or false.

sbyte
1
SByte

Signed (values -128 to 127).

short
2
Int16

Signed (short) (values -32,768 to 32,767).

ushort
2
UInt16

Unsigned (short) (values 0 to 65,535).

int
4
Int32

Signed integer values between -2,147,483,648 and 2,147,483,647.

uint
4
UInt32

Unsigned integer values between 0 and 4,294,967,295.

float
4
Single

Floating-point number. Holds the values from approximately +/-1.5 * 10-45 to approximately +/-3.4 * 1038 with seven significant figures.

double
8
Double

Double-precision floating point. Holds the values from approximately +/-5.0 * 10-324 to approximately +/-1.8 * 10308 with 15-16 significant figures.

decimal
16
Decimal

Fixed-precision up to 28 digits and the position of the decimal point. This is typically used in financial calculations. Requires the suffix “m” or “M.”

long
8
Int64

Signed integers from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.

ulong
8
UInt64

Unsigned integers ranging from 0 to 0xffffffffffffffff.

Tip

C and C++ programmers take note: in C#, Boolean variables can only have the values true or false. Integer values don’t equate to Boolean values in C# and there is no implicit conversion.

In addition to these primitive types, C# has two other value types: enum (considered later in this chapter) and struct (see Chapter 4). Chapter 4 also discusses other subtleties of value types, such as forcing value types to act as reference types through a process known as boxing, and that value types don’t “inherit.”

Tip

C and C++ programmers take note: C# manages all memory with a garbage collection system—there is no delete operator.

Choosing a built-in type

Typically you decide which size integer to use (short , int, or long) based on the magnitude of the value you want to store. For example, a ushort can only hold values from 0 through 65,535, while a uint can hold values from 0 through 4,294,967,295.

That said, memory is fairly cheap, and programmer time is increasingly expensive; most of the time you’ll simply declare your variables to be of type int, unless there is a good reason to do otherwise.

Tip

Integers are often faster than smaller types because modern CPUs are optimized for dealing with them. Further, because of padding inserted for alignment, there’s often no space gain to be had from smaller datatypes.

Float , double, and decimal offer varying degrees of size and precision. For most small fractional numbers, float is fine. Note that the compiler assumes that any number with a decimal point is a double unless you tell it otherwise. To assign a literal float, follow the number with the letter f (assigning values to literals is discussed in detail later in this chapter):

*Footnote: You need to use the f suffix for a float, however there are no other suffixes required for other types.

float someFloat = 57f;

The char type represents a Unicode character. char literals can be simple, Unicode, or escape characters enclosed by single quote marks. For example, A is a simple character while u0041 is a Unicode character. Escape characters are special two-character tokens in which the first character is a backslash. For example, is a horizontal tab. The common escape characters are shown in Table 3-2.

Table 3-2. Common escape characters

Char

Meaning

'

Single quote

"

Double quote

\

Backslash


                              

Null

a

Alert



Backspace

f

Form feed


                              

Newline


                              

Carriage return

	

Horizontal tab

v

Vertical tab

Converting built-in types

Objects of one type can be converted into objects of another type either implicitly or explicitly. Implicit conversions happen automatically; the compiler takes care of it for you. Explicit conversions happen when you "cast” a value to a different type. The semantics of an explicit conversion are “Hey! Compiler! I know what I’m doing.” This is sometimes called “hitting it with the big hammer” and can be very useful or very painful, depending on whether your thumb is in the way of the nail.

Tip

VB6 programmers take note: in VB6 you can easily mix strings and the character datatype; a character is treated as a string with a length of 1. But C# is type-safe. To assign a literal character to a char variable, you must surround it with single quotes.

Note also that the VB6 functions to convert between a character and its ASCII equivalent (Chr( ) and Asc()) don’t exist in C#. To convert a char to its ASCII equivalent, cast it as an int (integer):

(int)'A'

To convert a number to a char, cast the number as a char:

(char)65

Implicit conversions happen automatically and are guaranteed not to lose information. For example, you can implicitly cast from a short int (2 bytes) to an int (4 bytes). No matter what value is in the short, it is not lost when converting to an int:

short x = 5;
int y = x; // implicit conversion

If you convert the other way, however, you certainly can lose information. If the value in the int is greater than 32,767, it will be truncated in the conversion. The compiler will not perform an implicit conversion from int to short:

short x;
int y = 500;
x = y;  // won't compile

You must explicitly convert using the cast operator:

short x;
int y = 500;
x = (short) y;  // OK

All the intrinsic types define their own conversion rules. At times it is convenient to define conversion rules for your user-defined types, as discussed in Chapter 5.

Variables and Constants

A variable is a storage location with a type. In the preceding examples, both x and y are variables. Variables can have values assigned to them, and those values can be changed programmatically.

Create a variable by declaring its type and then giving it a name. You can initialize the variable when you declare it, and you can assign a new value to that variable at any time, changing the value held in the variable. This is illustrated in Example 3-1.

Example 3-1. Initializing and assigning a value to a variable

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace InitializingVariables
{
   class Values
   {
      static void Main( )
      {

         int myInt = 7;
         System.Console.WriteLine("Initialized, myInt: {0}",
            myInt);

         myInt = 5;
         System.Console.WriteLine("After assignment, myInt: {0}",
            myInt);

      }   
   }
}

Output:
Initialized, myInt: 7
After assignment, myInt: 5

Tip

Visual Studio 2005 creates a namespace and using directive (as well as a using region) for every program. To save space, these are left out of most of the code examples, though they are shown in the example code you can download from O’Reilly or LibertyAssociates.com.

Here we initialize the variable myInt to the value 7, display that value, reassign the variable with the value 5, and display it again.

Tip

VB6 programmers take note: in C#, the datatype comes before the variable name.

Definite Assignment

C# requires definite assignment: that is, variables must be initialized or assigned to before they are used. To test this rule, change the line that initializes myInt in Example 3-1 to:

int myInt;

and save the revised program shown in Example 3-2.

Tip

C and C++ programmers take note: C# requires that every variable must be assigned a definite value before use; this is checked by the compiler.

Example 3-2. Using an uninitialized variable

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace UninitializedVariable
{
   class UninitializedVariable
   {
      static void Main(string[] args)
      {
         int myInt;
         System.Console.WriteLine
         ("Uninitialized, myInt: {0}", myInt);
         myInt = 5;
         System.Console.WriteLine("Assigned, myInt: {0}", myInt);

      }
   }
}

When you try to compile this listing, the C# compiler will display an error message as shown in Figure 3-1.

Error message resulting from using an unassigned variable

Figure 3-1. Error message resulting from using an unassigned variable

Double-clicking the error message will bring you to the problem in the code.

It isn’t legal to use an uninitialized variable in C#. So, does this mean you must initialize every variable in a program? In fact, no. You don’t actually need to initialize a variable, but you must assign a value to it before you attempt to use it. Example 3-3 illustrates a correct program.

Example 3-3. Assigning without initializing

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace AssigningWithoutInitializing
{
   class AssigningWithoutInitializing
   {
      static void Main(string[] args)
      {
         int myInt;
         myInt = 7;
         System.Console.WriteLine("Assigned, myInt: {0}", myInt);
         myInt = 5;
         System.Console.WriteLine("Reassigned, myInt: {0}", myInt);

      }
   }
}

Constants

A constant is a variable whose value can’t be changed. Variables are a powerful tool, but there are times when you want to manipulate a defined value, one whose value you want to ensure remains constant. For example, you might need to work with the Fahrenheit freezing and boiling points of water in a program simulating a chemistry experiment. Your program will be clearer if you name the variables that store the values FreezingPoint and BoilingPoint, but you don’t want to permit their values to be reassigned. How do you prevent reassignment? The answer is to use a constant.

Constants come in three flavors: literals , symbolic constants, and enumerations. In this assignment:

x = 32;

the value 32 is a literal constant. The value of 32 is always 32. You can’t assign a new value to 32; you can’t make 32 represent the value 99 no matter how you might try.

Symbolic constants assign a name to a constant value. You declare a symbolic constant using the const keyword and the following syntax:

consttype 
                  identifier = value;

A constant must be initialized when it is declared, and once initialized it can’t be altered. For example:

const int FreezingPoint = 32;

In this declaration, 32 is a literal constant and FreezingPoint is a symbolic constant of type int. Example 3-4 illustrates the use of symbolic constants.

Example 3-4. Using symbolic constants

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace SymbolicConstants
{
   class SymbolicConstants
   {
      static void Main(string[] args)
      {
         const int FreezingPoint = 32;   // degrees Fahrenheit
         const int BoilingPoint = 212;

         System.Console.WriteLine("Freezing point of water: {0}",
              FreezingPoint);
         System.Console.WriteLine("Boiling point of water: {0}",
              BoilingPoint);//BoilingPoint = 21;

      }
   }
}

Example 3-4 creates two symbolic integer constants: FreezingPoint and BoilingPoint. As a matter of style, constant names are written in Pascal notation, but this is certainly not required by the language.

These constants serve the same purpose as always using the literal values 32 and 212 for the freezing and boiling points of water in expressions that require them, but because these constants have names, they convey far more meaning. Also, if you decide to switch this program to Celsius, you can reinitialize these constants at compile time, to 0 and 100, respectively; all the rest of the code ought to continue to work.

To prove to yourself that the constant can’t be reassigned, try uncommenting the last line of the program (shown in bold). When you recompile, you should receive the error shown in Figure 3-2.

Warning that occurs when you try to reassign a constant

Figure 3-2. Warning that occurs when you try to reassign a constant

Enumerations

Enumerations provide a powerful alternative to constants. An enumeration is a distinct value type, consisting of a set of named constants (called the enumerator list).

In Example 3-4, you created two related constants:

const int FreezingPoint = 32;  
const int BoilingPoint = 212;

You might wish to add a number of other useful constants to this list, such as:

const int LightJacketWeather = 60;
const int SwimmingWeather = 72;
const int WickedCold = 0;

This process is somewhat cumbersome, and there is no logical connection between these various constants. C# provides the enumeration to solve these problems:

enum Temperatures
{
   WickedCold = 0,
   FreezingPoint = 32,
   LightJacketWeather = 60,
   SwimmingWeather = 72,
   BoilingPoint = 212,
}

Every enumeration has an underlying type, which can be any integral type (integer, short, long, etc.) except for char. The technical definition of an enumeration is:

[attributes] [modifiers] enum 
                  identifier 
     [:base-type] {
                  enumerator-list
                  };

The optional attributes and modifiers are considered later in this book. For now, let’s focus on the rest of this declaration. An enumeration begins with the keyword enum , which is generally followed by an identifier, such as:

enum Temperatures

The base type is the underlying type for the enumeration. If you leave out this optional value (and often you will), it defaults to int, but you are free to use any of the integral types (e.g., ushort, long) except for char. For example, the following fragment declares an enumeration of unsigned integers (uint):

enum ServingSizes :uint
{
    Small = 1,
    Regular = 2,
    Large = 3
}

Notice that an enum declaration ends with the enumerator list. The enumerator list contains the constant assignments for the enumeration, each separated by a comma.

Example 3-5 rewrites Example 3-4 to use an enumeration.

Example 3-5. Using enumerations to simplify your code

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace EnumeratedConstants
{
   class EnumeratedConstants
   {

      enum Temperatures
      {
         WickedCold = 0,
         FreezingPoint = 32,
         LightJacketWeather = 60,
         SwimmingWeather = 72,
         BoilingPoint = 212,
      }

      static void Main(string[] args)
      {
         System.Console.WriteLine("Freezing point of water: {0}",
            (int)Temperatures.FreezingPoint);
         System.Console.WriteLine("Boiling point of water: {0}",
            (int)Temperatures.BoilingPoint);
      }
   }
}

As you can see, an enum must be qualified by its enumtype (e.g., Temperatures.WickedCold). By default, an enumeration value is displayed using its symbolic name (such as BoilingPoint or FreezingPoint). When you want to display the value of an enumerated constant, you must cast the constant to its underlying type (int). The integer value is passed to WriteLine, and that value is displayed.

Each constant in an enumeration corresponds to a numerical value—in this case, an integer. If you don’t specifically set it otherwise, the enumeration begins at 0 and each subsequent value counts up from the previous.

If you create the following enumeration:

enum SomeValues
{
   First,
   Second,
   Third = 20,
   Fourth
}

the value of First will be 0, Second will be 1, Third will be 20, and Fourth will be 21.

Enums are formal types; therefore an explicit conversion is required to convert between an enum type and an integral type.

Tip

C++ programmers take note: C#’s use of enums is subtly different from C++’s, which restricts assignment to an enum type from an integer but allows an enum to be promoted to an integer for assignment of an enum to an integer.

Strings

It is nearly impossible to write a C# program without creating strings. A string object holds a string of characters.

You declare a string variable using the string keyword much as you would create an instance of any object:

string myString;

A string literal is created by placing double quotes around a string of letters:

"Hello World"

It is common to initialize a string variable with a string literal:

string myString = "Hello World";

Strings are covered in much greater detail in Chapter 10.

Identifiers

Identifiers are names programmers choose for their types, methods, variables, constants, objects, and so forth. An identifier must begin with a letter or an underscore.

The Microsoft naming conventions suggest using camel notation (initial lowercase such as someName) for variable names and Pascal notation (initial uppercase such as SomeOtherName) for method names and most other identifiers.

Tip

Microsoft no longer recommends using Hungarian notation (e.g., iSomeInteger) or underscores (e.g., Some_Value).

Identifiers are case-sensitive, so C# treats myVariable and MyVariable as two different variable names.

Expressions

Statements that evaluate to a value are called expressions. You may be surprised how many statements do evaluate to a value. For example, an assignment such as:

myVariable = 57;

is an expression; it evaluates to the value assigned, which, in this case, is 57.

Note that the preceding statement assigns the value 57 to the variable myVariable. The assignment operator (=) doesn’t test equality; rather it causes whatever is on the right side (57) to be assigned to whatever is on the left side (myVariable). All the C# operators (including assignment and equality) are discussed later in this chapter (see “Operators”).

Because myVariable = 57 is an expression that evaluates to 57, it can be used as part of another assignment operator, such as:

mySecondVariable = myVariable = 57;

What happens in this statement is that the literal value 57 is assigned to the variable myVariable. The value of that assignment (57) is then assigned to the second variable, mySecondVariable. Thus, the value 57 is assigned to both variables. You can therefore initialize any number of variables to the same value with one statement:

a = b = c = d = e = 20;

Whitespace

In the C# language, spaces, tabs, and newlines are considered to be "whitespace” (so named because you see only the white of the underlying “page”). Extra whitespace is generally ignored in C# statements. You can write:

myVariable = 5;

or:

myVariable    =                             5;

and the compiler will treat the two statements as identical.

The exception to this rule is that whitespace within strings isn’t ignored. If you write:

Console.WriteLine("Hello World")

each space between “Hello” and “World” is treated as another character in the string.

Most of the time the use of whitespace is intuitive. The key is to use whitespace to make the program more readable to the programmer; the compiler is indifferent.

However, there are instances in which the use of whitespace is quite significant. Although the expression:

int x = 5;

is the same as:

int x=5;

it is not the same as:

intx=5;

The compiler knows that the whitespace on either side of the assignment operator is extra, but the whitespace between the type declaration int and the variable name x is not extra, and is required. This is not surprising: the whitespace allows the compiler to parse the keyword int rather than some unknown term intx. You are free to add as much or as little whitespace between int and x as you care to, but there must be at least one whitespace character (typically a space or tab).

Tip

VB programmers take note: in C# the end-of-line has no special significance; statements are ended with semicolons, not newline characters. There is no line-continuation character because none is needed.

Statements

In C# a complete program instruction is called a statement. Programs consist of sequences of C# statements. Each statement must end with a semicolon (;). For example:

int x;     // a statement
x = 23;    // another statement
int y = x; // yet another statement

C# statements are evaluated in order. The compiler starts at the beginning of a statement list and makes its way to the bottom. This would be entirely straightforward, and terribly limiting, were it not for branching. There are two types of branches in a C# program: unconditional branching and conditional branching.

Program flow is also affected by looping and iteration statements, which are signaled by the keywords for , while, do, in, and foreach. Iteration is discussed later in this chapter. For now, let’s consider some of the more basic methods of conditional and unconditional branching.

Unconditional Branching Statements

An unconditional branch is created in one of two ways. The first way is by invoking a method. When the compiler encounters the name of a method, it stops execution in the current method and branches to the newly “called” method. When that method returns a value, execution picks up in the original method on the line just below the method call. Example 3-6 illustrates.

Example 3-6. Calling a method

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace CallingAMethod
{
   class CallingAMethod
   {
      static void Main( )
      {
         Console.WriteLine("In Main! Calling SomeMethod( )...");
         SomeMethod( );
         Console.WriteLine("Back in Main( ).");
      }
      static void SomeMethod( )
      {
         Console.WriteLine("Greetings from SomeMethod!");
      }
   }
}
Output:
In Main! Calling SomeMethod( )...
Greetings from SomeMethod!
Back in Main( ).

Program flow begins in Main( ) and proceeds until SomeMethod() is invoked (invoking a method is sometimes referred to as “calling” the method). At that point, program flow branches to the method. When the method completes, program flow resumes at the next line after the call to that method.

The second way to create an unconditional branch is with one of the unconditional branch keywords: goto, break, continue, return, or throw. Additional information about the first three jump statements is provided later in this chapter; the return statement returns control to the calling method; the final statement, throw, is discussed in Chapter 11.

Conditional Branching Statements

A conditional branch is created by a conditional statement, which is signaled by keywords such as if, else, or switch. A conditional branch occurs only if the condition expression evaluates true.

Tip

C and C++ programmers take note: unlike C and C++, in which any expression can be used in a conditional, C# requires that all conditional expressions evaluate to a Boolean value.

if...else statements

if...else statements branch based on a condition. The condition is an expression, tested in the head of the if statement. If the condition evaluates true, the statement (or block of statements) in the body of the if statement is executed.

if statements may contain an optional else statement. The else statement is executed only if the expression in the head of the if statement evaluates false:

if (expression)
   statement1
[else
   statement2]

This is the kind of if statement description you are likely to find in your compiler documentation. It shows you that the if statement takes a Boolean expression (an expression that evaluates true or false) in parentheses, and executes statement1 if the expression evaluates true. Note that statement1 can actually be a block of statements within braces.

You can also see that the else statement is optional, as it is enclosed in square brackets. Although this gives you the syntax of an if statement, an illustration will make its use clear. See Example 3-7.

Example 3-7. if...else statements

using System;
class Values
{
   static void Main( )
   {
      int valueOne = 10;
      int valueTwo = 20;

      if ( valueOne > valueTwo )
      {
         Console.WriteLine(
           "ValueOne: {0} larger than ValueTwo: {1}", 
                valueOne, valueTwo);
      }
      else
      {
         Console.WriteLine(
          "ValueTwo: {0} larger than ValueOne: {1}",  
                valueTwo,valueOne);
      }

      valueOne = 30; // set valueOne higher

      if ( valueOne > valueTwo )
      {
         valueTwo = valueOne++;
         Console.WriteLine("
Setting valueTwo to valueOne value, ");
         Console.WriteLine("and incrementing ValueOne.
");
            Console.WriteLine("ValueOne: {0}  ValueTwo: {1}", 
                valueOne, valueTwo);
      }
      else
      {
         valueOne = valueTwo;
         Console.WriteLine("Setting them equal. ");
            Console.WriteLine("ValueOne: {0}  ValueTwo: {1}", 
                valueOne, valueTwo);
      }         
   }
}

In Example 3-7, the first if statement tests whether valueOne is greater than valueTwo. The relational operators such as greater than (>), less than (<), and equal to (==) are fairly intuitive to use.

The test of whether valueOne is greater than valueTwo evaluates false (because valueOne is 10 and valueTwo is 20, so valueOne is not greater than valueTwo). The else statement is invoked, printing the statement:

ValueTwo: 20 is larger than ValueOne: 10

The second if statement evaluates true and all the statements in the if block are evaluated, causing two lines to print:

Setting valueTwo to valueOne value,
and incrementing ValueOne.

ValueOne: 31  ValueTwo: 30

Nested if statements

It is possible, and not uncommon, to nest if statements to handle complex conditions. For example, suppose you need to write a program to evaluate the temperature, and specifically to return the following types of information.

  • If the temperature is 32 degrees or lower, the program should warn you about ice on the road.

  • If the temperature is exactly 32 degrees, the program should tell you that there may be ice patches.

There are many good ways to write this program. Example 3-8 illustrates one approach, using nested if statements.

Example 3-8. Nested if statements

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace NestedIf
{
   class NestedIf
   {
      static void Main( )
      {
         int temp = 32;

         if ( temp <= 32 )
         {
            Console.WriteLine( "Warning! Ice on road!" );
            if ( temp == 32 )
            {
               Console.WriteLine(
                  "Temp exactly freezing, beware of water." );
            }
            else
            {
               Console.WriteLine( "Watch for black ice! Temp: {0}", temp );
            }      // end else
         }         // end if (temp <= 32)
      }            // end main
   }               // end class
}                  // end namespace

The logic of Example 3-8 is that it tests whether the temperature is less than or equal to 32. If so, it prints a warning:

if (temp <= 32)
{
   Console.WriteLine("Warning! Ice on road!");

The program then checks whether the temp is equal to 32 degrees. If so, it prints one message; if not, the temp must be less than 32 and the program prints the second message. Notice that this second if statement is nested within the first if, so the logic of the else is “since it has been established that the temp is less than or equal to 32, and it isn’t equal to 32, it must be less than 32.”

switch statements: an alternative to nested ifs

Nested if statements are hard to read, hard to get right, and hard to debug. When you have a complex set of choices to make, the switch statement is a more readable alternative. The logic of a switch statement is “pick a matching value and act accordingly.”

switch (expression)
{
   case 
                     constant-expression:
      statement
                     jump-statement
   [default: statement]
}

As you can see, like an if statement, the expression is put in parentheses in the head of the switch statement. Each case statement then requires a constant expression; that is, a literal or symbolic constant or an enumeration.

If a case is matched, the statement(s) associated with that case is executed. This must be followed by a jump statement. Typically, the jump statement is break , which transfers execution out of the switch. An alternative is a goto statement, typically used to jump into another case, as illustrated in Example 3-9.

Example 3-9. The switch statement

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace SwitchStatement
{
   class SwitchStatement
   {
      static void Main( string[] args )
      {
         const int Democrat = 0;
         const int LiberalRepublican = 1;
         const int Republican = 2;
         const int Libertarian = 3;
         const int NewLeft = 4;
         const int Progressive = 5;

         int myChoice = Libertarian;

         switch ( myChoice )
         {
            case Democrat:
               Console.WriteLine( "You voted Democratic.
" );
               break;
            case LiberalRepublican:  // fall through
            //Console.WriteLine(
            //"Liberal Republicans vote Republican
");
            case Republican:
               Console.WriteLine( "You voted Republican.
" );
               break;
            case NewLeft:
               Console.WriteLine( "NewLeft is now Progressive" );
               goto case Progressive;
            case Progressive:
               Console.WriteLine( "You voted Progressive.
" );
               break;
            case Libertarian:
               Console.WriteLine( "Libertarians are voting Republican" );
               goto case Republican;
            default:
               Console.WriteLine( "You did not pick a valid choice.
" );
               break;
         }

         Console.WriteLine( "Thank you for voting." );
      }
   }
}

In this whimsical example, we create constants for various political parties. We then assign one value (Libertarian) to the variable myChoice and switch on that value. If myChoice is equal to Democrat, we print out a statement. Notice that this case ends with break. break is a jump statement that takes us out of the switch statement and down to the first line after the switch, on which we print “Thank you for voting.”

Tip

VB6 programmers take note: the equivalent of the C# switch statement is the VB6 Select Case statement. Also, while VB6 allows you to test a range of values using a single Case statement, C# syntax doesn’t provide for this contingency. The following two Case statements are syntactically correct in VB6:

Case Is > 100
Case 50 to 60

Note

However, these statements aren’t valid in C#. In C#, you can test only a single constant expression. To test a range, you must test each value independently and “fall through” to a common case block.

The value LiberalRepublican has no statement under it, and it "falls through” to the next statement: Republican. If the value is LiberalRepublican or Republican, the Republican statements execute. You can “fall through” in this way only if there is no body within the statement. If you uncomment WriteLine( ) under LiberalRepublican, this program won’t compile.

Tip

C and C++ programmers take note: you can’t fall through to the next case unless the case statement is empty. Thus, you can write this:

case 1: // fall through ok 
case 2:

In this example, case 1 is empty. You can’t, however, write this:

case 1:
    TakeSomeAction();
        // fall through not OK 
case 2:

Here case 1 has a statement in it, and you can’t fall through. If you want case 1 to fall through to case 2, you must explicitly use goto:

case 1:
    TakeSomeAction(); 
    goto case 2; // explicit fall through 
case 2:

If you do need a statement but you then want to execute another case, you can use the goto statement, as shown in the NewLeft case:

goto case Progressive;

It is not required that the goto take you to the case immediately following. In the next instance, the Libertarian choice also has a goto, but this time it jumps all the way back up to the Republican case. Because our value was set to Libertarian, this is just what occurs. We print out the Libertarian statement, go to the Republican case, print that statement, and then hit the break, taking us out of the switch and down to the final statement. The output for all of this is:

Libertarians are voting Republican
You voted Republican.

Thank you for voting.

Note the default case, excerpted from Example 3-9:

default:
    Console.WriteLine(
     "You did not pick a valid choice.
");

If none of the cases match, the default case will be invoked, warning the user of the mistake.

Switch on string statements

In the previous example, the switch value was an integral constant. C# offers the ability to switch on a string, allowing you to write:

case "Libertarian":

If the strings match, the case statement is entered.

Iteration Statements

C# provides an extensive suite of iteration statements, including for, while and do...while loops, as well as foreach loops (new to the C family but familiar to VB programmers). In addition, C# supports the goto, break, continue, and return jump statements.

The goto statement

The goto statement is the seed from which all other iteration statements have been germinated. Unfortunately, it is a semolina seed, producer of spaghetti code and endless confusion. Most experienced programmers properly shun the goto statement, but in the interest of completeness, here’s how you use it:

  1. Create a label.

  2. goto that label.

The label is an identifier followed by a colon. The goto command is typically tied to a condition, as illustrated in Example 3-10.

Example 3-10. Using goto

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace UsingGoTo
{
   class UsingGoTo
   {
      static void Main( string[] args )
      {
         int i = 0;
      repeat:            // the label
         Console.WriteLine( "i: {0}", i );
         i++;
         if ( i < 10 )
            goto repeat;  // the dastardly deed
         return;
      }
   }
}

If you were to try to draw the flow of control in a program that makes extensive use of goto statements, the resulting morass of intersecting and overlapping lines might look like a plate of spaghetti; hence the term “spaghetti code.” It was this phenomenon that led to the creation of alternatives, such as the while loop. Many programmers feel that using goto in anything other than a trivial example creates confusion and difficult-to-maintain code.

The while loop

The semantics of the while loop are “while this condition is true, do this work.” The syntax is:

while (expression) statement

As usual, an expression is any statement that returns a value. While statements require an expression that evaluates to a Boolean (true/false) value, and that statement can, of course, be a block of statements. Example 3-11 updates Example 3-10, using a while loop.

Example 3-11. Using a while loop

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace WhileLoop
{
   class WhileLoop
   {
      static void Main( string[] args )
      {
         int i = 0;
         while ( i < 10 )
         {
            Console.WriteLine( "i: {0}", i );
            i++;
         }
         return;
      }
   }
}

The code in Example 3-11 produces results identical to the code in Example 3-10, but the logic is a bit clearer. The while statement is nicely self-contained, and it reads like an English sentence: “while i is less than 10, print this message and increment i.”

Notice that the while loop tests the value of i before entering the loop. This ensures that the loop will not run if the condition tested is false; thus if i is initialized to 11, the loop will never run.

The do...while loop

A while statement may never execute if the condition tested returns false. If you want to ensure that your statement is run at least once, use a do...while loop:

do 
                     statement 
                     while 
                     expression

An expression is any statement that returns a value. Example 3-12 shows the do... while loop.

Example 3-12. The do...while loop

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace DoWhile
{
   class DoWhile
   {
      static void Main( string[] args )
      {
         int i = 11;
         do
         {
            Console.WriteLine( "i: {0}", i );
            i++;
         } while ( i < 10 );
         return 0;
      }
   }
}

Here i is initialized to 11 and the while test fails, but only after the body of the loop has run once.

The for loop

A careful examination of the while loop in Example 3-11 reveals a pattern often seen in iterative statements: initialize a variable (i = 0), test the variable (i < 10), execute a series of statements, and increment the variable (i++). The for loop allows you to combine all these steps in a single loop statement:

for ([initializers]; [expression]; [iterators]) 
                     statement

The for loop is illustrated in Example 3-13.

Example 3-13. The for loop

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ForLoop
{
   class ForLoop
   {
      static void Main( string[] args )
      {
         for ( int i = 0; i < 100; i++ )
         {
            Console.Write( "{0} ", i );

            if ( i % 10 == 0 )
            {
               Console.WriteLine( "	{0}", i );
            }
         }
         return ;
      }
   }
}

Output:
0       0
1 2 3 4 5 6 7 8 9 10    10
11 12 13 14 15 16 17 18 19 20   20
21 22 23 24 25 26 27 28 29 30   30
31 32 33 34 35 36 37 38 39 40   40
41 42 43 44 45 46 47 48 49 50   50
51 52 53 54 55 56 57 58 59 60   60
61 62 63 64 65 66 67 68 69 70   70
71 72 73 74 75 76 77 78 79 80   80
81 82 83 84 85 86 87 88 89 90   90
91 92 93 94 95 96 97 98 99

This for loop makes use of the modulus operator described later in this chapter. The value of i is printed until i is a multiple of 10:

if ( i % 10   == 0)

A tab is then printed, followed by the value. Thus, the 10s (20, 30, 40, etc.) are called out on the right side of the output.

Tip

VB6 programmers take note: in C#, looping variables are declared within the header of the for or foreach statement (rather than before the statement begins). This means that they are in scope only within the block, and you can’t refer to them outside the loop. The foreach statement is covered in detail in Chapter 9.

The individual values are printed using Console.Write() , which is much like WriteLine( ) but which doesn’t enter a newline character, allowing the subsequent writes to occur on the same line.

A few quick points to notice: in a for loop, the condition is tested before the statements are executed. Thus, in the example, i is initialized to 0, then it is tested to see if it is less than 100. Because i < 100 returns true, the statements within the for loop are executed. After the execution, i is incremented (i++).

Note that the variable i is scoped to within the for loop (that is, the variable i is visible only within the for loop). Example 3-14 will not compile.

Example 3-14. Scope of variables declared in a for loop

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ForLoopScope
{
   class ForLoopScope
   {
      static void Main( string[] args )
      {
         for ( int i = 0; i < 100; i++ )
         {
            Console.Write( "{0} ", i );

            if ( i % 10 == 0 )
            {
               Console.WriteLine( "	{0}", i );
            }
         }         Console.WriteLine( "
 Final value of i: {0}", i );
      }
   }
}

The line shown in bold fails, as the variable i is not available outside the scope of the for loop itself.

The foreach statement

The foreach statement is new to the C family of languages; it is used for looping through the elements of an array or a collection. Discussion of this incredibly useful statement is deferred until Chapter 9.

The continue and break statements

There are times when you would like to return to the top of a loop without executing the remaining statements in the loop. The continue statement causes the loop to skip the remaining steps in the loop.

The obverse side of that coin is the ability to break out of a loop and immediately end all further work within the loop. For this purpose the break statement exists.

Tip

break and continue create multiple exit points and can make for hard-to-understand, and thus hard-to-maintain, code. Use them with some care.

Example 3-15 illustrates the mechanics of continue and break. This code, suggested to me by one of my technical reviewers, Donald Xie, is intended to create a traffic signal processing system. The signals are simulated by entering numerals and uppercase characters from the keyboard, using Console.ReadLine( ), which reads a line of text from the keyboard.

The algorithm is simple: receipt of a 0 (zero) means normal conditions, and no further action is required except to log the event. (In this case, the program simply writes a message to the console; a real application might enter a timestamped record in a database.) On receipt of an abort signal (here simulated with an uppercase “A”), the problem is logged and the process is ended. Finally, for any other event, an alarm is raised, perhaps notifying the police. (Note that this sample doesn’t actually notify the police, though it does print out a harrowing message to the console.) If the signal is “X,” the alarm is raised, but the while loop is also terminated.

Example 3-15. Using continue and break

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace ContinueBreak
{
   class ContinueBreak
   {
      static void Main( string[] args )
      {
         string signal = "0";      // initialize to neutral
         while ( signal != "X" )      // X indicates stop
         {
            Console.Write( "Enter a signal: " );
            signal = Console.ReadLine( );

            // do some work here, no matter what signal you 
            // receive 
            Console.WriteLine( "Received: {0}", signal );

            if ( signal == "A" )
            {
               // faulty - abort signal processing
               // Log the problem and abort.
               Console.WriteLine( "Fault! Abort
" );
               break;
            }

            if ( signal == "0" )
            {
               // normal traffic condition
               // log and continue on
               Console.WriteLine( "All is well.
" );
               continue;
            }

            // Problem. Take action and then log the problem
            // and then continue on
            Console.WriteLine( "{0} -- raise alarm!
",
               signal );
         }  // end while
      }     // end main
   }        // end class
}           // end namespace

Output:
Enter a signal: 0
Received: 0
All is well.

Enter a signal: B
Received: B
B -- raise alarm!

Enter a signal: A
Received: A
Fault! Abort

Press any key to continue

The point of this exercise is that when the A signal is received, the action in the if statement is taken and then the program breaks out of the loop, without raising the alarm. When the signal is 0, it is also undesirable to raise the alarm, so the program continues from the top of the loop.

Operators

An operator is a symbol that causes C# to take an action. The C# primitive types (e.g., int) support a number of operators such as assignment, increment, and so forth.

The Assignment Operator (=)

The section titled “Expressions,” earlier in this chapter, demonstrates the use of the assignment operator. This symbol causes the operand on the left side of the operator to have its value changed to whatever is on the right side of the operator.

Mathematical Operators

C# uses five mathematical operators: four for standard calculations and a fifth to return the remainder in integer division. The following sections consider the use of these operators.

Simple arithmetical operators (+, -, *, /)

C# offers operators for simple arithmetic: the addition (+), subtraction (-), multiplication (*), and division (/) operators work as you might expect, with the possible exception of integer division.

When you divide two integers, C# divides like a child in fourth grade: it throws away any fractional remainder. Thus, dividing 17 by 4 returns the value 4 (17/4 = 4, with a remainder of 1). C# provides a special operator (modulus, %, which is described in the next section) to retrieve the remainder.

Note, however, that C# does return fractional answers when you divide floats, doubles, and decimals.

The modulus operator (%) to return remainders

To find the remainder in integer division, use the modulus operator (%). For example, the statement 17%4 returns 1 (the remainder after integer division).

The modulus operator turns out to be more useful than you might at first imagine. When you perform modulus n on a number that is a multiple of n, the result is 0. Thus 80%10 = 0 because 80 is an even multiple of 10. This fact allows you to set up loops in which you take an action every nth time through the loop, by testing a counter to see if %n is equal to 0. This strategy comes in handy in the use of the for loop, as described earlier in this chapter. The effects of division on integers, floats, doubles, and decimals are illustrated in Example 3-16.

Example 3-16. Division and modulus

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace DivisionModulus
{
   class DivisionModulus
   {
      static void Main( string[] args )
      {
         int i1, i2;
         float f1, f2;
         double d1, d2;
         decimal dec1, dec2;

         i1 = 17;
         i2 = 4;
         f1 = 17f;
         f2 = 4f;
         d1 = 17;
         d2 = 4;
         dec1 = 17;
         dec2 = 4;
         Console.WriteLine( "Integer:	{0}
float:		{1}",
             i1 / i2, f1 / f2 );
         Console.WriteLine( "double:		{0}
decimal:	{1}",
             d1 / d2, dec1 / dec2 );
         Console.WriteLine( "
Modulus:	{0}", i1 % i2 );

      }
   }
}

Output:
Integer:        4
float:          4.25
double:         4.25
decimal:        4.25

Modulus:        1

Now consider this line from Example 3-16:

Console.WriteLine("Integer:	{0}
float:		{1}
",
    i1/i2, f1/f2);

It begins with a call to Console.WriteLine( ), passing in this partial string:

"Integer:	{0}

This will print the characters Integer:, followed by a tab ( ), followed by the first parameter ({0}), followed by a newline character ( ). The next string snippet:

float:		{1}

is very similar. It prints float:, followed by two tabs (to ensure alignment), the contents of the second parameter ({1}), and then another newline. Notice the subsequent line, as well:

Console.WriteLine("
Modulus:	{0}", i1%i2);

This time the string begins with a newline character, which causes a line to be skipped just before the string Modulus: is printed. You can see this effect in the output.

Increment and Decrement Operators

A common requirement is to add a value to a variable, subtract a value from a variable, or otherwise change the mathematical value, and then to assign that new value back to the same variable. You might even want to assign the result to another variable altogether. The following two sections discuss these cases respectively.

Calculate and reassign operators

Suppose you want to increment the mySalary variable by 5,000. You can do this by writing:

mySalary = mySalary + 5000;

The addition happens before the assignment, and it is perfectly legal to assign the result back to the original variable. Thus, after this operation completes, mySalary will have been incremented by 5,000. You can perform this kind of assignment with any mathematical operator:

mySalary = mySalary * 5000;
mySalary = mySalary - 5000;

and so forth.

The need to increment and decrement variables is so common that C# includes special operators for self-assignment. Among these operators are += , -=, *=, /= , and %=, which, respectively, combine addition, subtraction, multiplication, division, and modulus with self-assignment. Thus, you can alternatively write the previous examples as:

mySalary += 5000;
mySalary *= 5000;
mySalary -= 5000;

The effect of this is to increment mySalary by 5,000, multiply mySalary by 5,000, and subtract 5,000 from the mySalary variable, respectively.

Because incrementing and decrementing by 1 is a very common need, C# (like C and C++ before it) also provides two special operators. To increment by 1, use the ++ operator, and to decrement by 1, use the -- operator.

Thus, if you want to increment the variable myAge by 1 you can write:

myAge++;

The prefix and postfix operators

To complicate matters further, you might want to increment a variable and assign the results to a second variable:

firstValue = secondValue++;

The question arises: do you want to assign before you increment the value, or after? In other words, if secondValue starts out with the value 10, do you want to end with both firstValue and secondValue equal to 11, or do you want firstValue to be equal to 10 (the original value) and secondValue to be equal to 11?

C# (again, like C and C++) offers two flavors of the increment and decrement operators: prefix and postfix. Thus, you can write:

firstValue = secondValue++;  // postfix

which will assign first, and then increment (firstValue=10, secondValue=11). You can also write:

firstValue = ++secondValue;  // prefix

which will increment first, and then assign (firstValue=11, secondValue=11).

It is important to understand the different effects of prefix and postfix, as illustrated in Example 3-17.

Example 3-17. Prefix versus postfix increment

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace PrefixPostfix
{
   class PrefixPostfix
   {
      static void Main( string[] args )
      {
         int valueOne = 10;
         int valueTwo;
         valueTwo = valueOne++;
         Console.WriteLine( "After postfix: {0}, {1}", valueOne,
         valueTwo );
         valueOne = 20;
         valueTwo = ++valueOne;
         Console.WriteLine( "After prefix: {0}, {1}", valueOne,
         valueTwo );

      }
   }
}

Output:
After postfix: 11, 10
After prefix: 21, 21

Relational Operators

Relational operators are used to compare two values, and then return a Boolean (true or false). The greater-than operator (>), for example, returns true if the value on the left of the operator is greater than the value on the right. Thus, 5 > 2 returns the value true, while 2 > 5 returns the value false.

The relational operators for C# are shown in Table 3-3. This table assumes two variables: bigValue and smallValue, in which bigValue has been assigned the value 100 and smallValue the value 50.

Table 3-3. C# relational operators (assumes bigValue = 100 and smallValue = 50)

Name

Operator

Given this statement

The expression evaluates to

Equals

==
bigValue == 100
bigValue == 80
true 
false

Not equals

!=
bigValue != 100
bigValue != 80
false
true

Greater than

>
bigValue > smallValue
true

Greater than or equals

>=
bigValue >= smallValue
smallValue >= bigValue
true
false

Less than

<
bigValue < smallValue
false

Less than or equals

<=
smallValue <= bigValue
bigValue <= smallValue
true
false

Each relational operator acts as you might expect. However, take note of the equals operator (==), which is created by typing two equals signs (=) in a row (i.e., without any space between them); the C# compiler treats the pair as a single operator.

The C# equality operator (==) tests for equality between the objects on either side of the operator. This operator evaluates to a Boolean value (true or false). Thus, the statement:

myX == 5;

evaluates to true if and only if myX is a variable whose value is 5.

Tip

It is not uncommon to confuse the assignment operator (=) with the equals operator (==). The latter has two equals signs, the former only one.

Use of Logical Operators with Conditionals

If statements (discussed earlier in this chapter) test whether a condition is true. Often you will want to test whether two conditions are both true, or whether only one is true, or none is true. C# provides a set of logical operators for this, as shown in Table 3-4. This table assumes two variables, x and y, in which x has the value 5 and y the value 7.

Table 3-4. C# logical operators (assumes x = 5, y = 7)

Name

Operator

Given this statement

The expression evaluates to

and
&&
(x == 3) && (y == 7)
false
or
||
(x == 3) || (y == 7)
true
not
!
! (x == 3)
true

The and operator tests whether two statements are both true. The first line in Table 3-4 includes an example that illustrates the use of the and operator:

(x == 3) && (y == 7)

The entire expression evaluates false because one side (x == 3) is false.

With the or operator, either or both sides must be true; the expression is false only if both sides are false. So, in the case of the example in Table 3-4:

(x == 3) || (y == 7)

the entire expression evaluates true because one side (y==7) is true.

With a not operator, the statement is true if the expression is false, and vice versa. So, in the accompanying example:

! (x == 3)

the entire expression is true because the tested expression (x==3) is false. (The logic is “it is true that it is not true that x is equal to 3.”)

Operator Precedence

The compiler must know the order in which to evaluate a series of operators. For example, if I write:

myVariable = 5 + 7 * 3;

there are three operators for the compiler to evaluate (=, +, and *). It could, for example, operate left to right, which would assign the value 5 to myVariable, then add 7 to the 5 (12) and multiply by 3 (36)—but of course then it would throw that 36 away. This is clearly not what is intended.

The rules of precedence tell the compiler which operators to evaluate first. As is the case in algebra, multiplication has higher precedence than addition, so 5+7*3 is equal to 26 rather than 36. Both addition and multiplication have higher precedence than assignment, so the compiler will do the math, and then assign the result (26) to myVariable only after the math is completed.

In C#, parentheses are also used to change the order of precedence much as they are in algebra. Thus, you can change the result by writing:

myVariable = (5+7) * 3;

Grouping the elements of the assignment in this way causes the compiler to add 5+7, multiply the result by 3, and then assign that value (36) to myVariable. Table 3-5 summarizes operator precedence in C#.

Table 3-5. Operator precedence

Category

Operators

Primary

(x) x.y x->y f(x) a[x] x++ x-- new typeof sizeof checked unchecked stackalloc

Unary

+ - ! ~ ++x -- x (T)x *x &x

Multiplicative

* / %

Additive

+ -

Shift

<< >>

Relational

< > <= >= is as

Equality

== !=

Logical AND

&

Logical XOR

^

Logical OR

|

Conditional AND

&&

Conditional OR

||

Conditional

?:

Assignment

= *= /= %= += -= <<= >>= &= ^= |=

In some complex equations you might need to nest your parentheses to ensure the proper order of operations. Let’s assume I want to know how many seconds my family wastes each morning. It turns out that the adults spend 20 minutes over coffee each morning and 10 minutes reading the newspaper. The children waste 30 minutes dawdling and 10 minutes arguing.

Here’s my algorithm:

(((minDrinkingCoffee  + minReadingNewspaper )* numAdults ) + 
((minDawdling + minArguing) * numChildren)) * secondsPerMinute.

Although this works, it is hard to read and hard to get right. It’s much easier to use interim variables:

wastedByEachAdult = minDrinkingCoffee  +  minReadingNewspaper;
wastedByAllAdults =  wastedByEachAdult * numAdults;
wastedByEachKid =  minDawdling  + minArguing;
wastedByAllKids =  wastedByEachKid * numChildren;
wastedByFamily = wastedByAllAdults + wastedByAllKids;
totalSeconds =  wastedByFamily * 60;

The latter example uses many more interim variables, but it is far easier to read, understand, and (most important) debug. As you step through this program in your debugger, you can see the interim values and make sure they are correct.

The Ternary Operator

Although most operators require one term (e.g., myValue++) or two terms (e.g., a+b), there is one operator that has three: the ternary operator (?:):

conditional-expression 
                  ? 
                  expression1 : expression2

This operator evaluates a conditional expression (an expression that returns a value of type bool), and then invokes either expression1 if the value returned from the conditional expression is true, or expression2 if the value returned is false. The logic is “if this is true, do the first; otherwise do the second.” Example 3-18 illustrates.

Example 3-18. The ternary operator

#region Using directives

using System;
using System.Collections.Generic;
using System.Text;

#endregion

namespace TernaryOperator
{
   class TernaryOperator
   {
      static void Main( string[] args )
      {
         int valueOne = 10;
         int valueTwo = 20;

         int maxValue = valueOne > valueTwo ? valueOne : valueTwo;

         Console.WriteLine( "ValueOne: {0}, valueTwo: {1}, maxValue: {2}",
             valueOne, valueTwo, maxValue );

      }
   }
}

Output:
ValueOne: 10, valueTwo: 20, maxValue: 20

In Example 3-18, the ternary operator is being used to test whether valueOne is greater than valueTwo. If so, the value of valueOne is assigned to the integer variable maxValue; otherwise the value of valueTwo is assigned to maxValue.

Preprocessor Directives

In the examples you’ve seen so far, you’ve compiled your entire program whenever you compiled any of it. At times, however, you might want to compile only parts of your program—for example, depending on whether you are debugging or building your production code.

Before your code is compiled, another program called the preprocessor runs and prepares your program for the compiler. The preprocessor examines your code for special preprocessor directives, all of which begin with the pound sign (#). These directives allow you to define identifiers and then test for their existence.

Defining Identifiers

#define DEBUG defines a preprocessor identifier, DEBUG. Although other preprocessor directives can come anywhere in your code, identifiers must be defined before any other code, including using statements.

Tip

C and C++ programmer take note: the C# preprocessor implements only a subset of the C++ preprocessor and doesn’t support macros.

You can test whether DEBUG has been defined with the #if statement. Thus, you can write:

#define DEBUG

//... some normal code - not affected by preprocessor

#if DEBUG
   // code to include if debugging
#else
  // code to include if not debugging
#endif

//... some normal code - not affected by preprocessor

When the preprocessor runs, it sees the #define statement and records the identifier DEBUG. The preprocessor skips over your normal C# code and then finds the #if - #else - #endif block.

The #if statement tests for the identifier DEBUG, which does exist, and so the code between #if and #else is compiled into your program—but the code between #else and #endif is not compiled. That code doesn’t appear in your assembly at all; it is as if it were left out of your source code.

Had the #if statement failed—that is, if you had tested for an identifier that did not exist—the code between #if and #else would not be compiled, but the code between #else and #endif would be compiled.

Tip

Any code not surrounded by #if/#endif is not affected by the preprocessor and is compiled into your program.

Undefining Identifiers

Undefine an identifier with #undef. The preprocessor works its way through the code from top to bottom, so the identifier is defined from the #define statement until the #undef statement, or until the program ends. Thus, if you write:

#define DEBUG

#if DEBUG
   // this code will be compiled
#endif

#undef DEBUG

#if DEBUG
   // this code will not be compiled
#endif

the first #if will succeed (DEBUG is defined), but the second will fail (DEBUG has been undefined).

#if, #elif, #else, and #endif

There is no switch statement for the preprocessor, but the #elif and #else directives provide great flexibility. The #elif directive allows the else-if logic of “if DEBUG then action one, else if TEST then action two, else action three”:

#if DEBUG
   // compile this code if debug is defined
#elif TEST
   // compile this code if debug is not defined
   // but TEST is defined
#else
  // compile this code if neither DEBUG nor TEST
  // is defined
#endif

In this example, the preprocessor first tests to see if the identifier DEBUG is defined. If it is, the code between #if and #elif will be compiled, and the rest of the code until #endif will not be compiled.

If (and only if) DEBUG is not defined, the preprocessor next checks to see if TEST is defined. Note that the preprocessor will not check for TEST unless DEBUG is not defined. If TEST is defined, the code between the #elif and the #else directives will be compiled. If it turns out that neither DEBUG nor TEST is defined, the code between the #else and the #endif statements will be compiled.

#region

The #region preprocessor directive marks an area of text with a comment. The principal use of this preprocessor directive is to allow tools such as Visual Studio .NET to mark off areas of code and collapse them in the editor with only the region’s comment showing.

For example, when you create a Windows application (covered in Chapter 13), Visual Studio creates a region for code generated by the designer. When the region is expanded, it looks like Figure 3-3. (Note: I’ve added the rectangle and highlighting to make it easier to find the region.)

Expanding the Visual Studio code region

Figure 3-3. Expanding the Visual Studio code region

You can see the region marked by the #region and #endregion preprocessor directives. When the region is collapsed, however, all you see is the region comment (Windows Form Designer generated code), as shown in Figure 3-4.

Code region is collapsed

Figure 3-4. Code region is collapsed



[1] All the intrinsic types are value types except for Object (discussed in Chapter 5) and String (discussed in Chapter 10). All user-defined types are reference types except for structs (discussed in Chapter 7) and enumerated types (discussed in Chapter 3).

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

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