O'Reilly logo

.NET Gotchas by Venkat Subramaniam

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 1. CLR/Framework Gotchas

The Common Language Runtime (CLR) provides a common platform for the execution of programs in .NET. The code written in a .NET language is first translated into the Microsoft Intermediate Language (MSIL). The CLR then executes the MSIL. The .NET Framework exposes a rich class library to give your applications access to its underlying capabilities.

In this chapter I discuss the features in the CLR and the Framework that can impact the behavior and performance of your application. I will also discuss items that are somewhat confusing, misleading, or prone to misuse.

Developers coming into .NET knowing other languages expect behavior similar to what they’re used to. C++ and Java programmers expect C# to act almost the same, since it looks almost the same. VB6 programmers have like expectations of VB.NET.

However, when it comes to the aliases for data types, and the behavior of value types versus reference types, .NET has a few surprises. Furthermore, some features may be convenient to use, but may not provide the best performance. Suffice it to say that when dealing with a rich but new API, you need to clearly understand its behavior. Idiosyncrasies often cost you precious time.

GOTCHA #1 Type alias size doesn’t match what you’re familiar with

The Common Language Specification (CLS) provides rules to enable interoperation of types written in different languages. The Common Type System (CTS) enables cross-language integration, type safety, and high-performance execution of managed code. However, not all types supported in the .NET Framework are interoperable or CLS-compliant. When developing your class library, make sure the types you write and expose are CLS-compliant. Run your code through FxCop to make sure it complies with the Microsoft “Design Guidelines for Class Library Developers” (see "On the Web" in the Appendix).

As a convenience to C++, Java, and VB6 programmers, .NET languages provide aliases for the CTS data types, like int and long in C# and Integer and Long in VB.NET [Albahari02, Drayton03, Evjen04, Hamilton03, Robinson04, Thai03]. The aliases appear to match data types in those other languages, but some of the most important do not.

If you’re a C++ programmer, you might assume that a long in C# corresponds to a long in C++. If you’re coming from VB6, you might think that Integer and Long in VB.NET are equivalent to the Integer and Long you’re used to in VB6. But in both cases, you’d be wrong.

Let’s look at an example. How do I make an application beep? One possibility is to use PInvoke to call the Win32 Beep() method. PInvoke is a .NET facility that allows managed code to call unmanaged functions in DLLs. In VB6, Beep() has this declaration:

Beep(dwFreq as Long, dwDuration as Long) as Boolean

The Long type in VB6 is a 32-bit integer.

The underlying Win32 prototype is:

Beep(dwFreq as DWORD, dwDuration as DWORD) as Boolean

Now, let’s use Beep() in .NET. The code to do this is shown in Example 1-1:

Example 1-1. Producing Beeps

C# (Aliases)

using System;
using System.Runtime.InteropServices;

namespace InvokeBeep
{
    class Test
    {
        [DllImport("kernel32")]
        public static extern bool Beep(long dwFreq,
            long dwDuration);

        [STAThread]
        static void Main(string[] args)
        {
            Beep(1000, 1000);
        }
    }
}

VB.NET# (Aliases)

Imports System.Runtime.InteropServices

Module Module1

    Public Declare Function Beep Lib "Kernel32" ( _
        ByVal dwFreq As Long, ByVal dwDuration As Long) As Boolean

    Sub Main()
        Beep(1000, 1000)
    End Sub

End Module

When you compile and run this example, it fails to produce the desired result. On my system, I don’t hear any beeps. What went wrong?

While the CTS defines the data types available to all .NET languages, such as System.Int32 and System.Double, each .NET language defines its own aliases for these types. For instance, C# uses int as an alias for System.Int32 and VB.NET uses Integer. These aliases appear to correspond to the types C++ developers (in the case of C#) and VB6 developers (in the case of VB.NET) are familiar with.

Table 1-1 shows the CTS types and the size differences between C# and C++. Table 1-2 shows the CTS types and the size differences between VB.NET and VB6.

Table 1-1. Some CTS types and aliases in C#

CTS type

Size

C# alias

Type equivalent in C++

System.Int32

4 bytes

int

int or long

System.Int64

8 bytes

long

_ _int64

System.Char

2 bytes

char

WCHAR

System.Double

8 bytes

double

double

Table 1-2. Some CTS types and aliases in VB.NET

CTS type

Size

VB.NET alias

Type equivalent in VB

System.Int32

4 bytes

Integer

Long

System.Int64

8 bytes

Long

N/A

System.Char

2 bytes

Char

String * 1

System.Double

8 bytes

Double

Double

As you can see from these tables, a long in C++ is not the same size as a long in C#. Nor is a Long in VB6 the same size as a Long in VB.NET. Long in VB6 corresponds to Integer in VB.NET. In the code of Example 1-1, you were actually passing 64-bit arguments to a function that expected 32 bits due to the improper declaration of the Beep() method. The code change in Example 1-2 fixes the problem.

Technically, sending an Integer in VB.NET is not a completely accurate mapping. Beep() expects two unsigned 32-bit (DWORD) arguments. A VB.NET Integer is a signed 32-bit integer. There is no VB.NET alias for System.UInt32 (which is not CLS-compliant). You may use System.UInt32 as the parameter type in the Beep method.

You can take a trial-and-error approach to figuring out the PInvoke signature in C#/ VB.NET. Or you can quickly look up the signature for most Win32 and other APIs at http://www.pinvoke.net.

Example 1-2. Proper declaration for the Beep method

C# (Aliases )

        [DllImport("kernel32")]
        public static extern bool Beep(uint dwFreq,
            uint dwDuration);

VB.NET (Aliases)

    Public Declare Function Beep Lib "Kernel32" ( _
        ByVal dwFreq As Integer, ByVal dwDuration As Integer) _
As Boolean

IN A NUTSHELL

Be mindful of the sizes of the aliases used in .NET languages. If in doubt, use the fully qualified name from the CTS, as in System.Integer. For PInvoke signatures, look up the correct type mapping at http://www.pinvoke.net.

SEE ALSO

Gotcha #2, "struct and class differ in behavior.”

GOTCHA #2 struct and class differ in behavior

In C++, you can create an object on the stack or on the heap. When you use the new keyword, you create the object on the heap. In Java, you can only create objects on the heap; primitive built-in types are created on the stack, unless they are embedded within an object. Also, you can’t use the new keyword on primitive built-in types.

.NET behaves more like Java than C++. An object is created on the heap if it is a reference type. If it is a value type, it is created on the stack, unless it is embedded within an object. Whether an object is a value type or reference type depends on how it is defined. If it is defined using the class keyword, it is a reference type. If it is defined with the struct keyword in C# or Structure in VB.NET, it’s a value type. Even though you are using the same new keyword in the syntax, an instance of a reference type is created on the managed heap but an instance of a value type is created on the stack. This leads to some confusion when looking at code. Specifically, the effect of an assignment statement varies between structures (value types) and classes (reference types). This is illustrated in Example 1-3. The potentially troublesome assignment statements are highlighted.

