Topics in This Chapter
Overview of a C# Program: In 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.
Primitives: Primitives are the basic data types defined by the FCL to represent numbers, characters, and dates.
Operators: C# uses traditional operator syntax to perform arithmetic and conditional operations.
Program Flow Statements: Program flow can be controlled using if
and switch
statements for selection; and while
, do
, for
, and foreach
clauses for iteration.
String: The string
class supports the expected string operations: concatenation, extracting substrings, searching for instances of a character pattern, and both case sensitive and insensitive comparisons.
Enums: An enumeration is a convenient way to assign descriptions that can be used to reference an underlying set of values.
Using Arrays: Single- 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 Types: All 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.
Figure 2-1 illustrates some of the basic features 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:
The using
statement specifies the namespace System
. Recall 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
.
All programming logic and data must be contained within a type definition. All 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.
A Main()
method is required for every executable C# application. This 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
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.
All variable and keywords are distinguished by case sensitivity. Replace class
with Class
in Figure 2-1 and the code will not compile.
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 |
|
Constant | Pascal |
|
Enum Type | Pascal |
|
Event | Pascal |
|
Exception | Pascal |
|
Interface | Pascal |
|
Local Variable | Camel |
|
Pascal |
| |
Namespace | Pascal |
|
Property | Pascal |
|
Parameter | Camel |
|
Note that the case of a name may be based on two capitalization schemes:
Pascal. The first character of each word is capitalized (for example, MyClassAdder
).
Camel. The 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.
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 |
---|---|
| Text illustrating an example of using a particular program feature goes between the beginning and ending tags. |
|
///<exceptioncref="NoParmException"> </exception> |
|
|
|
|
| Most of the time this is set to the following: ///<permissioncref="System.Security.Permis- sionSet"> </permission> |
| Provides additional information about a type not found in the |
| Place a textual description of what is returned from a method or property between the beginning and ending tags. |
| The |
| 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.
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 |
---|---|---|
|
| Ultimate base type of all other types. |
|
| A sequence of Unicode characters. |
|
| Precise decimal with 28 significant digits. |
|
| A value represented as true or false. |
|
| A 16-bit Unicode character. |
|
| 8-bit unsigned integral type. |
|
| 8-bit signed integral type. |
|
| 16-bit signed integral type. |
|
| 32-bit signed integral type. |
| 64-bit signed integral type. | |
|
| 16-bit unsigned integral type. |
|
| 32-bit unsigned integral type. |
|
| 64-bit unsigned integral type. |
|
| Single-precision floating-point type. |
|
| 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.
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);
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
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);
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];
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
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.
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.
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.
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
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
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.
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
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.
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.
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.
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; }
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);
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.
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.
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 |
---|---|---|
| Redirects program control to the end point of a containing loop construct. |
while (true) { ndx+=1; if (ndx >10) break; } |
| 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 The |
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 |
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.
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 |
#if #elif #else #endif | Analogues to the C# |
| 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 |
|
The three most common uses for preprocessing directives are to perform conditional compilation, add diagnostics to report errors and warnings, and define code regions.
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 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.
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
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.
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.