Quick Guide to C# for Experienced Programmers
Quick Guide to C# for Experienced Programmers
Welcome to Presenting C#. This book is your ticket to quickly getting up to speed
with the enterprise programming language that ships with the Next Generation
Windows Services (NGWS) Runtime: C# (pronounced C sharp).
The Next Generation Windows Services Runtime is a runtime environment that not
only manages the execution of code, but also provides services that make
programming easier. Compilers produce managed code to target this managed
execution environment. You get cross-language integration, cross-language
exception handling, enhanced security, versioning and deployment support, and
debugging and profiling services for free.
The premier language for the NGWS Runtime is C#. Much of the supporting NGWS
framework is written in C#; therefore, its compiler can be considered the most
tested and optimized compiler of those shipping with the NGWS Runtime. The C#
language borrows power from C++, but with modernization and the addition of type
safety--making C# the number one choice for enterprise solutions.
Acknowledgements
What would you expect when you receive an email from an executive editor,
mentioning that he wants to talk with you about an exciting book project? I didn't
quite expect this book when Chris Webb called me. Chris's offer was way too exciting
to turn down—writing a book about a new programming language.
Thanks to both Chris Webb and Brad Jones (the associate publisher) for putting their
trust in me and my tight writing schedule. Both of them—as well as the development
editor, Kevin Howard—had to put up with my changes to the table of contents as the
book progressed. You just can't kill old habits.
Special thanks to Christian Koller for reviewing my chapters and telling me when I
once again left out details that non-C++ programmers would need for better
understanding. Although I don't know everyone who was involved in this book
project at Sams Publishing, I thank them all for bringing it to the shelves--especially
within this tight schedule!
Finally, and of course most importantly, I want to thank my family for their continual
support throughout all my book projects.
Chapter 1 Introduction to C#
Welcome to the world of C#! This chapter takes you on a tour of C# and answers
questions such as why you should use C#, what the main differences are between
C++ and C#, and why C# will make development easier and more fun for you.
• Simple
• Modern
• Object-oriented
• Type-safe
• Versionable
• Compatible
• Flexible
Simple
One thing you definitely wouldn't attribute to C++ is that learning it is simple. This is
not so with C#. The foremost goal for this programming language was simplicity.
Many features—or the lack thereof—contribute to the overall simplicity of C#.
Pointers are a prominent feature that is missing in C#. By default, you are working
with managed code, where unsafe operations, such as direct memory manipulation,
are not allowed. I don't think any C++ programmer can claim never to have
accessed memory that didn't belong to him via a pointer.
Closely related to the pointer "drama" is operator "madness." In C++, you have ::, .,
and -> operators that are used for namespaces, members, and references. For a
beginner, operators make for yet another hard day of learning. C# does away with
the different operators in favor of a single one: the . (the "dot"). All that a
programmer now has to understand is the notion of nested names.
You no longer have to remember cryptic types that originate from different processor
architectures—including varying ranges of integer types. C# does away with them by
providing a unified type system. This type system enables you to view every type as
an object, be it a primitive type or a full-blown class. In contrast to other
programming languages, treating a simple type as an object does not come with a
performance penalty because of a mechanism called boxing and unboxing. Boxing
and unboxing is explained later in detail, but basically, this technique provides object
access to simple types only when requested.
At first, seasoned programmers might not like it, but integer and Boolean data types
are now finally two entirely different types. That means a mistaken assignment in an
if statement is now flagged as an error by the compiler because it takes a Boolean
result only. No more comparison-versus-assignment errors!
C# also gets rid of redundancies that crept into C++ over the years. Such
redundancies include, for example, const versus #define, different character types,
and soon. Commonly used forms are available in C#, whereas the redundant forms
were eliminated from the language.
Modern
The effort you put into learning C# is a great investment because C# was designed
to be the premier language for writing NGWS applications. You will find many
features that you had to implement yourself in C++, or that were simply unavailable,
are part of the base C# language implementation.
The financial types are a welcome addition for an enterprise-level programming
language. You get a new decimal data type that is targeted at monetary calculations.
If you are not fond of the provided simple types, you can easily create new ones
specifically crafted for your application.
I have already mentioned that pointers are no longer part of your programming
weaponry. You won't be too surprised then that the entire memory management is
no longer your duty—the runtime of NGWS provides a garbage collector that is
responsible for memory management in your C# programs. Because memory and
your application are managed, it is imperative that type safety be enforced to
guarantee application stability.
It is not exactly news to C++ programmers, but exception handling is a main feature
of C#. The difference from C++, however, is that exception handling is cross-
language (another feature of the runtime).Prior to C#, you had to deal with quirky
HRESULTs—this is now over because of robust error handling that is based on
exceptions.
Security is a top requirement for a modern application. C# won't leave you alone on
this either: It provides metadata syntax for declaring capabilities and permissions for
the underlying NGWS security model. Metadata is a key concept of the NGWS
runtime, and the next chapter deals with its implications in more depth.
Object-Oriented
You wouldn't expect a new language to not support object-oriented features, would
you? C#, of course, supports all the key object-oriented concepts such as
encapsulation, inheritance, and polymorphism. The entire C# class model is built on
top of the NGWS runtime's Virtual Object System(VOS), which is described in the
next chapter. The object model is part of the infrastructure, and is no longer part of
the programming language.
One thing you will notice right from the start is that there are no more global
functions, variables, or constants. Everything must be encapsulated inside a class,
either as an instance member (accessible via an instance of a class—an object) or a
static member (via the type). This makes your C# code more readable and also
helps to reduce potential naming conflicts.
The methods you can define on classes are, by default, non virtual (they cannot be
overridden by deriving classes). The main point of this is that another source of
errors disappears—the accidental overriding of methods. For a method to be able to
be overridden, it must have the explicit virtual modifier. This behavior not only
reduces the size of the virtual function table, but also guarantees correct versioning
behavior.
When you are used to programming classes in C++, you know that you can set
different access levels for class members by using access modifiers. C# also supports
the private, protected, and public access modifiers, and also adds a fourth one:
internal. Details about these access modifiers are presented in Chapter 5, "Classes."
How many of you have ever created a class that derives from multiple base classes?
(ATL programmers, your vote doesn't count!) In most cases, you need to derive from
only one class. Multiple base classes usually add more problems than they solve.
That is why C# allows only one base class. If you feel the need for multiple
inheritance, you can implement interfaces.
A question that might come up is how to emulate function pointers when there are
no pointers in C#. The answer to this question is delegates, which provide the
underpinnings for the NGWS runtime's event model. Again, I have to put off a full
explanation until Chapter 5.
Type-Safe
Once again, I have to pick on pointers as an example. When you had a pointer in
C++, you were free to cast it to any type, including doing rather idiotic things such
as casting an int* (int pointer) to a double* (double pointer). As long as memory
backed that operation, it kind of "worked." This is not the kind of type safety you
would envision for an enterprise-level programming language.
Because of the outlined problems, C# implements strictest type safety to protect
itself and the garbage collector. Therefore, you must abide by a few rules in C# with
regard to variables:
• You cannot use uninitialized variables. For member variables of an object, the
compiler takes care of zeroing them. For local variables, you are incharge.
However, if you use an uninitialized variable, the compiler will tell you so. The
advantage is that you get rid of those errors when using an uninitialized
variable to compute a result and you don't know how these funny results are
produced.
• C# does away with unsafe casts. You cannot cast from an integer to a
reference type (object, for example), and when you downcast, C# verifies
that this cast is okay. (That is, that the derived object is really derived from
the class to which you are down casting it.)
• Bounds checking is part of C#. It is no longer possible to use that "extra"
array element n, when the array actually has n-1 elements. This makes it
impossible to overwrite unallocated memory.
• Arithmetic operations could overflow the range of the result data type. C#
allows you to check for overflow in such operations on either an application
level or a statement level. With overflow checking enabled, an exception is
thrown when an overflow happens.
• Reference parameters that are passed in C# are type-safe.
Versionable
Over the past few years, almost every programmer has had to deal at least once
with what has become known as "DLL Hell." The problem stems from the fact that
multiple applications install different versions of the same DLL to the computer.
Sometimes, older applications happily work with the newer version of the DLL;
however, most of the time, they break. Versioning is a real pain today.
As you will see in Chapter 8, "Writing Components in C#," the versioning support for
applications you write is provided by the NGWS runtime. C# does its best to support
this versioning. Although C# itself cannot guarantee correct versioning, it can ensure
that versioning is possible for the programmer. With this support in place, a
developer can guarantee that as his class library evolves, it will retain binary
compatibility with existing client applications.
Compatible
C# does not live in a closed world. It allows you access to different APIs, with the
foremost being the NGWS Common Language Specification (CLS). The CLS defines a
standard for interoperation between languages that adhere to this standard. To
enforce CLS compliance, the compiler of C# checks that all publicly exported entities
comply, and raises an error if they do not.
Of course, you also want to be able to access your older COM objects. The NGWS
runtime provides transparent access to COM. Integration with legacy code is
presented in Chapter 10, "Interoperating with Unmanaged Code."
OLE Automation is a special kind of animal. Anyone who ever created an OLE
Automation project in C++ will have come to love the various Automation data
types. The good news is that C# supports them, without bothering you with details.
Finally, C# enables you to inter operate with C-style APIs. Any entry point in a DLL—
given its C-styledness—is accessible from your applications. This feature for
accessing native APIs is called Platform Invocation Services (PInvoke), and Chapter
10 shows a few examples of inter operating with C APIs.
Flexible
The last paragraph of the previous section might have raised an alert with C
programmers. You might ask, "Aren't there APIs to which I have to pass a pointer?"
You are right. There are not only a few such APIs, but quite a large number (a small
understatement). This access to native WIN32 code sometimes makes using unsafe
classic pointers mandatory (although some of it can be handled by the support of
COM and PInvoke).
Although the default for C# code is safe mode, you can declare certain classes or
only methods of classes to be unsafe. This declaration enables you to use pointers,
structs, and statically allocated arrays. Both safe code and unsafe code run in the
managed space, which implies that no marshaling is incurred when calling unsafe
code from safe code.
What are the implications of dealing with your own memory in unsafemode? Well,
the garbage collector, of course, may not touch your memory locations and move
them just as it does for managed code. Unsafe variables are pinned into the memory
block managed by the garbage collector.
Summary
The C# language is derived from C and C++, and it was created for the enterprise
developer who is willing to sacrifice a bit of C++'s raw power for more convenience
and productivity. C# is modern, simple, object-oriented, and type-safe. It borrows a
lot from C and C++, but it is considerably different in specific areas such as
namespaces, classes, methods, and exception handling.
C# provides you with convenient features such as garbage collection, type safety,
versioning, and more. The only "expense" is that by default your code operates in
safe mode, where no pointers are allowed. Type safety pays off. However, if you
need pointers, you can still use them via unsafe code--and no marshaling is involved
when calling the unsafe code.
Chapter 2 The Underpinnings—The NGWS
Runtime
Now that you have the big picture of C#, I want to give you the big picture of the
NGWS runtime. C# depends on the runtime provided by NGWS; therefore, It is good
to know how that runtime works, and what concepts are behind it.
The chapter, therefore, is split into two parts—everyday implications and use and the
foundations. There is some overlap between both sections, but it helps to reinforce
the concepts you are learning.
You are provided with a runtime environment by NGWS, the NGWS runtime. This
runtime manages the execution of code, and it provides services that make
programming easier. As long as the compiler that you use supports this runtime, you
will benefit from this managed execution environment.
Your guess—that the C# compiler supports the NGWS runtime — is correct.
However, it is not the only compiler that supports the NGWS runtime; Visual Basic
and C++ do so also. The code that these compilers generate for NGWS runtime
support is called managed code. The benefits your applications gains from the NGWS
runtime are
For the NGWS runtime to provide all these benefits, the compiler must emit
metadata along with the managed code. The metadata describes the types in your
code, and is stored along with your code (in the same PE—portable executable—file).
As you can see from the many cross-language features, he NGWS runtime is mainly
about tight integration across multiple different programming languages. This
support goes as far as allowing you to derive a C# class from a Visual Basic object
(given that certain prerequisites that I'll discuss later are met).
One feature that C# programmers will like is that they don't have to worry about
memory management—namely the all-famous memory leaks. The NGWS runtime
provides the memory management, and the garbage collector releases the objects or
variables when their lifetimes areover—when they are no longer referenced. I really
like this feature because memory management in COM was my personal bane.
There are even bonuses when deploying a managed application or component.
Because managed applications contain metadata, the NGWS runtime can use this
information to ensure that your application has the specified versions of everything it
needs. The net result is that your code is less likely to break because some
dependency is not met. Another advantage of the metadata approach is that type
information resides in the same file where the code resides—no more problems with
the Registry!
The remainder of this section is split into two parts, each ofwhich discusses various
aspects of the NGWS runtime until your C# application is executed:
• Intermediate Language (IL) and metadata
• JITters
JITters
The managed code generated by C#—and other compilers capable of generating
managed code—is IL code. Although the IL code is packaged in a valid PE file, you
cannot execute it unless it is converted to managed native code. That is where the
NGWS runtime JIT Just-in-Time (JIT) compilers—which are also referred to as
JITters—come into the picture.
Why would you bother compiling code just-in-time? Why not take the whole IL PE file
and compile it to native code? The answer is time—the time that is necessary to
compile the IL code to CPU-specific code. It is much more efficient to compile as you
go because some code pieces might never be executed—for example, in my word
processor, the mail merge feature would never be compiled.
Technically speaking, the whole process works like this: When a type is loaded, the
loader creates and attaches a stub to each method of the type. When a method is
called for the first time, the stub passes control to the JIT. The JIT compiles the IL to
native code, and changes the stub to point to the cached native code. Subsequent
calls will execute the native code. At some point, all IL is converted to native code,
and the JITter sits idle.
As I mentioned earlier, there is no single JIT compiler, but there are multiple ones.
On Windows platforms, NGWS runtime ships with three different JITters:
Two of the listed JITters are runtime JITters. However, how can you determine which
JIT is to be used, and how it uses memory? There is a small utility named the JIT
Compiler Manager ([Link]), which resides in the bin folder of the NGWS SDK
installation folder. When executing the program, it adds an icon to the system tray—
double-clicking that icon opens the program's dialog box (see Figure 2.1).
Figure 2.1 The JIT Compiler Manager enables you to set various performance-
related options.
Although it is a small dialog box, the options you can choose in it are quite powerful.
Each of the options is described in the following list:
• Use EconoJIT only—When this option is unchecked, the NGWS runtime uses
the regular JIT compiler by default. The differences between the two JITters
are explained earlier in this section.
• Max Code Pitch Overhead (%)—This setting pertains only to EconoJIT. It
controls the percentage of time spent JITting versus executing code. If the
threshold is exceeded, the code cache is expanded to reduce the amount of
time spent JITting.
• Limit Size of Code Cache—This setting is unchecked by default. Not checking
this option means that the cache will use as much memory as it can get. If
you want to limit the cache size, enable this option, which allows you to use
the Max Size of Cache (bytes) option.
• Max Size of Cache (bytes)—Controls the maximum size of the buffer that
holds JITted code. Although you can very strictly limit this size, you should
take care that the largest method fits this cache because otherwise the JIT
compilation for this method will fail!
• Optimize For Size—Tells the JIT compiler to go for smaller code instead of
faster code. By default, this setting is turned off.
• Enable Concurrent GC [garbage collection]—By default, garbage collection
runs on the thread on which the user code runs. That means when GC
happens, a slight delay in responsiveness might be noticeable. To prevent this
from happening, turn on concurrent GC. Notice that concurrent garbage
collection is slower than standard GC, and that it is available only on Windows
2000 at the time of this writing.
You can experiment with the different settings when creating projects with C#. When
creating UI-intensive applications, you'll see the biggest difference by enabling
concurrent GC.
• The VOS type system—Provides a rich type system that is intended to support
the complete implementation of a wide range of programming languages.
• Metadata—Describes and references the types defined by the VOS type
system. The persistence format of metadata is independent of the
programming language; therefore, metadata lends itself as an interchange
mechanism for use between tools as well as with the Virtual Execution
System of NGWS.
• The Common Language Specification (CLS)—The CLS defines a subset of
types found in the VOS, as well as usage conventions. If a class library abides
by the rules of the CLS, it is guaranteed that the class library can be used in
all other programming languages that implement the CLS.
• The Virtual Execution System (VES)—This is the actual real-life
implementation of the VOS. The VES is responsible for loading and executing
programs that were written for the NGWS runtime.
Together, these four parts comprise the NGWS runtime architecture. Each of these
parts is described at length in the following sections.
The VOS type system provides a rich type system that is intended to support the
complete implementation of a wide range of programming languages. Therefore, the
VOS has to support object-oriented languages as well as procedural programming
languages.
Today, there are many similar—but subtly incompatible—types around. Consider the
integer data type, for example: In VB, it is 16 bits in length, whereas in C++, it is 32
bits. There are many more examples, especially the data types used for date and
time, and database types. This incompatibility unnecessarily complicates the creation
and maintenance of distributed applications, especially when multiple programming
languages are involved.
Another problem is that because of the subtle differences in programming languages,
you cannot reuse a type created in one language in a different one. (COM partially
solves this with the binary standard of interfaces.) Code reuse is definitely limited
today.
The biggest hurdle for distributed applications is that the object models of the
various programming languages are not uniform. Almost everything differs: events,
properties, persistence—you name it.
The VOS is here to change that picture. The VOS defines types that describe values
and specify a contract that all values of the type must support. Because of the
aforementioned support for object-oriented (OO) and procedural programming
languages, two kinds of entities exist: values and objects.
For a value, the type describes the storage representation as well as operations that
can be performed on it. Objects are more powerful because the type is explicitly
stored in its representation. Each object has an identity that distinguishes it from all
other objects. The different VOS types supported by C# are presented in Chapter 4,
"C# Types."
Metadata
Although metadata is used to describe and reference the types defined by the VOS
type system, it is not exclusively locked to this single purpose. When you write a
program, the types you declare—be they value types or reference types—are
introduced to the NGWS runtime type system by using type declarations, which are
described in the metadata stored inside the PEexecutable.
Basically, metadata is used for various tasks: To represent the information that the
NGWS runtime uses to locate and load classes, to layout instances of these classes in
memory, to resolve method invocations, to translate IL to native code, to enforce
security, and to set up runtime context boundaries.
You do not have to care about the generation of the metadata. Metadata generation
is done for you by C#'s code-to-IL compiler (not theJIT compiler). The code-to-IL
compiler emits the binary metadata information into the PE file for you, and it does
so in a standardized way—not like C++ compilers that create their own decorated
names for exported functions.
The main advantage you gain from combining the metadata with the executable
code is that the information about the types is persisted along with the type itself,
and it is no longer spread across multiple locations. It also helps to address
versioning problems that exist in COM. Furthermore, in NGWS runtime, you can use
different versions of a library in the same context because the libraries are
referenced not by the Registry, but by the metadata contained in the executable
code.
The Common Language Specification
The Common Language Specification is not exactly a part of the Virtual Object
System—it is a specialization. The CLS defines a subset of types found in the VOS, as
well as usage conventions that must be followed to be CLS-compliant.
So, what is this fuss all about? If a class library abides by the rules of CLS, it is
guaranteed to be usable by clients of other programming languages that also adhere
to the CLS. CLS is about language interoperability. Therefore, the conventions must
be followed only on the externally visible items such as methods, properties, events,
and soon.
The advantage of what I have described is that you can do the following: Write a
component in C#, derive from it in Visual Basic and, because the functionality added
in VB is so great, derive again from the VB class in C#. This works as long as all the
externally visible (accessible) items abide by the rules of CLS.
The code I present in this book does not care about CLS compliance. However, you
should care about CLS compliance when building your class library. Therefore, I have
provided Table 2.1 to define the compliance rules for types and items that might be
externally visible.
This list is not complete—it contains only some of the most-important items. I don't
point out the CLS compliance of every type presented in this book, so it is a good
idea to at least glance over the table to see which functionality is available when you
are hunting for CLS compliance. Don't worry if you are not familiar with every term
in this table—you will learn about these terms during the course of this book.
Table 2.1 Types and Features in the Common LanguageSpecification
Primitive Types
bool
char
byte
short
int
long
float
double
string
object(The mother of all objects)
Arrays
The dimension must be known (>=1), and the lower bound must be zero.
Element type must be a CLS type.
Types
Can be abstract or sealed.
Zero or more interfaces can be implemented. Different interfaces are allowed to have
methods with the same name and signature.
A type can be derived from exactly one type. Member overriding and hiding are
allowed.
Can have zero or more members, which are fields, methods, events, or types.
The type can have zero or more constructors.
The visibility of the type can be public or local to the NGWS component; however,
only public members are considered part of the interface of the type.
All value types must inherit from System. ValueType. The exception is an
enumeration—it must inherit from System. Enum.
Type Members
Type members are allowed to hide or override other members in another type.
The types of both the arguments and return values must be CLS-compliant types.
Constructors, methods, and properties can be overloaded.
A type can have abstract members, but only as long as the type is not sealed.
Methods
A method can be either static, virtual, or instance.
Virtual and instance methods can be abstract or have an implementation. Static
methods must always have an implementation.
Virtual methods can be final (or not).
Fields
Can be static or nonstatic.
Static fields can be literal or initialize-only.
Properties
Can be exposed as get and set methods instead of using property syntax.
The return type of get and the first argument of the set method must be the same
CLS type—the type of the property.
Properties must differ by name; a different property type is not sufficient for
differentiation.
Because property access is implemented with methods, you cannot implement
methods named get_PropertyName and set_PropertyName ifPropertyName is a
property defined in the same class.
Properties can be indexed.
Property accessors must follow this naming pattern:get_PropName, set_PropName.
Enumerations
Underlying type must be byte, short, int, or long.
Each member is a static literal field of the enum's type.
An enumeration cannot implement any interfaces.
You are allowed to assign the same value to multiple fields.
An enumeration must inherit from System. Enum (performed implicitly in C#).
Exceptions
Can be thrown and caught.
Self-programmed exceptions must inherit from [Link].
Interfaces
Can require implementation of other interfaces.
An interface can define properties, events, and virtual methods. The implementation
is up to the deriving class.
Events
Add and remove methods must be either both provided or both absent. Each of
these methods takes one parameter, which is a class derived from [Link].
Must following this naming pattern: add_EventName,remove_EventName, and
raise_EventName.
Custom Attributes
Can use only the following types: Type, string, char, bool, byte, short, int, long,
float, double, enum (of a CLS type), and object.
Delegates
Can be created and invoked.
Identifiers
An identifier's first character must come from a restricted set.
It is not possible to distinguish two or more identifiers solely by case in a single
scope (no case sensitivity).
The Virtual Execution System
The Virtual Execution System implements the Virtual Object System. The VES is
created by implementing an execution engine (EE) that is responsible for the runtime
of NGWS. This execution engine executes your applications that were written and
compiled in C#.
The following components are part of the VES:
The list provided is not complete, but it is enough for you to understand how the
runtime is backed by the infrastructure provided by the VES. There certainly will be
books dedicated completely to the NGWS runtime, and those books will drill deeper
into each topic.
Summary
In this chapter, I took you on a tour of the NGWS runtime. I described how it works
for you when creating, compiling, and deploying C# programs. You learned about the
Intermediate Language (IL), and how metadata is used to describe the types that
are compiled to IL. Both metadata and IL are used by JITters to examine and
execute your code. You can even choose which JITters to use to execute your
application.
The second part of this chapter dealt with the theory of why the runtime behaves the
way it does. You learned about the Virtual Object System (VOS), and the parts that
comprise it. Most interesting for class library designers is the Common Language
Specification (CLS), which sets up rules for language interoperation based on the
VOS. Finally, you saw how the Virtual Execution System (VES) is an implementation
of the VOS by the NGWS runtime.
Chapter 3 Your First C# Application
Keeping with the tradition of programming books, I'll start presenting C# with the
famous Hello World program. This chapter is short because its intention is to show
you the basic building blocks of a C# application, how to write and compile the
application, as well as the input and output code that is used throughout this book in
the examples.
Choosing an Editor
Although I am a hardcore Notepad fanatic, I don't recommend it this time for editing
C# source files. The reason is that you are dealing with a real programming
language, one with a compiler that can produce a rather large number of error
messages. (C++ programmers know what I am talking about.)
You have several choices for an editor. You could reconfigure your trusty old Visual
C++ 6.0 to work with C# source files. A second option is to work with the new Visual
Studio 7. Third, you can use any third-party programmer's editor, preferably one
that supports line numbers, color coding, tool integration (for the compiler), and a
good search function. One example of such a tool is CodeWright, which is shown in
Figure 3.1.
Figure 3.1 CodeWright is one of many possible editors you can use for creating C#
code files.
Of course, none of the mentioned editors is mandatory to create a C# program—
Notepad will definitely do. However, if you are considering writing larger projects, it
is a good idea to switch.
The Hello World Code
After this short detour about editors, let's get back to the most famous little
application. The shortest C# version of this application can be seen in Listing 3.1.
Save it to a file named [Link] so that you can follow the remaining steps, such
as compiling the application.
1: class HelloWorld
2: {
3: public static void Main()
4: {
5: [Link]("Hello World");
6: }
7: }
In C#, code blocks (statements) are enclosed by braces ({ and }). Therefore, even if
you don't have prior experience in C++, you can tell that the Main() method is part
of the HelloWorld class statement because it is enclosed in the angle brackets of its
definition.
The entry point to a C# application (executable) is the static Main method, which
must be contained in a class. There can be only one class defined with this signature,
unless you advise the compiler which Main method it should use (otherwise, a
compiler error is generated).
In contrast to C++, Main has a capital M, not the lowercase you are already used to.
In this method, your program starts and ends. You can call other methods—as in this
example, for text output—or create objects and invoke methods on those.
As you can see, the Main method has a void return type:
public static void Main()
Although C++ programmers definitely feel at home looking at these statements,
programmers of other languages might not. First, the public access modifier tells us
that this method is accessible by everyone—which is a prerequisite for it to be called.
Next, static means that the method can be called without creating an instance of the
class first—all you have to do is call it with the class's name:
[Link]();
However, I do not recommend executing this code in the Main method. Recursions
cause stack overflows.
Another important aspect is the return type. For the method Main, you have a choice
of either void (which means no return value at all), or int for an integer result (the
error level returned by an application). Therefore, two possible Main methods look
like
public static void Main()
public static int Main()
C++ programmers will also know what I am presenting next—the command-line
parameters array that can be passed to an application. This looks like
public static void Main(string[] args)
I won't elaborate now on how to access the parameters, but I want to give C++
programmers an upfront warning: In contrast to C++, the application path is not
part of this array. Only the parameters are contained in this array.
After this not-so-short introduction to the Main method, let's move on to the only
real line of code—the one that prints "Hello World" to the screen:
[Link]("Hello World");
If it weren't for the System part, one would immediately be able to guess that
WriteLine is a static method of the Console object. So what does this System stand
for? It is the namespace (scope) in which the Console object is contained. Because
it's not really practical to prefix the Console object with this namespace part every
time, you can import the namespace in your application as shown in Listing 3.2.
1: using System;
2:
3: class HelloWorld
4: {
5: public static void Main()
6: {
7: [Link]("Hello World");
8: }
9: }
All you have to do is add a using directive for the System namespace. From then on,
you can use elements of that namespace without having to qualify them. There are
many namespaces in the NGWS framework, and we will explore only a few objects
from this huge pool. However, Chapter 8, "Writing Components in C#," will introduce
you to creating your own namespaces for your objects.
1: using System;
2:
3: class InputOutput
4: {
5: public static void Main()
6: {
7: [Link]("Please enter your name: ");
8: string strName = [Link]();
9: [Link]("Hello " + strName);
10: }
11: }
Line 7 uses a new method of the Console object for presenting textual information to
the user: the Write method. Its only difference from the WriteLine method is that
Write does not add a line break to the output. I used this approach so that the user
can enter the name on the same line as the question.
After the user enters his name (and presses the Enter key), the input is read into a
string variable using the ReadLine method. The name string is concatenated with the
"Hello " constant string and presented to the user with the already familiar WriteLine
method (see Figure 3.3).
1: using System;
2:
3: class InputOutput
4: {
5: public static void Main()
6: {
7: [Link]("Please enter your name: ");
8: string strName = [Link]();
9: [Link]("Hello {0}",strName);
10: }
11: }
Line 9 contains a [Link] statement that uses a formatted string. The
format string in this example is
"Hello {0}"
The {0} is replaced for the first variable following the format string in the argument
list of the WriteLine method. You can format up to three variables using this
technique:
[Link]("Hello {0} {1}, from {2}",
strFirstname, strLastname, strCity);
Of course, you are not limited to supplying only string variables. You can supply any
type, and types are discussed at length in Chapter 4, "C# Types."
Adding Comments
When writing code, you should also write comments to accompany that code—with
notes about implementation details, change history, and so on. Although what
information (if any) you provide in comments is up to you, you must stick to C#'s
way of writing comments. Listing 3.5 shows the two different approaches you can
take.
1: using System;
2:
3: class HelloWorld
4: {
5: public static void Main()
6: {
7: // this is a single-line comment
8: /* this comment spans
9: multiple lines */
10: [Link](/*"Hello World"*/);
11: }
12: }
The // characters denote a single-line comment. You can use // on a line of their
own, or they can follow a code statement:
int nMyVar = 10; // blah blah
Everything after the // on a line is considered a comment; therefore, you can also
use them to comment out an entire line or part of a line of source code. This is the
same kind of comment introduced in C++.
If you want a comment to span multiple lines, you must use the /* */ combination of
characters. This type of comment is available in C, and is also available in C++ and
C# in addition to single-line comments. Because C/C++ and C# share this multiline
comment type, they also share the same caveat. Look at the following line of code:
/* [Link]("Hello World"); */
I simply commented out the entire line using /* */. Now I assume that this line is
part of a larger piece of code, and I decide that I want to temporarily disable the
entire block:
/*
...
/* [Link]("Hello World"); */
...
*/
The problem with this construct is that the */ in the "Hello World" line closes the /*
of the first line. The remainder of the code is available to the compiler, and you will
see some interesting error messages. At least the last */ is flagged as an easily
attributable error. I just wanted to raise your awareness about such errors.
Summary
In this chapter, you created, compiled, and executed your first C# application: the
famous "Hello World" application. I used this sweet little application to introduce you
to the Main method, which is an application's entry point—and also its exit point.
This method can return either no result or an integer error level. If your application
is called with parameters, you can—but need not—read and use them.
After compiling and testing the application, you learned more about the input and
output methods that are provided by the Console object. They are just enough to
create meaningful console examples for learning C#, however, most of your user
interface will be WFC, WinForms, or ASP+.
Chapter 4 C# Types
Now that you know how to build a simple C# program, I'm introducing you to the
type system of C#. In this chapter, you learn how to use the different value and
reference types, and what the boxing and unboxing mechanism can do for you.
Although this chapter isn't heavy on examples, you learn a lot of important
information about how to build a program that gains most from the provided types.
Value Types
A variable of a certain value type always contains a value of that type. C# forces you
to initialize variables before you use them in a calculation—no more problems with
uninitialized variables because the compiler will tell you when you try to use them.
When assigning a value to a value type, the value is actually copied. In contrast, for
reference types, only the reference is copied; the actual value remains at the same
memory location, but now two objects point to it (reference it).
The value types of C# can be grouped as follows:
• Simple types
• struct types
• Enumeration types
Simple Types
The simple types that are present in C# share some characteristics. First, they are
all aliases of the NGWS system types. Second, constant expressions consisting of
simple types are evaluated only at compilation time, not at runtime. Last, simple
types can be initialized with literals.
The simple types of C# are grouped as follows:
• Integral types
• bool type
• char type (special case of integral type)
• Floating-point types
• The decimal type
Integral Types
There are nine integral types in C#: sbyte, byte, short, ushort, int, uint, long, ulong,
and char (discussed in a section of its own). They have the following characteristics:
• The sbyte type represents signed 8-bit integers with values between -128 and
127.
• The byte type represents unsigned 8-bit integers with values between 0 and
255.
• The short type represents signed 16-bit integers with values between -32,768
and 32,767.
• The ushort type represents unsigned 16-bit integers with values between 0
and 65,535.
• The int type represents signed 32-bit integers with values between -
2,147,483,648 and 2,147,483,647.
• The uint type represents unsigned 32-bit integers with values between 0 and
4,294,967,295.
• The long type represents signed 64-bit integers with values between -
9,223,372,036,854,775,808 and 9,223,372,036,854,775,807.
• The ulong type represents unsigned 64-bit integers with values between 0
and 18,446,744,073,709,551,615.
bool Type
The bool data type represents the Boolean values true and false. You can assign
either true or false to a Boolean variable, or you can assign an expression that
evaluates to either value:
bool bTest = (80 > 90);
In contrast to C and C++, in C#, the value true is no longer represented by any
nonzero value. There is no conversion between other integral types to bool to
enforce this convention.
char Type
The char type represents a single Unicode character. A Unicode character is 16 bits
in length, and it can be used to represent most of the languages in the world. You
can assign a character to a char variable as follows:
char chSomeChar = 'A';
In addition, you can assign a char variable via a hexadecimal escape sequence
(prefix \x) or Unicode representation (prefix \u):
char chSomeChar = '\x0065';
char chSomeChar = '\u0065';
There are no implicit conversions from char to other data types available. That
means treating a char variable just as another integral data type is not possible in
C#—this is another area where C programmers have to change habits. However, you
can perform an explicit cast:
char chSomeChar = (char)65;
int nSomeInt = (int)'A';
There are still the escape sequences (character literals) that there are in C. To
refresh your memory, take a look at Table 4.1.
Table 4.1 Escape Sequences
Floating-Point Types
Two data types are referred to as floating-point types: float and double. They differ
in range and precision:
When performing calculations with either of the floating-point types, the following
values can be produced:
Another rule for calculations is that if one of the types in an expression is a floating-
point type, all other types are converted to the floating-point type before the
calculation is performed.
The decimal type is a high-precision, 128-bit data type that is intended to be used
for financial and monetary calculations. It can represent values ranging from
approximately 1.0x10-28 to 7.9x1028 with 28 to 29 significant digits. It is important
to note that the precision is given in digits, not decimal places. Operations are exact
to a maximum of 28 decimal places.
As you can see, the range is narrower as for the double data type, however, it is
much more precise. Therefore, no implicit conversions exist between decimal and
double—in one direction you might generate an overflow; in the other you might lose
precision. You have to explicitly request conversion with a cast.
When defining a variable and assigning a value to it, use the m suffix to denote that
it is a decimal value:
decimal decMyValue = 1.0m;
If you omit the m, the variable will be treated as double by the compiler before it is
assigned.
struct Types
1: using System;
2:
3: struct IP
4: {
5: public byte b1,b2,b3,b4;
6: }
7:
8: class Test
9: {
10: public static void Main()
11: {
12: IP myIP;
13: myIP.b1 = 192;
14: myIP.b2 = 168;
15: myIP.b3 = 1;
16: myIP.b4 = 101;
17: [Link]("{0}.{1}.",myIP.b1,myIP.b2);
18: [Link]("{0}.{1}",myIP.b3,myIP.b4);
19: }
20: }
Enumeration Types
When you want to declare a distinct type consisting of a set of named constants, the
enum type is what you are looking for. In its most simple form, it can look like this:
enum MonthNames { January, February, March, April };
Because I stuck with the defaults, the enumeration elements are of type int, and the
first element has the value 0. Each successive element is increased by one. If you
want to assign an explicit value for the first element, you can do so by setting it to 1:
enum MonthNames { January=1, February, March, April };
If you want to assign arbitrary values to every element—even duplicate values—this
is no problem either:
enum MonthNames { January=31, February=28, March=31,
April=30 };
The final choice is a data type different from int. You can assign it in a statement like
this:
enum MonthNames : byte { January=31, February=28, March=31,
April=30 };
The types you can use are limited to long, int, short, and byte.
Reference Types
In contrast to value types, reference types do not store the actual data they
represent, but they store references to the actual data. The following reference types
are present in C# for you to use:
A class type can contain data members, function members, and nested types. Data
members are constants, fields, and events. Function members include methods,
properties, indexers, operators, constructors, and destructors. The functionality of
class and struct are very similar; however, as stated earlier, structs are value types
and classes are reference types.
In contrast to C++, only single inheritance is allowed. (You cannot have multiple
base classes from which a new object derives.) However, a class in C# can derive
from multiple interfaces, which are described in the next section.
Chapter 5, "Classes," is dedicated to programming with classes. This section is
intended only to give an overview of where C# classes fit into the type picture.
Interfaces
An interface declares a reference type that has abstract members only. Similar
concepts in C++ are members of a struct, and methods equal to zero. If you don't
know any of those concepts, here is what an interface actually does in C#: Only the
signature exists, but there is no implementation code at all. An implication of this is
that you cannot instantiate an interface, only an object that derives from that
interface.
You can define methods, properties, and indexers in an interface. So, what is so
special about an interface as compared to a class? When defining a class, you can
derive from multiple interfaces, whereas you can derive from only one class.
You might ask, "Okay, but I have to do all the implementation work for the
interface's members, so what do I gain from this approach?" I want to take an
example from the NGWS framework: Many classes implement the IDictionary
interface. You can get access to that interface with a simple cast:
IDictionary myDict = (IDictionary)someobjectthatsupportsit;
Now your code can access the dictionary. But wait, I said many classes can
implement this interface--therefore, you can reuse the code for accessing the
IDictionary interface in multiple places! Learn once, use everywhere.
When you decide to use interfaces in your class design, it is a good idea to learn
more about object-oriented design. This book cannot teach you those concepts.
However, you can learn how to build the interface. The following piece of code
defines the interface IFace, which has a single method:
interface IFace
{
void ShowMyFace();
}
As I mentioned, you cannot instantiate an object from this definition, but you can
derive a class from it. However, that class must implement the ShowMyFace abstract
method:
class CFace:IFace
{
public void ShowMyFace()
{
[Link]("implementation");
}
}
The only difference between interface members and class members is that interface
members do not have an implementation. Therefore, I won't duplicate information
presented in the next chapter.
Delegates
C programmers might be surprised, but yes, C# has a base type string for
manipulating string data. The string class derives directly from object, and it is
sealed, which means that you cannot derive from it. Just as with all other types,
string is an alias for a predefined class: [Link].
Its usage is very simple:
string myString = "some text";
Concatenation of strings is easy, too:
string myString = "some text" + " and a bit more";
And if you want to access a single character, all you need to do is access the
indexer:
char chFirst = myString[0];
When you compare two strings for equality, you simply use the == comparison
operator:
if (myString == yourString) ...
I just want to mention that although string is a reference type, the comparison it
performs compares the values, not the references (memory addresses).
The string type is used in almost every example in this book, and in the course of
these examples, I'll introduce you to some of the most interesting methods that are
exposed by the string object.
Arrays
An array contains variables that are accessed through computed indices. All variables
contained in an array--referred to as elements--must be of the same type. This type
is then called the "type of the array." Arrays can store integer objects, string objects,
or any type of object you can come up with.
The dimensions of an array are the so-called rank, which determines the number of
indices associated with an array element. The most commonly used array is a single
dimensional array (rank one). A multidimensional array has a rank greater than one.
Each dimension's index starts at zero and runs to dimension length minus one.
That should be enough theory. Let's take a look at an array that is initialized with an
array initializer:
string[] arrLanguages = { "C", "C++", "C#" };
This is, in effect, a shorthand for
arrLanguages[0]="C"; arrLanguages[1]="C++";
arrLanguages[2]="C#";
but the compiler does all the work for you. Of course, this would also work for
multidimensional array initializers:
int[,] arr = {{0,1}, {2,3}, {4,5}};
This is just a shorthand for
arr[0,0] = 0; arr[0,1] = 1;
arr[1,0] = 2; arr[1,1] = 3;
arr[2,0] = 4; arr[2,1] = 5;
If you do not want to initialize an array upfront, but do know its size, the declaration
looks like this:
int[,] myArr = new int[5,3];
If the size must be dynamically computed, the statement for array creation can be
written as
int nVar = 5;
int[] arrToo = new int[nVar];
As I stated at the beginning of this section, you may stuff anything inside an array as
long as all elements are of the same type. Therefore, if you want to put anything
inside one array, declare its type to be object.
Boxing Conversions
Boxing a value refers to implicitly converting any value type to the type object. When
a value type is boxed, an object instance is allocated and the value of the value type
is copied into the new object.
Look at the following example:
int nFunny = 2000;
object oFunny = nFunny;
The assignment in the second line implicitly invokes a boxing operation. The value of
the nFunny integer variable is copied to the object oFunny. Now both the integer
variable and the object variable exist on the stack, but the value of the object
resides on the heap.
So, what does that imply? The values are independent of each other—there is no link
between them. (oFunny does not reference the value of nFunny.) The following code
illustrates the consequences:
int nFunny = 2000;
object oFunny = nFunny;
oFunny = 2001;
[Link]("{0} {1}", nFunny, oFunny);
When the code changes the value of oFunny, the value of nFunny is not changed. As
long as you keep this copy behavior in mind, you'll be able to use the object
functionality of value types to your greatest advantage!
Unboxing Conversions
Summary
In this chapter, you learned about the various types that are available in C#. The
simple value types include integral, bool, char, floating-point, and decimal. These are
the types you will use most often for mathematical and financial calculations, as well
as for logical expressions.
Before diving into the reference types, I showed one look-alike to the class, the
struct type. It behaves almost like a class, but it is a value type, which makes it
more suitable for scenarios in which you need a large number of small objects.
The reference type section started with the mother of all objects, the object itself. It
is the base class for all objects in C#, and it is also used for boxing and unboxing of
value types. In addition, I took you on a tour of delegates, strings, and arrays.
The type that will most haunt you as C# programmer is the class. It is the heart of
object-oriented programming in C#, and the next chapter is entirely dedicated to
getting you up to speed with this exciting and powerful type.
Chapter 5 Classes
The previous chapter discussed data types and their usage at length. Now we move
on to the most important construct in C#—the class. Without a class, no single C#
program would compile. This chapter assumes that you know the basic building
blocks of a class: methods, properties, constructors, and destructors. C# adds to
these with indexers as well as events.
In this chapter, you learn about the following class-related topics:
public ~TestClass()
{
Release();
}
The invocation of the Release method in the destructor is not mandatory—the
garbage collection would take care of releasing the objects anyway. But it is good
practice not to forget to clean up.
Methods
Now that your object initializes and terminates properly, all that is left to do is to add
functionality to your class. In most cases, the major part of functionality is
implemented in methods. You have seen static methods in use already. However,
those are part of the type (class), but not of the instance (object).
To get you started quickly, I have arranged the nagging questions about methods
into three sections:
• Method parameters
• Overriding methods
• Method hiding
Method Parameters
For a method to process changing values, you somehow must pass the values into
the method, and also get back results from the method. The following three sections
deal with issues that arise from passing values in and getting results back to the
caller:
• In parameters
• ref parameters
• out parameters
In Parameters
A parameter type you have seen already in examples is the in parameter. You use an
in parameter to pass a variable by value to a method—the method's variable is
initialized with a copy of the value from the caller. Listing 5.1 demonstrates the use
of in parameters.
1: using System;
2:
3: public class SquareSample
4: {
5: public int CalcSquare(int nSideLength)
6: {
7: return nSideLength*nSideLength;
8: }
9: }
10:
11: class SquareApp
12: {
13: public static void Main()
14: {
15: SquareSample sq = new SquareSample();
16: [Link]([Link](25).ToString());
17: }
18: }
Because I pass the value and not a reference to a variable, I can use a constant
expression (25) when I call the method (see line 16). The integer result is passed
back to the caller as a return value, which is written to the console immediately
without storing it in an intermediary variable.
The in parameters work the way that C/C++ programmers are already used to. If
you come from Visual Basic, please note that no implicit ByVal or ByRef is done by
the compiler—if there is no modifier, the parameters are always passed by value.
This is the point at which I have to seemingly contradict my previous statement: For
certain variable types, by value actually means by reference. Confusing? Not with a
bit of background information: Everything in COM is an interface, and every class can
have one or more interfaces. An interface is nothing more than an array of function
pointers; it does not contain data. Duplicating this array would be a waste of
memory resources; therefore, only the start address is copied to the method, which
still points to the same address of the interface as the caller. That's why objects pass
a reference by value.
ref Parameters
Although you can create many methods using in parameters and return values, you
are out of luck as soon as you want to pass a value and have it modified in place
(the same memory location, that is). That is where the reference parameter comes
in handy:
void myMethod(ref int nInOut)
Because you pass a variable to the method (and not its value only), the variable
nInOut must be initialized. Otherwise, the compiler will complain. Listing 5.2 shows
how to create a method with a ref parameter.
1: // class SquareSample
2: using System;
3:
4: public class SquareSample
5: {
6: public void CalcSquare(ref int nOne4All)
7: {
8: nOne4All *= nOne4All;
9: }
10: }
11:
12: class SquareApp
13: {
14: public static void Main()
15: {
16: SquareSample sq = new SquareSample();
17:
18: int nSquaredRef = 20; // must be initialized
19: [Link](ref nSquaredRef);
20: [Link]([Link]());
21: }
22: }
As you can see, all you have to do is to add the ref modifier to both the definition
and the call. Because the variable is passed by reference, you can use it to compute
the result and pass back the result. However, in a real-world application, I strongly
recommend having two variables, one in parameter and one ref parameter.
out Parameters
1: using System;
2:
3: public class SquareSample
4: {
5: public void CalcSquare(int nSideLength, out int
nSquared)
6: {
7: nSquared = nSideLength * nSideLength;
8: }
9: }
10:
11: class SquareApp
12: {
13: public static void Main()
14: {
15: SquareSample sq = new SquareSample();
16:
17: int nSquared; // need not be initialized
18: [Link](15, out nSquared);
19: [Link]([Link]());
20: }
21: }
Overriding Methods
1: using System;
2:
3: class Triangle
4: {
5: public virtual double ComputeArea(int a, int b, int c)
6: {
7: // Heronian formula
8: double s = (a + b + c) / 2.0;
9: double dArea = [Link](s*(s-a)*(s-b)*(s-c));
10: return dArea;
11: }
12: }
13:
14: class RightAngledTriangle:Triangle
15: {
16: public override double ComputeArea(int a, int b, int
c)
17: {
18: double dArea = a*b/2.0;
19: return dArea;
20: }
21: }
22:
23: class TriangleTestApp
24: {
25: public static void Main()
26: {
27: Triangle tri = new Triangle();
28: [Link]([Link](2, 5, 6));
29:
30: RightAngledTriangle rat = new RightAngledTriangle();
31: [Link]([Link](3, 4, 5));
32: }
33: }
The base class Triangle defines the method ComputeArea. It takes three integer
parameters, returns a double result, and is publicly accessible. Derived from the
class Triangle is RightAngledTriangle, which overrides the ComputeArea method and
implements its own area calculation formula. Both classes are instantiated and tested
in the Main() method of the test application class named TriangleTestApp.
I owe you an explanation for line 14:
class RightAngledTriangle : Triangle
The colon (:) in the class statement denotes that RightAngledTriangle derives from
the class Triangle. That is all you have to do to let C# know that you want Triangle
as the base class for RightAngledTriangle.
When you take a close look at the ComputeArea method for a right-angle triangle,
you will see that the third parameter isn't used for the calculation. However, one can
create a "right angleness" check by using the third parameter as shown in Listing
5.5.
Listing 5.5 Calling the Base Class Implementation
1: class RightAngledTriangle:Triangle
2: {
3: public override double ComputeArea(int a, int b, int
c)
4: {
5: const double dEpsilon = 0.0001;
6: double dArea = 0;
7: if ([Link]((a*a + b*b - c*c)) > dEpsilon)
8: {
9: dArea = [Link](a,b,c);
10: }
11: else
12: {
13: dArea = a*b/2.0;
14: }
15:
16: return dArea;
17: }
18: }
The check is simply the formula of Pythagoras, which must yield zero for a right-
angled triangle. If the result differs from zero (and a delta epsilon), the class calls
the ComputeArea implementation of its base class:
dArea = [Link](a,b,c);
The point of the example is that you can easily call the base class implementation of
an overridden method explicitly using the base. qualifier. This is very helpful when
you need the functionality implemented in the base class, but don't want to duplicate
it in the overridden method.
Method Hiding
A different way of redefining methods is to hide base class methods. This feature is
especially valuable when you derive from a class provided by someone else. Look at
Listing 5.6, and assume that BaseClass was written by someone else and that you
derived DerivedClass from it.
Listing 5.6 Derived Class Implements a Method Not Contained in the Base
Class
1: using System;
2:
3: class BaseClass
4: {
5: }
6:
7: class DerivedClass:BaseClass
8: {
9: public void TestMethod()
10: {
11: [Link]("DerivedClass::TestMethod");
12: }
13: }
14:
15: class TestApp
16: {
17: public static void Main()
18: {
19: DerivedClass test = new DerivedClass();
20: [Link]();
21: }
22: }
In this example, your DerivedClass implements an additional feature via
TestMethod(). However, what happens if the developer of the base class thinks that
TestMethod() is a good idea to have in the base class, and implements it with the
same signature? (See Listing 5.7.)
Listing 5.7 Base Class Implements the Same Method as Derived Class
1: class BaseClass
2: {
3: public void TestMethod()
4: {
5: [Link]("BaseClass::TestMethod");
6: }
7: }
8:
9: class DerivedClass:BaseClass
10: {
11: public void TestMethod()
12: {
13: [Link]("DerivedClass::TestMethod");
14: }
15: }
In a classic programming language, you would now have a really big problem. C#,
however, offers you some advice:
[Link](13,14): warning CS0114:
'[Link]()' hides inherited member
'[Link]()'. To make the current method
override that implementation, add the override keyword.
Otherwise add the new keyword.
With the modifier new, you can tell the compiler that your method should hide the
newly added base class method, without you having to rewrite your derived class or
code using your derived class. Listing 5.8 shows how to use the new modifier in the
example.
Listing 5.8 Hiding the Method of the Base Class
1: class BaseClass
2: {
3: public void TestMethod()
4: {
5: [Link]("BaseClass::TestMethod");
6: }
7: }
8:
9: class DerivedClass:BaseClass
10: {
11: new public void TestMethod()
12: {
13: [Link]("DerivedClass::TestMethod");
14: }
15: }
With the addition of the new modifier, the compiler knows that you redefine the base
class's method, and that it should hide the base class method. However, if you do
the following
DerivedClass test = new DerivedClass();
((BaseClass)test).TestMethod();
the base class's implementation of TestMethod() is invoked. This behavior is different
from overriding the method, where one is guaranteed that the most-derived method
is called.
Class Properties
There are two ways to expose named attributes for a class—either via fields or via
properties. The former are implemented as member variables with public access; the
latter do not correspond directly to a storage location, but are accessed via
accessors.
The accessors specify the statements that are executed when you want to read or
write the value of a property. The accessor for reading a property's value is marked
by the keyword get, and the accessor for modifying a value is marked by set.
Before you become cross-eyed from the theory, take a look at the example in Listing
5.9. The property SquareFeet is implemented with get and set accessors.
1: using System;
2:
3: public class House
4: {
5: private int m_nSqFeet;
6:
7: public int SquareFeet
8: {
9: get { return m_nSqFeet; }
10: set { m_nSqFeet = value; }
11: }
12: }
13:
14: class TestApp
15: {
16: public static void Main()
17: {
18: House myHouse = new House();
19: [Link] = 250;
20: [Link]([Link]);
21: }
22: }
The class House has one property named SquareFeet, which can be read and
written. The actual value is stored in a variable that is accessible from inside the
class—if you want to rewrite it as a field, all you would have to do is leave out the
accessors and redefine the variable as
public int SquareFeet;
For a variable that is simple such as this one, it would be okay. However, if you want
to hide details about the inner storage structure of your class, you should resort to
accessors. In this case, the set accessor is passed the new value for the property in
the value parameter. (You can't rename that; see line 10.)
Besides being able to hide implementation details, you are also free to define which
operations are allowed:
• get and set implemented: Read and write access to the property are allowed.
• get only: Reading the property value is allowed.
• set only: Setting the property's value is the only possible operation.
In addition, you gain the chance to implement validation code in the set accessor.
For example, you are able to reject a new value for any reason (or none at all). And
best of all, no one tells you that it can't be a dynamic property—one that comes into
existence only when you request it for the first time, thus delaying resource
allocation as long as possible.
Indexers
Did you ever want to include easy indexed access to your class, just like an array?
The wait is over with the indexer feature of C#.
Basically, the syntax looks like this:
attributes modifiers declarator { declarations }
A sample implementation could be
public string this[int nIndex]
{
get { ... }
set { ... }
}
This indexer returns or sets a string at a given index. It has no attributes, but uses
the public modifier. The declarator part consists of type string and this to denote the
class's indexer.
The implementation rules for get and set are the same as for properties. (You can
drop either one.) There is one difference, though: You are almost free in defining the
parameter list in the square brackets. The restrictions are that you must specify at
least one parameter, and ref and out modifiers are not allowed.
The this keyword warrants an explanation. Indexers do not have user-defined
names, and this denotes the indexer on the default interface. If your class
implements multiple interfaces, you can add more indexers denoted with
[Link].
To demonstrate the use of an indexer, I created a small class that is capable of
resolving a hostname to an IP address—or, as is the case for
[Link] resolving to a list of IP addresses. This list is accessible
via an indexer, and you can take a look at the implementation in Listing 5.10.
1: using System;
2: using [Link];
3:
4: class ResolveDNS
5: {
6: IPAddress[] m_arrIPs;
7:
8: public void Resolve(string strHost)
9: {
10: IPHostEntry iphe = [Link](strHost);
11: m_arrIPs = [Link];
12: }
13:
14: public IPAddress this[int nIndex]
15: {
16: get
17: {
18: return m_arrIPs[nIndex];
19: }
20: }
21:
22: public int Count
23: {
24: get { return m_arrIPs.Length; }
25: }
26: }
27:
28: class DNSResolverApp
29: {
30: public static void Main()
31: {
32: ResolveDNS myDNSResolver = new ResolveDNS();
33: [Link]("[Link]
34:
35: int nCount = [Link];
36: [Link]("Found {0} IP's for hostname",
nCount);
37: for (int i=0; i < nCount; i++)
38: [Link](myDNSResolver[i]);
39: }
40: }
To resolve the hostname, I use the DNS class that is part of the [Link]
namespace. However, because this namespace is not contained in the core library, I
had to reference the library in my compiler statement:
csc /r:[Link] /out:[Link] [Link]
The resolver code is straightforward. In the Resolve method, the code calls the static
GetHostByName method of the DNS class, which returns an IPHostEntry object. This
object, in turn, contains the array I am looking for—the AddressList array. Before
exiting the Resolve method, I store a copy the AddressList array (objects of type
IPAddress are stored inside it) locally in the object's instance member m_arrIPs.
With the array now populated, the application code can enumerate the IP addresses
in lines 37 and 38 by using the indexer implemented in the class ResolveDNS. (There
is more information about for statements in Chapter 6, "Control Statements.")
Because there is no way to modify the IP addresses, only get is implemented for the
indexer. For simplicity's sake, I leave out-of-bounds checking to the array.
Events
When you write a class, you sometimes have a need to let clients of your class know
that a certain event has occurred. If you are a longtime programmer, you have seen
many different ways of achieving this, including function pointers for callback and
event sinks for ActiveX controls. Now you are going to learn another way of
attaching client code to class notifications—with events.
Events can be declared either as class fields (member variables) or as properties.
Both approaches share the commonality that the event's type must be delegate,
which is C#'s equivalent to a function pointer prototype.
Each event can be consumed by zero or more clients, and a client can attach and
detach from the event at any time. You can implement the delegates as either static
or instance methods, with the latter being a welcome feature for C++ programmers.
Now that I have mentioned all the main features of events as well as the
corresponding delegates, please take a look at the example in Listing 5.11. It
presents the theory in action.
1: using System;
2:
3: // forward declaration
4: public delegate void EventHandler(string strText);
5:
6: class EventSource
7: {
8: public event EventHandler TextOut;
9:
10: public void TriggerEvent()
11: {
12: if (null != TextOut) TextOut("Event triggered");
13: }
14: }
15:
16: class TestApp
17: {
18: public static void Main()
19: {
20: EventSource evsrc = new EventSource();
21:
22: [Link] += new EventHandler(CatchEvent);
23: [Link]();
24:
25: [Link] -= new EventHandler(CatchEvent);
26: [Link]();
27:
28: TestApp theApp = new TestApp();
29: [Link] += new
EventHandler([Link]);
30: [Link]();
31: }
32:
33: public static void CatchEvent(string strText)
34: {
35: [Link](strText);
36: }
37:
38: public void InstanceCatch(string strText)
39: {
40: [Link]("Instance " + strText);
41: }
42: }
Line 4 declares the delegate (the event method prototype), which is used to declare
the TextOut event field for the EventSource class in line 8. You can view the delegate
declaration as a new kind of type that can be used when declaring events.
The class has only one method, which allows us to trigger the event. Note that you
have to check the event field against null because it could happen that no one is
interested in the event.
The class TestApp houses the Main method, as well as two methods with the
necessary signature for the event. One of the methods is static, and the other is an
instance method.
The EventSource class is instantiated, and the static method is subscribed to the
TextOut event:
[Link] += new EventHandler(CatchEvent);
From now on, this method is called when the event is triggered. If you are no longer
interested in the event, simply unsubscribe:
[Link] -= new EventHandler(CatchEvent);
Note that you cannot unsubscribe handlers at will—only those that were created in
your class's code. To prove that event handlers work with instance methods, too, the
remaining code creates an instance of TestApp and hooks up the event handler
method.
Where will events be most useful for you? You will often deal with events and
delegates in ASP+ as well as when using the WFC (Windows Foundation Classes).
Applying Modifiers
During the course of this chapter, you have already seen modifiers such as public,
virtual, and so on. To summarize them in an easily accessible manner, I have split
them into the following three sections:
• Class modifiers
• Member modifiers
• Access modifiers
Class Modifiers
So far, I haven't dealt with class modifiers other than the access modifiers applied to
classes. However, there are two modifiers you can use for classes:
To see both modifiers in action, look at Listing 5.12, which creates a sealed class
based on an abstract one (definitely a quite extreme example).
1: using System;
2:
3: abstract class AbstractClass
4: {
5: abstract public void MyMethod();
6: }
7:
8: sealed class DerivedClass:AbstractClass
9: {
10: public override void MyMethod()
11: {
12: [Link]("sealed class");
13: }
14: }
15:
16: public class TestApp
17: {
18: public static void Main()
19: {
20: DerivedClass dc = new DerivedClass();
21: [Link]();
22: }
23: }
Member Modifiers
The number of class modifiers is small compared to the number of member modifiers
that are available. I have already presented some of these, and forthcoming
examples in this book describe the other member modifiers.
The following member modifiers are available:
Access modifiers define the level of access that certain code has to class members,
such as methods and properties. You have to apply the desired access modifier to
each member; otherwise, the default access type is implied.
You can apply one of the following four access modifiers:
To illustrate the use of access modifiers, I have modified the Triangle example just a
bit to contain additional fields and a new derived class (see Listing 5.13).
1: using System;
2:
3: internal class Triangle
4: {
5: protected int m_a, m_b, m_c;
6: public Triangle(int a, int b, int c)
7: {
8: m_a = a;
9: m_b = b;
10: m_c = c;
11: }
12:
13: public virtual double Area()
14: {
15: // Heronian formula
16: double s = (m_a + m_b + m_c) / 2.0;
17: double dArea = [Link](s*(s-m_a)*(s-m_b)*(s-m_c));
18: return dArea;
19: }
20: }
21:
22: internal class Prism:Triangle
23: {
24: private int m_h;
25: public Prism(int a, int b, int c, int h):base(a,b,c)
26: {
27: m_h = h;
28: }
29:
30: public override double Area()
31: {
32: double dArea = [Link]() * 2.0;
33: dArea += m_a*m_h + m_b*m_h + m_c*m_h;
34: return dArea;
35: }
36: }
37:
38: class PrismApp
39: {
40: public static void Main()
41: {
42: Prism prism = new Prism(2,5,6,1);
43: [Link]([Link]());
44: }
45: }
Both the Triangle and the Prism class are now marked as internal. This means that
they are accessible only in the current NGWS component. Please remember that the
term NGWS component refers to packaging, and not to the component that you may
be used to from COM+. The Triangle class has three protected members, which are
initialized in the constructor and used in the Area calculation method. Because these
members are protected, I can access them in the derived class Prism to perform a
different Area calculation there. Prism itself adds an additional member m_h, which
is private—not even a derived class could access it.
It is generally a good idea to invest time in planning the kind of protection level you
want for each class member, and even for each class. Thorough planning helps you
later when changes need to be introduced because no programmer could have
possibly used "undocumented" functionality of your class.
Summary
This chapter showed the various elements of a class, which is the template for the
running instances, the objects. The first code that is executed in the lifetime of an
object is the constructor. The constructor is used to initialize variables, which can be
later used in methods to compute results.
Methods enable you to pass values, pass references to variables, or transport an
output value only. Methods can be overridden to implement new functionality, or you
can hide base class members that implement a method with the same signature.
Named attributes can be implemented either as fields (member variables) or
property accessors. The latter are get and set accessors, and by leaving out one or
the other, you can create write-only or read-only properties. Accessors are well
suited for validation of value assignment to properties.
Another feature of a C# class is indexers, which make it possible to access values in
a class with an array-like syntax. And, if you want clients to be notified when
something happens in your class, you can have them subscribe to events.
The life of an object ends when the garbage collector invokes the destructor.
Because you cannot determine exactly when this will happen, you should create a
method to release expensive resources as soon as you are done using them.
Chapter 6 Control Statements
There is one kind of statement that you will find in every programming language--
control of flow statements. In this chapter, I present C#'s control statements, split
into two major sections:
• Selection statements
• Iteration statements
If you are a C or C++ programmer, most of this information will look very familiar to
you; however, there are some differences you must be aware of.
Selection Statements
When employing selection statements, you define a controlling statement whose
value controls which statement is executed. Two selection statements are available
in C#:
• The if statement
• The switch statement
The if Statement
The first and most commonly used selection statement is the if statement. Whether
the embedded statement is executed is determined by a Boolean expression:
if (boolean-expression) embedded-statement
Of course, you also can have an else branch that is executed when the Boolean
expression evaluates to false:
if (boolean-expression) embedded-statement else embedded-
statement
An example is to check for a nonzero-length string before executing certain
statements:
if (0 != [Link])
{
}
This is a Boolean expression. (!= means not equal.) However, if you come from C or
C++, you might be used to writing code like this:
if ([Link])
{
}
This no longer works in C# because the if statement only allows for results of the
bool data type, and the Length property of the string object returns an integer. The
compiler will complain with this error message:
error CS0029: Cannot implicitly convert type 'int' to
'bool'
The downside is that you have to change your habits; however, the upside is that
you will never again be bitten with assignment errors in if clauses:
if (nMyValue = 5) ...
The correct code would be
if (nMyValue == 5) ...
because comparison for equality is performed with ==, just as in C and C++. Look at
the following available comparison operators (not all are valid for every data type,
though!):
1: using System;
2:
3: class NestedIfApp
4: {
5: public static int Main(string[] args)
6: {
7: if ([Link] != 1)
8: {
9: [Link]("Usage: one argument");
10: return 1; // error level
11: }
12:
13: char chLetter = args[0][0];
14:
15: if (chLetter >= 'A')
16: if (chLetter <= 'Z')
17: {
18: [Link]("{0} is uppercase",chLetter);
19: return 0;
20: }
21:
22: chLetter = [Link](args[0]);
23: if (chLetter >= 'a' && chLetter <= 'z')
24: [Link]("{0} is lowercase",chLetter);
25:
26: if ([Link]((chLetter = args[0][0])))
27: [Link]("{0} is a digit",chLetter);
28:
29: return 0;
30: }
31: }
The first if block starting in line 7 checks for the existence of exactly one string in the
args array. If the condition is not met, the program writes a usage message to the
console and terminates.
Extracting a single character from a string can be done in multiple ways--either by
using the char indexer as shown in line 13 or by using the static FromString method
of the Char class, which returns the first character of a string.
The if block in lines 16-20 checks for an uppercase letter by using a nested if block.
Checking for a lowercase letter is done using the logical AND operator (&&), and the
final check for digits is performed using the IsDigit static function of the Char class.
Besides the && operator, there is a second conditional logical operator, which is ||
for OR. Both conditional logical operators are short-circuited. For the && operator,
that means the first non-true result of a conditional AND expression returns false,
and the remaining conditional AND expressions are not evaluated. The || operator, in
contrast, is short-circuited when the first true conditional is met.
What I want to get across is that, to cut computing time, you should put the
expression that is most likely to short-circuit the evaluation at the front. Also, you
should be aware that computing certain values in an if statement is potentially
dangerous:
if (1 == 1 || (5 == (strLength=[Link])))
{
[Link](strLength);
}
This is, of course, a greatly exaggerated example, but it shows the point--the first
statement evaluates to true, and hence the second statement is not executed, which
leaves the variable strLength at its original value. It is good advice to never put
assignments in if statements that have conditional logical operators!
In contrast to the if statement, the switch statement has one controlling expression
and embedded statements are executed based on the constant value of the
controlling expression they are associated with. The general syntax of the switch
statement looks like this:
switch (controlling-expression)
{
case constant-expression:
embedded-statements
default:
embedded-statements
}
The allowed data types for the controlling expression are sbyte, byte, short, ushort,
uint, long, ulong, char, string, or an enumeration type. As long as an implicit
conversion to any of these types exists for a different data type, it is fine to use it as
controlling expression, too.
The switch statement is executed in the following order:
1. The controlling expression is evaluated.
2. If a constant expression in a case label matches the value of the evaluated
controlling expression, the embedded statements are executed.
3. If no constant expression matches the controlling expression, the embedded
statements in the default label are executed.
4. If there is no match for a case label, and there is no default label, control is
transferred to the end of the switch block.
Before moving on to more details of the switch statement, take a look at Listing 6.2,
which shows a switch statement in action for displaying the number of days in a
month (ignoring leap years).
1: using System;
2:
3: class FallThrough
4: {
5: public static void Main(string[] args)
6: {
7: if ([Link] != 1) return;
8:
9: int nMonth = [Link](args[0]);
10: if (nMonth < 1 || nMonth > 12) return;
11: int nDays = 0;
12:
13: switch (nMonth)
14: {
15: case 2: nDays = 28; break;
16: case 4:
17: case 6:
18: case 9:
19: case 11: nDays = 30; break;
20: default: nDays = 31;
21: }
22: [Link]("{0} days in this month",nDays);
23: }
24: }
The switch block is contained in lines 13-21. For a C programmer, this looks very
familiar because it doesn't use break statements. However, there is one important
difference that makes life easier: You must add the break statement (or a different
jump statement) because the compiler will complain that fall-through to the next
section is not allowed in C#.
What is fall-through? In C (and C++), it was perfectly legal to leave out break and
write the following code:
nVar = 1
switch (nVar)
{
case 1:
DoSomething();
case 2:
DoMore();
}
In this example, after executing the code for the first case statement, execution
would fall-through and execute code in other case labels until a break statement
exits the switch block. Although this is sometimes a powerful feature, more often it
was the cause of hard-to-find bugs. That is why you don't find fall-through in C#.
But what if you want to execute code in other case labels? There is a way, and it is
shown in Listing 6.3.
Listing 6.3 Using goto label and goto default in a switch Statement
1: using System;
2:
3: class SwitchApp
4: {
5: public static void Main()
6: {
7: Random objRandom = new Random();
8: double dRndNumber = [Link]();
9: int nRndNumber = (int)(dRndNumber * 10.0);
10:
11: switch (nRndNumber)
12: {
13: case 1:
14: // do nothing
15: break;
16: case 2:
17: goto case 3;
18: case 3:
19: [Link]("Handler for 2 and 3");
20: break;
21: case 4:
22: goto default;
23: // everything beyond a goto will be warned as
24: // unreachable code
25: default:
26: [Link]("Random number {0}", nRndNumber);
27: }
28: }
29: }
In this example, I generate the value to be used as the controlling expression via the
Random class (lines 7-9). The switch block contains two jump statements that are
valid for the switch statement:
Iteration Statements
When you want to execute a certain statement or block of statements repeatedly,
C# offers you a choice of four different iteration statements to use depending on the
task at hand:
1: using System;
2:
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = [Link](args[0]);
9:
10: long nCurDig = 1;
11: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
12: nFactorial *= nCurDig;
13:
14: [Link]("{0}! is {1}",nComputeTo,
nFactorial);
15: }
16: }
The example is overly lengthy, but it serves as a starting point to show what one can
do with for statements. First, I could have declared the variable nCurDig inside the
initializer part:
for (long nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
nFactorial *= nCurDig;
Another option would have been to leave out the initializer as in the following line,
because line 10 initializes the variable outside the for statement. (Remember: C#
requires initialized variables!):
for (;nCurDig <= nComputeTo; nCurDig++) nFactorial *=
nCurDig;
Another change might be to move the ++ operation to the summation embedded
statement:
for ( ;nCurDig <= nComputeTo; ) nFactorial *= nCurDig++;
If I also want to get rid of the conditional statement, all I have to do is add an if
statement to terminate the loop using a break statement:
for (;;)
{
if (nCurDig > nComputeTo) break;
nFactorial *= nCurDig++;
}
Besides the break statement, which is used to exit the for statement, you can use
continue to skip the current iteration and continue with the next.
for (;nCurDig <= nComputeTo;)
{
if (5 == nCurDig) continue; // this "jumps" over remaining
code
nFactorial *= nCurDig++;
}
A feature that has been present in Visual Basic languages for a long time is that of
collection enumeration by using the For Each statement. C# also has a command for
enumerating elements of a collection via the foreach statement:
foreach (Type identifier in expression) embedded-statement
The iteration variable is declared by type and identifier, and expression corresponds
to the collection. The iteration variable represents the collection element for which an
iteration is currently performed.
You have to be aware that you cannot assign a new value to the iteration variable,
nor can you pass it to a function as a ref or out parameter. This refers to code that is
executed in the embedded statement.
How can you tell whether a certain class supports the foreach statement? The short
version is that the class must support a method with the signature GetEnumerator(),
and the struct, class, or interface returned by it must have the public method
MoveNext() and the public property Current. If you want to know more, please look
at the language reference, which has a lot of detail on this topic.
For the example in Listing 6.5, I happened to pick a class that, by chance,
implements all these requirements. I use it to enumerate all environment variables
that are defined.
1: using System;
2: using [Link];
3:
4: class EnvironmentDumpApp
5: {
6: public static void Main()
7: {
8: IDictionary envvars =
[Link]();
9: [Link]("There are {0} environment
variables declared", [Link]);
10: foreach (String strKey in [Link])
11: {
12: [Link]("{0} = {1}",strKey,
envvars[strKey].ToString());
13: }
14: }
15: }
The call to GetEnvironmentVariables (line 8) returns an interface of type IDictionary,
which is the dictionary interface implemented by many classes in the NGWS
framework. Two collections are accessible through the IDictionary interface: Keys
and Values. In this example, I use Keys in the foreach statement, and then do a
lookup for the value based on the current key value (line 12).
There is one single caution when using foreach: You should take extra care when
deciding about the type of the iteration variable. Choosing a wrong type isn't
necessarily detected by the compiler, but it is detected at runtime and it causes an
exception.
When you want to execute an embedded statement zero or more times, the while
statement is what you are looking for:
while (conditional) embedded-statement
The conditional statement—it is once again a Boolean expression—controls how often
(if at all) the embedded statement is executed. You can use the break and continue
statements to control execution in the while statement, which behave exactly the
same way as in the for statement.
To illustrate the usage of while, Listing 6.6 shows you how to use the StreamReader
class to output a C# source file to the console.
1: using System;
2: using [Link];
3:
4: class WhileDemoApp
5: {
6: public static void Main()
7: {
8: StreamReader sr = [Link] ("[Link]");
9: String strLine = null;
10:
11: while (null != (strLine = [Link]()))
12: {
13: [Link](strLine);
14: }
15:
16: [Link]();
17: }
18: }
The code opens the file [Link], and while the method ReadLine returns a
string different from null, outputs the read string to the console. Note that I use an
assignment in the while conditional. If there were more conditions linked with either
&& or ||, I shouldn't rely on the fact that they are executed because of possible
short-circuiting.
The do Statement
The final iteration statement available with C# is the do statement. It is very similar
to the while statement, only the condition is checked after the first iteration:
do
{
embedded statements
}
while (condition);
The do statement guarantees at least one execution of the embedded statements,
and as long as the condition evaluates to true, they continue to be executed. You can
force execution to leave the do block by using the break statement. If you want to
skip only one iteration, use the continue statement.
An example of how to use a do statement is presented in Listing 6.7. It requests one
or more numbers from the user, and computes the average when execution leaves
the do loop.
1: using System;
2:
3: class ComputeAverageApp
4: {
5: public static void Main()
6: {
7: ComputeAverageApp theApp = new ComputeAverageApp();
8: [Link]();
9: }
10:
11: public void Run()
12: {
13: double dValue = 0;
14: double dSum = 0;
15: int nNoOfValues = 0;
16: char chContinue = 'y';
17: string strInput;
18:
19: do
20: {
21: [Link]("Enter a value: ");
22: strInput = [Link]();
23: dValue = [Link](strInput);
24: dSum += dValue;
25: nNoOfValues++;
26: [Link]("Read another value?");
27:
28: strInput = [Link]();
29: chContinue = [Link](strInput);
30: }
31: while ('y' == chContinue);
32:
33: [Link]("The average is {0}",dSum /
nNoOfValues);
34: }
35: }
In this example, I instantiate an object of type ComputeAverageApp in the static
Main function. It also then invokes the Run method of the instance, which contains
all functionality necessary to compute the average.
The do loop spans lines 19-31. The condition is designed around whether the user
decides to add another value by answering y to the respective question. Any other
character causes execution to exit the do block, and the average is computed.
As you can see from the example presented, the do statement does not differ much
from the while statement—the only difference is when the condition is evaluated.
Summary
This chapter explained how to use the various selection and iteration statements that
are available in C#. The if statement is the statement you are likely to use most
often in your programs. The compiler will take care for you when it comes to
enforcing Boolean expressions. However, you must make sure that the short-
circuiting of conditional statements doesn't prevent necessary code from executing.
The switch statement—although also similar to its counterpart in the C world—has
been improved, too. Fall-throughs are no longer supported, and you can use string
labels, which are new for C programmers.
In the last part of this chapter, I showed how to use the for, foreach, while, and do
statements. The statements fulfill various needs, including executing a fixed number
of iterations, enumerating collection elements, and executing statements an arbitrary
number of times based on some condition.
Chapter 7 Exception Handling
One big advantage of the NGWS runtime is that exception handling is standardized
across languages. An exception thrown in C# can be handled in a Visual Basic client.
No more HRESULTs or ISupportErrorInfo interfaces.
Although that cross-language exception handling is great, this chapter focuses
entirely on C# exception handling. First, you slightly change the overflow-handling
behavior of the compiler, and then the fun begins: You handle the exceptions. To
add a further twist, you later throw exceptions that you created.
1: using System;
2:
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = [Link](args[0]);
9:
10: long nCurDig = 1;
11: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
12: nFactorial *= nCurDig;
13:
14: [Link]("{0}! is {1}",nComputeTo,
nFactorial);
15: }
16: }
When you execute the program with a command line such as
factorial 2000
the result presented is 0, and nothing else happens. Therefore, it is safe to assume
that C# silently handles overflow situations and does not explicitly warn you.
You can change this behavior by enabling overflow checking either for the entire
application (via a compiler switch) or on a statement-by-statement basis. Each of the
following two sections tackles one of the solutions.
Compiler Settings for Overflow Checking
If you want to control overflow checking for the entire application, the C# compiler
setting checked is what you are looking for. By default, overflow checking is
disabled. To explicitly request it, run the following compiler command:
csc [Link] /checked+
Now when you execute the application with a parameter of 2000, the NGWS runtime
notifies you about the overflow exception (see Figure 7.1).
Figure 7.1
With overflow checking enabled, the factorial code generates an exception.
Dismissing the dialog box with the OK button reveals the exception message:
Exception occurred: [Link]
at [Link]([Link][])
Now you know that overflow conditions throw a [Link]. How to
catch and handle such an exception is presented after we finish programmatic
overflow checking in the next section.
If you do not want to enable overflow checking for your entire application, you might
be more comfortable by enabling it only for certain code blocks. For this scenario,
you can use checked statement as presented in Listing 7.2.
1: using System;
2:
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = [Link](args[0]);
9:
10: long nCurDig = 1;
11:
12: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
13: checked { nFactorial *= nCurDig; }
14:
15: [Link]("{0}! is {1}",nComputeTo,
nFactorial);
16: }
17: }
Even if you compile this code with the flag checked-, overflow checking is still
performed for the multiplication in line 13 because a checked statement encloses it.
The error message will remain the same.
A statement that exhibits the opposite behavior is unchecked. Even if you enable
overflow checking (checked+ flag for the compiler), the code enclosed by the
unchecked statement will not raise overflow exceptions:
unchecked
{
nFactorial *= nCurDig;
}
Exception-Handling Statements
Now that you know how to generate an exception (and you'll find many more ways,
trust me), there is still the question of how to deal with it. If you are a C++ WIN32
programmer, you are definitely familiar with SEH (Structured Exception Handling).
You will find it comforting that the commands in C# are almost the same, and that
they also behave in a similar way.
The following three sections introduce C#'s exception-handling statements:
1: using System;
2:
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1;
8: long nComputeTo = [Link](args[0]);
9:
10: try
11: {
12: checked
13: {
14: for (;nCurDig <= nComputeTo; nCurDig++)
15: nFactorial *= nCurDig;
16: }
17: }
18: catch (OverflowException oe)
19: {
20: [Link]("Computing {0} caused an overflow
exception", nComputeTo);
21: return;
22: }
23:
24: [Link]("{0}! is {1}",nComputeTo,
nFactorial);
25: }
26: }
For clarity, I have expanded some of the code blocks, and I have also made sure
that exceptions are generated using the checked statement even if you forget the
compiler setting.
Exception handling is really no big deal, as you can see. All you need to do is to
enclose the exception-prone code in a try statement, and then catch the exception,
which, in this case, is of type OverflowException. Whenever an exception is thrown,
the code in the catch block takes care of proper processing.
If you do not know in advance which kind of exception to expect but still want to be
on the safe side, you can simply omit the type of the exception:
try
{
...
}
catch
{
...
}
However, with this approach, you cannot get access to the exception object, which
contains important error information. The generalized exception-handling code then
looks like this:
try
{
...
}
catch([Link] e)
{
...
}
Note that you cannot pass the e object to a method with ref or out modifiers, nor can
you assign it a different value.
If you are more concerned about cleanup than error handling, the try and finally
construct will catch your fancy. It does not suppress the error message, but all the
code contained in the finally block is still executed after the exception is raised.
Although your program terminates abnormally, you can get a message to the user,
as shown in Listing 7.4.
1: using System;
2:
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1;
8: long nComputeTo = [Link](args[0]);
9: bool bAllFine = false;
10:
11: try
12: {
13: checked
14: {
15: for (;nCurDig <= nComputeTo; nCurDig++)
16: nFactorial *= nCurDig;
17: }
18: bAllFine = true;
19: }
20: finally
21: {
22: if (!bAllFine)
23: [Link]("Computing {0} caused an overflow
exception", nComputeTo);
24: else
25: [Link]("{0}! is {1}",nComputeTo,
nFactorial);
26: }
27: }
28: }
By examining the code, you might guess that finally is executed even when no
exception is raised. This is true—the code in finally is always executed, with or
without an exception condition. To illustrate how to provide some meaningful
information to the user in both cases, I introduced the new variable bAllFine. bAllFine
tells the finally block whether it was called because of an exception or just because
the calculation completed successfully.
As a programmer used to SEH, you might be wondering whether there is an
equivalent to the __leave statement that is available in C++. If you don't know it
already, the __leave statement is used in C++ to prematurely stop executing code in
the try block, and to jump immediately to the finally block.
The bad news is, the __leave statement isn't in C#. However, the code in Listing 7.5
demonstrates a solution that you can implement.
1: using System;
2:
3: class JumpTest
4: {
5: public static void Main()
6: {
7: try
8: {
9: [Link]("try");
10: goto __leave;
11: }
12: finally
13: {
14: [Link]("finally");
15: }
16:
17: __leave:
18: [Link]("__leave");
19: }
20: }
When this application is run, the output is
try
finally
__leave
A goto statement can't exit a finally block. Even placing the goto statement in the try
block returns control immediately to the finally block. Therefore, the goto just leaves
the try block and jumps to the finally block. The __leave label isn't reached until all
code in finally finishes execution. In this way, you can simulate the __leave
statement that was present for SEH.
By the way, you might suspect that the goto statement was ignored because it was
the last statement in the try block and control was automatically transferred to
finally. To prove that is not the case, try placing the goto statement before the
[Link] method call. Although you will get a compiler warning because of
unreachable code, you'll see that the goto is actually being executed and no output is
being generated for the try string.
Handling All with try-catch-finally
The most likely approach for your applications is to merge the prior two error-
handling techniques—catch the error, clean up, and continue executing the
application. All you need to do is use try, catch, and finally statements in your error-
handling code. Listing 7.6 shows the approach for dealing with division-by-zero
errors.
1: using System;
2:
3: class CatchIT
4: {
5: public static void Main()
6: {
7: try
8: {
9: int nTheZero = 0;
10: int nResult = 10 / nTheZero;
11: }
12: catch(DivideByZeroException divEx)
13: {
14: [Link]("divide by zero occurred!");
15: }
16: catch(Exception Ex)
17: {
18: [Link]("some other exception");
19: }
20: finally
21: {
22: }
23: }
24: }
The twist with this example is that it contains multiple catch statements. The first
one catches the more likely DivideByZeroException exception, whereas the second
catch statement deals with all remaining exceptions by catching the general
exception.
You must always catch specialized exceptions first, followed by more general
exceptions. What happens if you don't catch exceptions in this order is illustrated by
the code in Listing 7.7.
1: try
2: {
3: int nTheZero = 0;
4: int nResult = 10 / nTheZero;
5: }
6: catch(Exception Ex)
7: {
8: [Link]("exception " + [Link]());
9: }
10: catch(DivideByZeroException divEx)
11: {
12: [Link]("never going to see that");
13: }
The compiler will catch the glitch and report an error similar to this one:
[Link](10,9): error CS0160: A previous catch clause
already catches all exceptions of this or a super type
('[Link]')
Finally, I have to report one shortcoming (or difference) of NGWS runtime exceptions
as compared to SEH: There is no equivalent to the
EXCEPTION_CONTINUE_EXECUTION identifier, which is available in SEH exception
filters. Basically, EXCEPTION_CONTINUE_EXECUTION enables you to re-execute the
piece of code that is responsible for the exception. You had the chance to change
variables or the like before the re-execution. My personal favorite technique was
performing memory allocation on demand by using access violation exceptions.
Throwing Exceptions
When you have to catch exceptions, someone else must be able to throw them in the
first place. And, not only is someone else capable of throwing, you can be in charge,
too. It is pretty simple:
throw new ArgumentException("Argument can't be 5");
All you need is the throw statement and an appropriate exception class. I have
picked an exception from the list provided in Table 7.1 for this example.
Re-Throwing Exceptions
While you are inside a catch statement, you can decide to throw the exception you
are currently handling again, leaving further handling to some outer try-catch
statement. An example of this approach is shown in Listing 7.8.
1: try
2: {
3: checked
4: {
5: for (;nCurDig <= nComputeTo; nCurDig++)
6: nFactorial *= nCurDig;
7: }
8: }
9: catch (OverflowException oe)
10: {
11: [Link]("Computing {0} caused an overflow
exception", nComputeTo);
12: throw;
13: }
Note that I do not need to specify the exception variable I have declared. Although it
is optional, you could also write
throw oe;
Now someone else has to take care of this exception!
Creating Your Own Exception Class
Although it is recommended that you use the predefined exception classes, for
programmatic scenarios it can be handy to create your own exception classes.
Creating your own exception class enables customers of your exception class to take
a different action based on that very exception class.
The exception class MyImportantException presented in Listing 7.9 follows two rules:
First, it ends the class name with Exception. Second, it implements all three
recommended common constructors. You should abide by these rules, too.
1: using System;
2:
3: public class MyImportantException:Exception
4: {
5: public MyImportantException()
6: :base() {}
7:
8: public MyImportantException(string message)
9: :base(message) {}
10:
11: public MyImportantException(string message, Exception
inner)
12: :base(message,inner) {}
13: }
14:
15: public class ExceptionTestApp
16: {
17: public static void TestThrow()
18: {
19: throw new MyImportantException("something bad has
happened.");
20: }
21:
22: public static void Main()
23: {
24: try
25: {
26: [Link]();
27: }
28: catch (Exception e)
29: {
30: [Link](e);
31: }
32: }
33: }
As you can see, the MyImportantException exception class does not implement any
special features, but is based entirely on the [Link] class. The remainder
of the program then tests the new exception class, using a catch statement for the
[Link] class.
If there is no special implementation other than three constructors for
MyImportantException, what is the point of creating it? It is the type that is
important--you can use it in a catch statement instead of a more general exception
class. A client of code that might throw your new exception can react with specific
catch code.
When programming a class library with your own namespace, place your exceptions
in that namespace, too. Although it is not presented in this example, you should
extend your exception classes with appropriate properties for extended error
information.
Summary
This chapter started by introducing you to overflow checking. You can enable or
disable overflow checking for your entire C# application by using a compiler switch
(the default is off). If you need finer control, you can use the checked and unchecked
statements, which enable you to execute a block of statements either with or without
overflow checking, regardless of the compiler settings for the application.
When an overflow occurs, an exception is raised. How that exception is handled is up
to you. I presented various approaches, including the one you are most likely to use
throughout your applications: employing try, catch, and finally statements. Along
with various examples, you learned differences to the structured exception handling
(SEH) of WIN32.
Handling exceptions is for users of classes; however, if you are in charge of creating
new classes, you can throw exceptions. You have multiple choices: throwing the
exceptions you already caught, throwing existing framework exceptions, or creating
new exception classes that are specific for the programmatic purpose.
Finally, you had a required reading of various Do's and Donts for the throwing and
handling of exceptions.
Chapter 8 Writing Components in C#
This chapter is about writing components in C#. You learn how to write a
component, how to compile it, and how to use it in a client application. A further step
down the road is using namespaces to organize your applications.
The chapter is structured into two major sections:
Listing 8.1 The RequestWebPage Class for Retrieving HTML Pages from
Web Servers
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: public class RequestWebPage
7: {
8: private const int BUFFER_SIZE = 128;
9: private string m_strURL;
10:
11: public RequestWebPage()
12: {
13: }
14:
15: public RequestWebPage(string strURL)
16: {
17: m_strURL = strURL;
18: }
19:
20: public string URL
21: {
22: get { return m_strURL; }
23: set { m_strURL = value; }
24: }
25: public void GetContent(out string strContent)
26: {
27: // check the URL
28: if (m_strURL == "")
29: throw new ArgumentException("URL must be
provided.");
30:
31: WebRequest theRequest = (WebRequest)
[Link](m_strURL);
32: WebResponse theResponse = [Link]();
33:
34: // set up the byte buffer for the response
35: int BytesRead = 0;
36: Byte[] Buffer = new Byte[BUFFER_SIZE];
37:
38: Stream ResponseStream =
[Link]();
39: BytesRead = [Link](Buffer, 0,
BUFFER_SIZE);
40:
41: // use StringBuilder to speed up the allocation
process
42: StringBuilder strResponse = new StringBuilder("");
43: while (BytesRead != 0 )
44: {
45:
[Link]([Link](Buffer,0,BytesR
ead));
46: BytesRead = [Link](Buffer, 0,
BUFFER_SIZE);
47: }
48:
49: // assign the out parameter
50: strContent = [Link]();
51: }
52: }
I could have done this with the parameterless constructor, but I decided that
initializing URL in the constructor might be useful. When I decide to change the URL
later—for retrieving a second page, for example—it is exposed via get and set
accessors of the URL property.
The fun begins in the GetContent method. First, the code performs a really simple
check on the URL, and if it is not appropriate, an ArgumentException is thrown. After
that, I ask the WebRequestFactory to create a new WebRequest object based on the
URL I pass to it.
Because I do not want to send cookies, additional headers, query strings, or the like,
I access the WebResponse immediately (line 32). If you need any of the
aforementioned features for the request, you must implement them before this line.
Lines 35 and 36 initialize a byte buffer that is used to read data from the response
stream. Ignoring the StringBuilder class for the moment, the while loop simply
iterates as long as there is still some data left to read from the response stream. The
last read operation would return zero, thus terminating the loop.
Now I want to come back to the StringBuilder class. Why do I use an instance of this
class instead of simply concatenating the byte buffer to a string variable? Look at the
following example:
strMyString = strMyString + "some more text";
Here, it is clear that you are copying values. The constant "some more text" is boxed
in a string variable, and a new string variable is created based on the addition
operation. This is then finally assigned to strMyString. That's a lot of copying, isn't it?
But you can argue that
strMyString += "some more text";
does not exhibit this behavior. Sorry, that's the wrong answer for C#. It behaves
exactly the same as the described assignment operation.
The way out of this problem is to use the StringBuilder class. It works with one
buffer, and you perform append, insert, remove, and replace operations without
incurring the copy behavior I have described. That is why I used it in this class to
concatenate the content that is read from the buffer.
The buffer brings me to the last important piece of code in this class—the encoding
conversion of line 45. It simply takes care that I get the character set I am asking
for.
Finally, when all content is read and converted, I explicitly request a string object
from the StringBuilder and assign it to the out variable. A return value would have
incurred yet another copy operation.
The work you have done so far isn't different from writing a class inside a normal
application. What makes it different is the compilation process. You have to create a
library instead of an application:
csc /r:[Link] /t:library /out:[Link] [Link]
The compiler switch /t:library tells the C# compiler to create a library and not to
search for a static Main method. Also, because I am using the [Link]
namespace, I have to reference (/r:) its library, which is [Link].
Your library named [Link] is now ready to be used in a client application. Because
we work only with private components in this chapter, you do not need to copy the
library to a special location other than the client application's directory.
Creating a Simple Client Application
When the component is written and successfully compiled, all you have to do is to
use it in a client application. I once again have created a simple command-line
application, which retrieves the start page of a development site I maintain (see
Listing 8.2).
1: using System;
2:
3: class TestWebReq
4: {
5: public static void Main()
6: {
7: RequestWebPage wrq = new RequestWebPage();
8: [Link] = "[Link]
9:
10: string strResult;
11: try
12: {
13: [Link](out strResult);
14: }
15: catch (Exception e)
16: {
17: [Link](e);
18: return;
19: }
20:
21: [Link](strResult);
22: }
23: }
Notice that I have enclosed the call to GetContent in a try catch statement. One
reason for this is because GetContent could throw an ArgumentException exception.
Furthermore, the NGWS framework classes I call inside the component could also
throw exceptions. Because I do not handle these exceptions inside the class, I have
to handle them here.
The remainder of the code is nothing more than straightforward component use—
calling the standard constructor, accessing a property, and executing a method. But
wait: You need to pay attention when compiling the application. You have to tell the
compiler to reference your new component's library DLL:
csc /r:[Link] [Link]
Now you are all set and can test the application. Output will scroll by, but you can
see that the application works. You could also add code to parse the returned HTML
using regular expressions, and extract information to your liking. I envision the use
of an SSL-modified version of this class for online credit card verification in ASP+
pages.
You might have noticed that there is no special using statement for the library you
created. The reason is that you didn't define a namespace in the component's source
file.
Now that you know what a namespace is in theory, let's implement one in real life. A
natural choice for the namespace in this and upcoming examples is
[Link]. To not bore you with just wrapping the RequestWebPage class
into it, I decided to write a class for a Whois lookup (see Listing 8.3).
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: namespace [Link]
7: {
8: public class WhoisLookup
9: {
10: public static bool Query(string strDomain, out string
strWhoisInfo)
11: {
12: const int BUFFER_SIZE = 128;
13:
14: if ("" == strDomain)
15: throw new ArgumentException("You must specify a
domain name.");
16:
17: TCPClient tcpc = new TCPClient();
18: strWhoisInfo = "N/A";
19:
20: // try to connect to the whois server
21: if ([Link]("[Link]", 43) !=
0)
22: return false;
23:
24: // get the stream
25: Stream s = [Link]();
26:
27: // send the request
28: strDomain += "\r\n";
29: Byte[] bDomArr =
[Link]([Link]());
30: [Link](bDomArr, 0, [Link]);
31:
32: Byte[] Buffer = new Byte[BUFFER_SIZE];
33: StringBuilder strWhoisResponse = new
StringBuilder("");
34:
35: int BytesRead = [Link](Buffer, 0, BUFFER_SIZE);
36: while (BytesRead != 0 )
37: {
38:
[Link]([Link](Buffer,0,B
ytesRead));
39: BytesRead = [Link](Buffer, 0, BUFFER_SIZE);
40: }
41:
42: [Link]();
43: strWhoisInfo = [Link]();
44: return true;
45: }
46: }
47: }
The namespace is declared in line 6, and it encloses the WhoisLookup class with the
angle brackets in lines 7 and 47. That's really all you have to do to declare your own
new namespace.
The class WhoisLookup has, of course, some interesting code in it, especially because
it shows how easy socket programming is in C#. After the not-so-stellar domain
name check in the static Query method, I instantiate an object of type TCPClient,
which is used to perform all communications on port 43 with the Whois server. The
connection to the server is established in line 21:
if ([Link]("[Link]", 43) != 0)
Because a failed connection attempt is an expected result, this method does not
throw an exception. (Do you still remember the Do's and Donts of exception
handling?) The return value is an error code, and zero indicates connection success.
For a Whois lookup, I must first send some information—the domain name I want to
look up—to the server. To achieve this, I first obtain a reference to the bidirectional
stream of the current TCP connection (line 25). I then append a carriage
return/linefeed pair to the domain name to denote the end of my query. Repackaged
in a byte array, I send the request to the Whois server (line 30).
The remainder of the code is very similar to the RequestWebPage class in that I
again use a buffer to read the response from the remote server. When the buffer is
finished reading, the connection is closed, and the retrieved response is returned to
the caller. The reason I explicitly call the Close method is that I do not want to wait
for the garbage collector to destroy the connection. Never hang on too long to scarce
resources such as TCP ports.
Before you can use the class in an NGWS component, you must compile it as a
library. Although there's now a namespace defined, the compilation command hasn't
changed:
csc /r:[Link] /t:library /out:[Link] [Link]
Note that it isn't necessary to specify the /out: switch if you want the library to be
named the same way as the original C# source file. It is just a good habit to specify
the switch because most projects won't consist of a single source file. If you specify
multiple source files, the library is named after the first source file in the list.
Because you developed your component with a namespace, the client either has to
import the namespace
using [Link];
or use fully qualified names for the elements in the namespace, such as
[Link](...);
If you don't have to expect conflicts between the elements in the namespaces you
want to import, the using directive is preferred, especially because you have less to
type. A sample client program using the component is implemented in Listing 8.4.
1: using System;
2: using [Link];
3:
4: class TestWhois
5: {
6: public static void Main()
7: {
8: string strResult;
9: bool bReturnValue;
10:
11: try
12: {
13: bReturnValue = [Link]("[Link]",
out strResult);
14: }
15: catch (Exception e)
16: {
17: [Link](e);
18: return;
19: }
20: if (bReturnValue)
21: [Link](strResult);
22: else
23: [Link]("Could not obtain information from
server.");
24: }
25: }
Line 2 imports the [Link] namespace with the using directive. Whenever
I reference the WhoisLookup class now, I can omit the namespace part of the fully
qualified name.
The program itself performs a Whois lookup for the [Link] domain—you can
replace [Link] with your own domain name. You could make the client even
more useful by allowing the domain name to be passed via a command-line
parameter. Listing 8.5 implements that functionality, but it doesn't implement proper
exception handling (to make the listing shorter).
1: using System;
2: using [Link];
3:
4: class WhoisShort
5: {
6: public static void Main(string[] args)
7: {
8: string strResult;
9: bool bReturnValue;
10:
11: bReturnValue = [Link](args[0], out
strResult);
12:
13: if (bReturnValue)
14: [Link](strResult);
15: else
16: [Link]("Lookup failed.");
17: }
18: }
All you have to do is compile this application:
csc /r:[Link] [Link]
You then can execute the application with a command-line parameter. For example,
to execute with [Link]
whoisclnt [Link]
When the query runs successfully, you are presented with the registration
information for [Link]. (An abbreviated version of the output is shown in
Listing 8.6.) This is a handy little application, written with a componentized
approach, in less than an hour. How long would it have taken to write in C++?
Luckily, I can no longer recall how long it took me when I did it for the first time.
D:\CSharp\Samples\Namespace>whoisclient
...
Registrant:
Microsoft Corporation (MICROSOFT-DOM)
1 microsoft way
redmond, WA 98052
US
Domain Name: [Link]
Administrative Contact:
Microsoft Hostmaster (MH37-ORG) msnhst@[Link]
Technical Contact, Zone Contact:
MSN NOC (MN5-ORG) msnnoc@[Link]
Billing Contact:
Microsoft-Internic Billing Issues (MDB-ORG)
msnbill@[Link]
[Link] [Link]
[Link] [Link]
[Link] [Link]
[Link] [Link]
Adding Multiple Classes to a Namespace
It would be nice to have both the WhoisLookup and the RequestWebPage class in a
single namespace. WhoisLookup is already part of the namespace, so you only have
to make the RequestWebPage class part of the namespace, too.
The necessary changes are applied easily. You only have to wrap the
RequestWebPage class with the namespace:
namespace [Link]
{
public class RequestWebPage
{
...
}
}
Although the two classes are contained in two different files, they are part of the
same namespace after compilation:
csc /r:[Link] /t:library /out:[Link]
[Link] [Link]
You are not required to name the DLL after the exact namespace name. However,
doing so helps you to remember more easily which libraries to reference when
compiling client applications.
Summary
In this chapter, you learned how to build a component that can be used in a client
application. At first, you didn't care about namespaces, but this feature was
introduced later with a second component. Namespaces are a great way to organize
your applications both internally and externally.
Components in C# can be built very easily, and you don't even need to perform a
special installation as long as the library resides in the same directory as the
application. When creating class libraries that must be used by multiple clients, this
picture changes a bit—and the next chapter will tell you why.
Chapter 9 Configuration and Deployment
In the last chapter, you learned how to create a component, and how to use it in a
simple test application. Although the component would be ready to ship, you should
also consider one of the following techniques:
• Conditional compilation
• Documentation comments
• Versioning your code
Conditional Compilation
A feature I couldn't live without is conditional compilation of my code. Conditional
compilation enables me to exclude or include code based on certain conditions; for
example, to build a debug version, demo version, or retail version of my application.
Examples of code that might be included or excluded are licensing code, nag
screens, or whatever you can come up with.
In C#, there are two ways to perform conditional compilation:
• Preprocessor usage
• The conditional attribute
Preprocessor Usage
In C++, the preprocessor is a separate step before the compiler starts compiling
your code. In C#, the preprocessor is "emulated" by the compiler itself—there is no
separate preprocessor. It is simply conditional compilation.
Although the C# compiler does not support macros, you have the necessary features
for conditional exclusion and inclusion of code based on the definitions of symbols.
The following sections introduce you to the various directives that are supported in
C#, which are quite similar to the ones found in C++:
• Defining symbols
• Excluding code based on symbols
• Raising errors and warnings
Defining Symbols
1: using System;
2:
3: public class SquareSample
4: {
5: public void CalcSquare(int nSideLength, out int
nSquared)
6: {
7: nSquared = nSideLength * nSideLength;
8: }
9:
10: public int CalcSquare(int nSideLength)
11: {
12: return nSideLength*nSideLength;
13: }
14: }
15:
16: class SquareApp
17: {
18: public static void Main()
19: {
20: SquareSample sq = new SquareSample();
21:
22: int nSquared = 0;
23:
24: #if CALC_W_OUT_PARAM
25: [Link](20, out nSquared);
26: #else
27: nSquared = [Link](15);
28: #endif
29: [Link]([Link]());
30: }
31: }
Note that no symbol is defined in this source file. The symbol is defined (or not)
when compiling the application:
csc /define:CALC_W_OUT_PARAM [Link]
Based on theif directive:symbols:code inclusion symbol definition, a different
CalcSquare method is called. The emulated preprocessor directives used to evaluate
the symbol are #if, #else, and #endif. They act the same as their C# counterpart,
the if statement. You can also use logical AND (&&), logical OR (||), as well as
negation (!). An example of this is shown in Listing 9.2.
1: // #define DEBUG
2: #define RELEASE
3: #define DEMOVERSION
4:
5: #if DEBUG
6: #undef DEMOVERSION
7: #endif
8:
9: using System;
10:
11: class Demo
12: {
13: public static void Main()
14: {
15: #if DEBUG
16: [Link]("Debug version");
17: #elif RELEASE && !DEMOVERSION
18: [Link]("Full release version");
19: #else
20: [Link]("Demo version");
21: #endif
22: }
23: }
In this exampleif directive:symbols:code inclusion, all symbols are defined in the
C# source file. Note the addition of the #undef statement in line 6. Because I don't
compile demo versions of my debug code (an arbitrary choice), I make sure that it
wasn't inadvertently defined by someone and undefine it always when DEBUG is
defined.
The preprocessor symbols are then used in lines 15-21 to include varying code. Note
the use of the #elif directive, which enables you to add multiple branches to the #if
directive. This code uses the logical operator && and the negation operator !. It is
also possible to use the logical operator ||, as well as equality and inequality
operators.
1: #define DEBUG
2: #define RELEASE
3: #define DEMOVERSION
4:
5: #if DEMOVERSION && !DEBUG
6: #warning You are building a demo version
7: #endif
8:
9: #if DEBUG && DEMOVERSION
10: #error You cannot build a debug demo version
11: #endif
12:
13: using System;
14:
15: class Demo
16: {
17: public static void Main()
18: {
19: [Link]("Demo application");
20: }
21: }
In this example, a compiler warning is issued when you build a demo version that is
not also a debug version (lines 5-7). An error is raised—which prevents generation of
the executable—when you try to build a debug demo version. In contrast to the
previous example, which simply undefined the offending symbol, this code tells you
that what you warning directiveerror directivetried to do is considered an error.
This is definitely the better behavior.
The conditional Attribute
The preprocessor of C++ is perhaps most often used for defining macros that resolve
to a function call in one build, and resolve to nothing in another build. Examples of
this include the ASSERT and TRACE macros, which evaluate to function calls when
the DEBUG symbol is defined and evaluate to nothing when a release version is built.
With the knowledge that macros are not supported, you might also guess that
conditional functionality is dead. Happily, I can report that is not the case. You can
include methods based on certain defined symbols by using the conditional attribute:
[conditional("DEBUG")]
public void SomeMethod() { }
This method is added to resulting executable only when the symbol DEBUG is
defined. And a call to it, such as
SomeMethod();
is also discarded by the compiler when the method is not included. The functionality
is basically the same as with C++ conditional macros.
Before starting an example, I want to point out that the conditional method must
have a return type of void. No other return types are allowed. However, you can
pass any parameters you want to use.
The example in Listing 9.4 demonstrates how to use the conditional attribute to
rebuild the functionality of the TRACE macros found in C++. For simplicity, the
output is directed to the console. You could direct it anywhere you want, including a
file.
1: #define DEBUG
2:
3: using System;
4:
5: class Info
6: {
7: [conditional("DEBUG")]
8: public static void Trace(string strMessage)
9: {
10: [Link](strMessage);
11: }
12:
13: [conditional("DEBUG")]
14: public static void TraceX(string strFormat,params
object[] list)
15: {
16: [Link](strFormat, list);
17: }
18: }
19:
20: class TestConditional
21: {
22: public static void Main()
23: {
24: [Link]("Cool!");
25: [Link]("{0} {1} {2}","C", "U", 2001);
26: }
27: }
There are two static methods in the class Info that are conditionally compiled based
on the DEBUG symbol: Trace, which takes one parameter, and TraceX, which takes n
parameters. Implementation of Trace is straightforward. However, TraceX
implements a keyword you haven't seen before: params.
The params keyword enables you to specify a method parameter that actually takes
any number of arguments. It is similar to the C/C++ ellipsis argument. Note that it
must be the last parameter of a method call, and that you can use it only once in the
parameter list. After all, these two limitations are pretty obvious.
The intention of using the params keyword is to have a Trace method that can take a
format string and an unlimited number of replacement objects. Luckily, there is also
a WriteLine method that supports a format string and an object array (line 16).
Which output this little program generates depends entirely on whether the DEBUG
symbol is defined. When the DEBUG symbol is defined, both methods are compiled
and executed. If DEBUG is not defined, the calls to Trace and TraceX are removed
along with their definitions.
Conditional methods are a really powerful means for adding conditional functionality
to your applications and components. With a few twists, you can build conditional
methods based on multiple symbols linked with logical OR (||) as well as logical AND
(&&). For those cases, however, I want to refer you to the C# documentation.
• Describing an element
• Adding remarks and lists
• Providing examples
• Describing parameters
• Describing properties
• Compiling the documentation
Describing an Element
A first step is to add a simple description to an element. You can do that by using the
<summary> tag:
/// <summary>This is .... </summary>
Every documentation comment starts with a triple forward-slash ///. You place the
documentation comment before the element that you want to describe:
/// <summary>Class to tear a Webpage from a
Webserver</summary>
public class RequestWebPage
You can add paragraphs to the description by using the <para> and </para> tags.
References to other elements are added using the <see> tag:
/// <para>Included in the <see cref="RequestWebPage"/>
class</para>
This adds a link to the description of the RequestWebPage class. Note that the
syntax for the tags is XML syntax, which means that the tags' capitalization matters,
and that tags must be nested correctly.
Another interesting tag when documenting an element is the <seealso> tag. It
enables you to describe other topics that might be of interest to the reader:
/// <seealso cref="[Link]"/>
The preceding example tells the reader that he might also want to look up the
documentation of the [Link] namespace. You have to always specify the fully
qualified name for items outside the current scope.
As promised, Listing 9.5 contains a full example of documentation at work in the
RequestWebPage class. Take a look at how tags can be used and nested to generate
documentation for a component.
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: /// <summary>Class to tear a Webpage from a
Webserver</summary>
7: public class RequestWebPage
8: {
9: private const int BUFFER_SIZE = 128;
10:
11: /// <summary>m_strURL stores the URL of the
Webpage</summary>
12: private string m_strURL;
13:
14: /// <summary>RequestWebPage() is the constructor for
the class
15: /// <see cref="RequestWebPage"/> when called without
arguments.</summary>
16: public RequestWebPage()
17: {
18: }
19:
20: /// <summary>RequestWebPage(string strURL) is the
constructor for the class
21: /// <see cref="RequestWebPage"/> when called with an
URL as parameter.</summary>
22: public RequestWebPage(string strURL)
23: {
24: m_strURL = strURL;
25: }
26:
27: public string URL
28: {
29: get { return m_strURL; }
30: set { m_strURL = value; }
31: }
32:
33: /// <summary>The GetContent(out string strContent)
method:
34: /// <para>Included in the <see cref="RequestWebPage"/>
class</para>
35: /// <para>Uses variable <see cref="m_strURL"/></para>
36: /// <para>Used to retrieve the content of a Webpage.
The URL
37: /// of the Webpage (including [Link] must already be
38: /// stored in the private variable m_strURL.
39: /// To do so, call the constructor of the
RequestWebPage
40: /// class, or set its property <see cref="URL"/> to
the URL string.</para>
41: /// </summary>
42: /// <seealso cref="[Link]"/>
43: /// <seealso cref="[Link]"/>
44: /// <seealso cref="[Link]"/>
45: /// <seealso cref="[Link]"/>
46: /// <seealso cref="[Link]"/>
47: /// <seealso cref="[Link]"/>
48: /// <seealso cref="[Link]"/>
49:
50: public bool GetContent(out string strContent)
51: {
52: strContent = "";
53: // ...
54: return true;
55: }
56: }
The <remarks> tag is where you should specify the bulk of your documentation. This
is in contrast to <summary>, where you should specify only a brief description of the
element.
You are not limited to supplying paragraph text only (using the <para> tag). For
example, you can include bulleted (and even numbered) lists in the remarks section:
/// <list type="bullet">
/// <item>Constructor
/// <see cref="RequestWebPage()"/> or
/// <see cref="RequestWebPage(string)"/>
/// </item>
/// </list>
This list has one item, and the item references two different constructor descriptions.
You are free to add as much content to a list item as you want.
Another tag that is good to use in the remarks section is <paramref>. For example,
you can use <paramref> to reference and describe a parameter that is passed to a
constructor:
/// <remarks>Stores the URL from the parameter ///
<paramref name="strURL"/> in
/// the private variable <see cref="m_strURL"/>.</remarks>
public RequestWebPage(string strURL)
You can see all these tags, as well as the previous ones, in action in Listing 9.6.
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: /// <summary>Class to tear a Webpage from a
Webserver</summary>
7: /// <remarks>The class RequestWebPage provides:
8: /// <para>Methods:
9: /// <list type="bullet">
10: /// <item>Constructor
11: /// <see cref="RequestWebPage()"/> or
12: /// <see cref="RequestWebPage(string)"/>
13: /// </item>
14: /// </list>
15: /// </para>
16: /// <para>Properties:
17: /// <list type="bullet">
18: /// <item>
19: /// <see cref="URL"/>
20: /// </item>
21: /// </list>
22: /// </para>
23: /// </remarks>
24: public class RequestWebPage
25: {
26: private const int BUFFER_SIZE = 128;
27:
28: /// <summary>m_strURL stores the URL of the
Webpage</summary>
29: private string m_strURL;
30:
31: /// <summary>RequestWebPage() is the constructor for
the class
32: /// <see cref="RequestWebPage"/> when called without
arguments.</summary>
33: public RequestWebPage()
34: {
35: }
36:
37: /// <summary>RequestWebPage(string strURL) is the
constructor for the class
38: /// <see cref="RequestWebPage"/> when called with an
URL as parameter.</summary>
39: /// <remarks>Stores the URL from the parameter
<paramref name="strURL"/> in
40: /// the private variable <see
cref="m_strURL"/>.</remarks>
41: public RequestWebPage(string strURL)
42: {
43: m_strURL = strURL;
44: }
45:
46: /// <remarks>Sets the value of <see cref="m_strURL"/>.
47: /// Returns the value of <see
cref="m_strURL"/>.</remarks>
48: public string URL
49: {
50: get { return m_strURL; }
51: set { m_strURL = value; }
52: }
53:
54: /// <summary>The GetContent(out string strContent)
method:
55: /// <para>Included in the <see cref="RequestWebPage"/>
class</para>
56: /// <para>Uses variable <see cref="m_strURL"/></para>
57: /// <para>Used to retrieve the content of a Webpage.
The URL
58: /// of the Webpage (including [Link] must already be
59: /// stored in the private variable m_strURL.
60: /// To do so, call the constructor of the
RequestWebPage
61: /// class, or set its property <see cref="URL"/> to
the URL string.</para>
62: /// </summary>
63: /// <remarks>Retrieves the content of the Webpage
specified in
64: /// the property<see cref="URL"/> and hands it over to
the out
65: /// parameter <paramref name="strContent"/>.
66: /// The method is implemented using:
67: /// <list>
68: /// <item>The <see
cref="[Link]"/>method.</item>
69: /// <item>The <see
cref="[Link]"/> method.</item>
70: /// <item>The <see
cref="[Link]"/>method</it
em>
71: /// <item>The <see cref="[Link]"/>
method</item>
72: /// <item>The <see
cref="[Link]"/> method</item>
73: /// <item>The <see cref="[Link]"/>
property together with its
74: /// <see cref="[Link]"/>
method</item>
75: /// <item>The <see cref="[Link]"/>
method for the
76: /// <see cref="[Link]"/> object.</item>
77: /// </list>
78: /// </remarks>
79: /// <seealso cref="[Link]"/>
80: public bool GetContent(out string strContent)
81: {
82: strContent = "";
83: // ...
84: return true;
85: }
86: }
Providing Examples
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: /// <summary>Class to tear a Webpage from a
Webserver</summary>
7: /// <remarks> ... </remarks>
8: public class RequestWebPage
9: {
10: private const int BUFFER_SIZE = 128;
11:
12: /// <summary>m_strURL stores the URL of the
Webpage</summary>
13: private string m_strURL;
14:
15: /// <summary>RequestWebPage() is ... </summary>
16: /// <example>This example shows you how to call the
constructor
17: /// of the class RequestWebPage() without arguments:
18: /// <code>
19: /// public class MyClass
20: /// {
21: /// public static void Main()
22: /// {
23: /// public
24: /// string strContent;
25: /// RequestWebPage objRWP = new RequestWebPage();
26: /// [Link] = "[Link]
27: /// [Link](out strContent);
28: /// [Link](strContent);
29: /// }
30: /// }
31: /// </code>
32: /// </example>
33: public RequestWebPage()
34: {
35: }
36:
37: /// <summary>RequestWebPage(string strURL) is ...
</summary>
38: /// <remarks> ... </remarks>
39: /// <example>This example shows you how to call
40: /// RequestWebPage() with the URL parameter:
41: /// <code>
42: /// public class MyClass
43: /// {
44: /// public static void Main()
45: /// {
46: /// string strContent;
47: /// RequestWebPage objRWP = new
RequestWebPage("[Link]
48: /// [Link](out strContent);
49: /// [Link]("\n\nContent of the Webpage "+
[Link]+":\n\n");
50: /// [Link](strContent);
51: /// }
52: /// }
53: /// </code>
54: /// </example>
55: public RequestWebPage(string strURL)
56: {
57: m_strURL = strURL;
58: }
59:
60: /// <remarks> ... </remarks>
61: public string URL
62: {
63: get { return m_strURL; }
64: set { m_strURL = value; }
65: }
66:
67: /// <summary>The GetContent(out string strContent)
method: ... </summary>
68: /// <remarks> ... </remarks>
69: /// <seealso cref="[Link]"/>
70: public bool GetContent(out string strContent)
71: {
72: strContent = "";
73: // ...
74: return true;
75: }
76: }
Describing Parameters
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: /// <summary>Class to tear a Webpage from a
Webserver</summary>
7: /// <remarks> ... </remarks>
8: public class RequestWebPage
9: {
10: private const int BUFFER_SIZE = 128;
11:
12: /// <summary>m_strURL stores the URL of the
Webpage</summary>
13: private string m_strURL;
14:
15: /// <summary>RequestWebPage() is ... </summary>
16: /// <example>This example ...
17: /// <code>
18: /// public class MyClass
19: /// {
20: /// ...
21: /// }
22: /// </code>
23: /// </example>
24: public RequestWebPage()
25: {
26: }
27:
28: /// <summary>RequestWebPage(string strURL) is ...
</summary>
29: /// <remarks> ... </remarks>
30: /// <param name="strURL">
31: /// Used to hand over the URL of the Webpage to the
object.
32: /// Its value is stored in the private variable <see
cref="m_strURL"/>.
33: /// </param>
34: /// <example> ... </example>
35: public RequestWebPage(string strURL)
36: {
37: m_strURL = strURL;
38: }
39:
40: /// <remarks> ... </remarks>
41: public string URL
42: {
43: get { return m_strURL; }
44: set { m_strURL = value; }
45: }
46:
47: /// <summary>The GetContent(out string strContent)
method: ... </summary>
48: /// <remarks>Retrieves the content of the Webpage
specified in the property
49: /// <see cref="URL"/> and hands it over to the out
parameter
50: /// <paramref name="strContent"/>.
51: /// The method is implemented using ...
52: /// </remarks>
53: /// <param name="strContent">Returns the Content of the
Webpage</param>
54: /// <returns>
55: /// <para>true: Content retrieved</para>
56: /// <para>false: Content not retrieved</para>
57: /// </returns>
58: /// <seealso cref="[Link]"/>
59: public bool GetContent(out string strContent)
60: {
61: strContent = "";
62: // ...
63: return true;
64: }
65: }
Describing Properties
To describe a class's properties, you must use a special tag: the <value> tag. With
this tag, you can specifically flag a property, and the <value> tag more or less
replaces the <summary> tag.
Listing 9.9 contains a property description for the URL property of the
RequestWebPage class (lines 30 and following). Take the time to once again look at
the other tags you can use to document your component.
1: using System;
2: using [Link];
3: using [Link];
4: using [Link];
5:
6: /// <summary>Class to tear a Webpage from a
Webserver</summary>
7: /// <remarks> ... </remarks>
8: public class RequestWebPage
9: {
10: private const int BUFFER_SIZE = 128;
11:
12: /// <summary>m_strURL stores the URL of the
Webpage</summary>
13: private string m_strURL;
14:
15: /// <summary>RequestWebPage() is ... </summary>
16: /// <example> ... </example>
17: public RequestWebPage()
18: {
19: }
20:
21: /// <summary>RequestWebPage(string strURL) is ...
</summary>
22: /// <remarks> ... </remarks>
23: /// <param name="strURL"> ... </param>
24: /// <example>This example ... </example>
25: public RequestWebPage(string strURL)
26: {
27: m_strURL = strURL;
28: }
29:
30: /// <value>The property URL is to get or set the URL
for the Webpage </value>
31: /// <remarks>Sets the value of <see cref="m_strURL"/>.
32: /// Returns the value of <see
cref="m_strURL"/>.</remarks>
33: public string URL
34: {
35: get { return m_strURL; }
36: set { m_strURL = value; }
37: }
38:
39: /// <summary>The GetContent(out string strContent)
method: ... </summary>
40: /// <remarks>Retrieves the content of the Webpage
specified in the property
41: /// <see cref="URL"/> and hands it over to the out
parameter
42: /// <paramref name="strContent"/>.
43: /// The method is implemented using: ...
44: /// </remarks>
45: /// <param name="strContent">Returns the Content of the
Webpage</param>
46: /// <returns>
47: /// <para>true: Content retrieved</para>
48: /// <para>false: Content not retrieved</para>
49: /// </returns>
50: /// <seealso cref="[Link]"/>
51: public bool GetContent(out string strContent)
52: {
53: strContent = "";
54: // ...
55: return true;
56: }
57: }
• N—Denotes a namespace.
• T—Identifies a type. This can be class, interface, struct, enum, or delegate.
• F—Describes a field of a class.
• P—Refers to a property, which can also be an indexer or indexed property.
• M—Identifies a method. This includes special methods such as constructors
and operators.
• E—Events are denoted by a capital E.
• !—Denotes an error string; provides information about a link that the C#
compiler could not resolve.
Following the colon is the fully qualified name of the element, including the root of
the namespace, as well as enclosing types. If the element has periods in its name,
these are replaced by the hash sign, #. Parameters for methods are enclosed in
parentheses, and commas separate the arguments. The element type is encoded by
its NGWS signature, and a list of these can be found in the NGWS SDK
documentation.
Under normal circumstances, you do not have to care about the preceding XML
documentation details. Just create and ship the XML file with your component and
users of programming tools will be very happy with your software!
Versioning Your Code
Versioning is a problem that is known today as "DLL Hell." Applications install and
use shared components, and one application eventually breaks because it is not
compatible with the currently installed version of the component. Shared
components today present more problems than they solve.
One of the primary goals of the NGWS runtime is to solve the versioning problem. At
center stage of the new approach are the NGWS components (again, this is a term
refering to the packaging, not the contents), which enable the developer to specify
version dependencies between different pieces of software, and the NGWS runtime
enforces those rules at runtime.
I want to introduce you to NGWS components, show what they can be used for, and
what differences exist from today's DLLs with regard to versioning.
NGWS Components
Although I didn't specifically call it an NGWS component back then, the first library
you compiled was an NGWS component—the C# compiler, by default, always creates
NGWS components for your executables. So, what then is an NGWS component?
First of all, an NGWS component is the fundamental unit of sharing and reuse in the
NGWS runtime. Therefore, versioning is enforced on the component level. An NGWS
component also is the boundary for security enforcement, class deployment, and
type resolution. An application you build will be typically comprised of multiple NGWS
components.
Because we are talking about versioning, what does an NGWS component version
number look like? In general, it is comprised of four parts:
major [Link] [Link] [Link]
This version number is called the compatibility version. It is used by the class loader
to decide which version of an NGWS component to load, if different versions exist. A
version is considered incompatible when major [Link] version is different
from the requested version. Maybe compatible means that build number is different
from the requested version. Finally, if revision is different, it is considered a QFE
(Quick Fix Engineering), and generally considered compatible.
A second version number is stored in your component: the informational version. As
the name implies, the informational version is considered only for documentation
purposes, and its contents are something like SuperControl Build 1890. The
informational version provides a textual representation that means something to a
human, but not to the machine.
Before going on to explain private and shared NGWS components, I still owe you the
command switch that you use for the compiler to add version information to your
component. It is the /[Link] switch.
csc /[Link]:[Link] /t:library /out:[Link] [Link]
This creates a library with version information of [Link]. You can verify this by
right-clicking the library in Explorer and inspecting the Version tab of the Properties
dialog box.
Private NGWS Components
If you are building software you want to share between multiple applications, you
have to install it as a shared NGWS component. There are some extra things you
must take care of, however.
For starters, you need a strong name for your NGWS component. Some of you might
already have wondered where the replacement is for the ubiquitous globally unique
ID (GUID) of COM. As long as you use private NGWS components, this is not
necessary. When you start using shared NGWS components, however, you must
guarantee that their names are unique.
Their uniqueness is guaranteed via standard public key cryptography: You use a
private key to sign your NGWS component, and applications that link to your
component have the public key to verify the component's originator (you). After
signing your NGWS component, you can deploy it to the global NGWS component
cache or the application directory. The runtime takes care of mapping to all
applications.
Is it a good idea to create a shared NGWS component? Personally, I don't think so.
You once again take the risk of creating something similar to DLL Hell, although
application developers depending on your component could avoid those problems by
specifying binding policies. Because disk space isn't expensive today, I highly
recommend using private NGWS components, and assigning strong names to them.
Summary
In this chapter, I introduced three techniques you should consider before deploying
your components or applications. The first consideration is using conditional
compilation. Using either the C# preprocessor or the conditional attribute, you can
exclude or include code based on a single or several defined symbols. This enables
you to conditionally compile debug versions, release versions, or whatever versions
you want to build.
The documentation of your components should play an important part during
development, and not just be a mere afterthought. Because C# offers you
automated generation of documentation via documentation comments, I explained
this feature at great length. This feature is especially useful because it enables your
software to integrate its help and documentation easily with tools such as Visual
Studio 7.
Finally, I talked about versioning in the NGWS runtime and its smallest unit: the
NGWS component. You have a choice of creating private or shared NGWS
components, but I recommend that you stick to private ones because you avoid all
the problems that are associated with shared components.
Chapter 10 Interoperating with Unmanaged Code
NGWS runtime is definitely a cool technology. But a cool technology isn't worth a
dime if it doesn't allow you to use the (unmanaged) code that already exists,
whether the code is in the form of COM components or functions implemented in C
DLLs. Furthermore, sometimes managed code might get into the way of writing high-
performance code—you must be able to write unmanaged, unsafe code.
NGWS and C# offer you the following techniques to interoperate with unmanaged
code:
• COM Interoperability
• Platform Invocation Services
• Unsafe code
COM Interoperability
The first and most interesting interoperability technique is interoperability with COM.
The reason is that for a long time to come, COM and NGWS must coexist. Your
NGWS runtime clients must be able to call your legacy COM components, and COM
clients must make use of new NGWS runtime components.
The following two sections deal with both issues:
Though the interoperability discussion is centered around C#, please note that you
could replace C# with VB or managed C++. It is an interoperability feature provided
by the NGWS runtime to all programming languages emitting managed code.
In COM, you first have to register an object before it can be used. When registering a
COM object, you use the regsvr32 application, which you obviously can't use for a
COM+ 2.0 application. However, there is a similar tool for NGWS runtime
components: [Link].
The regasm tool enables you to register an NGWS component in the Registry
(including all classes that are contained, given that they are publicly accessible), and
it also creates a Registry file for you when you request it. The latter is useful when
you want to examine what entries are added to the Registry.
The command is as follows:
regasm [Link] /reg:[Link]
The output file ([Link]) that is generated is shown in Listing 10.1. When you are
used to COM programming, you'll recognize the entries that are being made to the
Registry. Note that the ProgId is composed of the namespace and class names.
1: REGEDIT4
2:
3: [HKEY_CLASS_ROOT\[Link]]
4: @="COM+ class: [Link]"
5:
6:
[HKEY_CLASS_ROOT\[Link]\CLSID]
7: @="{6B74AC4D-4489-3714-BB2E-58F9F5ADEEA3}"
8:
9: [HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-
58F9F5ADEEA3}]
10: @="COM+ class: [Link]"
11:
12: [HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-
58F9F5ADEEA3}\InprocServer32]
13: @="D:\WINNT\System32\[Link]"
14: "ThreadingModel"="Both"
15: "Class"="[Link]"
16: "Assembly"="csharp, Ver=[Link]"
17:
18: [HKEY_CLASS_ROOT\CLSID\{6B74AC4D-4489-3714-BB2E-
58F9F5ADEEA3}\ProgId]
19: @="[Link]"
20:
21: [HKEY_CLASS_ROOT\[Link]]
22: @="COM+ class: [Link]"
23:
24: [HKEY_CLASS_ROOT\[Link]\CLSID]
25: @="{8B5D2461-07DB-3B5C-A8F9-8539A4B9BE34}"
26:
27: [HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-
8539A4B9BE34}]
28: @="COM+ class: [Link]"
29:
30: [HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-
8539A4B9BE34}\InprocServer32]
31: @="D:\WINNT\System32\[Link]"
32: "ThreadingModel"="Both"
33: "Class"="[Link]"
34: "Assembly"="csharp, Ver=[Link]"
35:
36: [HKEY_CLASS_ROOT\CLSID\{8B5D2461-07DB-3B5C-A8F9-
8539A4B9BE34}\ProgId]
37: @="[Link]"
Take a closer look at lines 30-34. As you can see, the execution engine
([Link]) is called when an instance of your object is requested, not your library
itself. The execution engine is responsible for providing the CCW (COM Callable
Wrapper) for your object.
If you want to register the component without a Registry file, all you have to do is
issue this command:
regasm [Link]
Now the component can be used in programming languages that support late
binding. If you are not content with late binding (and you shouldn't be), the tlbexp
utility enables you to generate a type library for your NGWS component:
tlbexp [Link] /out:[Link]
This type library can be used in programming languages that support early binding.
Now your NGWS component is a good citizen in COM society.
Now that we are in the COM world, I want to dive right into the type library and point
out a few important things. I have used the OLE View application, which comes with
Visual Studio, to open the type library and extract the IDL (Interface Description
Language) of the classes contained in the NGWS component. Listing 10.2 shows the
results I obtained.
Listing 10.2 The IDL File for the WhoisLookup and RequestWebPage
Classes
The NGWS component and all classes are is registered, and you have a type library
for environments that prefer early binding—you are all set. To demonstrate that the
component works as expected, I choose Excel as the environment to script it.
To be able to use early binding in Excel, you must reference the type library. In the
VBA Editor, run the References command in the Tools menu. Choose Browse in the
References dialog box and then select the type library in the Add Reference dialog
box (see Figure 10.3).
Figure 10.3
Importing the type library for the component.
The only task left is coding the retrieve operation. As you can see from Listing 10.3,
it isn't complicated. Note that I added an On Error GoTo statement to perform the
necessary COM error handling.
1: Option Explicit
2:
3: Sub GetSomeInfo()
4: On Error GoTo Err_GetSomeInfo
5: Dim wrq As New [Link]
6: Dim strResult As String
7:
8: [Link] = "[Link]
9: [Link] strResult
10: [Link] strResult
11:
12: Exit Sub
13: Err_GetSomeInfo:
14: MsgBox [Link]
15: Exit Sub
16: End Sub
NGWS runtime exceptions are translated to HRESULTs, and the exception
information is passed via the error information interfaces. Excel then raises an error
based on this information.
When you run the code in Listing 10.3, the output is written to the immediate
window. Try entering an invalid URL to see how the exceptions are propagated from
the NGWS runtime to a COM client.
Interoperation also works the other way around—NGWS runtime clients can
interoperate with classic COM objects. Accessing legacy objects is the more likely
scenario during the transition period from COM to NGWS.
There are two ways to access COM objects from an NGWS runtime client application:
For the examples presented in this section I chose the AspTouch component, which
can change the file date of a given file. AspTouch has a dual interface and a type
library, and it is free. If you want to follow the examples in this section, you can
download AspTouch from [Link]
For a component to be used early-bound in COM, it must have a type library. For the
NGWS runtime, this translates to the metadata that is stored with the types. But
wait—metadata is associated with a type, but what is the NGWS runtime type for the
COM component?
To be able to call the COM component from an NGWS runtime application, you need
a wrapper around the unsafe code. Such a wrapper is called an RCW (Runtime
Callable Wrapper), and it is built from the type library information. A tool generates
the wrapper code for you, based on the information obtained from the type library.
The tool to use is tlbimp (type library import). Its command line is simple:
tlbimp [Link] /out:[Link]
This command imports the COM type library from [Link] (it is contained in the
DLL as a resource), and creates and stores an RCW that can be used in the NGWS
runtime in the file [Link]. You can use [Link] to view the metadata for
the RCW (see Figure 10.4). Chapter 11, "Debugging C# Code," covers the use of
ILDasm at greater length.
Figure 10.4
Using ILDasm to view the metadata of [Link].
When you look at the ILDasm output, you can see that ASPTOUCHlib is the
namespace (it was the name of the type library), and TouchIt is the class name of
the proxy that was generated for the original COM object. With this information, you
can write an NGWS runtime application that uses the COM component (see Listing
10.4).
1: using System;
2: using ASPTOUCHLib;
3:
4: class TouchFile
5: {
6: public static void Main()
7: {
8: TouchIt ti = new TouchIt();
9: bool bResult = false;
10: try
11: {
12: bResult = [Link]("[Link]");
13: }
14: catch(Exception e)
15: {
16: [Link](e);
17: }
18: finally
19: {
20: if (true == bResult)
21: {
22: [Link]("Successfully changed file time!");
23: }
24: }
25: }
26: }
This code looks and feels just like any other C# code that uses a class. There is a
using statement, method invocation, and exception handling (this time, the
HRESULTs are wrapped as exceptions). Even the compilation command is familiar to
you:
csc /r:[Link] /out:[Link] [Link]
It works just like with any other NGWS component. After you have created the RCW,
working with COM components is a walk in the park.
If you have a component without a type library, or you have to call it on-the-fly
without prior generation of an RCW, you aren't lost at all. A cool feature of NGWS
runtime will help you out: reflection. Now you can find out all about a component at
runtime.
Reflection is the way to go when dealing with late-bound objects. The code in Listing
10.5 uses reflection to create the object and to invoke its methods. It performs the
same actions as the previous script, but it doesn't have a wrapper class.
1: using System;
2: using [Link];
3:
4: class TestLateBound
5: {
6: public static void Main()
7: {
8: Type tTouch;
9: tTouch = [Link]("[Link]");
10:
11: Object objTouch;
12: objTouch = [Link](tTouch);
13:
14: Object[] parameters = new Object[1];
15: parameters[0] = "[Link]";
16: bool bResult = false;
17:
18: try
19: {
20: bResult =
(bool)[Link]("SetToCurrentTime",
21: [Link],
22: null, objTouch, parameters);
23: }
24: catch(Exception e)
25: {
26: [Link](e);
27: }
28:
29: if (bResult)
30: [Link]("Changed successfully!");
31: }
32: }
The class to use for reflection is Type, which is included in the [Link]
namespace. Line 9 then calls GetTypeFromProgID with the ProgId of the COM
component in question to get the component's type. Although I don't check for an
exception, you should do so; an exception is thrown if the type could not be loaded.
Now that the type is loaded, I can create an instance of it by using the
CreateInstance static method of the Activator class. The TouchIt object is ready to
be used. But the really ugly part of late-bound programming has just begun—
invoking methods.
If you loved late-bound programming with C++ and COM, you'll find yourself at
home with this code immediately. All parameters—in this case, the name of the file—
must be packaged in an array (lines 14-15), and the call to the method is performed
indirectly via the InvokeMember of the Type object (lines 20-22). You have to pass it
the name of the method, the binding flags, a binder, the object, and finally, the
parameters. The result returned by the invocation must be cast to the appropriate
type of C#/NGWS runtime.
Looks and feels ugly, doesn't it? And the call I use in this example is not even the
most complicated one you can come up with. Passing parameters by reference is
much more fun, I promise.
Although the complexity of working with late-bound objects is manageable after all,
there is exactly one reason why you always should work with RCWs instead: speed.
Late-bound invocation is a magnitude slower than working with early-bound objects.
1: using System;
2:
3: class TestPInvoke
4: {
5: [sysimport(dll="[Link]")]
6: public static extern int MessageBoxA(int hWnd, string
strMsg,
7: string strCaption, int nType);
8:
9: public static void Main()
10: {
11: int nMsgBoxResult;
12: nMsgBoxResult = MessageBoxA(0, "Hello C#", "PInvoke",
0);
13: }
14: }
Line 5 uses the sysimport attribute to specify that the function I am going to call is
declared in [Link]. Because I do not specify a name argument, the following
definition for the extern method must exactly match the name of the function I want
to call: MessageBoxA, where A is for the ANSI version of this function. The output of
this simple application is a message box with a "Hello C#" message.
Listing 10.7 demonstrates that by using the name argument, you can rename the
extern method to your liking.
Listing 10.7 Modifying the sysimport Attribute Still Yields the Desired
Result
1: using System;
2:
3: class TestPInvoke
4: {
5: [sysimport(dll="[Link]", name="MessageBoxA")]
6: public static extern int PopupBox(int h, string m,
string c, int type);
7:
8: public static void Main()
9: {
10: int nMsgBoxResult;
11: nMsgBoxResult = PopupBox(0, "Hello C#", "PInvoke", 0);
12: }
13: }
Although I demonstrated only a very straightforward and simple WIN32 method, you
can invoke any method that comes to your mind. If you get extremely fancy, you
can access WIN32 resource data or implement your own data marshaling. For this,
however, you have to take a look into the SDK documentation of NGWS runtime.
Unsafe Code
Programming unsafe code yourself is definitely not a task you will perform every day
when using C#. However, it is good to know that you can use pointers when you
have to do so. C# supports you with two keywords for writing unsafe code:
Unless you really need to work with raw blocks of memory—with pointers, that is—
COM Interoperability and the Platform Invocation Services should cover almost all
your needs to talk to COM or WIN32 functions.
To give you an idea what unsafe code might look like, take a look at Listing 10.8. It
shows how to use the unsafe and fixed keywords to create a program that performs
the square calculation just a little bit differently. To learn more about writing unsafe
code, please take a look at the C# reference.
1: using System;
2:
3: public class SquareSampleUnsafe
4: {
5: unsafe public void CalcSquare(int nSideLength, int
*pResult)
6: {
7: *pResult = nSideLength * nSideLength;
8: }
9: }
10:
11: class TestUnsafe
12: {
13: public static void Main()
14: {
15: int nResult = 0;
16:
17: unsafe
18: {
19: fixed(int* pResult = &nResult)
20: {
21: SquareSampleUnsafe sqsu = new SquareSampleUnsafe();
22: [Link](15,pResult);
23: [Link](nResult);
24: }
25: }
26: }
27: }
Summary
This chapter was entirely about how managed code can interoperate with
unmanaged code. At first, you learned how COM Interoperability can make NGWS
components work with COM clients, as well as how you can use COM components in
NGWS runtime clients. You learned about the differences of calling an object with
late binding or early binding, and what metadata and type libraries look like for the
conversion process.
A further interoperability service is the Platform Invocation Service PInvoke. It
enables you to call WIN32 functions, and it takes care of the data marshaling for
you. However, if you want to do it on your own, PInvoke allows you to do so.
The last feature presented is unsafe code. Although C# prefers managed code, you
still can work with pointers, pin blocks of memory to a specific location, and do all
the stuff you always wanted to do but that managed C# didn't allow.
Chapter 11 Debugging C# Code
How many times do you write code, run it once to verify the result, and then declare
the code tested? I hope that doesn't happen too often. You should test your code
line-by-line using a debugger. Even then, you can prove only the existence of bugs,
but not their absence.
Debugging is an important task in the software development process. The NGWS
SDK provides tools that enable you to debug your components thoroughly. My
recommendation: Use them! This chapter tells you how to use the following two
tools:
Debugging Tasks
Two debuggers ship with the NGWS SDK: a command-line debugger named CORDBG
and a UI debugger named SDK debugger. The latter is a stripped-down version of
the Visual Studio 7 debugger, and it is the one discussed in this chapter. The SDK
debugger has the following limitations when compared to its Visual Studio
counterpart:
• The SDK debugger doesn't support debugging of native code. You can debug
only managed code.
• No remote machine debugging is supported. To debug code on a remote
machine, you must use the Visual Studio debugger.
• The Registers window, although implemented, is not functional.
• The Disassembly window, although implemented, is not functional.
These limitations are of concern only when you debug in mixed-language or remote
environments. For the bulk of debugging tasks, the SDK debugger is just fine when
The first step you must take before you can debug your application code is to create
a debug version of your application. The debug build contains debugging information,
is not optimized, and an additional PDB (program database) file for debugging and
project state information is created.
To create such a debug build, you add two switches to the compilation process:
csc /optimize- /debug+ [Link]
This command creates two files: [Link] and [Link]. Now your
application is ready to be debugged. Listing 11.1 contains the source code of
[Link] for your review, as it is used again in the upcoming sections.
1: using System;
2: using [Link];
3:
4: class WhileDemoApp
5: {
6: public static void Main()
7: {
8: StreamReader sr = [Link] ("[Link]");
9: String strLine = null;
10:
11: while (null != (strLine = [Link]()))
12: {
13: [Link](strLine);
14: }
15:
16: [Link]();
17: }
18: }
The first step in setting up a debugging session is to select which application you
want to debug. Although you can attach to already-running applications (shown
later), the usual case is that you know upfront which application to debug. Therefore,
you start that application from inside the debugger.
You have already built one application for debugging in the previous section:
[Link]. You now set up the SDK debugger (shown in Figure 11.1) to debug
it. To start the SDK debugger, execute the application [Link], which resides in
the folder drive:\Program Files\NGWSSDK\GuiDebug.
Figure 11.1
The SDK debugger main window.
To select an executable for the debugging session, open the Debug menu and choose
the Program to Debug menu item. In the Program To Debug dialog box, select the
appropriate program by using the browse button (its caption is ...) next to the
Program text box (see Figure 11.2).
Figure 11.2
Selecting the executable for the debugging session.
Note that you can also specify command-line arguments in the Arguments text box,
which are passed to the application when the debugging session is started. Because
the current application does not take any arguments, leave this text box empty.
Basically, you could start the application in debugging mode immediately. However,
it is a good idea to define where you want to start inspecting the code during
execution by setting breakpoints.
Setting Breakpoints
The most commonly used kind of breakpoint is definitely the file breakpoint.
Complete the following two steps to create a file breakpoint for line 11 of
[Link], which is the start of the while loop.
1. From the File menu, choose Open/File. Search for the file [Link] and
open it.
2. Go to the line where you want to place the breakpoint and right-click. Select
Insert Breakpoint from the context menu. Your SDK debugger window should
now resemble the one in Figure 11.3. A red dot next to the line indicates that
the line contains a breakpoint (except in the case of data breakpoints).
Figure 11.3
Defining a breakpoint in the SDK debugger.
That is all there is to adding a breakpoint. If you want to edit the breakpoint's
properties, simply right-click and then select Breakpoint Properties from the context
menu. There you can set a breakpoint condition and click Count. This technique can
be used to tell the debugger to break at the breakpoint when the breakpoint
condition is satisfied for the nth time.
If you want to gain a quick overview of which breakpoints are set and which
conditions and hit counts are defined, simply open the Breakpoints window. It can be
accessed via the Windows/Breakpoints option in the Debug menu (see Figure 11.4).
Figure 11.4
Inspecting a breakpoint in the Breakpoints window.
With a breakpoint defined, you can now start the program in debugging mode. Either
select Start from the Debug menu, or click the play-button-like symbol on the Debug
toolbar. Execution will break at your breakpoint, enabling you to step through your
application.
The execution of your application is halted at a breakpoint, and you are in charge of
how the application continues to run. You can execute the code statements by using
the following commands (available via the Debug toolbar or menu):
Give the various commands a try in the current debugging session. When done, close
the debugger.
Attaching to a Process
Instead of specifying the executable upfront for the debugging session, you can pick
one from the list of currently executing applications and attach to that application to
debug it. This works for applications either that are executed as a service, or that
depend on user interaction. Basically, the point is that you must have enough time to
attach to the application before it finishes executing.
To demonstrate how this works, I will reuse the do-while example that prompts the
user to enter numbers to compute an average (see Listing 11.2).
1: using System;
2:
3: class ComputeAverageApp
4: {
5: public static void Main()
6: {
7: ComputeAverageApp theApp = new ComputeAverageApp();
8: [Link]();
9: }
10:
11: public void Run()
12: {
13: double dValue = 0;
14: double dSum = 0;
15: int nNoOfValues = 0;
16: char chContinue = 'y';
17: string strInput;
18:
19: do
20: {
21: [Link]("Enter a value: ");
22: strInput = [Link]();
23: dValue = [Link](strInput);
24: dSum += dValue;
25: nNoOfValues++;
26: [Link]("Read another value?");
27:
28: strInput = [Link]();
29: chContinue = [Link](strInput);
30: }
31: while ('y' == chContinue);
32:
33: [Link]("The average is {0}",dSum /
nNoOfValues);
34: }
35: }
Compile it using the following command (just a reminder):
csc /optimize- /debug+ [Link]
Execute the application at the command prompt and wait until it shows the Enter a
value: prompt. Then switch to the SDK debugger.
In the NGWS RUNTIME Debugger, choose Programs from the Debug menu. This
opens the Programs dialog box, where you can choose the application that you want
to debug (see Figure 11.5). Note that the SDK debugger can only be used to debug
applications that are of type COM+.
Figure 11.5
Attaching to a running program.
Click the Attach button, and click OK in the Attach to Process dialog box that opens.
Note that the Programs dialog box has now changed (see Figure 11.6). A welcome
addition is that you can choose either to detach from the process when you are
finished debugging, or to simply terminate it.
Figure 11.6
You can choose how to detach from a process after you have attached to it.
For now, click the Break button and then click Close. The source file is automatically
loaded, and the cursor waits in the line where the application is waiting for the user
input. Switch back to the application window, and enter a numeric value.
The next section continues with this sample. It shows you how to read and change
values that are assigned to variables.
When you return to the SDK debugger, you will notice that the debugger is still
waiting in the [Link] line. Step over it to read in the value you entered.
Place the cursor in line 26 and select Run to Cursor. All calculation code is executed.
Because this section is about inspecting and modifying variables, let's begin to do so.
Open the Locals window via the Debug, Windows/Locals menu option. The Locals
window shows all variables that are local to the currently executing method (see
Figure 11.7).
Figure 11.7
Viewing the variables that are local to the current method.
To modify a variable's value, double-click in the Value column of that variable. Enter
a new value and press the Enter key. That's all you have to do.
Another window of interest is Watch. In contrast to the Locals window, Watch doesn't
show any variables by default. You must enter the variables you want to watch by
clicking the Name column and entering the variable's name. However, the variables
always stay in the Watch window even if you jump between methods. Use the Watch
window to track variables of interest.
A really cool feature of the SDK debugger is how you can deal with exceptions. With
an application selected for debugging, you can open the configuration window for
exceptions via the Debug, Exceptions menu choice. Figure 11.8 shows the Exceptions
dialog where you can configure how the debugger should react to various exceptions.
Figure 11.8
Defining how the SDK debugger should react to different exceptions.
The default setting is to continue execution when an exception is thrown and, if the
exception is not handled by your code, to break into the debugger. All listed
exceptions inherit this default—their Use Parent Setting radio button is selected.
Although the defaults in place enable you to find exceptions that are not handled in
your code, you might feel the need to change the behavior for certain exceptions.
You might want to continue execution when an argument exception is thrown but not
handled, or you might decide to break into the debugger automatically when a
FileIOException is thrown (before the handler is invoked).
JIT Debugging
Exceptions are an excellent starting point for a debugging session anyway. When an
exception is not handled properly by your code, you are prompted to start debugging
(see Figure 11.9). This is called JIT (just in time) debugging.
Figure 11.9
Exceptions enable you to JIT debug your application.
The SDK debugger starts when you choose to perform JIT debugging. Give your okay
to attach to the process in question, and the debugger automatically opens the
source file and places the cursor in the offending line. In addition, you are notified
about which exception has occurred (see Figure 11.10).
You can now debug the application to your heart's content by using the techniques
that were outlined in this chapter.
Debugging Components
Figure 11.10
The debugger tells you which exception caused the JIT debugging session.
Summary
In this chapter, you learned how to use the SDK debugger that ships with the NGWS
SDK. Because the SDK debugger is a minimally stripped-down version of the Visual
Studio debugger, it provides a wealth of debugging functionality, which makes it a
snap to test your applications.
You can start a debugging session with a certain executable, or you can attach to
one that is already running. On hitting a breakpoint, you can step into, step out of,
step over, or run to a cursor position. Variable inspection and modification are, of
course, also possible. With the very flexible exception handling provided, you can
very thoroughly examine your application.
For those who like assembly language, the IL Disassembler is a tool they'll really
enjoy—they can easily learn the IL statements and interpret the code. For the rest of
us, the IL Disassembler is an important tool to learn more about a component's
manifest and metadata.
Chapter 12 Security
Security is an important topic, especially so because today's code comes from
multiple sources, including the Internet. Therefore, addressing security needs was an
important design goal for NGWS. These needs are addressed on the code level itself,
as well as on the user permission level.
Because the security provided by NGWS could fill a book on its own, this chapter
only introduces you to the concepts and possibilities. There are no code examples. I
don't want to convey the false impression that security is an afterthought, and short,
unrelated examples would give that impression.
Therefore, the following topics are covered in this chapter from a conceptual
viewpoint:
• Code-access security
• Role-based security
Code-Access Security
Today, code can come to a user's desk not only via a setup application executed
from a company's network server, but also from the Internet via a Web page or an
email. Recent experiences have shown that this can be quite dangerous. So how can
this threat be answered with NGWS?
The NGWS solution is code-access security. It controls access to protected resources
and operations. Code is trusted to varying degrees, depending on its identity and
where it comes from. The amount of code that must be fully trusted is reduced to a
minimum.
The following are the most notable functions of code access security:
From reading this list, you can see that less-trusted code will be prevented from
calling highly trusted code because permissions of the less-trusted code are
enforced. You will especially like that for Internet scenarios.
The two important points of code-access security are verification of the type safety of
managed code, and the permissions that are requested by the code. The minimum
requirement for you to benefit from code-access security is to generate type-safe
code.
Verification of Type Safety
The first step for the runtime in enforcing security restrictions on managed code is
being able to determine whether the code is type safe. This matters because the
runtime must be able to check the permissions of callers reliably.
The runtime checks permissions for all callers in the call stack to circumvent the
security hole that is created when less-trusted code calls highly trusted code. For this
stack-walking, the managed code must be verifiably type safe—every access to types
is performed only in allowed ways.
The good news is that the C# code you write is type safe unless you want to write
unsafe code. Both the IL and the metadata are inspected before the okay is given
regarding the type safety of code.
Permissions
The next step is to work actively with permissions. The benefit from actively
requesting permissions is that you know when you have proper permissions to
perform your code's actions, or how to degrade gracefully when you don't get them.
Additionally, you can prevent your code from getting extra permissions it wouldn't
need. Minimal permissions guarantee that your code will run on tightly restricted
systems where code that requests too much permission without need will fail.
Although I mentioned the kinds of permissions already, here is the list again:
• Standard permissions
• Identity permissions
Standard Permissions
Securing access to resources that are exposed by the NGWS framework is taken care
of by the code-access permissions. With those permissions, you gain either access to
a protected resource, or the right to perform a protected operation. Your code can
demand any permission at runtime, and the runtime decides whether your code gets
that permission.
Table 12.1 shows a list of standard permissions and a brief description of each. Note,
for example, that the net classes have separate network access security.
Identity Permissions
Permission Description
PublisherIdentityPermission The signature on an NGWS component
provides proof of the software's
publisher.
StrongNameIdentityPermission Defines the cryptographically strong
name of a component. The strong
name key and the simple name part
comprise the identity.
ZoneIdentityPermission Defines the zone from which the code
originates. A URL can belong to only
one zone.
SiteIdentityPermission Permissions derived based on the Web
site from which the code originates.
URLIdentityPermission Permissions derived based on the URL
from which the code originates.
Role-Based Security
The system of role-based security might be already familiar for you because the
NGWS role-based security system is, to some degree, similar to the one found in
COM+. However, there are some differences you need to be aware of, so read on.
The NGWS role-based security is modeled around a principal, which represents either
a user, or an agent that is acting on behalf of a given user. An NGWS application
makes security decisions based on either the principal's identity, or its role
membership.
So, what is a role? For example, a bank has clerks and managers. A clerk can
prepare a loan application, but the manager must approve it. It doesn't matter which
instance of manager (principal) approves it, but he or she must be a member of the
manager role.
In more technical terms, a role is a named set of users who share the same
privileges. One principal can be a member of multiple roles and, therefore, you can
use role membership to determine whether certain requested actions may be
performed for a principal.
I have already mentioned briefly that a principal is not necessarily a user, but it can
be also an agent. More generally, there are three kinds of principals:
How does it work for you in your application? NGWS provides you with the
PrincipalPermission class, which provides consistency with code-access security. It
enables the runtime to perform authorization in a way similar to code-access security
checks, but you can directly access a principal's identity information and perform role
and identity checks in your code when you need to do so.
Summary
In this final chapter of this book, I introduced you to the concepts of security that are
part of NGWS. I took you on a tour of code-access security and role-based security.
You learned about standard and identity permissions, which are used to enforce
code-access security, as well as about principals and roles in role-based security
scenarios.
The NGWS Runtime provides several key benefits to C# programming, including cross-language integration through the Common Language Specification, automatic memory management via garbage collection, cross-language exception handling, enhanced security features like type safety, support for versioning, and a simplified model for component interaction. These features ensure robust, secure, and manageable application development .
C# enforces type safety more strictly than C++, preventing many common programming errors by ensuring that operations on incompatible types are detected at compile time. In C++, more flexibility is allowed, leading to possible runtime errors such as type mismatches or invalid memory access. Type safety in C# minimizes such risks, enhancing security and reliability by catching errors early in the development process rather than at runtime .
C# uses structured exception handling with try-catch blocks, where code that might throw exceptions is enclosed in a try block, and exceptions are caught and processed in a catch block. Additionally, a finally block can be used to ensure that specific cleanup code is executed regardless of whether an exception is thrown. Developers can choose to catch specific exceptions like OverflowException or handle all exceptions generally, but catching general exceptions may obscure specific error details. The approach allows applications to handle errors gracefully without abrupt termination .
C# uses automatic memory management via the NGWS Runtime's garbage collector, which automatically releases memory for objects or variables that are no longer referenced. This prevents common issues like memory leaks, which are prevalent in C++ where developers must manually manage memory allocation and deallocation. The automatic approach of C# reduces programming errors and simplifies development .
Combining managed and unmanaged code in a C# application can lead to challenges such as memory leaks and security risks due to the lack of type safety and automatic memory management in unmanaged code. These issues can be addressed by using interoperability techniques like COM Interoperability and Platform Invocation Services cautiously, ensuring proper error handling, and confining unsafe code sections using the 'unsafe' keyword in C#. Developers need to balance the efficiency benefits of unmanaged code with the robustness features of managed code .
Conditional compilation in C# allows developers to include or exclude code during compilation based on specific symbols, facilitating the creation of different build configurations such as debug or release versions. Documentation comments automate software documentation generation, integrating help information directly into development environments like Visual Studio. These features improve code maintainability, clearly communicate component purposes and configurations, and streamline development processes by automating repetitive tasks .
Metadata in NGWS Runtime applications describes the types in the code and is stored in the same PE file as the code itself. It enables various runtime services, such as type safety, cross-language operation, and versioning. The inclusion of metadata ensures that applications can verify the presence of necessary component versions before execution, reducing runtime errors due to missing dependencies. This approach simplifies the deployment and execution processes by eliminating the registry problems associated with the previous generation of Windows components .
Developers might prefer private NGWS components over shared ones to avoid the 'DLL Hell' problem, where shared libraries conflict with different versions used by various applications. By using private components, developers ensure consistency and compatibility of components for their specific applications. Although this approach might increase disk usage, it leverages the advantage of strong naming and version control, minimizing dependency conflicts and simplifying application maintenance and deployment .
C# can interact with unmanaged code via COM Interoperability, Platform Invocation Services, and writing unsafe code. This interoperability is crucial because it allows C# applications to utilize existing unmanaged code, such as legacy COM components or C DLL functions, thus integrating new functionality without completely rewriting older, yet essential, components. This capability fosters a transition to modern frameworks while maintaining compatibility with existing systems .
The 'checked' statement in C# forces runtime overflow checking for arithmetic operations, throwing an OverflowException when an overflow occurs. This ensures that developers are alerted to arithmetic errors that could lead to incorrect calculations. Conversely, the 'unchecked' statement allows arithmetic operations to exceed their limits without generating exceptions, which can be useful for performance optimization where overflow is not a concern. Effective use of these statements helps balance performance needs with error detection, improving the reliability of mathematical computations in an application .