Example 1-3. Assignment of reference type

C# (ValueReferenceTypes)

//AType.cs
using System;

namespace ValTypeRefTypeAssignment
{
    public class AType
    {
        private int aField;

        public int TheValue
        {
            get { return aField; }
            set { aField = value; }
        }
    }
}

//Test.cs
using System;

namespace ValTypeRefTypeAssignment
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            AType firstInstance = new AType();

            firstInstance.TheValue = 2;

            AType secondInstance = new AType();
            secondInstance.TheValue = 3;

            Console.WriteLine("The values are {0} and {1}",
                firstInstance.TheValue,
                secondInstance.TheValue);

            firstInstance = secondInstance; // Line A

            Console.Write("Values after assignment ");
            Console.WriteLine("are {0} and {1}",
                firstInstance.TheValue,
                secondInstance.TheValue);

            secondInstance.TheValue = 4;

            Console.Write("Values after modifying TheValue ");
            Console.Write("in secondInstance are ");
            Console.WriteLine("{0} and {1}",
                firstInstance.TheValue,
                secondInstance.TheValue);
        }
    }
}

VB.NET (ValueReferenceTypes)

'AType.vb
Public Class AType
    Private aField As Integer

    Public Property TheValue() As Integer
        Get
            Return aField
        End Get
        Set(ByVal Value As Integer)
            aField = Value
        End Set
    End Property
End Class

'Test.vb
Public Class Test
    Public Shared Sub Main()
        Dim firstInstance As New AType

        firstInstance.TheValue = 2

        Dim secondInstance As New AType
        secondInstance.TheValue = 3

        Console.WriteLine("The values are {0} and {1}", _
            firstInstance.TheValue, _
            secondInstance.TheValue)

        firstInstance = secondInstance ' Line A

        Console.Write("Values after assignment ")
        Console.WriteLine("are {0} and {1}", _
            firstInstance.TheValue, _
            secondInstance.TheValue)

        secondInstance.TheValue = 4

        Console.Write("Values after modifying TheValue ")
        Console.Write("in secondInstance are ")

        Console.WriteLine("{0} and {1}", _
            firstInstance.TheValue, _
            secondInstance.TheValue)
    End Sub
End Class

The output produced by the above code is shown in Figure 1-1.

Output from Example 1-3

Figure 1-1. Output from Example 1-3

In the assignment statement:

firstInstance = secondInstance

you are modifying the reference firstInstance. The effect of that statement is shown in Figure 1-2.

Effect of assignment on reference type

Figure 1-2. Effect of assignment on reference type

Therefore, when you change secondInstance.TheValue, you also change firstInstance.TheValue, since firstInstance and secondInstance now refer to the same object.

Let’s make just one change. Let’s modify AType from a class to a struct (Structure in VB.NET). This is the only change. There is no other change to the Test class or its Main() method. The output produced by the program now is shown in Figure 1-3.

Output after modification of class to struct/Structure in Example 1-3

Figure 1-3. Output after modification of class to struct/Structure in Example 1-3

This time, the assignment statement at Line A (firstInstance = secondInstance) changes the value stored in the firstInstance structure. The effect of that statement is shown in Figure 1-4.

Effect of assignment on value type

Figure 1-4. Effect of assignment on value type

Therefore, changing secondInstance.TheValue has no effect on firstInstance.TheValue, since in this case they are still different objects. The assignment made a bitwise copy.

The effect of an assignment statement differs for the two types of object. In the case of the reference type (where AType is declared as a class), firstInstance refers to the object on the heap. Therefore, after the assignment statement, firstInstance and secondInstance end up referring to the same instance on the heap. This is very similar to pointer manipulation in C++. However, when AType is declared as a struct/Structure, the firstInstance becomes a variable local to the stack representing an instance of the value type. Therefore, the assignment statement copies the memory content from secondInstance to firstInstance.

Given this confusion, is it worth using a value type? Well, since the core CLS types (such as System.Int32, System.Char, and System.Double), are value types, they must have their uses and benefits. What are they?

Value types are allocated on the stack. They are passed by value as method parameters (unless tagged as ref/ByRef), so the called method cannot change them inadvertently. This argues for keeping value types small: by-value parameters are copied, and copying large objects can be expensive.

Another good candidate for value types is objects used to represent the internal state of a larger object (and not exposed to users of that object).

You need to exercise caution when using value types with collections in .NET 1.1 (non-generic collections). See Gotcha #9, "Typeless ArrayList isn’t type-safe.”

I recommend that you use a class unless you have a specific need for a struct/Structure, such as when:

  • The object is really small and you want to eliminate the overhead of a reference

  • You intend to create a large array of these types and do not want the overhead of constructor calls for each object in the array

IN A NUTSHELL

Assignment may lead to confusion because you can’t quite figure out if you are using a value type or a reference type merely by looking at the code. Do not make any assumptions when you see an assignment statement—explore the object further to make sure you understand how the assignment will behave. Also, limit the use of value types, as much as possible, to small objects.

GOTCHA #3 Returning value types from a method/property is risky

Value types are either stored on the stack or embedded within objects on the heap. What happens when a property or a method of a class returns a member which is a struct/Structure (i.e., a value type)? It makes a copy of the object. While value types may be passed by reference as method parameters, C# and VB.NET (unlike C++) do not provide any mechanism to return them by reference. Consider Example 1-4.

Example 1-4. Example of returning a value type

C# (ReturningValueType )

using System;

namespace ValTypeProp
{
    struct A
    {
        public int val;
    }

    class Test
    {
        private A theA;

        public Test()
        {
            theA = new A();
        }
public A MyValue
        {
            get { return theA; }
            set { theA = value; }
        }

        [STAThread]
        static void Main(string[] args)
        {
            Test obj = new Test();

            A myValue = obj.MyValue;
            myValue.val = 4;

            Console.WriteLine(obj.MyValue.val);
        }
    }
}

VB.NET (ReturningValueType)

Structure A
    Public val As Integer
End Structure

Class Test
    Private theA As A

    Public Sub New()
        theA = New A
    End Sub

    Public Property MyValue() As A
        Get
            Return theA
        End Get
        Set(ByVal Value As A)
            theA = Value
        End Set
    End Property

    Public Shared Sub Main()
        Dim obj As New Test

        Dim myValue As A = obj.MyValue
               
        myValue.val = 4

        Console.WriteLine(obj.MyValue.val)
    End Sub
End Class

In the above example, the MyValue property of the Test class returns the object theA of type A. In Main, you get this property and set its val field to 4. Then you again fetch the property from the Test object (obj) and print its val field.

The output from the above program is 0 and not 4. Why? Well, when the property MyValue returns theA, it returns a copy of the structure, not a reference to it. The assignment statement myValue.val = 4 has no effect on obj.MyValue.val. In fact, you will get an error if you try to modify the obj.MyValue property directly.

Consider this small change shown in Example 1-5.

Example 1-5. Modifying value type returned from a property

C# (ReturningValueType)

        //...
        [STAThread]
        static void Main(string[] args)
        {
            Test obj = new Test();

            obj.MyValue.val = 4;

            Console.WriteLine(obj.MyValue.val);
        }

VB.NET (ReturningValueType)

    '...
    Public Shared Sub Main()
        Dim obj As New Test

        obj.MyValue.val = 4

        Console.WriteLine(obj.MyValue.val)
    End Sub

Now you get a compilation error. In C#, you get the message:

error CS1612: Cannot modify the return value of 'ValTypeProp.Test.MyValue' because it 
is not a variable.

In VB.NET, you get:

error BC30068: Expression is a value and therefore cannot be the target of an assignment.

Tip

If you replace struct/structure with class you get the intuitive result of 4 instead of 0.

IN A NUTSHELL

If you call a method or access a property that returns a value-type object, do not modify it or call mutators (methods that modify its state or data) on it. You are dealing with a copy and any change you make does not affect the real instance.

GOTCHA #4 You can’t force calls to your value-type constructors

Among other differences between reference types and value types, one of the most surprising is that you are not allowed to define your own no-parameter constructor (one that takes no parameter) for a value type. If you try, you get the following error:

error CS0568: Structs cannot contain explicit parameterless constructors.

C# and VB.NET provide a no-parameter constructor and won’t let you write an alternate implementation.

The consequence of this is that you have no control over how your value-type object is created by a user. For instance, in the case of a reference type (class), you can dictate what parameters are necessary to create an object by writing different constructors. The compiler makes sure that a user calls one of these constructors when creating the object. In the case of a value type, you can’t define a no-parameter constructor. So you can’t force developers to enter some specific values before using a value type. They can create an instance of your value type with no parameters. Take a look at Example 1-6.

Example 1-6. Example of using no-parameter constructor of value type

C# (ValueTypeConstructor)

//MyType.cs
using System;

namespace ValueTypeInstantiation
{
    public struct MyType
    {
        private int val;

        public override string ToString()
        {
            return "The value is " + val;
        }
//public MyType() {} // Can't be provided
               public MyType(int initialValue)
        {
            val = initialValue;
        }
    }
}



using System;

namespace ValueTypeInstantiation
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            MyType instance1 = new MyType(10);
            Console.WriteLine("instance1: " + instance1);

            MyType instance2 = new MyType();
            Console.WriteLine("instance2: " + instance2);
        }
    }
}

VB.NET (ValueTypeConstructor)

'MyType.vb

Public Structure MyType
    Private val As Integer

    Public Overrides Function ToString() As String
        Return "The value is " & val
    End Function

    'Public Sub New() ' Can't be provided
    'End Sub
               
    Public Sub New(ByVal initialValue As Integer)
        val = initialValue
    End Sub
End Structure


'Test.vb

Public Class Test
    Public Shared Sub Main()
            Dim instance1 as new MyType(10)
            Console.WriteLine("instance1: " & instance1)

            Dim instance2  as new M y Type
            Console.WriteLine("instance2: " & instance2)
    End Sub
End Class

Note that the value type MyType has one constructor which takes an integer. However, in Main of the Test class you are able to create an instance of MyType not only using the constructor provided, but also using the no-parameter constructor. What if you want to enforce a rule that MyType.val must be set to the value given in the constructor or to a value of, say, 10? Unfortunately that is not possible. Each field of a value type is initialized to its default value. For instance, int/Integer fields will be initialized to 0 and bool/Boolean types to false. The output from Example 1-6 is shown in Figure 1-5.

Output from Example 1-6

Figure 1-5. Output from Example 1-6

IN A NUTSHELL

Be aware that for value types, no matter how many constructors you write, you are not allowed to write your own no-parameter constructor. A user of your type may create an object using the no-parameter constructor and there is no guarantee that any of the constructors you write is ever used.

GOTCHA #5 String concatenation is expensive

Objects of the String class in .NET are immutable. An immutable object is one that can’t be modified once created. What is the effect of using the + operator (or & in VB.NET) to append a String? Each call creates a new String object. This can cause object-allocation overhead and put pressure on the garbage collector. What alternative do you have?

System.Text.StringBuilder provides a means for appending almost anything to a StringBuilder object. The benefits of using StringBuilder over String’s +/& is illustrated in Example 1-7.

Example 1-7. Using StringBuilder versus +/ &

C# (StringAppend)

using System;

namespace StringAppendPerformance
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            Console.Write("Enter number of strings to append:");
            int count = Convert.ToInt32(Console.ReadLine());

            string str = null;
            int startTick = Environment.TickCount;
            for (int i = 0; i < count; i++)
            {
                str = str +".";
            }
            int endTick = Environment.TickCount;

            double timeTakenByPlus =
                (endTick - startTick) / 1000.0;

            System.Text.StringBuilder bldr =
                new System.Text.StringBuilder();

            startTick = Environment.TickCount;
            for (int i = 0; i < count; i++)
            {
                bldr.Append(".");
            }
            endTick = Environment.TickCount;

            double timeTakenByStringBuilder =
                (endTick - startTick) / 1000.0;

            Console.Write("+ and StringBuilder took ");
            Console.WriteLine("{0} and {1} seconds",
                    timeTakenByPlus,
                    timeTakenByStringBuilder);
        }
    }
}

VB.NET (StringAppend)

Module Test

    Sub Main()
        Console.Write("Enter number of strings to append:")
        Dim count As Integer = Convert.ToInt32(Console.ReadLine())

        Dim str As String = Nothing
        Dim startTick As Integer = Environment.TickCount
        Dim i As Integer

        For i = 0 To count - 1
            str = str &"."
        Next
        Dim endTick As Integer = Environment.TickCount

        Dim timeTakenByPlus As Double = _
            (endTick - startTick) / 1000.0

        Dim bldr As New System.Text.StringBuilder

        startTick = Environment.TickCount

        For i = 0 To count - 1
            bldr.Append(".")
        Next
        endTick = Environment.TickCount

        Dim timeTakenByStringBuilder As Double = _
            (endTick - startTick) / 1000.0

        Console.Write("& and StringBuilder took ")
        Console.WriteLine("{0} and {1} seconds", _
          timeTakenByPlus, _
          timeTakenByStringBuilder)
    End Sub
End Module

Executing the above program with different values for the number of strings to append produces the results shown in Table 1-3.

Table 1-3. Performance, in seconds, of concatenation versus StringBuilder

# of appends

+

StringBuilder

10

0.000

0.00

100

0.000

0.00

1,000

0.000

0.00

2,500

0.000

0.00

5,000

0.020

0.00

7,500

0.050

0.00

10,000

0.090

0.00

15,000

0.250

0.00

25,000

1.052

0.00

35,000

2.373

0.00

50,000

5.699

0.00

65,000

10.625

0.00

75,000

14.831

0.01

85,000

19.418

0.01

100,000

27.159

0.01

150,000

65.374

0.01

250,000

209.221

0.02

350,000

441.615

0.02

500,000

910.129

0.04

650,000

1521.708

