Wrapping Up
In order to release the unmanaged resources obtained during construction as soon as possible, consider employing:
- The using statement if an exception can be thrown during the usage part.
- Explicit try/catch blocks in the constructor if an exception can be thrown from the constructor.
- You may prefer to allocate resources outside of the constructors, allowing them to initialize only data members, thus constructing your memory models in two stages. In this case, the using statement will work as it was intended.
New Roadblocks
Adding try/catch blocks in the constructor and releasing unmanaged resources brings an additional issue into discussion: How do you return from the failed constructor? One possible way is to rethrow the exception. Another method is to throw another exception and then catch it in the calling code. You may not like over-engineering with the additional try\catch block around the using statement that is required in these cases. So another way is to use an A constructor with parameters instead of the parameterless constructor shown above. You may set the bool Result status flag in the o object, which is then passed to the A(UserRefType o) constructor in the using statement. In this case, you would add something like if (o.Result) after returning from the constructor. If you dislike using status flags, you might also consider dropping the using statement altogether and employing just a try/catch/finally block.
Take care to carefully construct the Dispose() method with the following considerations in mind:
- The method may need to know how the constructor or usage block was exitedat its end or after an exception was thrown somewhere in its body. It might be necessary to execute some actionsfor instance “undo”based on this knowledge.
- The method may throw its own exceptions before it processes a thrown exception. This behavior will mask the original exception and may change an expected execution order.
- The method may change the state of the object that could be passed to the A(UserRefType o) constructor. This new state may cause runtime errors or misbehavior when the o object is used later.
Additional Considerations
The following code presents an IL instruction that looks at the value on the stack:
IL_0009: brfalse.s IL_0011
If it is
0, or a
null reference, it will break. Generally, having this instruction prevents failure to call the
Dispose() method when
c is
null, as in the following code:
C c = C(); //C does not derive from IDisposable
using(c as IDisposable)
{
. . .
}
Because
c must be of the
System.IDisposable type or a type that can be implicitly converted to
System.IDisposable, you need to use the
as operator to allow the C# compiler to compile this code without errors.
This article doesn't discuss why you would write code like thisit just highlights the IL implementation of the finally block. Of course, if C is derived from IDisposable() and the C() constructor failed, Dispose() will not be called here either.
Those who are familiar with native C++ and the RAII (Resource Acquisition Is Initialization) pattern will remember that the following block:
{
classX x; //x is allocated on the stack
//point3
…..
//point4
}
guarantees the call of the
~ClassX() destructoreven if an exception has been thrown in this block between
point3 and
point4. If the constructor throws an exception, the object will not be fully constructed, and it won't call the destructor. In that case, those resources allocated before the exception is thrown will be leaked.
In .NET, the garbage collector will call the Finalize() method (only if you provide the destructor) on the object to clean up its unmanaged resources. Therefore, by providing an implementation for the destructor, like ~A() {Dispose(false);}, you eliminate leaks of unmanaged resources. Of course, the time when this destructor is called is non-deterministic, and it takes longer for the Finalize() to be called.
In order to be able to step through your IL code with the DbgCLR debugger, will need to do the following:
- Build your project and create the <YourApplicationName>.exe output file.
- Create <YourApplicationName>.il file.
- Run the ildasm.exe MS .NET Framework IL Disassembler.
- Open <YourApplicationName>.exe file.
- Dump (save As) <YourApplicationName>.il file.
- Finish the round trip, create the <YourApplicationName>.exe (PE format) and <YourApplicationName>.pdb files.
- Run the ilasm.exe MS .NET Framework IL Assembler and specify the <YourApplicationName>.il filename and/debug option.