Other Language Details in C#

This chapter covers some miscellaneous details about the language, including how to use the Main() function, how the preprocessor works, and how to write literal values.

1. The Main Function

The simplest version of the Main() function will already be familiar to you from other examples:

using System; class Test {

public static void Main()

{

Console.WriteLine(“Hello, Universe!”);

}

}

1.1. Returning an int Status

It’s often useful to return a status from the Main() function, particularly if the program is called programmatically; this is because you can use the return status to determine if the application executed. You do this by declaring the return type of Main() as an integer:

using System; class Test {

public static int Main()

{

Console.WriteLine(“Hello, Universe!”);

return(0);

}

}

1.2. Command-Line Parameters

You can access the command-line parameters to an application by declaring the Main() function with a string array as a parameter, as in the following code. You can then process the parameters by indexing the array.

using System; class Test {

public static void Main(string[] args)

{

foreach (string arg in args)

Console.WriteLine(“Arg: {0}”, arg);

}

}

1.3. Multiple Main() Functions

It’s often useful for testing purposes to include a static function in a class that tests the class to make sure it does the right thing. In C#, you can write this static test function as a Main() function, which makes automating such tests easy.

If the compiler encounters a single Main() function during a compilation, the C# compiler will use it. If more than one Main() function exists, you should specify the class that contains the desired Main() on the command line with the /main:<classname> option.

// error using System; class Complex {

static int Main()

{

// test code here

Console.WriteLine(“Console: Passed”);

return(0);

}

}

class Test {

public static void Main(string[] args)

{

foreach (string arg in args)

Console.WriteLine(arg);

}

}

Compiling this file with /main:Complex will use the test version of Main(), whereas compiling with /main:Test will use the real version of Main(). Compiling it without either will result in

an error.

The Main() declared in the Complex type isn’t declared public. In fact, it has no requirement that Main() should be public; keeping it private is useful in cases such as these where the test function shouldn’t be visible to users of the class.

2. Preprocessing

The most important thing to remember about the C# preprocessor is that it doesn’t exist. The features from the C/C++ processor are either totally absent or present only in a limited form. In the absent category are include files and the ability to do text replacement with #define. The #ifdef and associated directives are present and can control the compilation of code.

Getting rid of the macro version of #define allows you to understand more clearly what the program is saying. A name that isn’t familiar must come from one of the namespaces, and you don’t have to hunt through include files to find it.

One reason for this change is that getting rid of preprocessing and #include enables a simplified compilation structure, and therefore you get some impressive improvements in compilation speed. Additionally, you don’t need to write a separate header file and keep it in sync with the implementation file.

Getting rid of #define also ensures that C# code always means what it says and that no time is wasted hunting in include files for macro definitions.

When C# source files are compiled, the order of the compilation of the individual files is unimportant, and it’s equivalent to them all being in one big file. You don’t need forward declarations or have to worry about the order of #includes.

2.1. Preprocessing Directives

Table 28-1 lists the preprocessing directives that are supported.

Here’s an example of how you can use these directives:

#define DEBUGLOG

using System;

class Test

{

public static void Main()

{

#if DEBUGLOG

Console.WriteLine(“In Main – Debug Enabled”);

#else

Console.WriteLine(“In Main – No Debug”);

#endif

}

}

#define and #undef must precede any “real code” in a file; otherwise, an error occurs. For example, you can’t write the previous example as follows:

// error

using System;

class Test {

#define DEBUGLOG

public static void Main()

{

#if DEBUGLOG

Console.WriteLine(“In Main – Debug Enabled”);

#else

Console.WriteLine(“In Main – No Debug”);

#endif

}

}

C# also supports the Conditional attribute for controlling function calls based upon preprocessor identifiers; see Chapter 39 for more information.

2.2. Preprocessor Expressions

You can use the operators described in Table 28-2 in preprocessor expressions.

You can use parentheses to group expressions:

#if !(DEBUGLOG && (TESTLOG || USERLOG))

If TESTLOG or USERLOG is defined and DEBUGLOG is defined, then the expression within the parentheses is true, which is then negated by the !.

2.3. Other Preprocessor Functions

In addition to the #if and #define functions, you can use a few other preprocessor functions.

#warning and #error

#warning and #error allow warnings or errors to be reported during the compilation process. All text following the #warning or #error will be output when the compiler reaches that line.

For a section of code, you could do the following:

#warning Check algorithm with John

This results in the string “Check algorithm with John” being output when the line is compiled.

#line

With #line, you can specify the name of the source file and the line number that are reported when the compiler encounters errors. You’d typically use this with machine-generated source code so you could sync the reported lines with a different naming or numbering system. You can also use this directive to hide lines from the debugger, as discussed in the next section.

#region

You can use the #region and corresponding #endregion directives to define a block of code that can be collapsed and expanded inside the code editor. In .NET 1.0 and 1.1, one of the main uses of the #region directive was to separate machine-generated code from human-generated code, but partial classes (see Chapter 8) have largely solved this problem.

Using the #region directive doesn’t prevent the debugger from stepping into a code block. To accomplish this, you also need the #line directive:

class Program {

static void Main(string[] args)

{

#region Logging Code

#line hidden

//debugger will not hit this line

Console.WriteLine(“In Program.Main”);

#line default

#endregion

Console.WriteLine(“Main app stuff here”);

}

}

3. Inline Warning Control