0.06

750,000

1999.305

0.06

850,000

2576.575

0.06

1,000,000

3562.933

0.07

The timing using the ampersand (&) in VB.NET is comparable to that of the concatenation operator (+) in C#. As the above example shows, StringBuilder is much less expensive than using + (or &) to build up a string. Furthermore, if you use StringBuilder you create fewer objects than using the +/&. This can be seen using the CLR Profiler (see "On the Web" in the Appendix). For instance, if you run 10,000 appends, the number of String instances created using +/& is 10,039. However, if you replace the +/& with StringBuilder.Append(), the number of String instances drops to 51.

For an interactive client application with a few concatenations here and there, it may not make a big difference. For a server-side application, however, the difference may be significant and the use of StringBuilder is probably better. It must be noted that the instance members of a StringBuilder are not thread-safe, so you may have to take care to appropriately synchronize access to them.

IN A NUTSHELL

If you find yourself appending a large number of strings, you will improve performance by using StringBuilder.Append() instead of the concatenation operators (+/&). This is especially important in server-side/backend applications.

GOTCHA #6 Exceptions may go unhandled

No one likes an application to crash. It’s embarrassing if your application presents the user with an unhandled exception dialog like the one in Figure 1-6.

Unhandled exception dialog

Figure 1-6. Unhandled exception dialog

There is a low-tech solution: comb through your code to make sure that you are handling all exceptions properly. But that is a lot of work, and what if you miss something? Can you protect your application from the exception that slips through? And if not, do you want the unhandled exception to jump up abruptly in your users’ faces? Wouldn’t you rather have it presented to them more gracefully, and maybe reported to you by logging, emailing, or some other means?

You can register a method to catch unhandled exceptions. There are two ways to achieve this. In a Windows application, you add a ThreadExceptionEventHandler to the Application.ThreadException delegate. In a console application, you add an UnhandledExceptionEventHandler to AppDomain.CurrentDomain.UnhandledException.

Examples 1-8 through Example 1-11 show a console application that uses a class in a library.

Example 1-8. Exception that goes unhandled (C# library)

C# (HandleException), library code

//Utility.cs part of ALibrary.dll
using System;

namespace ALibrary
{
    public class Utility
    {
        public double Operate(int value1, int value2)
        {
            // Some operation
            // Of course, this is an enormous programming error
            // Never do division without making sure the denominator
            // is not zero.
            // We're just doing it here for the sake of example.
            double val = value1 / value2;
            return Math.Sqrt(val);
        }
    }
}

Example 1-9. Exception that goes unhandled (C# client)

C# (HandleException), client code

//Program.cs part of UnhandledExceptionConsoleApp.exe
using System;
using ALibrary;
using System.Threading;

namespace UnhandledExceptionConsoleApp
{
    class Program
    {
        private static void Worker()
        {
            Console.WriteLine(
                "Enter two numbers separated by a return");

            int number1 = Convert.ToInt32(Console.ReadLine());
            int number2 = Convert.ToInt32(Console.ReadLine());

            double result = new Utility().Operate(number1, number2);

            Console.WriteLine("Result is {0}", result);
        }

        [STAThread]
        static void Main(string[] args)
        {
            try
            {
                //AppDomain.CurrentDomain.UnhandledException
                //    += new UnhandledExceptionEventHandler(
                //        CurrentDomain_UnhandledException);

                new Thread(new ThreadStart(Worker)).Start();
            }
            catch(Exception ex)
            {
                Console.WriteLine("Exception: " + ex.Message);
            }
        }

        private static void CurrentDomain_UnhandledException(
            object sender, UnhandledExceptionEventArgs e)
        {
            Console.WriteLine("Send the following to support");
                Console.WriteLine("Unexpected error:");
            Console.WriteLine(e.ExceptionObject);
            Console.WriteLine("Is CLR terminating: {0}",
                e.IsTerminating);
        }
    }
}

Example 1-10. Exception that goes unhandled (VB.NET library)

VB.NET (HandleException), library code

'Utility.vb part of ALibrary.dll

Public Class Utility
    Public Function Operate( _
        ByVal value1 As Integer, ByVal value2 As Integer) As Double
        'Some operation
        ' Of course, this is an enormous programming error
        ' Never do division without making sure the denominator
        ' is not zero.
        ' We're just doing it here for the sake of example.
        Dim val As Double = value1 / value2

        If Double.IsInfinity(val) Then
            Throw New DivideByZeroException( _
                "Attempted to Divide by Zero")
        End If
        Return Math.Sqrt(val)
    End Function
End Class

Example 1-11. Exception that goes unhandled (VB.NET client)

VB.NET (HandleException), client code

'Program.vb part of UnhandledExceptionConsoleApp.exe

Imports ALibrary
Imports System.Threading

Module Program
    Private Sub Worker()
            Console.WriteLine( _
                "Enter two numbers separated by a return")

        Dim number1 As Integer = Convert.ToInt32(Console.ReadLine())
        Dim number2 As Integer = Convert.ToInt32(Console.ReadLine())

        Dim result As Double = New Utility().Operate(number1, number2)

        Console.WriteLine("Result is {0}", result)
    End Sub

    Public Sub Main()
        Try
            'AddHandler AppDomain.CurrentDomain.UnhandledException, _
            '     New UnhandledExceptionEventHandler( _
            '    AddressOf CurrentDomain_UnhandledException)

            Dim aThread As New Thread(AddressOf Worker)
            aThread.Start()
        Catch ex As Exception
            Console.WriteLine("Exception: " + ex.Message)
        End Try
    End Sub

    Private Sub CurrentDomain_UnhandledException( _
        ByVal sender As Object, ByVal e As UnhandledExceptionEventArgs)
        Console.WriteLine("Send the following to support")
        Console.WriteLine("Unexpected error:")
        Console.WriteLine(e.ExceptionObject)
        Console.WriteLine("Is CLR terminating: {0}", _
         e.IsTerminating)
    End Sub
End Module

In this example, you have a Utility class with an Operate() method that throws a DivisionByZeroException if its second parameter is zero. The method is invoked from a thread in Program. You don’t have a try-catch block within the Worker() method to handle exceptions. When you execute the above code, the output shown in Figure 1-7 is produced.

Output from Example 1-8

Figure 1-7. Output from Example 1-8

The exception thrown from the thread is reported as an unhandled exception. If you uncomment the first statement in Main(), thereby registering your own handler for uncaught exceptions, you get the output shown in Figure 1-8.

Output from Example 1-8 after registering for the UnhandledException event

Figure 1-8. Output from Example 1-8 after registering for the UnhandledException event

By adding your handler to the AppDomain.CurrentDomain.UnhandledException event, you let the CLR know that it should send unhandled exceptions to the subscribed method.

Of course, you should not use this as a substitute for using good try-catch logic where necessary. Furthermore, your code should have finally blocks where actions have to be taken regardless of exceptions.

If your application is a Windows application, you register a handler for the Application.ThreadException event instead. Consider a simple WinForm application with one button named RunButton. The handler for that button’s Click event is shown in Example 1-12, along with the Main() method and the exception-handler code.

Example 1-12. Taking care of unhandled exceptions in a WinForm app

C# (HandleException)

        static void Main()
        {
//            Application.ThreadException
//                += new ThreadExceptionEventHandler(
//                    Application_ThreadException);
            Application.Run(new Form1());
        }

        private void RunButton_Click(
            object sender, System.EventArgs e)
        {
            MessageBox.Show(
                new Utility().Operate(1, 0).ToString());
        }

        private static void Application_ThreadException(
            object sender,
            System.Threading.ThreadExceptionEventArgs e)
        {
            MessageBox.Show(
                "Send the following to support: " +
                e.Exception);
        }

VB.NET (HandleException)

    Public Shared Sub Main()
        'AddHandler Application.ThreadException, _
        '    New ThreadExceptionEventHandler( _
        '    AddressOf Application_ThreadException)

        Application.Run(New Form1)
    End Sub

    Private Sub RunButton_Click( _
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs) _
        Handles RunButton.Click
        MessageBox.Show(New Utility().Operate(1, 0).ToString())
    End Sub

    Private Shared Sub Application_ThreadException( _
    ByVal sender As Object, _
    ByVal e As System.Threading.ThreadExceptionEventArgs)
        MessageBox.Show( _
         "Send the following to support: " & _
         e.Exception.ToString())
    End Sub

When you click the Run button, you get the output in Figure 1-6. If you uncomment the Application.ThreadException registration code in Main(), you see something more like Figure 1-9.

From the event handler, you may take an appropriate action such as logging the exception for future diagnostics or sending the details to your support team. This can prove useful in development, testing, and even after deployment.

Warning

Unlike this example, you shouldn’t let your application keep running after an unhandled exception, because it’s in an inconsistent state.

Refer to Jason Clark’s article “Unexpected Errors in Managed Applications” (see the section "On the Web" in the Appendix).

Handling the Unhandled Exception

Figure 1-9. Handling the Unhandled Exception

IN A NUTSHELL

Properly handle exceptions in your application. You can guard against unhandled exceptions by registering a handler for them. In a Windows application, you add a ThreadExceptionEventHandler to the Application.ThreadException delegate. In a console application, you add an UnhandledExceptionEventHandler to AppDomain.CurrentDomain.UnhandledException.

GOTCHA #7 Uninitialized event handlers aren’t treated gracefully

Delegates are very effective for implementing callbacks in .NET. A delegate encapsulates a pointer to a method, and an instance of an object on which that method needs to be executed. A delegate can also encapsulate a pointer to a static/Shared method. The syntax provided to use a delegate is intuitive. You do not have to deal with messy pointers to functions as in C++.

Delegates are used to specify the handlers that will be called when an event occurs. If you want to register multiple methods of a class as event handlers, you can do so very easily without having to resort to something as complicated as anonymous inner classes, as you do in Java.

In order to call the handler that a delegate represents, you can either use the DynamicInvoke() method, or you can just call the delegate as if it were itself a method:

MyDelegate.DynamicInvoke(...)

Or:

MyDelegate(...)

Their ease of use sometimes obscures the fact that delegates are just classes, created when the compiler sees the delegate keyword. When you use a delegate, you are using an object through a special syntax. Of course, you know not to invoke methods on an object reference that you haven’t initialized. However, it may not be readily apparent when a delegate is uninitialized.

When raising an event, you should consider the possibility that no handlers have been added or registered. Consider the code in Example 1-13.

Example 1-13. Accessing an uninitialized delegate

C# (Delegate)

 // AComponent.cs
using System;

namespace UnInitializedDelegate
{
    public delegate void DummyDelegate();

    public class AComponent
    {
        public event DummyDelegate myEvent;

        protected virtual void OnMyEvent()
        {
            myEvent();
        }

        public void Fire()
        {
            Console.WriteLine("Raising event");
            OnMyEvent(); // Raising the event
            Console.WriteLine("Done raising event");
        }
    }
}

//Test.cs
using System;

namespace UnInitializedDelegate
{
    public class Test
    {
        private void callback1()
        {
            Console.WriteLine("callback1 called");
        }

        private void callback2()
        {
            Console.WriteLine("callback2 called");
        }

        private void Work()
        {
            AComponent obj = new AComponent();

            Console.WriteLine("Registering 2 callbacks");
            obj.myEvent += new DummyDelegate(callback1);
            obj.myEvent += new DummyDelegate(callback2);
            obj.Fire();

            Console.WriteLine("Removing 1 callback");
            obj.myEvent -= new DummyDelegate(callback2);
            obj.Fire();
         
            Console.WriteLine("Removing the other callback");
            obj.myEvent -= new DummyDelegate(callback1);
            obj.Fire();
        }

        [STAThread]
        static void Main(string[] args)
        {
            Test testObj = new Test();
            testObj.Work();
        }
    }
}

VB.NET (Delegate)

 'AComponent.vb
Public Delegate Sub DummyDelegate()

Public Class AComponent
    Public Event myEvent As DummyDelegate

    Protected Overridable Sub OnMyEvent()
        RaiseEvent myEvent()
    End Sub

    Public Sub Fire()
        Console.WriteLine("Raising event")
        OnMyEvent() ' Raising the event
        Console.WriteLine("Done raising event")
    End Sub
End Class

'Test.vb
Public Class Test

    Private Sub callback1()
        Console.WriteLine("callback1 called")
    End Sub

    Private Sub callback2()
        Console.WriteLine("callback2 called")
    End Sub

    Private Sub Work()
        Dim obj As New AComponent

        Console.WriteLine("Registering 2 callbacks")
        AddHandler obj.myEvent, New DummyDelegate(AddressOf callback1)
        AddHandler obj.myEvent, New DummyDelegate(AddressOf callback2)
        obj.Fire()

        Console.WriteLine("Removing 1 callback")
        RemoveHandler obj.myEvent, New DummyDelegate(AddressOf callback2)
        obj.Fire()

        Console.WriteLine("Removing the other callback")
        RemoveHandler obj.myEvent, New DummyDelegate(AddressOf callback1)
        obj.Fire()
    End Sub
    Shared Sub Main(ByVal args As String())
        Dim testObj As New Test
        testObj.Work()
    End Sub
End Class

When executed, the C# version of the program produces the result shown in Figure 1-10.

As Figure 1-10 shows, a NullReferenceException is thrown when the third call to the Fire() method tries to raise the event. The reason for this is that no event handler delegates are registered at that moment.

Output from the C# version of Example 1-13

Figure 1-10. Output from the C# version of Example 1-13

The VB.NET version of the program, however, does not throw an exception. It works just fine.[1] Why? In the MSIL generated for RaiseEvent() (shown in Example 1-14), a check for the reference being Nothing is made.

Example 1-14. MSIL translation of a RaiseEvent() statement

  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldfld      class UnInitializedDelegate.DummyDelegate
                       UnInitializedDelegate.AComponent::myEventEvent
  IL_0007:  brfalse.s  IL_0015
  IL_0009:  ldarg.0
  IL_000a:  ldfld      class UnInitializedDelegate.DummyDelegate
                       UnInitializedDelegate.AComponent::myEventEvent
  IL_000f:  callvirt   instance void UnInitializedDelegate.DummyDelegate::Invoke()
  IL_0014:  nop
  IL_0015:  nop

Tip

You can view the MSIL generated for your code using the tool ildasm.exe that comes with the .NET Framework. Simply run the tool and open the assembly you are interested in. You can view the MSIL generated for methods, properties, etc.

The correct way to implement this code in C# is to program defensively by checking for a null reference before raising the event, as shown in Example 1-15.

Example 1-15. Checking for an uninitialized delegate

C# (Delegate)

        protected virtual void OnMyEvent()
        {
            if(myEvent != null)
            {
                myEvent();
            }
        }

Checking to see if the delegate is not null prevents the NullReferenceException. The delegate will be null if no one has asked to be notified when the event triggers.

Note that there is still a problem. It is possible that the last registered event handler has been removed between the line where you check if myEvent is null and the line where you raise the event, and the code may still fail. You need to consider this possibility and raise the event in a thread-safe way. See Gotcha #64, "Raising events lacks thread-safety" for details on this.

IN A NUTSHELL

Use caution when raising an event. If no event handler has been registered, an exception is thrown in C# when you raise an event. Check to make sure that the delegate is not null before raising the event. In both C# and VB.NET, you need to worry about thread-safety when raising events.

SEE ALSO

Gotcha #64, "Raising events lacks thread-safety.”

GOTCHA #8 Division operation isn’t consistent between types

When you divide by zero, you expect a DivisionByZeroException to be thrown. While this does happen for integer division, floating point division does not cause this exception. Consider Example 1-16, where I use an NUnit test to assert for division by zero.

Example 1-16. NUnit test to assert DivisionByZeroException

C# (DivByZero)

//Calculator.cs

using System;

namespace DivisionByZeroExample
{
    public class Calculator
    {
        public int Divide(int operand1, int operand2)
        {
            return operand1 / operand2;
        }
    }
}

//Test.cs
using System;
using NUnit.Framework;

namespace DivisionByZeroExample
{
    [TestFixture]
    public class Test
    {
        private Calculator calc;

        [SetUp]
        public void Setup()
        {
            calc = new Calculator();
        }

        [Test]
        public void TestSimpleDivide()
        {
            Assert.AreEqual(2, calc.Divide(4, 2));
        }

        [Test, ExpectedException(typeof(DivideByZeroException))]
        public void TestDivisionByZero()
        {
            calc.Divide(4, 0);
        }
    }
}

VB.NET (DivByZero)

'Calculator.vb
Public Class Calculator
    Public Function Divide(ByVal operand1 As Integer, _
        ByVal operand2 As Integer) As Integer
        Return operand1 \ operand2
    End Function
End Class

'Test.vb
Imports NUnit.Framework

<TestFixture()> _
Public Class Test
    Private calc As Calculator

    <SetUp()> _
    Public Sub Setup()
        calc = New Calculator
    End Sub

    <Test()> _
    Public Sub TestSimpleDivide()
        Assert.AreEqual(2, calc.Divide(4, 2))
    End Sub
    <Test(), ExpectedException(GetType(DivideByZeroException))> _
    Public Sub TestDivisionByZero()
        calc.Divide(4, 0)
    End Sub
End Class

The Divide() method divides its first parameter by its second one. There are two test cases. The first one invokes Divide() with parameters 4 and 2; the second calls it with the values 4 and 0. When the code in Example 1-16 is executed in NUnit, both the test cases succeed as shown in Figure 1-11.

NUnit GUI output for code in Example 1-16

Figure 1-11. NUnit GUI output for code in Example 1-16

The TestDivisionByZero test case has declared the ExpectedException attribute, and announced that it expects a DivisionByZeroException. This test succeeds since the Divide() method does indeed throw a DivisionByZeroException.

Now, if the Divide() method performs floating-point division instead of integer division, the result will be different. Let’s change the Divide() method of the Calculator class to use double instead of int (Integer in VB.NET), as shown in Example 1-17.

Example 1-17. Example of floating point division by zero

C# (DivByZero )

public double Divide(double operand1, double operand2)
{
    return operand1 / operand2;
}

VB.NET (DivByZero)

    Public Function Divide(ByVal operand1 As Double, _
        ByVal operand2 As Double) As Double
        Return operand1 / operand2
    End Function

The effect of the code change in Example 1-17 can be seen in the NUnit GUI shown in Figure 1-12.

Note that the TestDivisionByZero test case fails because the DivisionByZeroException is not thrown. What is the result then? If either of the operands is a System.Double or System.Single, then the operation appears to succeed; no exception is thrown. But the result is an invalid number defined as either Double.PositiveInfinity or Double.NegativeInfinity, depending on the signs of the operands. However, you cannot do a simple comparison to test this; you must call Double.IsInfinity(). Floating-point operations do not throw exceptions.

Failure to throw DivisionByZeroException

Figure 1-12. Failure to throw DivisionByZeroException

(If the operands are Single instead of Double, the appropriate values are Single.PositiveInfinity and Single.NegativeInfinity. Single also offers the IsInfinity() method.)

IN A NUTSHELL

If dealing with integer division, expect a DivisionByZeroException. Otherwise, check the result by calling the IsInfinity() method of System.Double or System.Single as appropriate. Remember that floating-point operations don’t throw exceptions. You can avoid problems like this by checking the denominator before you perform the operation.

GOTCHA #9 Typeless ArrayList isn’t type-safe

Boxing and unboxing enable value types to be treated as objects. Boxing is an implicit conversion of a value type to the System.Object type; i.e., an Object instance is created (on the heap) and the value is copied into it. Unboxing is an explicit conversion from the Object type to a value type.

Collections (i.e., non-generic collections) treat every one of their elements as the Object type. When dealing with primitive value types, adding a value to a collection involves boxing, and accessing it from the collection involves unboxing. As a result, you have two problems to worry about. One, the boxing and unboxing will affect performance due to the copy overhead. Second, the value has to be unboxed to the proper type. In this gotcha we focus mainly on the latter problem. Code like that in Example 1-18 might compile OK but fail at run time.

Example 1-18. Behavior of ArrayList

C# (ArrayList)

using System;
using System.Collections;

namespace ArrayListClassCastException
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            ArrayList myList = new ArrayList();

            myList.Add(3.0);
            myList.Add(3);
                // Oops. 3 is boxed in as int not double

            double total = 0;
            foreach(double val in myList) // Exception here.
            {
                total += val;
            }

            Console.WriteLine(total);
        }
    }
}

VB.NET (ArrayList)

Module Test

    Sub Main()
        Dim myList As New ArrayList

        myList.Add(3.0)
        myList.Add(3)
        ' Oops. 3 is boxed in as integer not double

        Dim total As Double = 0
        Dim val As Double
        For Each val In myList ' No Exception here.
            total += val
        Next

        Console.WriteLine(total)
    End Sub

End Module

The behavior of the C# code is different from the equivalent VB.NET version (even withOption Strict On).

Let’s first consider the C# code. In the example, you first add 3.0 to the ArrayList myList. This gets boxed in as a double. Then you add a 3. However, this gets boxed in as an integer. When you enumerate over the items in the collection and treat them as double s, an InvalidCastException is thrown as shown in Figure 1-13.

Output from the C# version of Example 1-18

Figure 1-13. Output from the C# version of Example 1-18

What is the reason for this exception? The value 3 that was boxed as an int is unboxed as a double.

Let’s now consider the VB.NET code. The VB.NET version appears to be doing the same thing as the C# version. That is, you add 3.0 to the ArrayList myList. This is boxed in as a Double. Then you add a 3. This is boxed in as an Integer. But when you enumerate the items in the collection and treat them as Double, you get the correct total, 6, as shown in Figure 1-14.

Output from the VB.NET version of Example 1-18

Figure 1-14. Output from the VB.NET version of Example 1-18

That is interesting! Why would C# fail, but not VB.NET? The answer is in the translation of source to MSIL. Let’s take a look at the MSIL generated from C# and the MSIL generated from VB.NET.

Example 1-19 shows what MSIL command is generated from the C# code for unboxing. It is the unbox statement, which instructs the CLR to unbox the object to a System.Double.

Example 1-19. Unboxing in MSIL translated from C#

IL_0041:  unbox      [mscorlib]System.Double

Example 1-20, on the other hand, shows the MSIL that VB.NET produces. Instead of a simple unbox statement, it invokes the FromObject method on the DoubleType class in the Microsoft.VisualBasic namespace.

This method silently converts the Integer to Double, so you get the correct answer rather than an exception.

Example 1-20. Unboxing in MSIL translated from VB.NET

IL_0051:  call       float64 [Microsoft.VisualBasic]
    Microsoft.VisualBasic.CompilerServices.DoubleType::FromObject(
object)

Of course, if you modify the VB.NET code to add a Char instead of an Integer, you will get an exception. Let’s take a look at this in Example 1-21.

Example 1-21. Adding a Character in the VB.NET example of ArrayList

VB.NET (ArrayList)

        Dim myList As New ArrayList

        myList.Add(3.0)
        myList.Add(3)
        ' Oops. 3 is boxed in as integer not double
        myList.Add("a"c)
        ' Boxing "a" as Char not double

    ...

Now the VB.NET version behaves like the C# one, although the exception originates in the DoubleType class instead of the unbox command, as shown in Figure 1-15.

Output from Example 1-21

Figure 1-15. Output from Example 1-21

The problems mentioned in this gotcha are specific to non-generic collections. This will not be a problem in .NET 2.0 if you use generics. Generics provide type-safe data structures that resemble C++ templates in some ways (though they differ in their capabilities and implementation). This leads to code that’s more reusable and better in performance.

The C# code that utilizes generics to perform the same function as in Example 1-18 is shown in Example 1-22. The corresponding VB.NET code is shown in Example 1-23.

Example 1-22. Generics version of the C# code from Example 1-18

C# (ArrayList )

using System;
using System.Collections.Generic;

namespace ArrayListClassCastException
{
    class Test
    {
        [STAThread]
        static void Main(string[] args)
        {
            Collection<double> myList = new Collection<double>();

            myList.Add(3.0);
            myList.Add(3); // No problem. 3 is stored as 3.0.

            double total = 0;
            foreach(double val in myList)
            {
                total += val;
            }

            Console.WriteLine(total);
        }
    }
}

Example 1-23. Generics version of the VB.NET code from Example 1-21

VB.NET (ArrayList)

               Imports System.Collections.Generic

Module Test

    Sub Main()
        Dim myList As New Collection(Of Double)

        myList.Add(3.0)
        myList.Add(3) 'No problem 3 stored as 3.0

        myList.Add("a"c)
        'error BC30311: Value of type 'Char'
        'cannot be converted to 'Double'

        Dim total As Double = 0
        Dim val As Double
        For Each val In myList
            total += val
        Next

        Console.WriteLine(total)
    End Sub

End Module

When you use generics, there is no boxing and unboxing overhead. The value of 3 is converted to 3.0 at compile time based on the parametric type double/Double of the Collection. (You can see this by looking at the MSIL code generated.) In the case of the VB.NET code, if you pass a character to the Collection’s Add() method, you get a compilation error since it can’t be converted to double/Double.

IN A NUTSHELL

Be careful with collections that treat elements as objects. For one thing, you may incur some boxing and unboxing overhead; for another, you may trigger an InvalidCastException. This problem goes away with Generics, so once they are available use them for type safety and performance.

GOTCHA #10 Type.GetType() may not locate all types

The Abstract Factory pattern [Freeman04, Gamma95] is a common and useful pattern that abstracts object creation. It isolates the code that decides which type of object to create from the code that uses the objects. It is almost effortless in .NET to use Abstract Factory due to the powerful GetType() method of the Type class.

Suppose you need to create different kinds of objects depending on runtime conditions. Perhaps the name of the class is read from a configuration file, or provided as an input to the program. Or you may be dealing with plug-ins that are dynamically introduced when an application is launched (or even while it is running). How do you create an object when you don’t know what class it belongs to until the moment you need to create it?

The Type.GetType() method can help you achieve this. Here’s how to use it:

  1. Obtain the class’s type information by calling Type.GetType().

  2. Use the Activator.CreateInstance() method to create an object of that class, assuming you have a no-parameter constructor.

  3. Cast the object reference (using the as operator in C# or CType function in VB.NET) to a known interface (that the class implements) and invoke the interface methods on it.

This flexibility paves the way for a lot of extensibility in applications.

The core of this facility is the Type class’s GetType() method. In writing an application, if you pass the GetType() method the name of a class in your assembly, it will fetch the Type metadata for that class. However, when you ask for type information for a class or plug-in from another assembly, GetType() will fail.

Consider Example 1-24. It is a simple WinForm application with a button. When the button is clicked, you get the Type object for three types and display the information on them.

Example 1-24. Behavior of Type.GetType()

C# (GetType)

        private void CallGetTypeButton_Click(object sender,
            System.EventArgs e)
        {
            try
            {
                Type theType;
             
                theType = Type.GetType(
                    "CallingGetType.Form1", true);
                MessageBox.Show("First type is "
                    + theType.FullName);

                theType = Type.GetType(
                    "System.Collections.Queue", true);
                MessageBox.Show("Second type is "
                    + theType.FullName);

                theType = Type.GetType(
                    "System.Windows.Forms.Form",
                    true);
                MessageBox.Show("Third type is "
                    + theType.FullName);
            }
            catch(Exception ex)
            {
                MessageBox.Show("Error: " + ex.Message);
            }
        }

VB.NET (GetType)

    Private Sub CallGetTypeButton_Click( _
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs) _
        Handles CallGetTypeButton.Click
        Try
            Dim theType As Type

            theType = Type.GetType( _
             "CallingGetType.Form1", True)
            MessageBox.Show("First type is " _
             & theType.FullName)

            theType = Type.GetType( _
            "System.Collections.Queue", True)
            MessageBox.Show("Second type is " _
             & theType.FullName)

            theType = Type.GetType( _
             "System.Windows.Forms.Form", _
             True)

            MessageBox.Show("Third type is " _
             & theType.FullName)

        Catch ex As Exception

            MessageBox.Show("Error: " & ex.Message)
        End Try
    End Sub

Figure 1-16, Figure 1-17, and Figure 1-18 show the output from the code in Example 1-24.

CallingGetType.Form1

Figure 1-16. CallingGetType.Form1

System.Collections.Queue

Figure 1-17. System.Collections.Queue

System.Windows.Forms.Form

Figure 1-18. System.Windows.Forms.Form

While there was no problem getting the Type metadata for Form1 and System.Collections.Queue, the call to GetType() with the class name System.Windows.Forms.Form failed. (The Form class is the base class of Form1 within which this code is executing).

In developing code that creates objects based on runtime class information, and applications that require the use of plug-ins, you may run into problems like this. If you test your application using plug-in components that are part of your own assembly, they’ll probably work just fine. However, when you try to load a class from another assembly, things won’t work quite so well. The reason is that GetType(), if given a class name, only looks in the calling object’s assembly and mscorlib.dll. Since Form1 belongs to the calling assembly and System.Collections.Queue belongs to mscorlib.dll, there is no problem with them.

There is a workaround that enables you to load classes from other assemblies, like System.Windows.Forms. When you tell GetType() which class you need, you must specify the full name of the class including the full identity of the assembly. This identity includes the name, version, culture information, and the strong name public key token.

An example of correct usage of GetType() for getting information on the System.Windows.Forms.Form class is shown in Example 1-25.

Example 1-25. Correct usage of GetType()

C# (GetType)

...
                theType = Type.GetType(
                    "System.Windows.Forms.Form , " +
                    "System.Windows.Forms, " +
                    "Version=1.0.5000.0, " +
                    "Culture=neutral, " +
                    "PublicKeyToken=b77a5c561934e089",
                    true);

VB.NET (GetType)

...
            theType = Type.GetType( _
             "System.Windows.Forms.Form , " & _
                "System.Windows.Forms, " & _
                "Version=1.0.5000.0, " & _
                "Culture=neutral, " & _
                "PublicKeyToken=b77a5c561934e089", _
             True)

In Example 1-25, the class name System.Windows.Forms.Form is followed (comma separated) by the name of the assembly in which that class is located (System.Windows.Forms), the version number (1.0.5000.0), the culture (neutral), and the public key token of that assembly (b77a5c561934e089). The public key token for an assembly can be obtained by using the sn (strong name) tool. Only if you specify the fully qualified class name along with the assembly identity will you correctly retrieve the type information. As long as the application can locate the specified assembly, the type information will be loaded.

How does the application find the assembly? First the CLR looks for it in the Global Assembly Cache (GAC). Then it looks in the application base directory. If the assembly is still not found, it searches for subdirectories with the assembly name or the specific culture under the application base directory. If it still hasn’t found the assembly, it continues looking for directories specified in the codebase setting in the application’s configuration file. You can also explicitly load the assembly using the LoadFrom() method of the System.Reflection.Assembly class.

If you are using some API or library that expects you to send the name of a class, you have to follow the recommendations in this gotcha. If the class name is specified in a configuration file or is given as input for the program during runtime, you must make sure that the fully qualified name as discussed above is provided.

IN A NUTSHELL

When specifying class names in a configuration file or providing them as input for dynamically creating objects, make sure you provide the fully qualified class name, along with the full identity of the assembly.

GOTCHA #11 Public key reported by sn.exe is inconsistent

The utility sn.exe creates a strong name to sign your code with a public/private key pair. When you specify the assembly identity as in Example 1-25, or when you define the binding redirect for assembly version forwarding in the configuration file, you need to specify the public key token of the assembly. If you have a strong name key (.snk) file for your assembly, use caution in extracting the public key token from it. The -t option of sn.exe gives you the public key token. However, I have seen a number of developers fall into a trap when extracting the public key token from the .snk file.

Let’s look at an example. Suppose you use the .NET command prompt to execute sn.exe as shown in Example 1-26.

Example 1-26. Using the sn utility to create key and extract public key token

>sn  -k mykey.snk

Microsoft (R) .NET Framework Strong Name Utility
Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All
 rights reserved.

Key pair written to mykey.snk

>sn -t mykey.snk

Microsoft (R) .NET Framework Strong Name Utility
Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All
 rights reserved.

Public key token is 1cf34646172fcb74

>sn -p mykey.snk mykeypublic.snk

Microsoft (R) .NET Framework Strong Name Utility
Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All
 rights reserved.

Public key written to mykeypublic.snk

>sn -t mykeypublic.snk

Microsoft (R) .NET Framework Strong Name Utility
Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All
 rights reserved.

Public key token is bab446454bf67c07

In this example, you first run sn -k mykey.snk. This creates the strong-name file named mykey.snk that contains the public and private key pair. Then you run sn -t mykey.snk on the generated file. This command prints the public key token. Then you execute the command sn -p mykey.snk mykeypublic.snk to extract the public key from mykey.snk and save it in mykeypublic.snk. Finally, you run sn -t mykeypublic.snk on the public key file. Note that the tokens displayed are not the same! Which one should you use? Which one is correct?

When I ran into this problem, I scratched my head wondering why my binding redirect didn’t work properly. Eventually, when I used ildasm.exe to view the manifest of the assembly that was linking to my assembly, I realized that the public key token was not the same as the one I had in the configuration file.

If you read through the documentation for sn -t, you’ll find the statement:

Displays the token for the public key stored in infile. The contents of infile must be previously generated using -p.

So this was the problem: the first time I ran sn -t, it extracted the public key from a file that had not been generated using sn -p. The next time I executed sn -t, it targeted mykeypublic.snk, which had been created using sn -p. The first sn -t was incorrect; the second was right.

It would be nice if there were an error or warning when you use -t on an input file that wasn’t generated using the -p option.

How does this differ in .NET 2.0 Beta 1? This problem has been fixed in .NET 2.0 Beta 1. When you run sn -t mykey.snk, you get the following error:

Microsoft (R) .NET Framework Strong Name Utility  Version 2.0.40607.16
Copyright (C) Microsoft Corporation. All rights reserved.

Failed to convert key to token -- Bad Version of provider.

IN A NUTSHELL

Do not extract the public key token from a .snk file directly. Instead use the file generated from the -p option. Or better still, get it from the manifest of a binding assembly by viewing it in ildasm.exe.



[1] Actually, there is a problem we are not seeing--RaiseEvent() is not thread-safe. See Gotcha #64.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required