|
C# 2.0:
14 More Reasons to Use C# for .NET
Development
by
Herbert Schildt
C# is the premier language for .NET
development. Although you can write .NET code using VB, J#, and Managed C++, C#
was designed from the start with .NET programming in mind. Thus, of the
available choices, C# is the one most tightly integrated with the .NET
environment. For example:
 |
The C# library is the .NET API. |
 |
C#'s data types are the standard .NET data
types. |
 |
C#'s built-in threading primitive lock is
essentially shorthand for features defined by the .NET class System.Threading.Monitor. |
Furthermore, C#'s support for managed
execution via the CLR (Common Language Runtime) execution environment was
designed in, rather than being added on. For the serious .NET developer, C# is
both your first and your best choice.
Given that C# is already the language of
choice for .NET development, why would anyone need more reasons to use it, let
alone 14 more reasons? The answer is that 14 is the number of major new features
Microsoft is adding to the language with the release of C# 2.0.
The new features in C# 2.0 are not
simply incremental improvements. Rather, they fundamentally expand the power of
the language, streamline certain common constructs, simplify project management,
and in one case, solve a longstanding problem. The 14 major additions to C#
added by C# 2.0 are listed here.
|
1. Generics
2. Nullable Types
3. Iterators
4. Partial Class Definitions
5. Anonymous Methods
6. The :: Alias Qualifier
7. Static Classes |
8. Covariance and Contravariance
9. Fixed-Size Buffers
10. Friend Assemblies
11. extern Aliases
12. Method Group Conversion
13. Accessor Access Control
14. The #pragma Directive |
Of these, the most significant in its
impact on the programmer is generics. However, all are substantive enhancements.
The following presents a brief overview of each.
Generics
Of the many new features added by C# 2.0, the one
that has the most profound effect is generics. Not only does it add a
new syntactical element, it also causes many additions to the core API.
With generics, it is now possible for the C# programmer to easily create type-safe,
reusable code. As a point of reference, generics in C# are similar to
(but not the same as) generics in Java and templates in C++.
At its core, the term generics means parameterized
types. A parameterized type is a class, interface, method, or
delegate in which the type of data upon which it operates is specified as a
parameter. A class, interface, method, or delegate that operates on a
parameterized type is called generic, as in generic class
or generic method.
Generics are a very powerful feature because they let you
define the general form of an algorithm and then apply that algorithm to
several different types of data. As most readers know, many algorithms are
logically the same no matter what type of data they are being applied to.
For example, the mechanism that supports a stack is the same whether that
stack is storing items of type int, string, object, or
a user-defined class.
With generics, you can define an algorithm once,
independently of any specific type of data, and then apply that algorithm to
a wide variety of data simply by specifying a different type for the type
parameter. Thus, generics make it possible to create a single stack class,
for example, that automatically works with different types of data. You no
longer need to create a separate version of the class for each different
data type.
To get an idea of how generics work, let's look at an
example from the new generic Collections library. C# 2.0 defines a generic Stack
class that is declared like this:
class Stack<T>
Here, T is a type parameter. It is used
inside Stack as a placeholder for the actual type that is passed when
a Stack object is created. For example, Stack<T> defines
the following version of Push( ).
void Push(T obj)
Here, the type parameter T specifies the type of the
object to be pushed on the stack. When a stack is created, T is
replaced by the actual type. For example, the following creates a stack for
integers.
Stack<int> iStck = new Stack<int>();
This declaration passes int to T. This creates
a stack that can hold objects of type int. Thus, for this version of Stack,
Push( ) acts as if it were declared like this:
void Push(int obj)
It is important to understand that C# has always given you
the ability to create generalized classes, interfaces, methods, and
delegates by operating through references of type object. Because object
is the base class of all other classes, an object reference can refer
to any type object. Thus, before generics, generalized code used object
references to operate on a variety of different types of data. The problem
was that it could not do so with type safety.
Generics add the type safety that was lacking. Because the
type of data being operated upon is specified as a parameter, C# can enforce
at compile-time that only compatible data is used. Furthermore, it is no
longer necessary to employ casts to translate between object and the
type of data that is actually being operated upon. Thus, generics expand
your ability to re-use code, and let you do so safely and easily.
Generics are a major addition to C# that will affect all C#
programmers. Generics help you create more resilient, reliable code. It is
well worth the effort it takes to learn to use generics effectively.
Nullable Types
Nullable types provide an elegant solution to what has been
a long-standing, irritating problem: how to recognize and handle fields that
do not contain values (in other words, unassigned fields). For example, in
database applications, it is not uncommon to have fields that are
unassigned.
In the past, handling the possibility of an unassigned field
required either the use of placeholder values, or an extra field that simply
indicated whether a field was in use or not. Of course, placeholder values
can work only if there is a value that would otherwise not be valid, which
won't be the case in all situations. Adding an extra field to indicate if a
field is in use works in all cases, but having to manually create and manage
such a field is an unsatisfying solution. The nullable type solves both
problems.
A nullable type is a special version of a value type that is
represented by a structure. In addition to the values defined by the
underlying type, a nullable type can also store the value null. Thus,
a nullable type has the same range and characteristics as its underlying
type. It simply adds the ability to represent a value (null) that
indicates that a variable of that type is unassigned. Nullable types are
objects of System.Nullable<T>, where T must be a value
type.
You can create a nullable type by explicitly declaring
objects of type Nullable<T>, but there is an easier way: Simply
follow the type name with a ?. For example, the following declares a
nullable int and bool type.
int? count;
bool? done;
Nullable types are not something needed by all programmers,
but for those who do need them, they provide a welcome solution to an old
problem.
Iterators
Prior to C# 2.0, if you wanted to be able to cycle through
the members of a class using a foreach loop, that class must
implement the methods defined by the IEnumerator and IEnumerable
interfaces. While neither of these interfaces is difficult to implement, C#
2.0 offers a better way: the iterator.
An iterator is a method, operator, or accessor that returns
the members of a set of objects, one member at a time, from start to finish.
For example, given a five-element array, an iterator for that array
will return those five elements, in order, one at a time. To implement an iterator,
you simply provide a GetEnumerator( ) method, which returns each
element in the set through the use of the new yield keyword. The
advantage of using an iterator is that it requires much less code than does
implementing IEnumerator and IEnumerable.
Partial Class Definitions
Beginning with C# 2.0, a class definition can be broken into
two or more pieces, with each piece residing in a separate file. This is
accomplished through the use of the partial keyword. When your
program is compiled, the pieces of the class are united, forming a single
class.
Anonymous Methods
An anonymous method is, essentially, a block of code that is
passed to a delegate. The main advantage to using an anonymous method is
simplicity. In many cases, there is no need to actually declare a separate
method whose only purpose is to be passed to a delegate. In this situation,
it is easier to pass a block of code to a delegate than it is to first
create a method and then pass that method to the delegate.
Here is a simple example that uses an anonymous method.
using System;
// Declare a delegate.
delegate void CountIt();
class AnonMethDemo {
public static void Main() {
// Here, the code for counting
// is passed as an anonymous method.
CountIt count = delegate {
// This block of code is an
// anonymous method that is
// passed to the delegate.
for(int i=0; i <= 5; i++)
Console.WriteLine(i);
}; // notice the semicolon
// Call the anonymous method
// through the delegate.
count();
}
}
This example first declares a delegate type called CountIt
that has no parameters and returns void. Inside Main( ), a CountIt
delegate called count is created and it is passed the block of code
that follows the delegate keyword. This block of code is the
anonymous method that will be executed when count is called.
The :: Alias Qualifier
Although namespaces help prevent name conflicts, they do not
completely eliminate them. One way that a conflict can still occur is when
the same name is declared within two different namespaces, and you then try
to bring both namespaces into view. For example, assume that two different
namespaces contain a class called MyClass. If you attempt to bring
these two namespaces into view via using statements, MyClass
in the first namespace will conflict with MyClass in the second
namespace, causing an ambiguity error. In this situation, you can use the ::
namespace alias qualifier to explicitly specify which namespace is
intended. You can also use the :: to access a name in the global
namespace that is hidden by a local name.
Static Classes
Beginning with C# 2.0 you can now declare a class static.
A static class must contain only static members. No instance members are
allowed. The main benefit of declaring a class static is that it enables the
compiler to prevent any instances of that class from being created.
Covariance and Contravariance
Covariance and contravariance are two new
features that relate to delegates. Normally, the method that you pass
to a delegate must have the same return type and signature as the delegate.
However, covariance and contravariance relax this rule slightly, as it
pertains to derived types. Covariance enables a method to be assigned to a
delegate when the method's return type is a class derived from the class
specified by the return type of the delegate. Contravariance enables a
method to be assigned to a delegate when a method's parameter type is a base
class of the class specified by the delegate's declaration.
Fixed-Size Buffers
C# 2.0 expanded the use of the fixed keyword to
enable you to create fixed-sized, single-dimension arrays, which are called fixed-size
buffers. A fixed-size buffer is always a member of a struct. The
purpose of a fixed-size buffer is to allow the creation of a struct
in which the array elements that make up the buffer are contained within the
struct. Normally, when you include an array member in a struct,
only a reference to the array is actually held within the struct. By
using a fixed-size buffer, you cause the entire array to be contained within
the struct. This results in a structure that can be used in
situations in which the size of a struct is important, such as in
mixed-language programming; interfacing to data not created by a C# program;
or whenever a non-managed struct containing an array is required.
Fixed-size buffers can only be used within an unsafe context.
Friend Assemblies
C# 2.0 added the ability to make one assembly the friend
of another. A friend has access to the non-public members of the assembly of
which it is a friend. This feature makes it possible to share types between
selected assemblies without making those types public.
extern Aliases
C# 2.0 defines an additional use for the extern
keyword that provides an alias for an external assembly. It is used in cases
in which a program includes two separate assemblies which both contain the
same type name. For example, if an assembly called test1 contains a
class called MyClass and test2 also contains a class called MyClass,
then a conflict will arise if both classes need to be used within the same
program. To solve this problem, you must create an extern alias for
each assembly. Doing so allows you to reference each version of MyClass
separately.
Method Group Conversion
C# 2.0 includes a feature called method group conversion
that simplifies the syntax used to assign a method to a delegate. Method
group conversion allows you to assign the name of a method to a delegate, without the use new or explicitly invoking the delegate's
constructor. For example, assume a delegate called StrMod that is
declared like this:
delegate string StrMod(string str);
In the past, to assign a method called removeSpaces( )
to that delegate, you would use a statement like this:
strOp = new StrMod(removeSpaces);
With the addition of method group conversion, this statement
can now be written more compactly:
strOp = removeSpaces;
This syntax is both shorter and more to the point.
Accessor Access Control
You can now specify an access modifier, such as private,
when declaring a get or set accessor. Doing so enables you to
control access to an accessor. For example, you might want to make the set
accessor private to prevent the value of a property or an indexer from being
set by code outside its class. In this case, the value of the property or
indexer could still be obtained by any code, but set only by a member of its
class.
#pragma Directive
The #pragma preprocessor directive gives an
instruction to the compiler. Currently, C# supports #pragma warning¸ which
turns on or off a compiler warning, and #pragma checksum, which
generates a checksum.
Beyond the direct benefits of the
features themselves, the preceding list makes another important point about C#:
It is still a vibrant, evolving language. One of the immutable realities of
programming is captured by the phrase "adapt or die." As all
programmers know, things do not stand still for long in our profession.
Programmers who fail to adopt new technologies and better techniques quickly
find themselves marginalized. The same is true of computer languages. A language
that fails to keep pace with advances in programming soon fades from the scene.
Version 2.0 is a major upgrade for C# that clearly places it at the forefront of
computer language development. C# is here to stay.
As stated at the start of this article,
C# is the preeminent language for .NET development. The powerful new features
added by version 2.0 simply underscores this point. If you are programming for
.NET, then C# 2.0 is the best way to get the job done.
|