简体   繁体   中英

strange exception behaviour…why?

I've made a class inspired by C++'s std::numeric_limits for getting min and max value of a type. It fills two static read only members using reflection to read out the MaxValue and MinValue properties of the type. It throws an exception if T does not have that property.

public class Limits<T>
{
    public static readonly T MaxValue = Read("MaxValue");
    public static readonly T MinValue = Read("MinValue");

    private static T Read(string name)
    {
        FieldInfo field = typeof(T).GetField(name, BindingFlags.Public | BindingFlags.Static);

        if (field == null)
        {
            throw new ArgumentException("No " + name + " property in " + typeof(T).Name);
        }

        return (T)field.GetValue(null);
    }
}

Now when stepping through the following program Im seeing some strange behaviour.

    try
    {
        Console.WriteLine(Limits<int>.MaxValue);
        Console.WriteLine("1");
        Console.WriteLine(Limits<object>.MaxValue);
    }
    catch
    {
        Console.WriteLine("2");
    }

There's a breakpoint on reading the MaxValue property. When stepping through Limits<int> the breakpoint is hit and the property is read. Then before executing WriteLine("1") the breakpoint is hit again for reading Limits<object> . This throws an exception since object does not have MaxValue so one would expect the exception being caught in Main. But that doesn't happen, WriteLine("1") is executed and only then the exception is caught....why is this? Does the CLR store the exception until the actual line is executed?

From the C# language spec on static field initialization :

If a static constructor (Section 10.11) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor. Otherwise, the static field initializers are executed at an implementation-dependent time prior to the first use of a static field of that class.

So that means:

  • The user has no direct control about when this initialization process is run.
  • It is guaranteed to be run once and before the field is used.

If an exception is thrown in this process, the type becomes unusable for the rest of the life of the AppDomain, and every time you try to use that type you will get a TypeInitializationException thrown (with the inner exception being the original exception) Check the msdn on static constructors. For these purposes, Limits<int> and Limits<object> are considered different types, so you could still use Limits<int> .

That´s why you get the exception when you try to get Limits<object>.MaxValue , because the initialization code was called by the clr for you and the exception stored so it can be thrown as TypeInitializationException every time you use it.

It is also important to notice that all static fields will be initialized before the type is used for the first time. So you could add the following static property to your static class:

public static T Default = default(T);

And then change your test program as follows:

static void Main(string[] args)
{
    for (int x = 0; x < 3; x++)
    {
        try
        {
            Console.WriteLine(Limits<int>.Default);
            Console.WriteLine("1");
            Console.WriteLine(Limits<object>.Default);
        }
        catch (TypeInitializationException e)
        {
            Console.WriteLine("TypeInitializationException: " + e.Message);
        }
    }
    Console.ReadKey();
}

You are not using directly the MaxValue static fields, but because you are using the type (by accessing Default) all the static fields are still being initialized prior to first usage of the type. You will also notice that you will get the same exception 3 times, always after Limits.Default and "1" have been written.

The ecma standard says:

17.4.5.1: "If a static constructor (§17.11) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor. Otherwise, the static field initializers are executed at an implementation-dependent time prior to the first use of a static field of that class."

ie it can run the initializer on a separate thread.

and this http://msdn.microsoft.com/en-us/library/aa664609(v=vs.71).aspx

•If the search for a matching catch clause reaches a static constructor (Section 10.11) or static field initializer, then a System.TypeInitializationException is thrown at the point that triggered the invocation of the static constructor.

says that yes, the exception is held and thrown at the line of code causing initialisation. This makes sense as otherwise you'd have an exception thrown at a 'random' time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM