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.
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
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 |
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.
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.
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
//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.
In the assignment statement:
firstInstance = secondInstance
you are modifying the reference firstInstance
. The effect of that statement is shown in Figure 1-2.
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.
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.
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
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 #1, "Type alias size doesn’t match what you’re familiar with,” Gotcha #3, "Returning value types from a method/property is risky,” Gotcha #4, "You can’t force calls to your value-type constructors,” and Gotcha #9, "Typeless ArrayList isn’t type-safe.”
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
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
//...
[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.
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 #2, "struct and class differ in behavior, Gotcha #4, "You can’t force calls to your value-type constructors,” and Gotcha #9, "Typeless ArrayList isn’t type-safe.”
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
//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.
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 #2, "struct and class differ in behavior,” Gotcha #3, "Returning value types from a method/property is risky,” and Gotcha #9, "Typeless ArrayList isn’t type-safe.”
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 +/ &
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); } } }
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.
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 #9, "Typeless ArrayList isn’t type-safe" and Gotcha #21, "Default performance of Data.ReadXML is poor.”
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.
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.
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.
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
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); }
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).
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 #61, "Exceptions thrown from threads in the pool are lost.”
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
// 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();
}
}
}
'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.
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.
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.
Gotcha #64, "Raising events lacks thread-safety.”
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
//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); } } }
'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.
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.
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.
(If the operands are Single
instead of Double
, the appropriate values are Single.PositiveInfinity
and Single.NegativeInfinity
. Single
also offers the IsInfinity()
method.)
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 #29, "Unit testing private methods is tricky.”
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.
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.
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-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.
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.
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
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
.
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 #2, "struct and class differ in behavior,” Gotcha #3, "Returning value types from a method/property is risky,” Gotcha #4, "You can’t force calls to your value-type constructors,” Gotcha #5, "String concatenation is expensive,” and Gotcha #30, "Common Language Specification Compliance isn’t the default.”
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:
Obtain the class’s type information by calling
Type.GetType()
.Use the
Activator.CreateInstance()
method to create an object of that class, assuming you have a no-parameter constructor.Cast the object reference (using the
as
operator in C# orCType
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); } }
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.
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);
... 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.
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" and Gotcha #14, "Type.GetType() might fail at run-time"
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.
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.
Gotcha #10, "Type.GetType() may not locate all types.”
[1] Actually, there is a problem we are not seeing--RaiseEvent()
is not thread-safe. See Gotcha #64.
Get .NET Gotchas now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.