This chapter discusses three C# features that give you greater control over the organization and accessibility of a program. These are namespaces, the preprocessor, and assemblies.
The namespace was mentioned briefly in Chapter 2 because it is a concept fundamental to C#. In fact, every C# program makes use of a namespace in one way or another. We didn’t need to examine namespaces in detail before now because C# automatically provides a default, global namespace for your program. Thus, the programs in earlier chapters simply used the global namespace. In the real world, however, many programs will need to create their own namespaces or interact with other namespaces. Here, they are examined in detail.
A namespace defines a declarative region that provides a way to keep one set of names separate from another. In essence, names declared in one namespace will not conflict with the same names declared in another. The namespace used by the .NET Framework library (which is the C# library) is System. This is why you have included
using System;
near the top of every program. As explained in Chapter 14, the I/O classes are defined within a namespace subordinate to System called System.IO. There are many other namespaces subordinate to System that hold other parts of the C# library.
Namespaces are important because there has been an explosion of variable, method, property, and class names over the past few years. These include library routines, third-party code, and your own code. Without namespaces, all of these names would compete for slots in the global namespace and conflicts would arise. For example, if your program defined a class called Finder, it could conflict with another class called Finder supplied by a third-party library that your program uses. Fortunately, namespaces prevent this type of problem because a namespace restricts the visibility of names declared within it.
A namespace is declared using the namespace keyword. The general form of namespace is shown here:
namespace name {
// members
}
Here, name is the name of the namespace. A namespace declaration defines a scope. Anything declared immediately inside the namespace is in scope throughout the namespace. Within a namespace, you can declare classes, structures, delegates, enumerations, interfaces, or another namespace.
Here is an example of a namespace that creates a namespace called Counter. It localizes the name used to implement a simple countdown counter class called CountDown.
// Declare a namespace for counters.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
public void Reset(int n) {
val = n;
}
public int Count() {
if(val > 0) return val--;
else return 0;
}
}
} // This is the end of the Counter namespace.
Notice how the class CountDown is declared within the scope defined by the Counter namespace. To follow along with the example, put this code into a file called Counter.cs.
Here is a program that demonstrates the use of the Counter namespace:
// Demonstrate the Counter namespace.
using System;
class NSDemo {
static void Main() {
// Notice how CountDown is qualified by Counter.
Counter.CountDown cd1 = new Counter.CountDown(10);
int i;
do {
i = cd1.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
// Again, notice how CountDown is qualified by Counter.
Counter.CountDown cd2 = new Counter.CountDown(20);
do {
i = cd2.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
cd2.Reset(4);
do {
i = cd2.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
}
}
The output from the program is shown here:
10 9 8 7 6 5 4 3 2 1 0
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
4 3 2 1 0
To compile this program, you must include both the preceding code and the code contained in the Counter namespace. Assuming you called the preceding code NSDemo.cs and put the source code for the Counter namespace into a file called Counter.cs as mentioned earlier, then you can use this command line to compile the program:
csc NSDemo.cs counter.cs
Some important aspects of this program warrant close examination. First, since CountDown is declared within the Counter namespace, when an object is created, CountDown must be qualified with Counter, as shown here:
Counter.CountDown cd1 = new Counter.CountDown(10);
This rule can be generalized. Whenever you use a member of a namespace, you must qualify it with the namespace name. If you don’t, the member of the namespace won’t be found by the compiler.
Second, once an object of type Counter has been created, it is not necessary to further qualify it or any of its members with the namespace. Thus, cd1.Count( ) can be called directly without namespace qualification, as this line shows:
i = cd1.Count();
Third, for the sake of illustration, this example uses two separate files. One holds the Counter namespace and the other holds the NSDemo program. However, both could have been contained in the same file. Furthermore, a single file can contain two or more named namespaces, with each namespace defining its own declarative region. When a named namespace ends, the outer namespace resumes, which in the case of the Counter is the global namespace. For clarity, subsequent examples will show all namespaces required by a program within the same file, but remember that separate files would be equally valid (and more commonly used in production code).
REMEMBER For clarity, the remaining namespace examples in this chapter show all namespaces required by a program within the same file. In real-world code, however, a namespace will often be defined in its own file, as the preceding example illustrates.
The key point about a namespace is that names declared within it won’t conflict with similar names declared outside of it. For example, the following program defines two namespaces. The first is Counter, shown earlier. The second is called Counter2. Both contain classes called CountDown, but because they are in separate namespaces, the two classes do not conflict. Also notice how both namespaces are specified within the same file. As just explained, a single file can contain multiple namespace declarations. Of course, separate files for each namespace could also have been used.
// Namespaces prevent name conflicts.
using System;
// Declare the Counter namespace.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
public void Reset(int n) {
val = n;
}
public int Count() {
if(val > 0) return val--;
else return 0;
}
}
}
// Declare the Counter2 namespace.
namespace Counter2 {
/* This CountDown is in the Counter2 namespace and
does not conflict with the one in Counter. */
class CountDown {
public void Count() {
Console.WriteLine("This is Count() in the " +
"Counter2 namespace.");
}
}
}
class NSDemo2 {
static void Main() {
// This is CountDown in the Counter namespace.
Counter.CountDown cd1 = new Counter.CountDown(10);
// This is CountDown in the Counter2 namespace.
Counter2.CountDown cd2 = new Counter2.CountDown();
int i;
do {
i = cd1.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
cd2.Count();
}
}
The output is shown here:
10 9 8 7 6 5 4 3 2 1 0
This is Count() in the Counter2 namespace.
As the output confirms, the CountDown class inside Counter is separate from the CountDown class in the Counter2 namespace, and no name conflicts arise. Although this example is quite simple, it is easy to see how putting classes into a namespace helps prevent name conflicts between your code and code written by others.
If your program includes frequent references to the members of a namespace, having to specify the namespace each time you need to refer to a member quickly becomes tedious. The using directive alleviates this problem. Throughout this book, you have been using it to bring the C# System namespace into view, so you are already familiar with it. As you would expect, using can also be employed to bring namespaces that you create into view.
There are two forms of the using directive. The first is shown here:
using name;
Here, name specifies the name of the namespace you want to access. This is the form of using that you have already seen. All of the members defined within the specified namespace are brought into view and can be used without qualification. A using directive must be specified at the top of each file, prior to any other declarations, or at the start of a namespace body.
The following program reworks the counter example to show how you can employ using to bring a namespace that you create into view:
// Demonstrate the using directive.
using System;
// Bring Counter into view.
using Counter;
// Declare a namespace for counters.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
public void Reset(int n) {
val = n;
}
public int Count() {
if(val > 0) return val--;
else return 0;
}
}
}
class NSDemo3 {
static void Main() {
// now, CountDown can be used directly.
CountDown cd1 = new CountDown(10);
int i;
do {
i = cd1.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
CountDown cd2 = new CountDown(20);
do {
i = cd2.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
cd2.Reset(4);
do {
i = cd2.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
}
}
This version of the program contains two important changes. The first is this using statement, near the top of the program:
using Counter;
This brings the Counter namespace into view. The second change is that it is no longer necessary to qualify CountDown with Counter, as this statement in Main( ) shows:
CountDown cd1 = new CountDown(10);
Because Counter is now in view, CountDown can be used directly.
The program illustrates one other important point: Using one namespace does not override another. When you bring a namespace into view, it simply lets you use its contents without qualification. Thus, in the example, both System and Counter have been brought into view.
The using directive has a second form that creates another name, called an alias, for a type or a namespace. This form is shown here:
using alias = name;
Here, alias becomes another name for the type (such as a class type) or namespace specified by name. Once the alias has been created, it can be used in place of the original name.
Here the example from the preceding section has been reworked so that an alias for Counter.CountDown called MyCounter is created:
// Demonstrate a using alias.
using System;
// Create an alias for Counter.CountDown.
using MyCounter = Counter.CountDown;
// Declare a namespace for counters.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
public void Reset(int n) {
val = n;
}
public int Count() {
if(val > 0) return val--;
else return 0;
}
}
}
class NSDemo4 {
static void Main() {
// Here, MyCounter is used as a name for Counter.CountDown.
MyCounter cd1 = new MyCounter(10);
int i;
do {
i = cd1.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
MyCounter cd2 = new MyCounter(20);
do {
i = cd2.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
cd2.Reset(4);
do {
i = cd2.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
}
}
The MyCounter alias is created using this statement:
using MyCounter = Counter.CountDown;
Once MyCounter has been specified as another name for Counter.CountDown, it can be used to declare objects without any further namespace qualification. For example, in the program, this line
MyCounter cd1 = new MyCounter(10);
creates a CountDown object.
There can be more than one namespace declaration of the same name. This allows a namespace to be split over several files or even separated within the same file. For example, the following program defines two Counter namespaces. One contains the CountDown class. The other contains the CountUp class. When compiled, the contents of both Counter namespaces are added together.
// Namespaces are additive.
using System;
// Bring Counter into view.
using Counter;
// Here is one Counter namespace.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
public void Reset(int n) {
val = n;
}
public int Count() {
if(val > 0) return val--;
else return 0;
}
}
}
// Here is another Counter namespace.
namespace Counter {
// A simple count-up counter.
class CountUp {
int val;
int target;
public int Target {
get{
return target;
}
}
public CountUp(int n) {
target = n;
val = 0;
}
public void Reset(int n) {
target = n;
val = 0;
}
public int Count() {
if(val < target) return val++;
else return target;
}
}
}
class NSDemo5 {
static void Main() {
CountDown cd = new CountDown(10);
CountUp cu = new CountUp(8);
int i;
do {
i = cd.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
do {
i = cu.Count();
Console.Write(i + " ");
} while(i < cu.Target);
}
}
This program produces the following output:
10 9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8
Notice one other thing: The directive
using Counter;
brings into view the entire contents of the Counter namespace. Thus, both CountDown and CountUp can be referred to directly, without namespace qualification. It doesn’t matter that the Counter namespace was split into two parts.
One namespace can be nested within another. Consider this program:
// Namespaces can be nested.
using System;
namespace NS1 {
class ClassA {
public ClassA() {
Console.WriteLine("constructing ClassA");
}
}
namespace NS2 { // a nested namespace
class ClassB {
public ClassB() {
Console.WriteLine("constructing ClassB");
}
}
}
}
class NestedNSDemo {
static void Main() {
NS1.ClassA a = new NS1.ClassA();
// NS2.ClassB b = new NS2.ClassB(); // Error!!! NS2 is not in view
NS1.NS2.ClassB b = new NS1.NS2.ClassB(); // this is right
}
}
This program produces the following output:
constructing ClassA
constructing ClassB
In the program, the namespace NS2 is nested within NS1. Thus, to refer to ClassB, you must qualify it with both the NS1 and NS2 namespaces. NS2, by itself, is insufficient. As shown, the namespace names are separated by a period. Therefore, to refer to ClassB within Main( ), you must use NS1.NS2.ClassB.
Namespaces can be nested by more than two levels. When this is the case, a member in a nested namespace must be qualified with all of the enclosing namespace names.
You can specify a nested namespace using a single namespace statement by separating each namespace with a period. For example,
namespace OuterNS {
namespace InnerNS {
// ...
}
}
can also be specified like this:
namespace OuterNS.InnerNS {
// ...
}
If you don’t declare a namespace for your program, then the default global namespace is used. This is why you have not needed to use namespace for the programs in the preceding chapters. Although the global namespace is convenient for the short, sample programs found in this book, most real-world code will be contained within a declared namespace. The main reason for encapsulating your code within a declared namespace is that it prevents name conflicts. Namespaces are another tool that you have to help you organize programs and make them viable in today’s complex, networked environment.
Although namespaces help prevent name conflicts, they do not completely eliminate them. One way that a conflict can still occur is when the same name is declared within two different namespaces, and you then try to bring both namespaces into view. For example, assume that two different namespaces contain a class called MyClass. If you attempt to bring these two namespaces into view via using statements, MyClass in the first namespace will conflict with MyClass in the second namespace, causing an ambiguity error. In this situation, you can use the :: namespace alias qualifier to explicitly specify which namespace is intended.
The:: operator has this general form:
namespace-alias::identifier
Here, namespace-alias is the name of a namespace alias and identifier is the name of a member of that namespace.
To understand why the namespace alias qualifier is needed, consider the following program. It creates two namespaces, Counter and AnotherCounter, and both declare a class called CountDown. Furthermore, both namespaces are brought into view by using statements. Finally, in Main( ), an attempt is made to instantiate an object of type CountDown.
// Demonstrate why the :: qualifier is needed.
//
// This program will not compile.
using System;
// Use both the Counter and AnotherCounter namespace.
using Counter;
using AnotherCounter;
// Declare a namespace for counters.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
// ...
}
}
// Declare another namespace for counters.
namespace AnotherCounter {
// Declare another class called CountDown, which
// is in the AnotherCounter namespace.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
// ...
}
}
class WhyAliasQualifier {
static void Main() {
int i;
// The following line is inherently ambiguous!
// Does it refer to CountDown in Counter or
// to CountDown in AnotherCounter?
CountDown cd1 = new CountDown(10); // Error! ! !
// ...
}
}
If you try to compile this program, you will receive an error message stating that this line in Main( ) is ambiguous:
CountDown cd1 = new CountDown(10); // Error! ! !
The trouble is that both namespaces, Counter and AnotherCounter, declare a class called CountDown, and both namespaces have been brought into view. Thus, to which version of CountDown does the preceding declaration refer? The :: qualifier was designed to handle these types of problems.
To use the ::, you must first define an alias for the namespace you want to qualify. Then, simply qualify the ambiguous element with the alias. For example, here is one way to fix the preceding program:
// Demonstrate the :: qualifier.
using System;
using Counter;
using AnotherCounter;
// Give Counter an alias called Ctr.
using Ctr = Counter;
// Declare a namespace for counters.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
// ...
}
}
// Another counter namespace.
namespace AnotherCounter {
// Declare another class called CountDown, which
// is in the AnotherCounter namespace.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
// ...
}
}
class AliasQualifierDemo {
static void Main() {
// Here, the :: operator
// tells the compiler to use the CountDown
// that is in the Counter namespace.
Ctr::CountDown cd1 = new Ctr::CountDown(10);
// ...
}
}
In this version, the alias Ctr is specified for Counter by the following line:
using Ctr = Counter;
Then, inside Main( ), this alias is used to qualify CountDown, as shown here:
Ctr::CountDown cd1 = new Ctr::CountDown(10);
The use of the :: qualifier removes the ambiguity because it specifies that the CountDown in Ctr (which stands for Counter) is desired, and the program now compiles.
You can use the :: qualifier to refer to the global namespace by using the predefined identifier global. For example, in the following program, a class called CountDown is declared in both the Counter namespace and in the global namespace. To access the version of CountDown in the global namespace, the predefined alias global is used.
// Use the global alias.
using System;
// Give Counter an alias called Ctr.
using Ctr = Counter;
// Declare a namespace for counters.
namespace Counter {
// A simple countdown counter.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
// ...
}
}
// Declare another class called CountDown, which
// is in the global namespace.
class CountDown {
int val;
public CountDown(int n) {
val = n;
}
// ...
}
class GlobalAliasQualifierDemo {
static void Main() {
// Here, the :: qualifier tells the compiler
// to use the CountDown in the Counter namespace.
Ctr::CountDown cd1 = new Ctr::CountDown(10);
// Next, create CountDown object from global namespace.
global::CountDown cd2 = new global::CountDown(10);
// ...
}
}
Notice how the global identifier is used to access the version of CountDown in the default namespace:
global::CountDown cd2 = new global::CountDown(10);
This same approach can be generalized to any situation in which you need to specify the default namespace.
One final point: You can also use the namespace alias qualifier with extern aliases, which are described in Chapter 20.
C# defines several preprocessor directives, which affect the way that your program’s source file is interpreted by the compiler. These directives affect the text of the source file in which they occur, prior to the translation of the program into object code. The term preprocessor directive comes from the fact that these instructions were traditionally handled by a separate compilation phase called the preprocessor. Today’s modern compiler technology no longer requires a separate preprocessing stage to handle the directives, but the name has stuck.
C# defines the following preprocessor directives:
All preprocessor directives begin with a # sign. In addition, each preprocessor directive must be on its own line.
Given C#’s modern, object-oriented architecture, there is not as much need for the preprocessor directives as there is in older languages. Nevertheless, they can be of value from time to time, especially for conditional compilation. Each directive is examined in turn.
The #define directive defines a character sequence called a symbol. The existence or nonexistence of a symbol can be determined by #if or #elif and is used to control compilation. Here is the general form for #define:
#define symbol
Notice that there is no semicolon in this statement. There may be any number of spaces between the #define and the symbol, but once the symbol begins, it is terminated only by a newline. For example, to define the symbol EXPERIMENTAL, use this directive:
#define EXPERIMENTAL
NOTE In C/C++ you can use #define to perform textual substitutions, such as defining a name for a value, and to create function-like macros. C# does not support these uses of #define. In C#, #define is used only to define a symbol.
The #if and #endif directives enable conditional compilation of a sequence of code based upon whether an expression involving one or more symbols evaluates to true. A symbol is true if it has been defined. It is false otherwise. Thus, if a symbol has been defined by a #define directive, it will evaluate as true.
The general form of #if is
#if symbol-expression
statement sequence
#endif
If the expression following #if is true, the code that is between it and #endif is compiled. Otherwise, the intervening code is skipped. The #endif directive marks the end of an #if block.
A symbol expression can be as simple as just the name of a symbol. You can also use these operators in a symbol expression: !, = =, !=, &&, and ||. Parentheses are also allowed.
Here’s an example:
// Demonstrate #if, #endif, and #define.
#define EXPERIMENTAL
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Compiled for experimental version.");
#endif
Console.WriteLine("This is in all versions.");
}
}
This program displays the following:
Compiled for experimental version.
This is in all versions.
The program defines the symbol EXPERIMENTAL. Thus, when the #if is encountered, the symbol expression evaluates to true, and the first WriteLine( ) statement is compiled. If you remove the definition of EXPERIMENTAL and recompile the program, the first WriteLine( ) statement will not be compiled, because the #if will evaluate to false. In all cases, the second WriteLine( ) statement is compiled because it is not part of the #if block.
As explained, you can use operators in a symbol expression in an #if. For example,
// Use an operator in a symbol expression.
#define EXPERIMENTAL
#define TRIAL
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Compiled for experimental version.");
#endif
#if EXPERIMENTAL && TRIAL
Console.Error.WriteLine("Testing experimental trial version.");
#endif
Console.WriteLine("This is in all versions.");
}
}
The output from this program is shown here:
Compiled for experimental version.
Testing experimental trial version.
This is in all versions.
In this example, two symbols are defined, EXPERIMENTAL and TRIAL. The second WriteLine( ) statement is compiled only if both are defined.
You can use the ! to compile code when a symbol is not defined. For example,
#if !EXPERIMENTAL
Console.WriteLine("Code is not experimental!");
#endif
The call to WriteLine( ) will be compiled only if EXPERIMENTAL has not been defined.
The #else directive works much like the else that is part of the C# language: It establishes an alternative if #if fails. The previous example can be expanded as shown here:
// Demonstrate #else.
#define EXPERIMENTAL
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Compiled for experimental version.");
#else
Console.WriteLine("Compiled for release.");
#endif
#if EXPERIMENTAL && TRIAL
Console.Error.WriteLine("Testing experimental trial version.");
#else
Console.Error.WriteLine("Not experimental trial version.");
#endif
Console.WriteLine("This is in all versions.");
}
}
The output is shown here:
Compiled for experimental version.
Not experimental trial version.
This is in all versions.
Since TRIAL is not defined, the #else portion of the second conditional code sequence is used.
Notice that #else marks both the end of the #if block and the beginning of the #else block. This is necessary because there can only be one #endif associated with any #if. Furthermore, there can be only one #else associated with any #if.
The #elif directive means “else if” and establishes an if-else-if chain for multiple compilation options. #elif is followed by a symbol expression. If the expression is true, that block of code is compiled and no other #elif expressions are tested. Otherwise, the next block in the series is checked. If no #elif succeeds, then if there is a #else, the code sequence associated with the #else is compiled. Otherwise, no code in the entire #if is compiled.
The general form for #elif is
#if symbol-expression
statement sequence
#elif symbol-expression
statement sequence
#elif symbol-expression
// . . .
#endif
Here’s an example:
// Demonstrate #elif.
#define RELEASE
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Compiled for experimental version.");
#elif RELEASE
Console.WriteLine("Compiled for release.");
#else
Console.WriteLine("Compiled for internal testing.");
#endif
#if TRIAL && !RELEASE
Console.WriteLine("Trial version.");
#endif
Console.WriteLine("This is in all versions.");
}
}
The output is shown here:
Compiled for release.
This is in all versions.
The #undef directive removes a previously defined symbol. That is, it “undefines” a symbol. The general form for #undef is
#undef symbol
Here’s an example:
#define SMALL
#if SMALL
// ...
#undef SMALL
// at this point SMALL is undefined.
After the #undef directive, SMALL is no longer defined.
#undef is used principally to allow symbols to be localized to only those sections of code that need them.
The #error directive forces the compiler to stop compilation. It is used for debugging. The general form of the #error directive is
#error error-message
When the #error directive is encountered, the error message is displayed. For example, when the compiler encounters this line:
#error This is a test error!
compilation stops and the error message “This is a test error!” is displayed.
The #warning directive is similar to #error, except that a warning rather than an error is produced. Thus, compilation is not stopped. The general form of the #warning directive is
#warning warning-message
The #line directive sets the line number and filename for the file that contains the #line directive. The number and the name are used when errors or warnings are output during compilation. The general form for #line is
#line number“filename”
where number is any positive integer, which becomes the new line number, and the optional filename is any valid file identifier, which becomes the new filename. #line is primarily used for debugging and special applications.
#line allows two options. The first is default, which returns the line numbering to its original condition. It is used like this:
#line default
The second is hidden. When stepping through a program, the hidden option allows a debugger to bypass lines between a
#line hidden
directive and the next #line directive that does not include the hidden option.
The #region and #endregion directives let you define a region that will be expanded or collapsed when using outlining in the Visual Studio IDE. The general form is shown here:
#region text
// code sequence
#endregion text
Here, text is an optional string.
The #pragma directive gives instructions, such as specifying an option, to the compiler. It has this general form:
#pragma option
Here, option is the instruction passed to the compiler.
There are currently two options supported by #pragma. The first is warning, which is used to enable or disable specific compiler warnings. It has these two forms:
#pragma warning disable warnings
#pragma warning restore warnings
Here, warnings is a comma-separated list of warning numbers. To disable a warning, use the disable option. To enable a warning, use the restore option. If no warnings are specified, then all warnings are affected.
For example, this #pragma statement disables warning 168, which indicates when a variable is declared but not used:
#pragma warning disable 168
The second #pragma option is checksum. It is used to generate checksums for ASP.NET projects. It has this general form.
#pragma checksum “filename” “{GUID}” “check-sum”
Here, filename is the name of the file, GUID is the globally unique identifier associated with filename, and check-sum is a hexadecimal number that contains the checksum. This string must contain an even number of digits.
An integral part of C# programming is the assembly. An assembly is a file (or files) that contains all deployment and version information for a program. Assemblies are fundamental to the .NET environment. They provide mechanisms that support safe component interaction, cross-language interoperability, and versioning. An assembly also defines a scope.
An assembly is composed of four sections. The first is the assembly manifest. The manifest contains information about the assembly, itself. This data includes such things as the name of the assembly, its version number, type mapping information, and cultural settings. The second section is type metadata, which is information about the data types used by the program. Among other benefits, type metadata aids in cross-language interoperability. The third part of an assembly is the program code, which is stored in Microsoft Intermediate Language (MSIL) format. The fourth constituent of an assembly is the resources used by the program.
Fortunately, when using C#, assemblies are produced automatically, with little or no extra effort on your part. The reason for this is that the exe file created when you compile a C# program is actually an assembly that contains your program’s executable code as well as other types of information. Thus, when you compile a C# program, an assembly is automatically produced.
There are many other features and topics that relate to assemblies, but a discussion of these is outside the scope of this book. (Assemblies are an integral part of .NET development, but are not technically a feature of the C# language.) However, there is one part of C# that relates directly to the assembly: the internal access modifier, which is examined next.
In addition to the access modifiers public, private, and protected, which you have been using throughout this book, C# also defines internal. The internal modifier declares that a member is known throughout all files in an assembly, but unknown outside that assembly. Thus, in simplified terms, a member marked as internal is known throughout a program, but not elsewhere. The internal access modifier is particularly useful when creating software components.
The internal modifier can be applied to classes and members of classes and to structures and members of structures. The internal modifier can also be applied to interface and enumeration declarations.
You can use protected in conjunction with internal to produce the protected internal access modifier pair. The protected internal access level can be given only to class members. A member declared with protected internal access is accessible within its own assembly or to derived types.
Here is an example that uses internal:
// Use internal.
using System;
class InternalTest {
internal int x;
}
class InternalDemo {
static void Main() {
InternalTest ob = new InternalTest();
ob.x = 10; // can access -- in same file
Console.WriteLine("Here is ob.x: " + ob.x);
}
}
Inside InternalTest, the field x is declared internal. This means that it is accessible within the program, as its use in InternalDemo shows, but unavailable outside the program.