HerbSchildt.com

 

Home
Up
About Herb Schildt
Beginner's Corner
Current Books
What's New?
Where to Buy
Book Code
Speaking/Training
Errata
Contact Info

New Releases!

Herb's  Programming Cookbooks

C++

Java

 

 

 

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.

 

 

  © 2008  HerbSchildt.com  All rights reserved worldwide. No duplication allowed without prior written permission.