The C# compiler provides the ability to suppress warnings using the /nowarn switch. This switch, which is covered in more detail in Chapter 41, is a setting that affects all the source code files processed as part of a compile, and a per-source code file setting is often more desirable. For various reasons, living with compiler warnings is often a fact of life in real-world applica­tions, but using the compiler switch means new warnings of the same warning number aren’t reported to the developer. Deciding to tolerate the warning output is often no better, as an increase in the number of warnings from, say, 116 to 117 is much less noticeable than the warning count jumping from 0 to 1.

C# 2.0 augments the global compiler switch with a new #pragma warning directive that allows you to control warning production at a granular level. At a basic level, you can use a #pragma warning disable XXX statement (where XXX is the number of the error being suppressed) to suppress all warnings of that number in the source file. Accompanying the warning suppression with a comment indicating why the error is unavoidable or irrelevant in this instance will help the maintenance programmer significantly and also jog your memory. For example:

class Program {

static void Main(string[] args)

{

//Use the original DoStuff method (now marked Obsolete) until the bug //in new version that is documented in vendor KB article 123 is fixed.

#pragma warning disable 612 DoStuff();

#pragma warning restore 612

}

[Obsolete]

static void DoStuff() {}

}

In this case, the #pragma warning restore statement has restored the warning, which means if it occurs again in the same source file and outside the scope of the #pragma region, it will generate a warning.

You can disable multiple warnings in the same line by placing commas between the warning numbers:

//disable Obsolete and GetHashCode warnings

#pragma warning disable 612, 661

The ability to control warnings at such a granular level, combined with the existing ability to globally disable particular warnings (missing XML comments in third-party libraries often leads to a global /nowarn on 1591), means that for production-quality code, having zero warnings is an attainable and desirable goal. Additionally, depending on the development methodology and the formality of the development process, beginning with or progressing to the practice of treating warnings as errors (using the /warnaserror compiler switch) is recommended.

4. Lexical Details

The lexical details of the language specify features that are important at the single-character level: how to write numerical constants, identifiers, and other low-level entities of the language.

4.1. Identifiers

An identifier is a name that’s used for some program element, such as a variable or a function.

Identifiers must have a letter or an underscore as the first character, and the remainder of the identifier can also include numeric characters. You can specify Unicode characters using \udddd, where dddd specifies the hex value of the Unicode character.

When using code that has been written in other languages, some names might be C# keywords. To write such a name, place an at (@) sign before the name, which merely indicates to C# that the name isn’t a keyword but an identifier.

Similarly, use @ to implement keywords as identifiers:

class Test {

public void @checked()

{

}

}

This class declares a member function named checked.

Using this feature so that identifiers can be the same as built-in identifiers isn’t recommended because of the confusion it can create.

4.2. Keywords

Keywords are reserved words that can’t be used as identifiers. The following are the keywords in C#:

Also, four keywords (partial, where, yield, and value) are contextual keywords. You can use a contextual keyword as a variable, class, or property name, but when you use it in a specific context, it becomes a keyword. You use partial in the specification of partial class, where to define generic constraints, yield to implement an iterator, and value in a set property block to access the incoming value.

4.3. Literals

Literals are the way in which values are written for variables.

Boolean

Two boolean literals exist: true and false.

Integer

You can write integer literals simply by writing the numeric value. Integer literals small enough to fit into the int data type[4] are treated as ints; if they’re too big to fit into an int, they will be created as the smallest type of uint, long, or ulong in which the literal will fit.

Here are some integer literal examples:

123

-15

You can also write integer literals in hexadecimal format by placing 0x in front of the constant:

0xFFFF

0x12AB

Real

Real literals are for the types float, double, and decimal. Float literals have f or F after them; double literals have d or D after them and are the default when nothing is specified, and decimal literals have m or M after them.

You can use exponential notation by appending e, followed by the exponent to the real literal.

Here are some examples:

String

You write string literals as a sequence of characters enclosed in double quotes, such as “Hello”. All the character escape sequences are supported within strings.

Strings can’t span multiple lines, but you can achieve the same effect by concatenating them:

string s = “What is your favorite color?” +

“Blue. No, Red. “;

When this code is compiled, a single string constant will be created that consists of the two strings concatenated.

Verbatim Strings

Verbatim strings allow you to specify some strings more simply.

If a string contains the backslash character, such as a filename, you can use a verbatim string to turn off the support for escape sequences. Instead of writing something like this:

string s = “c:\\Program Files\\Microsoft Office\\Office”;

you can write the following:

string s = @”c:\Program Files\Microsoft Office\Office”;

The verbatim string syntax is also useful if the code is generated by a program and you have no way to constrain the contents of the string. You can represent all characters within such a string, but you must double any occurrence of a double quote:

string s = @”She said, “”Hello”””;

In addition, strings that are written with the verbatim string syntax can span multiple lines, and you can preserve whitespace (spaces, tabs, and newlines):

using System; class Test {

public static void Main()

{

string s = @”

C: Hello, Miss?

O: What do you mean, ‘Miss’?

C: I’m Sorry, I have a cold. I wish to make a complaint.”;

Console.WriteLine(s);

}

}

5. Comments

Comments in C# are denoted by a double slash for a single-line comment and by /* and */ for the beginning and ending of a multiline comment:

// This is a single-line comment

/*

* Multiline comment here

*/

C# also supports a special type of comment that associates documentation with code; Chapter 38 describes those comments.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *