Chapter 2. C# Language Fundamentals

Topics in This Chapter

  • Overview of a C# ProgramIn addition to the basic elements that comprise a C# program, a developer needs to be aware of other .NET features such as commenting options and recommended naming conventions.

  • PrimitivesPrimitives are the basic data types defined by the FCL to represent numbers, characters, and dates.

  • OperatorsC# uses traditional operator syntax to perform arithmetic and conditional operations.

  • Program Flow StatementsProgram flow can be controlled using if and switch statements for selection; and while, do, for, and foreach clauses for iteration.

  • StringThe string class supports the expected string operations: concatenation, extracting substrings, searching for instances of a character pattern, and both case sensitive and insensitive comparisons.

  • EnumsAn enumeration is a convenient way to assign descriptions that can be used to reference an underlying set of values.

  • Using ArraysSingle- or multi-dimensional arrays of any type can be created in C#. After an array is created, the System.Array class can be used to sort and copy the array.

  • Reference and Value TypesAll types in .NET are either a value or reference type. It is important to understand the differences and how they can affect a program's performance.

In September 2000, an ECMA[1] (international standardization group for information and communication systems) task group was established to define a Microsoft proposed standard for the C# programming language. Its stated design goal was to produce “a simple, modern, general-purpose, object-oriented programming language.” The result, defined in a standard known as ECMA-334, is a satisfyingly clean language with a syntax that resembles Java, and clearly borrows from C++ and C. It's a language designed to promote software robustness with array bounds checking, strong type checking, and the prohibition of uninitialized variables.

This chapter introduces you to the fundamentals of the language: It illustrates the basic parts of a C# program; compares value and reference types; and describes the syntax for operators and statements used for looping and controlling program flow. As an experienced programmer, this should be familiar terrain through which you can move quickly. However, the section on value and reference types may demand a bit more attention. Understanding the differences in how .NET handles value and reference types can influence program design choices.

The Layout of a C# Program

Figure 2-1 illustrates some of the basic features of a C# program.

Basic elements of a C# program

Figure 2-1. Basic elements of a C# program

The code in Figure 2-1 consists of a class MyApp that contains the program logic and a class Apparel that contains the data. The program creates an instance of Apparel and assigns it to myApparel. This object is then used to print the values of the class members FabType and Price to the console. The important features to note include the following:

  1. The using statement specifies the namespace SystemRecall from Chapter 1, “Introduction to .NET and C#,” that the .NET class libraries are organized into namespaces and that the System namespace contains all of the simple data types. The using statement tells the compiler to search this namespace when resolving references, making it unnecessary to use fully qualified names. For example, you can refer to label rather than System.Web.UI.WebControls.Label.

  2. All programming logic and data must be contained within a type definitionAll program logic and data must be embedded in a class, structure, enum, interface, or delegate. Unlike Visual Basic, for instance, C# has no global variable that exists outside the scope of a type. Access to types and type members is strictly controlled by access modifiers. In this example, the access modifier public permits external classes—such as MyApp—to access the two members of the Apparel class.

  3. A Main() method is required for every executable C# applicationThis method serves as the entry point to the application; it must always have the static modifier and the M must be capitalized. Overloaded forms of Main()define a return type and accept a parameter list as input.

    Return an integer value:

    static int Main()
    {
       return 0; // must return an integer value
    }
    

    Receive a list of command-line arguments as a parameter and return an integer value:

    static int Main(string[] args)
    {
       // loop through arguments
       foreach(string myArg in args)
          Console.WriteLine(myArg);
       return 0;
    }
    

    The parameter is a string array containing the contents of the command line used to invoke the program. For example, this command line executes the program MyApparel and passes it two parameter values:

    C:> MyApparel 5 6
    

Core Note

Core Note

The contents of the command line are passed as an argument to the Main() method. The System.Environment.CommandLine property also exposes the command line's contents.

General C# Programming Notes

Case Sensitivity

All variable and keywords are distinguished by case sensitivity. Replace class with Class in Figure 2-1 and the code will not compile.

Naming Conventions

The ECMA standard provides naming convention guidelines to be followed in your C# code. In addition to promoting consistency, following a strict naming policy can minimize errors related to case sensitivity that often result from undisciplined naming schemes. Table 2-1 summarizes some of the more important recommendations.

Table 2-1. C# Naming Conventions

Type

Case

Notes and Examples

Class

Pascal

  • Use noun or noun phrases.

  • Try to avoid starting with I because this is reserved for interfaces.

  • Do not use underscores.

Constant

Pascal

public const double GramToPound = 454.0 ;

Enum Type

Pascal

  • Use Pascal case for the enum value names.

  • Use singular name for enums.

public enum WarmColor { Orange, Yellow, Brown}

Event

Pascal

  • The method that handles events should have the suffix EventHandler.

  • Event argument classes should have the suffix EventArgs.

Exception

Pascal

  • Has suffix Exception.

Interface

Pascal

  • Has prefix of I.

IDisposable

Local Variable

Camel

  • Variables with public access modifier use Pascal

int myIndex.

Method

Pascal

  • Use verb or verb phrases for name.

Namespace

Pascal

  • Do not have a namespace and class with the same name.

  • Use prefixes to avoid namespaces having the same name. For example, use a company name to categorize namespaces developed by that company.

Acme.GraphicsLib

Property

Pascal

  • Use noun or noun phrase.

Parameter

Camel

  • Use meaningful names that describe the parameter's purpose.

Note that the case of a name may be based on two capitalization schemes:

  1. PascalThe first character of each word is capitalized (for example, MyClassAdder).

  2. CamelThe first character of each word, except the first, is capitalized (for example, myClassAdder).

The rule of thumb is to use Pascal capitalization everywhere except with parameters and local variables.

Commenting a C# Program

The C# compiler supports three types of embedded comments: an XML version and the two single-line (//) and multi-line (/* */) comments familiar to most programmers:

//   for a single line 
/*   for one or more lines
                      */
/// <remarks> XML comment describing a class </remarks>

