In-Depth
Hidden Gems in C# 2.0
You''ve heard much about generics, iterators, partial types, and anonymous methods. But many other new features will change your daily programming life just as much.
You've likely heard about the four major features added to the C# language in version 2.0: Generics, Iterators, Partial Types, and Anonymous Delegates. But many other features were added to this new version of C#. These other features will help you express your designs more clearly, in less code. You should become familiar with these enhancements to your favorite language now. That knowledge will help you create code today that can be easily enhanced tomorrow. You want your code ready for C# 2.0 when you are.
Static Classes
You've probably already created classes that contain only static methods and fields. These methods are essentially global utilities for your application domain. It's not a sign of a bad design; the .NET Framework System.Math class is an example of this construct.
C# 2.0 adds a new use for the static keyword for just this purpose. Static classes have a number of restrictions, which communicate your design intent to other developers:
- Static classes cannot have instance constructors. This restriction means that developers cannot create an instance of any object of a static type. You are communicating that your static class is a means of organizing similar functionality. A static class is not an abstraction that will be used to create other types or instances of objects. From this first restriction, others follow:
- Static classes are implicitly sealed. No developer can create a class that is derived from a static class.
- Static classes cannot have instance members. If you can't create an instance of the object, it makes no sense to define instance members. (Obviously, this also means that static classes cannot have virtual members.)
To achieve similar behavior in C# 1.x, you define a private constructor in your class, and you define your class as sealed and abstract. Defining it as sealed prevents users from creating derived classes; defining it as abstract, combined with the private constructor, prevents users from creating an instance of this class. As you move to C# 2.0, use static classes for this idiom, because the same constructs implement the singleton pattern.
Accessor Accessibility
It's common to create a class where you have a public read-only property, and yet you want to allow derived classes or other classes in your assembly to modify the value of the property. In C# 1.x, you wrote this:
public int MyValue
{
get { return val; }
}
protected void SetMyValue(
int theNewValue )
{
This.val = theNewValue;
}
In C# 2.0, you can specify a more restrictive access to the set (or get) accessor. This creates much cleaner code that is easier to maintain. The access modifiers can only decrease the visibility of the property; you cannot increase the visibility using the access modifier:
public int MyValue
{
get { return val; }
protected set { val = value; }
}
That's a fine start, but there are a few other ways to use property accessor accessibility to write cleaner, easier-to-maintain code. First off, you can create a private property setter to use when you modify the value inside your class. That provides you, as the class author, with a single location in the code to write any validation on modifications to that value:
public int MyValue
{
get { return val; }
private set
{
if ( value < 0 )
throw new
ArgumentException(
@" non-negative only" );
val = value;
// Fire change event to
// notify client code that a
// value changed.
}
}
This same technique applies when you implement an interface that contains a read-only property. If the interface does not contain a definition for the property setter, you can add your own, at any access level that is appropriate. However, if the interface did contain both a set and get accessor, you cannot specify different accessibility to the implementations in your class:
public interface IFoo
{
public int MyValue
{ get; }
}
// elsewhere:
public int MyValue
{
get { return val; }
// Access modifier allowed.
// IFoo does not define a set
// method.
private set
{
if ( value < 0 )
throw new
ArgumentException(
@" non-negative only" );
val = value;
// Possibly fire events to
// notify clients of changed
// state.
}
}
You can use the same technique with virtual and overridden property accessors. However, the accessibility must be defined in the base class, and cannot be modified in any of the derived classes.
The purpose of access modifiers on properties is to create more consistent code: A property represents a data element exported from a type. Different accessibility can be used on the set and get methods to keep the code that accesses and modifies a value more cohesive.
Friend Assemblies
Friend assemblies provide a method to allow another assembly to be granted access to the internals of a given assembly. You do this through the use of an attribute:
[assembly:InternalsVisibleTo(
"AFriend")]
This designation provides AFriend.dll with access to all internal types, methods, fields, and properties in any type declared in the assembly where the attribute is defined.
I'd avoid using this in most production code. If you have internal types that need access from another assembly, that often indicates a poor design.
The right solution is to refactor those classes so that you can provide access only through public interfaces, or public classes. However, there is one idiom that I've found useful: unit tests for internal types and methods. I can create a separate assembly for all my unit tests, and make that assembly a friend of the target assembly. I separate all my unit tests in a second assembly, which means I don't need to deliver the unit tests to customers (or limit my unit tests to debug builds). In addition, I can still write unit tests for all my internal classes.
Nullable Types
Nullable types were developed to simplify creating C# logic that uses data retrieved from databases. Most databases provide a facility to define a missing (or null) value in a given column and record. This missing value is difficult to represent in .NET value types: Which integer value should be used to represent a nonexistent value? There is no one universal answer. So, the C# and CLR teams introduced the idea of a nullable value type.
A nullable type is a wrapper around a value type that can represent all the possible values of its underlying type, and can also hold the null value. Nullable types sound simple. But the more you examine the subtleties of the concept, the more complicated they become. The syntax is simple: You append a '?' to a value type to produce a nullable type. (This is a shorthand notation for a generic class, nullable <T>, where T must be a value type.) For example, int?, char?, and bool? all define nullable types for the respective underlying value types. However, because the string class is a reference type, you cannot define a nullable string. Of course, because the string class is a reference type, you don't need to, either.
When you use nullable types, you must remember that the nullable type is not a simple replacement for the value. In fact, nullable types introduce quite a few complications into your program logic. You'd best beware of all the pitfalls before you venture into nullable types where the regular value type would suffice. In these examples, I'll use integer nullables, but any nullable type would exhibit the same problem.
First, a regular integer cannot be assigned the value of a nullable type without a cast, or accessing its underlying Value property:
int? maybeAValue;
int val = maybeAValue.Value;
int val2 = (int ) maybeAvalue;
Both these assignments compile, but a System.InvalidOperationException will be thrown if maybeAValue stores null. That's just extra work. Any time you access the value inside a nullable type, you must check whether the instance has a valid value using the Nullable<T>.HasValue property. In fact, this idiom is common enough that the C# language has included a special syntax to perform assignments with nullable types, the ?? operator.
The ?? operator returns the value of the left operand if the operand has a value, and the right operand otherwise. For instance:
int result = maybeAValue ?? 0;
returns 0 if maybeAValue is null. Otherwise, it returns the value stored in the nullable maybeAValue.
Things get even more complicated when you start boxing and unboxing nullable types. The CLR and C# teams recently made a change to nullable types that creates more cohesive behavior when nullable types are boxed and unboxed. The CTP release that incorporates the change was being released as I was finishing this article, so I have not had the chance to work with the new code yet. Click here for more details.
Miscellaneous
There are a few other additions that warrant mention, but won't affect your daily coding as much. You can now enable and disable warnings inline in code. This will make it easier to get large projects to compile cleanly at higher warning levels. You can selectively disable certain warning conditions after reviewing the specific code that generates the warnings. Similar to the C++ compiler, the syntax is a pre-processor pragma:
#pragma warning disable 3021
C# 2.0 adds the ability to create fixed block arrays, but only in unsafe code blocks. You can use this feature to provide buffers that can be used between managed and unmanaged code layers. For instance, this structure takes 4 bytes on the stack, and the myArray variable is a reference that points to a different location in managed memory:
public struct MyArray
{
public char MyName[50];
}
However, in C# 2.0, you can ensure that the array is stored inline in the structure, as long as the structure is used only in unsafe code:
public struct MyArray
{
public fixed char MyName[50];
}
This second declaration takes 100 bytes. (Fixed size char buffers take 2 bytes per char.)
Finally, C# delegate declarations support Covariance and Contravariance. Covariance means that a delegate handler may specify a return type that is derived from the return type specified in the delegate signature. Contravariance means that a delegate handler may specify base classes for derived class arguments specified in the delegate signature. Because of covariance, this compiles:
public delegate object
Delegate1();
public static string func();
Delegate1 d = new Delegate1(
func );
func returns a type (string) that is derived from the type defined in the delegate signature (object).
Because of contravariance, this compiles:
public delegate void
Delegate2( string arg );
public static void func2(
object arg );
Delegate2 d2 = new Delegate1(
func2 );
func2 accepts a base class (object) of the type (string) defined as the argument for the delegate method. These additions will make it easier for you to create event handlers that handle multiple events: You can write one handler that receives a System.EventArgs parameter, and attach it to all events.
I've given you a small taste of some of the new features in C# 2.0 that have seen less coverage compared to the major new initiatives. But these features will change the way you write C# code, and change it for the better. You'll be able to create code that better expresses your designs, and is easier to maintain. By learning these features you'll be able to write less code that does more. That's what we all need.
About the Author
Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].