Other Language Details
This chapter deals with some miscellaneous details about the language, including how to use the Main() function, how the preprocessor works, and how to write literal values.
The simplest version of the Main() function should already be familiar from earlier in the book.
using System;
class Test
{
public static void Main()
{
Console.WriteLine("Hello, Universe!");
}
}
It may sometimes be useful to return a status from the Main() function, particularly if the program is called programmatically, because the return status can be used to determine whether the application executed successfully. This is done by declaring the return type of Main() as an integer.
using System;
class Test
{
public static int Main()
{
Console.WriteLine("Hello, Universe!");
return(0);
}
}
The command-line parameters to an application can be accessed by declaring the Main() function with a string array as a parameter. The parameters can then be processed by indexing the array.1
using System;
class Test
{
public static void Main(string[] args)
{
foreach (string arg in args)
{
Console.WriteLine("Arg: {0}", arg);
}
}
}
It is sometimes 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#, this static test function can be written as a Main() function, which makes automating such tests easy.
If there is a single main() function encountered during a compilation, the C# compiler will use it. If there is more than one main()function, the class that contains the desired Main() can be specified 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, there is no requirement that Main() should be public, and keeping it private is useful in cases such as these, where the test function shouldn’t be visible to users of the class.
The most important thing to remember about the C# preprocessor is that it doesn’t exist. The features in the C/C++ processor are either totally absent or present 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 in a slightly modified form and can be used to control the compilation of code.
Getting rid of the macro version (something like #define MYINT int) of #define allows the programmer to understand more clearly what the program is saying. A name that isn’t familiar must come from one of the namespaces, and there’s no need to hunt through include files to find it. The C# code means what it says it means.2
Getting rid of macro preprocessing and #include also enables a simplified compilation structure and faster compilation, and there is no need to create a separate header file and keep it in sync with the implementation file.
When C# source files are compiled, the order of the compilation of the individual files is unimportant, and it is equivalent to them all being in one big file. There is no need for forward declarations or worrying about the order of #includes.
The preprocessing directives that are supported include those shown in Table 31-1.
Table 31-1. C# Preprocessing Directives
Directive | Description |
---|---|
#define identifier | Defines an identifier. Note that a value can’t be set for it; it can merely be defined. Identifiers can also be defined via the command line. |
#undef identifier | Undefines an identifier. |
#if expression | Code in this section is compiled if the expression is true. |
#elif expression | Else-if construct. If the previous directive wasn’t taken and the expression is true, code in this section is compiled. |
#else | If the previous directive wasn’t taken, code in this section is compiled. |
#endif | Marks the end of a section. |
Here’s an example of how they might be used:
#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, or an error occurs. The previous example can’t be written 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.
The operators shown in Table 31-2 can be used in preprocessor expressions.
Table 31-2. C# Preprocessor Expressions
Operator | Description |
---|---|
! ex | Expression is true if ex is false |
ex == value | Expression is true if ex is equal to value |
ex != value | Expression is true if ex is not equal to value |
ex1 && ex2 | Expression is true if both ex1 and ex2 are true |
ex1 || ex2 | Expression is true if either ex1 or ex2 are true |
Parentheses can be used 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 !.
The C# compiler provides the ability to suppress compiler warnings using the /nowarn switch. Warnings that are suppressed on the command line are disabled for all files, but it may be desirable in some situations to disable a single instance of a warning. For example, if a program uses a method that has been marked as obsolete:
class Program
{
static void Main(string[] args)
{
ObsoleteMethod();
}
[Obsolete]
static void ObsoleteMethod()
{
}
}
the program will generate a warning message:
'Program.ObsoleteMethod()' is obsolete
In this case, it may be useful to disable that warning message, since the presence of an expected warning can hide other warnings.3 This can be done by adding a #pragma warning disable directive to the program.
class Program
{
static void Main(string[] args)
{
#pragma warning disable 612
ObsoleteMethod();
#pragma warning restore 612
}
[Obsolete]
static void ObsoleteMethod()
{
}
}
The disable directive turns off the warning, and then the restore directive turns it back on. It is possible to disable multiple warnings by separating the warning numbers by commas.
In addition to the #if and #define functions, there are a few other preprocessor functions that can be used.
#warning and #error allow warnings or errors to be reported during the compilation process. All text following #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 would result in the string “Check algorithm with John” being output when the line was compiled.
The #line directive provides two different capabilities.
First, it can be used to specify the line number and/or source filename to report when the compiler encounters warnings or errors. This can be useful in cases where user-written code is folded in with user-generated code; by specifying the line number and the filename of the user-written code, the user will see the error in their code, not the combined file. Either the line number or both the line number and the filename can be specified.
#line 255#line 300 "UserCode.cs"
The line number and filename can be reset to the default behavior using this:
#line 255 default
Second, #line can also be used to hide data from debugger stepping.
#line hidden
The debugger will then skip all subsequent lines until another #line directive is encountered.4 This is also very useful if the code is machine-generated.
The #region and #endregion directives are used to group code into named sections, which can then be collapsed or expanded inside an editor. For example, if a class implements the IXmlSerializable interface, it can be useful to group the implementation methods in a region.
class Employee: IXmlSerializable
{
public string Name { get; set; }
public Decimal Age { get; set; }
public Employee(String name, Decimal age)
{
Name = name;
Age = age;
}
#region IXmlSerializable
// IXmlSerializable methods go here. . .
#endregion
}
This region can then be collapsed so it is out of the way when looking at the other members of the type.
The lexical details of the language deal with things that are important at the single-character level: how to write numerical constants, identifiers, and other low-level entities of the language.
An identifier is a name that is used for some program element, such as a variable or a function.
When using code that has been written in other languages, some names might be C# keywords. To write such a name, an “at” character (@) can be placed before the name, which merely indicates to C# that the name is not a keyword but an identifier.
Identifiers must have a letter or an underscore as the first character, and the remainder of the identifier can also include numeric characters.5 Unicode characters can be specified using udddd, where dddd specifies the hex value of the Unicode character.
Similarly, use @ to use 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 is not recommended because of the confusion it can create.
Keywords
Keywords are reserved words that cannot be used as identifiers. The keywords in C# are shown in Table 31-3.
Table 31-3. C# Keywords
C# also defines contextual keywords. A contextual keyword can be used as a variable, class, or property name, but when it is used in a specific context, it becomes a keyword. The partial contextual keyword is used in the specification of a partial class, where is used to define generic contraints, yield is used to implement an iterator, and value is used in a set property block to access the incoming value. Table 31-4 lists the contextual keywords.
Table 31-4. C# Contextual Keywords
Literals are the way in which values are written for variables.
Integer literals are written simply by writing the numeric value. Integer literals that are small enough to fit into the int data type6 are treated as ints; if they are 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
Integer literals can also be written in hexadecimal format, by placing 0x in front of the constant.
0xFFFF
0x12AB
Real literals are used 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.
Exponential notation can be used by appending e followed by the exponent to the real literal.
Here are some examples:
1.345 // double constant
−8.99e12F // float constant
15.66m // decimal constant
A character literal is a single character enclosed in single quotes, such as x. Table 31-5 shows the supported escape sequences.
Table 31-5. Character Escape Sequences
Escape Sequence | Description |
---|---|
' | Single quote. |
" | Double quote. Double quotes do not need to be escaped if they are part of a character literal. |
\ | Backslash. |