An XML comment begins with three slashes (///) and usually contains XML tags that document a particular aspect of the code such as a structure, a class, or class member. The C# parser can expand the XML tags to provide additional information and export them to an external file for further processing.

The <remarks> tag—shown in Figure 2-1—is used to describe a type (class). The C# compiler recognizes eight other primary tags that are associated with a particular program element (see Table 2-2). These tags are placed directly above the lines of code they refer to.

Table 2-2. XML Documentation Tags

Tag

Description

<example>

Text illustrating an example of using a particular program feature goes between the beginning and ending tags.

<exception cref="Excep">

cref attribute contains name of exception.

///<exceptioncref="NoParmException">
</exception>

<include file="myXML">

file attribute is set to name of another XML file that is to be included in the XML documentation produced by this source code.

<param name="parm1">

name attribute contains the name of the parameter.

<permission cref= "">

Most of the time this is set to the following:

///<permissioncref="System.Security.Permis-
sionSet"> </permission>

<remarks>

Provides additional information about a type not found in the <summary> section.

<returns>

Place a textual description of what is returned from a method or property between the beginning and ending tags.

<seealso cref="price">

The cref attribute is set to the name of an associated type, field, method, or other type member.

<summary>

Contains a class description; is used by IntelliSense in VisualStudio.NET.

The value of the XML comments lies in the fact that they can be exported to a separate XML file and then processed using standard XML parsing techniques. You must instruct the compiler to generate this file because it is not done by default.

The following line compiles the source code consoleapp.cs and creates an XML file consoleXML:

C:> csc consoleapp.cs /doc:consoleXML.xml

If you compile the code in Figure 2-1, you'll find that the compiler generates warnings for all public members in your code:

Warning CS1591: Missing XML comment for publicly visible type ...

To suppress this, add the /nowarn:1591 option to the compile-line command. The option accepts multiple warning codes separated with a comma.

Core Note

Core Note

Many documentation tools are available to transform and extend the C# XML documentation output. One of the most advanced is NDoc (ndoc.sourceforge.net), an open source tool that not only formats the XML but uses reflection to glean further information about an assembly.

Primitives

The next three sections of this chapter describe features that you'll find in most programming languages: variables and data types, operators, expressions, and statements that control the flow of operations. The discussion begins with primitives. As the name implies, these are the core C# data types used as building blocks for more complex class and structure types. Variables of this type contain a single value and always have the same predefined size. Table 2-3 provides a formal list of primitives, their corresponding core data types, and their sizes.

Table 2-3. C# Primitive Data Types

C# Primitive Type

FCL Data Type

Description

object

System.Object

Ultimate base type of all other types.

string

System.String

A sequence of Unicode characters.

decimal

System.Decimal

Precise decimal with 28 significant digits.

bool

System.Boolean

A value represented as true or false.

char

System.Char

A 16-bit Unicode character.

byte

System.Byte

8-bit unsigned integral type.

sbyte

System.SByte

8-bit signed integral type.

short

System.Int16

16-bit signed integral type.

int

System.Int32

32-bit signed integral type.

long

System.Int64

64-bit signed integral type.

ushort

System.UInt16

16-bit unsigned integral type.

uint

System.UInt32

32-bit unsigned integral type.

ulong

System.UIint64

64-bit unsigned integral type.

single (float)

System.Single

Single-precision floating-point type.

double

System.Double

Double-precision floating-point type.

As the table shows, primitives map directly to types in the base class library and can be used interchangeably. Consider these statements:

System.Int32 age = new System.Int32(17);
int age = 17; 
System.Int32 age = 17;

They all generate exactly the same Intermediate Language (IL) code. The shorter version relies on C# providing the keyword int as an alias for the System.Int32 type. C# performs aliasing for all primitives.

Here are a few points to keep in mind when working with primitives:

  • The keywords that identify the value type primitives (such as int) are actually aliases for an underlying structure (struct type in C#). Special members of these structures can be used to manipulate the primitives. For example, the Int32 structure has a field that returns the largest 32-bit integer and a method that converts a numeric string to an integer value:

    int iMax = int.MaxValue;     // Return largest integer 
    int pVal = int.Parse("100"); // converts string to int
    

    The C# compiler supports implicit conversions if the conversion is a “safe” conversion that results in no loss of data. This occurs when the target of the conversion has a greater precision than the object being converted, and is called a widening conversion. In the case of a narrowing conversion, where the target has less precision, the conversion must have explicit casting. Casting is used to coerce, or convert, a value of one type into that of another. This is done syntactically by placing the target data type in parentheses in front of the value being converted: int i = (int)y;.

    short i16 = 50;    // 16-bit integer
    int i32 = i16;     // Okay: int has greater precision
    i16 = i32;         // Fails: short is 16 bit, int is 32
    i16 = (short) i32; // Okay since casting used
    
  • Literal values assigned to the types float, double, and decimal require that their value include a trailing letter: float requires F or f; double has an optional D or d; and decimal requires M or m.

    decimal pct = .15M; // M is required for literal value
    

The remainder of this section offers an overview of the most useful primitives with the exception of string, which is discussed later in the chapter.

decimal

The decimal type is a 128-bit high-precision floating-point number. It provides 28 decimal digits of precision and is used in financial calculations where rounding cannot be tolerated. This example illustrates three of the many methods available to decimal type. Also observe that when assigning a literal value to a decimal type, the M suffix must be used.

decimal iRate = 3.9834M;         // decimal requires M 
iRate = decimal.Round(iRate,2);  // Returns 3.98
decimal dividend = 512.0M;
decimal divisor = 51.0M;
decimal p = decimal.Parse("100.05");
// Next statement returns remainder = 2
decimal rem = decimal.Remainder(dividend,divisor);

bool

The only possible values of a bool type are true and false. It is not possible to cast a bool value to an integer—for example, convert true to a 1, or to cast a 1 or 0 to a bool.

bool bt = true;
string bStr = bt.ToString(); // returns "true"
bt = (bool) 1;               // fails

char

The char type represents a 16-bit Unicode character and is implemented as an unsigned integer. A char type accepts a variety of assignments: a character value placed between individual quote marks (' '); a casted numeric value; or an escape sequence. As the example illustrates, char also has a number of useful methods provided by the System.Char structure:

myChar =  'B';       // 'B' has an ASCII value of 66
myChar = (char) 66;  // Equivalent to 'B'
myChar = 'u0042';   // Unicode escape sequence
myChar = 'x0042';   // Hex escape sequence
myChar = '	';       // Simple esc sequence:horizontal tab
bool bt;
string pattern = "123abcd?";
myChar = pattern[0];                  // '1'
bt = char.IsLetter(pattern,3);        // true  ('a')
bt = char.IsNumber(pattern,3);        // false
bt = char.IsLower(pattern,0);         // false ('1')
bt = char.IsPunctuation(pattern,7);   // true  ('?')
bt = char.IsLetterOrDigit(pattern,1); // true
bt = char.IsNumber(pattern,2);        // true  ('3')
string kstr="K";
char k = char.Parse(kstr);

byte, sbyte

A byte is an 8-bit unsigned integer with a value from 0 to 255. An sbyte is an 8-bit signed integer with a value from –128 to 127.

byte[] b = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF};
string s = b[4].ToString(); // returns 170
char myChar = (char) b[3];

short, int, long

These represent 16-, 32-, and 64-bit signed integer values, respectively. The unsigned versions are also available (ushort, uint, ulong).

short i16 = 200;
i16 = 0xC8 ;     // hex value for 200
int i32 = i16;   // no casting required 

single, double

These are represented in 32-bit single-precision and 64-bit double-precision formats. In .NET 1.x, single is referred to as float.

  • The single type has a value range of 1.5 × 10 –45 to 3.4 × 1038 with 7-decimal digit precision.

  • The double type has a value range of 5 × 10–324 to 1.7 × 10308 with 15- to 16-decimal digit precision.

  • Floating-point operations return NaN (Not a Number) to signal that the result of the operation is undefined. For example, dividing 0.0 by 0.0 results in NaN.

  • Use the System.Convert method when converting floating-point numbers to another type.

float xFloat = 24567.66F;
int xInt = Convert.ToInt32(xFloat);  // returns 24567
int xInt2 = (int) xFloat;
if(xInt == xInt2) {  }               // False
string xStr = Convert.ToString(xFloat);
single zero = 0;
if (Single.IsNaN(0 / zero)) {  }     // True
double xDouble = 124.56D;

Note that the F suffix is used when assigning a literal value to a single type, and D is optional for a double type.

Using Parse and TryParse to Convert a Numeric String

The primitive numeric types include Parse and TryParse methods that are used to convert a string of numbers to the specified numeric type. This code illustrates:

short shParse  = Int16.Parse("100");
int iParse     = Int32.Parse("100");
long lparse    = Int64.Parse("100");
decimal dParse = decimal.Parse("99.99");
float sParse   = float.Parse("99.99");
double dbParse = double.Parse("99.99");

TryParse, introduced in .NET 2.0, provides conditional parsing. It returns a boolean value indicating whether the parse is successful, which provides a way to avoid formal exception handling code. The following example uses an Int32 type to demonstrate the two forms of TryParse:

int result;
// parse string and place result in result parameter
bool ok = Int32.TryParse("100", out result); 
bool ok = Int32.TryParse("100", NumberStyles.Integer, null,
                         out result);

In the second form of this method, the first parameter is the text string being parsed, and the second parameter is a NumberStyles enumeration that describes what the input string may contain. The value is returned in the fourth parameter.

Operators: Arithmetic, Logical, and Conditional

The C# operators used for arithmetic operations, bit manipulation, and conditional program flow should be familiar to all programmers. This section presents an overview of these operators that is meant to serve as a syntactical reference.

Arithmetic Operators

Table 2-4 summarizes the basic numerical operators. The precedence in which these operators are applied during the evaluation of an expression is shown in parentheses, with 1 being the highest precedence.

Table 2-4. Numerical Operators

Operator

Description

Example

+
-

(3)

Addition

Subtraction

int x = y + 10;

*
/
%

(2)

Multiplication

Division,

Modulo

int x = 60;
int y = 15;
int z = x * y / 2;  // 450
y = x % 29 ;  // remainder is 2
++
--

(1)

Prefix/postfix

Increment/decrement

x = 5;
Console.WriteLine(x++) // x = 5 
Console.WriteLine(++x) // x = 6

~

(1)

Bitwise complement

int x = ~127; // returns -128

>>
<<

(4)

Shift right

Shift left

byte x = 10; // binary 10 is 01010
int result = x << 1; // 20 = 10100
result = x >> 2;     //  5 = 00101

Works with byte, char, short, int, and long

&
|
^

(5-6-7)

Bitwise AND

Bitwise OR

Bitwise XOR

byte x = 12;        //    001100
byte y = 11;        //    001011
int result = x & y; //8 = 001000
result = x ^ y;     //7 = 000111

Core Note

Core Note

C# does not provide an exponentiation operator. Instead, use the Math.Pow() method to raise a number to a power, and Math.Exp() to raise e to a power.

Conditional and Relational Operators

Relational operators are used to compare two values and determine their relationship. They are generally used in conjunction with conditional operators to form more complex decision constructs. Table 2-5 provides a summary of C# relational and conditional operators.

Table 2-5. Relational and Conditional Boolean Operators

Statement

Description

Example

==
!=

Equality

Inequality

if (x == y) {...}

<
<=
>
>=

Numeric less than

Less than or equal to

Greater than

Greater than or equal to

if (x <= y) {...}

&&
||

Logical AND

Logical OR

if (x == y && y < 30) {...}

If first expression is false, second is not evaluated

&
|

Logical AND

Logical OR

if (x== y | y < 30) {...}

Always evaluates second expression

!

Logical negation

if !(x ==y && y < 30) {...}

Note the two forms of the logical AND/OR operations. The && and || operators do not evaluate the second expression if the first is false—a technique known as short circuit evaluation. The & and | operators always evaluate both expressions. They are used primarily when the expression values are returned from a method and you want to ensure that the methods are called.

In addition to the operators in Table 2-5, C# supports a ?: operator for conditionally assigning a value to a variable. As this example shows, it is basically shorthand for using an if-else statement:

string pass;
int grade=74;
If(grade >= 70) pass="pass"; else pass="fail";
//     expression   ? op1  : op2
pass = (grade >= 70)  ? "pass" : "fail";

If the expression is true, the ?: operator returns the first value; if it's false, the second is returned.

Control Flow Statements

The C# language provides if and switch conditional constructs that should be quite familiar to C++ and Java programmers. Table 2-6 provides a summary of these statements.

Table 2-6. Control Flow Statements

Conditional Statement

Example

if (boolean expression) {
   // statements
} else {
   // statements
}
if (bmi < 24.9) {
   weight = "normal";
   riskFactor = 2;
} else {
   weight = "over";
   riskFactor=6;
}
switch (expression) 
{
   case constant expression:
      // statements;
      // break/goto/return()
   case constant expression:
      // statements;
      // break/goto/return()
   default:
      // statements;
      // break/goto/return()
}
  • Constant expression may be an integer, enum value, or string.

  • No “fall through” is permitted. Each case block must end with a statement that transfers control.

switch (ndx)
{
   case 1:
      fabric = "cotton";
      blend = "100%";
      break;
   case 2:  // combine 2 & 3 
   case 3:
      fabric = "cotton";
      blend = "60%";
      break;
   default:  // optional
      fabric = "cotton";
      blend = "50%";
      break;
}

if-else

Syntax:

if ( boolean expression ) statement
if ( boolean expression ) statement1 else statement2

C# if statements behave as they do in other languages. The only issue you may encounter is how to format the statements when nesting multiple if-else clauses.

// Nested if statements
if (age > 16)                         if (age > 16)
{                                        if (sex == "M")
   if (sex == "M")                          type = "Man";
   {                                     else
      type = "Man";                         type = "Woman" ;
   } else {                           else
      type = "Woman" ;                   type = "child";
   }
} else {  
   type = "child";
}

Both code segments are equivalent. The right-hand form takes advantage of the fact that curly braces are not required to surround single statements; and the subordinate if clause is regarded as a single statement, despite the fact that it takes several lines. The actual coding style selected is not as important as agreeing on a single style to be used.

switch

Syntax:

switch( expression ) {switch block}

The expression is one of the int types, a character, or a string. The switch block consists of case labels—and an optional default label—associated with a constant expression that must implicitly convert to the same type as the expression. Here is an example using a string expression:

// switch with string expression
using System;
public class MyApp 
{
   static void Main(String[] args)
   {
      switch (args[0]) 
      {
         case "COTTON":   // is case sensitive
         case "cotton":
              Console.WriteLine("A good natural fiber.");
              goto case "natural";
         case "polyester":
              Console.WriteLine("A no-iron synthetic fiber.");
              break;
         case "natural":
              Console.WriteLine("A Natural Fiber. ");
              break;
         default:
              Console.WriteLine("Fiber is unknown.");
              break;
      }
   }
}

The most important things to observe in this example are as follows:

  • C# does not permit execution to fall through one case block to the next. Each case block must end with a statement that transfers control. This will be a break, goto. or return statement.

  • Multiple case labels may be associated with a single block of code.

  • The switch statement is case sensitive; in the example, "Cotton" and "COTTON" represent two different values.

Loops

C# provides four iteration statements: while, do, for, and foreach. The first three are the same constructs you find in C, C++, and Java; the foreach statement is designed to loop through collections of data such as arrays.

while loop

Syntax:

while ( boolean expression ) { body }

The statement(s) in the loop body are executed until the boolean expression is false. The loop does not execute if the expression is initially false.

Example:

byte[] r = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF};
int ndx=0;
int totVal = 0;
while (ndx <=6) 
{
   totVal +=    r[ndx];   
   ndx += 1;
}

do loop

Syntax:

do { do-body } while ( boolean expression );

This is similar to the while statement except that the evaluation is performed at the end of the iteration. Consequently, this loop executes at least once.

Example:

byte[] r = {0x00, 0x12, 0x34, 0x56, 0xAA, 0x55, 0xFF};
int ndx=0;
int totVal = 0;
do 
{
   totVal += r[ndx];
   ndx += 1;
}
while (ndx <= 6);      

for loop

Syntax:

for ( [initialization]; [termination condition]; [iteration] )
     { for-body }

The for construct contains initialization, a termination condition, and the iteration statement to be used in the loop. All are optional. The initialization is executed once, and then the condition is checked; as long as it is true, the iteration update occurs after the body is executed. The iteration statement is usually a simple increment to the control variable, but may be any operation.

Example:

int[] r = {80, 88, 90, 72, 68, 94, 83};
int totVal = 0;
for (int ndx = 0; ndx <= 6; ndx++) {
   totVal += r[ndx];
}

If any of the clauses in the for statement are left out, they must be accounted for elsewhere in the code. This example illustrates how omission of the for-iteration clause is handled:

for   (ndx = 0; ndx < 6; )
{
   totVal += r[ndx];
   ndx++;          // increment here
}

You can also leave out all of the for clauses:

for (;;) { body   }   // equivalent to while(true) { body }

A return, goto, or break statement is required to exit this loop.

foreach loop

Syntax:

foreach ( type identifier in collection )  { body }

The type and identifier declare the iteration variable. This construct loops once for each element in the collection and sets the iteration variable to the value of the current collection element. The iteration variable is read-only, and a compile error occurs if the program attempts to set its value.

For demonstration purposes, we will use an array as the collection. Keep in mind, however, that it is not restricted to an array. There is a useful set of collection classes defined in .NET that work equally well with foreach. We look at those in Chapter 4, “Working with Objects in C#.”

Example:

int totVal = 0;
foreach (int arrayVal in r)
{
   totVal += arrayVal;
}

In a one-dimensional array, iteration begins with index 0 and moves in ascending order. In a multi-dimensional array, iteration occurs through the rightmost index first. For example, in a two-dimensional array, iteration begins in the first column and moves across the row. When it reaches the end, it moves to the next row of the first column and iterates that row.

Transferring Control Within a Loop

It is often necessary to terminate a loop, or redirect the flow of statements within the loop body, based on conditions that arise during an iteration. For example, a while (true) loop obviously requires that the loop termination logic exists in the body. Table 2-7 summarizes the principal statements used to redirect the program flow.

Table 2-7. Statements to Exit a Loop or Redirect the Iteration

Statement

Description

Example

break

Redirects program control to the end point of a containing loop construct.

while (true) {
   ndx+=1;
   if (ndx >10) break;
}

continue

Starts a new iteration of enclosing loop without executing remaining statements in loop.

while (ndx <10) {
   ndx +=1;
   if(ndx %2 =1) continue;
   totVal += ndx;
}
goto
identifier;

goto case exp;

goto default;

Directs program control to a label, a case statement within a switch block, or the default statement within a switch block.

The goto may not transfer control into a nested scope—for example, a loop.

public int FindMatch(string myColor)
{
   string[] colorsAvail("blueaqua",
      "red", "green","navyblue");
   int loc;
   int matches=0;
   foreach (colorType in colorsAvail)
   {
      loc = colortype.IndexOf(myColor);
      if (loc >=0) goto Found;
      continue;
Found: 
      matches += 1;
   }   
      return(matches);
}
return
[expression] ;

Returns program control to the method that called the current method. Returns no argument if the enclosing method has a void return type.

public double Area(double w, double l) 
{ 
   return w * l;
}

There are few occasions where the use of a goto statement improves program logic. The goto default version may be useful in eliminating redundant code inside a switch block, but aside from that, avoid its use.

C# Preprocessing Directives

Preprocessing directives are statements read by the C# compiler during its lexical analysis phase. They can instruct the compiler to include/exclude code or even abort compilation based on the value of preprocessing directives.

A preprocessor directive is identified by the # character that must be the first nonblank character in the line. Blank spaces are permitted before and after the # symbol. Table 2-8 lists the directives that C# recognizes.

Table 2-8. Preprocessing Directives

C# Preprocessing Symbol

Description

#define
#undef

Used to define and undefine a symbol. Defining a symbol makes it evaluate to true when used in a #if directive.

#if
#elif
#else
#endif

Analogues to the C# if, else if, and else statements.

#line

Changes the line number sequence and can identify which file is the source for the line.

#region
#endregion

Used to specify a block of code that you can expand or collapse when using the outlining feature of Visual Studio.NET.

#error
#warning

#error causes the compiler to report a fatal error.

#warning causes the compiler to report a warning and continue processing.

The three most common uses for preprocessing directives are to perform conditional compilation, add diagnostics to report errors and warnings, and define code regions.

Conditional Compilation

The #if related directives are used to selectively determine which code is included during compilation. Any code placed between the #if statement and #endif statement is included or excluded based on whether the #if condition is true or false. This is a powerful feature that is used most often for debug purposes. Here is an example that illustrates the concept:

#define DEBUG
using System;
public class MyApp 
{      
   public static void Main() 
   {
      #if (DEBUG)
         Console.WriteLine("Debug Mode");      
      #else
         Console.WriteLine("Release Mode");
      #endif
   }
}

Any #define directives must be placed at the beginning of the .cs file. A conditional compilation symbol has two states: defined or undefined. In this example, the DEBUG symbol is defined and the subsequent #if (DEBUG) statement evaluates to true. The explicit use of the #define directive permits you to control the debug state of each source file. Note that if you are using Visual Studio, you can specify a Debug build that results in the DEBUG symbol being automatically defined for each file in the project. No explicit #define directive is required.

You can also define a symbol on the C# compile command line using the /Define switch:

csc /Define:DEBUG myproject.cs

Compiling code with this statement is equivalent to including a #Define DEBUG statement in the source code.

Diagnostic Directives

Diagnostic directives issue warning and error messages that are treated just like any other compile-time errors and warnings. The #warning directive allows compilation to continue, whereas the #error terminates it.

#define CLIENT
#define DEBUG
using System;
public class MyApp 
{      
   public static void Main() 
   {
      #if DEBUG && INHOUSE
         #warning Debug is on.  
      #elif DEBUG && CLIENT
         #error Debug not allowed in Client Code.
      #endif
   // Rest of program follows here

In this example, compilation will terminate with an error message since DEBUG and CLIENT are defined.

Code Regions

The region directives are used to mark sections of code as regions. The region directive has no semantic meaning to the C# compiler, but is recognized by Visual Studio.NET, which uses it to hide or collapse code regions. Expect other third-party source management tools to take advantage of these directives.

#region
   // any C# statements
#endregion

Strings

The System.String, or string class, is a reference type that is represented internally by a sequence of 16-bit Unicode characters. Unlike other reference types, C# treats a string as a primitive type: It can be declared as a constant, and it can be assigned a literal string value.

String Literals

Literal values assigned to string variables take two forms: literals enclosed in quotation marks, and verbatim strings that begin with @" and end with a closing double quote ("). The difference between the two is how they handle escape characters. Regular literals respond to the meaning of escape characters, whereas verbatim strings treat them as regular text. Table 2-9 provides a summary of the escape characters that can be placed in strings.

Table 2-9. String Escape Characters

Escape Character

Description

'

Inserts a single quote into a string

"

Inserts a double quote

\

Inserts a backslash; useful for file paths

a

System alert



Backspace

f

Form feed

Inserts a new line

Carriage return

Horizontal tab

u

Unicode character

v

Vertical tab

Null character

A verbatim string serves the purpose its name implies: to include any character placed between the beginning and ending double quote. The following segment provides several examples of using literals:

string myQuote, path;
myQuote = @"The solution is in the problem.";
myQuote = "The solution
is in the problem.";  
myQuote = "The Unicode representation of f is u0066";
// The next two statements assign the same value to myQuote.
myQuote = @"""The solution is in the problem. """;
myQuote = ""The solution is in the problem. "";
// The next two statements assign the same value to path.
path    = @"c:my documents
otes.txt";
path    = "c:\my documents\notes.txt";
path    = "c:my documents
otes.txt";      // Fails

The regular literal string is normally your best choice because it supports the escape sequences. The verbatim is to be favored when the text contains backslashes. Its most common use is with file path values and Regular Expression matching patterns (discussed in Chapter 5, “C# Text Manipulation and File I/O”).

String Manipulation

The System.String class contains a variety of string manipulation members. These include ways to determine a string's length, extract a substring, compare strings, and convert a string to upper- or lowercase. The following examples illustrate some of the more common operations.

Indexing Individual Characters in a String

The foreach and while loops offer the easiest way to iterate through the characters in a string. In both cases, the operations are read-only.

// Example 1 - using foreach statement
string myQuote = "The solution is in the problem."; 
foreach (char cc in myQuote)
{
   Console.Write(cc.ToString());
}

// Example 2 - using while loop
int ndx = 0;
while (ndx < myQuote.Length)
{
   Console.Write(myQuote[ndx].ToString());
   ndx += 1;
}

Note that before an individual character can be displayed or assigned to a string, it must be converted to a string type.

String Concatenation

The + operator is used for concatenating two strings: s1 + s2 . Only one of these has to be a string type; the other can be any type, and its ToString method is called automatically to convert it.

string s1 = "My age = ";
int myAge = 28;
string cat = s1 + myAge;       // My age = 28
MyClass clStr = new MyClass;    
Cat = "Class Name = " + clStr; // Class Name = MyClass

The concatenation operation is simple to use, but it is important to understand what is going on behind the scenes: During concatenation, the strings being joined are copied and a new combined string is allocated space. Each concatenation results in the allocation of more memory equal to the length of the new string. This is an acceptable use of resources as long as the number of concatenations is minimal. However, if concatenation occurs inside a long loop, an application's performance can suffer.

Consider an example where an HTML document is constructed by inserting the <br> tag between names in a list.

// assume names is an array containing 1000 names
string nameList = "";
foreach (string custName in names)
{
   // This is inefficient and should be avoided.
   nameList = nameList + custName+"<br>";
}

Each loop results in the creation of a new string consisting of the previous string plus the new appended name and tag. A better approach is to use the StringBuilder class as a replacement for the concatenation operator. This class sets aside memory to operate on strings and thus avoids the copying and memory allocation drawbacks of the concatenation (+) operator. It includes methods to append, insert, delete, remove, and replace characters. StringBuilder is discussed in Chapter 5.

Extracting and Locating Substrings

The Substring method extracts selected portions of a string. Its two overloads are illustrated here:

string poem = "In Xanadu did Kubla Khan";
string poemSeg;
poemSeg = poem.Substring(10);     // did Kubla Khan
// second argument specifies length
poemSeg = poem.Substring(0,9);    // In Xanadu

The IndexOf method locates the next occurrence of a character pattern within a string. It searches for the occurrence from the beginning of the string or a specified location. Listing 2-1 illustrates this.

IndexOf() performs a case-sensitive search. To ensure both upper- and lowercase instances are counted, you could convert the original string to lowercase (ToLower()) before searching it. Note that there is also a LastIndexOf method that locates the last instance of a character pattern within a string.

Example 2-1. Locating Text Occurrences in a String

// Method to count the occurrences of text in a given string
public static int CharCount(String strSource,String strToFind)
{
   int iCount=0;  // string type has index of 0
   int iPos=strSource.IndexOf(strToFind);
   while(iPos!=-1)
   {
      iCount++;
      iPos=strSource.IndexOf(strToFind, iPos+1);
   }
   return iCount;
}
public class MyApp
{
   static void Main()     
   {
      string txt = "In Xanadu did Kubla Khan";
      int ct = CharCount(txt, "a"); // ct = 4
   }
}

Comparing Strings

This topic is more complex than one would expect. The first hint of this is when you look at the System.String members and discover that there are four comparison methods: Compare, CompareOrdinal, CompareTo, and Equals. The choice of a comparison method is based on factors such as whether the comparison should be case sensitive and whether it should take culture into account.

The .NET environment is designed to handle international character sets, currencies, and dates. To support this, the handling and representation of strings can be tailored to different countries and cultures. Consider, for example, how to compare the same date in U.S. and European format. The dates “12/19/04” and “19/12/04” are logically equal, but do not have the same code value. Only a comparison method that takes culture into consideration would consider them equal. Chapter 5 explains how the various comparison methods work and the factors to be considered in selecting one.

For the majority of applications, nothing more than the standard equality (==) operator is required. This code segment illustrates its use:

bool isMatch;
string title = "Ancient Mariner";
isMatch = (title           == "ANCIENT MARINER");    // false
isMatch = (title.ToUpper() == "ANCIENT MARINER");    // true
isMatch = (title           == "Ancient"+" Mariner"); // true
isMatch =  title.Equals("Ancient Mariner");          // true

Note that the == operator is just a syntactical shortcut for calling the Equals method; it is actually faster to call Equals()directly.

Enumerated Types

An enumerated type, or enum as it's called in C#, offers a convenient way to create a structured set of symbols to represent constant values.

Syntax:

[access modifiers]enum <identifier> [:enum-base]{enum body}

Example:

enum Fabric :short {
   Cotton = 1,  
   Silk   = 2,  
   Wool   = 4,  
   Rayon  = 8,  
   Other  = 128 
}

Note: If the enum symbols are not set to a value, they are set automatically to the sequence 0, 1, 2, 3, and so on.

The access modifiers define the scope of the enum. The default is internal, which permits it to be accessed by any class in its assembly. Use public to make it available to any class in any assembly.

The optional enum-base defines the underlying type of the constants that correspond to the symbolic names. This must be an integral value of the type byte, sbyte, short, ushort, int, uint, long, or ulong. The default is int.

Working with Enumerations

Enumerated types not only improve program readability, but also minimize code changes when the underlying value changes. In such cases, all references to the value remain valid. Another advantage is that enumerated types are strongly typed. This means, for example, that when an enum type is passed as a parameter, the receiving method must have a matching parameter of the same type; otherwise, a compiler error occurs.

The code segment in Listing 2-2 illustrates these ideas using the Fabric enum from the preceding example.

Example 2-2. Using an Enumerated Type

static double GetPrice(Fabric fab)
{
   switch(fab)
   {
      case Fabric.Cotton: return(3.55);
      case Fabric.Silk:   return(5.65);
      case Fabric.Wool:   return(4.05);
      case Fabric.Rayon:  return(3.20);
      case Fabric.Other:  return(2.50);
      default: return(0.0);
   }
}
static void Main()     
{
   Fabric fab = Fabric.Cotton;
   int fabNum   = (int) fab;             // 1
   string fabType =  fab.ToString();     // "Cotton"
   string fabVal  =  fab.ToString("D");  // "1"
   double cost = GetPrice(fab);          // 3.55
} 

Things to note:

  • Casting is required to set the value of an enum to an integer variable: fabNum = (int) fab;

  • The character value of the underlying constant value can be obtained using the ToString() method with the parameter "D". "D" is a format character that converts a value to its decimal form.

  • Passing an instance of the Fabric enum to GetPrice requires that the corresponding parameter in the GetPrice method is declared as the same type.

This example shows how easy it is to obtain the symbol name or constant value when the instance of an enum is known—that is, Cotton. But suppose there is a need to determine whether an enum contains a member with a specific symbol or constant value. You could use foreach to loop through the enum members, but there is a better solution. Enumerations implicitly inherit from System.Enum, and this class contains a set of methods that can be used to query an enumeration about its contents.

System.Enum Methods

Three of the more useful System.Enum methods are Enum.IsDefined, Enum.Parse, and Enum.GetName. The first two methods are often used together to determine if a value or symbol is a member of an enum, and then to create an instance of it. The easiest way to understand them is to see them in use. In this example, the enum Fabric is queried to determine if it contains a symbol matching a given string value. If so, an instance of the enum is created and the GetName method is used to print one of its values.

string fabStr = "Cotton";
// Determine if symbol Cotton exists in Fabric enum
if (Enum.IsDefined(typeof(Fabric),fabStr)) 
{
   // Create enum instance
   Fabric fab = (Fabric)Enum.Parse(
                 typeof(Fabric) , fabStr);
   // Output from the following statement is: "Silk"
   Console.WriteLine("Second value of Fabric Enum is: " +
                     Enum.GetName(typeof(Fabric), 2));      
}

The IsDefined method takes two parameters: an enumeration type that the typeof operator returns and a string representing the symbol to be tested for. Another form of this method tests for a specified constant value if a numeric value is passed as the second parameter.

The Parse method takes the same arguments and creates an instance of an enumerated type. The variable fab created here is equivalent to the one created in Listing 2-2. It is important to ensure that the enum member exists before using the Parse method. If it does not, an exception is thrown.

The GetName method returns a string value of the enum whose value is passed as the second argument to the method. In this example, "Silk" is returned because its constant value is 2.

Enums and Bit Flags

It was not by accident that the values of the Fabric enum were set to powers of 2. Enum members are often used in logical operations where such values have obvious advantages in mapping to unique bit values. You may have code to identify a combination of values:

Fabric cotWool = Fabric.Cotton | Fabric.Wool;
Console.WriteLine(cotWool.ToString());  // Output: 5

It would be more meaningful if the output identified the variable as a combination of wool and cotton. This can be accomplished by adding the [Flags] attribute to the enum declaration:

[Flags]
enum Fabric :short {

The ToString() method checks an enum declaration to see if this attribute is present. If so, it treats the enum as a set of bitmapped flag elements. In this example, it cannot find a symbolic value equal to 5, so it uses the bit pattern “101” and prints the symbols having the bit patterns “001” and “100”. The new output is a comma- delimited list: “Cotton, Wool”.

Arrays

C#, like most programming languages, provides the array data structure as a way to collect and manipulate values of the same type. Unlike other languages, C# provides three types of arrays. One is implemented as an ArrayList object; another as a generic List object; and a third is derived from the System.Array class. The latter, which is discussed here, has the traditional characteristics most programmers associate with an array. The ArrayList is a more flexible object that includes special methods to insert and delete elements as well as dynamically resize itself. The List is a type-safe version of the ArrayList that was introduced with .NET 2.0 and may eventually replace the ArrayList. The List and ArrayList are discussed in Chapter 4, along with other Collection classes.

Before looking at the details of creating and using an array, you should be familiar with its general features:

  • Each element in the array is of the type specified in the array declaration. Besides the usual primitive types, an array may contain structures, objects, enums, and even other arrays.

  • Common Language Specification (CLS) compliance requires that all arrays be zero-based to ensure language interoperability (for example, an array reference in C# can be passed to code written in VB.NET). Although it is possible to create a non-zero–based array in C#, it is discouraged because array operations are optimized for zero-based dimensions.

  • When an array is created, it is allocated space for a fixed number of elements. Its capacity cannot be dynamically increased. The ArrayList/List is usually a better choice when the number of elements the array must hold is unknown.

Declaring and Creating an Array

Syntax:

<type> identifier [ ] = new <type> [n]  [{ initializer list}]

Example:

int[] myRating;         // declares an array
myRating = new int[5];  // creates array and allocates memory
int[] myRating = new int[5] {3,4,7,2,8};
int[] myRating = {3,4,7,2,8};  // shorthand version

// Create array containing instances of an Apparel class.
Apparel myApparel = {new Apparel(), new Apparel(),
                     new Apparel());   

// Set to an enum
Fabric[] enumArray = new Fabric[2];
enumArray[0] = Fabric.Cotton;

// Create a 2-dimensional array with 3 rows and 2 columns
int[ , ] myRatings = {{3 , 7}, {4 , 9}, {2, 6}};

The size of the array is determined from the explicit dimensions or the number of elements in the optional initializer list. If a dimension and an initializer list are both included, the number of elements in the list must match the dimension(s). If no initialization values are specified, the array elements are initialized to 0 for numeric types or null for all others. The CLR enforces bounds checking—any attempt to reference an array index outside its dimensions results in an exception.

Using System.Array Methods and Properties

The System.Array class defines a number of properties and methods that are used to query an array and manipulate its contents. Array operations include sorting, copying, searching for specific values, and clearing the contents. Table 2-10 summarizes some of the more useful members of the System.Array class.

Table 2-10. Selected Members of System.Array

Member

Type

Description

Length

Instance property

Total number of elements in the array.

Rank

Instance property

Number of dimensions of the array.

CreateInstance

Static method

Creates an Array object.

To create a single-dimensional array:

int[] rank1 =
   (int[]) Array.CreateInstance(typeof(int),4);

GetUpperBound(n)

Instance method

The upper bound of a specified dimension n. Returned value is Length – 1.

d0 = myArray.GetUpperBound(0);
d1=  myArray.GetUpperBound(1);

Sort

Static method

Sorts the elements in an array or a section of an array.

Reverse

Static method

Reverses the elements in a one-dimensional array.

IndexOf,
LastIndexOf

Static method

Returns the index of the first or last occurrence of a value in a one-dimensional array.

Clone

Instance method

Copies the contents of an array into a new array. The new array is a shallow copy of the original—that is, reference pointers, not values, are copied.

Copy

Static method

Copies a selected portion of one array to another.

Clear

Static method

Sets a specified range of elements in an array to zero or null.

The members are classified as static or instance. A static member is not associated with any particular array. It operates as a built-in function that takes any array as a parameter. The instance members, on the other hand, are associated with a specific instance of an array. The example shown in Listing 2-3 demonstrates how to use many of these class members.

Example 2-3. Working with Arrays Using System.Array Members

class MyApp
{
   static void Main()     
   {   
      string[] artists = {"Rembrandt", "Velazquez", 
            "Botticelli", "Goya", "Manet","El Greco"};
      // ..Sort array in ascending order
      Array.Sort(artists);
      // ..Invert the array
      Array.Reverse(artists); 
      PrintArray(artists);  // call method to list array
      int ndx = Array.IndexOf(artists,"Goya");   // ndx = 3
      // ..Clone the array
      string[] artClone = (string[]) artists.Clone();
      // Do arrays point to same address?  
      bool eq = Object.ReferenceEquals(
            artClone[0],artists[0]);  // true  
      Array.Clear(artClone,0,artClone.Length);
      // ..Copy selected members of artists to artClone
      Array.Copy(artists,1,artClone,0,4);
      eq = Object.ReferenceEquals(
            artClone[0],artists[1]);  // true
   }
   // List contents of Array
   public static void PrintArray(string[] strArray)
   {
      for ( int i = 0; i<= strArray.GetUpperBound(0); i++ )  
      {
         Console.WriteLine(strArray[i]);
      }
   }
}

Things to note:

  • The Sort method has many overloaded forms. The simplest takes a single-dimensional array as a parameter and sorts it in place. Other forms permit arrays to be sorted using an interface defined by the programmer. This topic is examined in Chapter 4.

  • The Clone method creates a copy of the artists array and assigns it to artClone. The cast (string[]) is required, because Clone returns an Object type. The Object.ReferenceEquals method is used to determine if the cloned array points to the same address as the original. Because string is a reference type, the clone merely copies pointers to the original array contents, rather than copying the contents. If the arrays had been value types, the actual contents would have been copied, and ReferenceEquals would have returned false.

  • The Copy method copies a range of elements from one array to another and performs any casting as required. In this example, it takes the following parameters

    (source, source index, target, target index, # to copy)
    

Reference and Value Types

The Common Language Runtime (CLR) supports two kinds of types: reference types and value types (see Figure 2-2 on the following page). Reference types include classes, arrays, interfaces, and delegates. Value types include the primitive data types such as int, char, and byte as well as struct and enum types. Value and reference types are distinguished by their location in the .NET class hierarchy and the way in which .NET allocates memory for each. We'll look at both, beginning with the class inheritance hierarchy.

Hierarchy of common reference and value types

Figure 2-2. Hierarchy of common reference and value types

System.Object and System.ValueType

Both reference and value types inherit from the System.Object class. The difference is that almost all reference types inherit directly from it, whereas value types inherit further down the hierarchy—directly from the System.ValueType class.

As the base for all types, System.Object provides a set of methods that you can expect to find on all types. This set includes the ToString method used throughout this chapter, as well as methods to clone a type, create a unique hash code for a type, and compare type instances for equality. Chapter 4 discusses these methods in detail and describes how to implement them on custom classes.

System.ValueType inherits from System.Object. It does not add any members, but does override some of the inherited methods to make them more suitable for value types. For example, Equals() is overridden to return true if the value of two objects' fields match. By definition, all value types implicitly inherit from the ValueType class.

Memory Allocation for Reference and Value Types

The primary difference between value and reference types is the way the CLR handles their memory requirements. Value types are allocated on a runtime stack, and reference types are placed on a managed heap that is referenced from the stack.

Figure 2-3 illustrates how the value and reference types from our example (refer to Figure 2-1) are represented in memory. Let's step through what happens when an instance of a reference type is created and is then assigned to a second variable:

Apparel myApparel  = new Apparel();
Apparel myApparel2 = myApparel;
Memory layout for value and reference types

Figure 2-3. Memory layout for value and reference types

  1. The CLR allocates memory for the object on the top of the managed heap.

  2. Overhead information for the object is added to the heap. This information consists of a pointer to the object's method table and a SyncBlockIndex that is used to synchronize access to the object among multiple threads.

  3. The myApparel object is created as an instance of the Apparel class, and its Price and FabType fields are placed on the heap.

  4. The reference to myApparel is placed on the stack.

  5. When a new reference variable myApparel2 is created, it is placed on the stack and given a pointer to the existing object. Both reference variables—myApparel and myApparel2—now point to the same object.

Creating a reference object can be expensive in time and resources because of the multiple steps and required overhead. However, setting additional references to an existing object is quite efficient, because there is no need to make a physical copy of the object. The reverse is true for value types.

Boxing

.NET contains a special object type that accepts values of any data type. It provides a generic way to pass parameters and assign values when the type of the value being passed or assigned is not tied to a specific data type. Anything assigned to object must be treated as a reference type and stored on the heap. Consider the following statements:

int age = 17;
object refAge = age;

The first statement creates the variable age and places its value on the stack; the second assigns the value of age to a reference type. It places the value 17 on the heap, adds the overhead pointers described earlier, and adds a stack reference to it. This process of wrapping a value type so that it is treated as a reference type is known as boxing. Conversely, converting a reference type to a value type is known as unboxing and is performed by casting an object to its original type. Here, we unbox the object created in the preceding example:

int newAge = (int) refAge;
string newAge = (string) refAge;   // Fails. InvalidCastException

Note that the value being unboxed must be of the same type as the variable to which it is being cast.

In general, boxing can be ignored because the CLR handles the details transparently. However, it should be considered when designing code that stores large amounts of numeric data in memory. To illustrate, consider the System.Array and ArrayList classes mentioned earlier. Both are reference types, but they perform quite differently when used to store simple data values.

The ArrayList methods are designed to work on the generic object type. Consequently, the ArrayList stores all its items as reference types. If the data to be stored is a value type, it must be boxed before it can be stored. The array, on the other hand, can hold both value and reference types. It treats the reference types as the ArrayList does, but does not box value types.

The following code creates an array and an ArrayList of integer values. As shown in Figure 2-4, the values are stored quite differently in memory.

// Create array with four values
Int[] ages = {1,2,3,4};   

// Place four values in ArrayList
ArrayList ages = new ArrayList();
For (int i=0; i<4; i++) {
   ages.add(i); // expects object parameter
}
Memory layout comparison of Array and ArrayList

Figure 2-4. Memory layout comparison of Array and ArrayList

The array stores the values as unboxed int values; the ArrayList boxes each value. It then adds overhead required by reference types. If your application stores large amounts of data in memory and does not require the special features of the ArrayList, the array is a more efficient implementation. If using .NET 2.0 or later, the List class is the best choice because it eliminates boxing and includes the more flexible ArrayList features.

Summary of Value and Reference Type Differences

Memory Allocation

We have seen that memory allocation is the most significant difference between value and reference types. Reference types are allocated on the heap and value types on the thread or call stack. When a reference type is created, it is initialized to null, indicating it does not point to anything. A value type is initialized to zero (0).

Releasing Memory

Memory on the stack is freed when a variable goes out of scope. A garbage collection process that occurs when a system memory threshold is reached releases memory on the heap. Garbage collection is controlled by .NET and occurs automatically at unpredictable intervals. Chapter 4 discusses it in detail.

Variable Assignments

When a variable is set to a reference type, it receives a pointer to the original object—rather than the object value itself. When a variable is set to a value type, a field-by-field copy of the original variable is made and assigned to the new variable.

Summary

This chapter offers an overview of the C# language, providing the syntax and examples for using the list of features that form the core of this and just about any programming language. These features include basic data types, numerical and relational operators, loop constructs, strings, enums, and arrays. The final section stresses how all .NET types can be classified as a value or reference type. It explains the different memory allocation schemes used for the two. In addition, it looks at the concepts of boxing and unboxing: converting a value type to a reference type and converting a reference type back to a value type.

Test Your Understanding

1:

Which method is required in each C# program, and what parameters does it take?

2:

What are the three types of inline comments available in C#? Which can be exported?

3:

What is a primitive?

4:

What is printed by the following statements?

int grade=78;
bool pass = (grade >=70) ? true : false;
Console.WriteLine(pass);

5:

Which loop construct is executed at least once?

6:

How do a break and a continue statement differ?

7:

Given the following variables

char c= 'c';
double d= 120.0D;
int i=10;
string s="Rodin";  

which of the following fails to compile?

  1. c = c+ i;
  2. s += i;
  3. c += s;
  4. d += i;

8:

Describe a potential drawback to using the + operator for concatenation. What is the recommended alternative?

9:

Name the two base classes from which all value types inherit.

10:

What prime value is printed by the final statement in this code?

int[] primes = new int[6] {1,3,5,7,11,13};
int[] primesClone = new int[6];
Array.Copy(primes,1,primesClone,1,5);
Console.WriteLine("Prime: "+primesClone[3]);  


[1] ECMA International was formerly known as European Computer Manufacturers Association and is referred to herein simply as ECMA.

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

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