NML

.Net and C#: What you thought you knew but didn't

By Charl Marais

As coders, we know things in our area of expertise. Of course, we had better, or else we wouldn’t really be able to build anything of value. However, we also sometimes know things, and are so sure that we know those things, when if fact we’ve just been deluding ourselves. I’m not here to tell you your entire programming knowledge needs re-programming; but I am going to share a couple of .Net and C# trivia that many developers I encounter think they know.

1. “throw ex” vs “throw”

Many developers aren’t aware that “throw” can be used on its own, let alone why. Of the various answers to the question – “what’s the difference?” – the most common assumption is that even if they are different, it doesn’t really matter.

Let’s just see if that is indeed true. Take the following simple, slightly convoluted, example:

  public static void Main() {
    try
      {
        Method1();
      }
    catch(Exception ex)
      {
        throw ex;
      }
    }

    private static void Method1() {
      try
        {
          Method2();
        }
      catch(Exception ex)
        {
          throw ex;
        }
    }

  private static void Method2()
    {
      throw new Exception("Yeah, it's horrible!");
    }

The stack trace for this code is:

  StackTrace:[System.Exception:Yeah, it's horrible!]
    at Program.Main(): line 18

Now let’s replace the “throw ex” in Main with just “throw”. Now the output is:

  StackTrace:[System.Exception:Yeah, it's horrible!]
    at Program.Method1(): line 30
    at Program.Main(): line 18

Whoohaaa! What is going on here?!

Well, “throw” has two meanings, depending on how it is used:

  1. First, when used in conjuction with an exception instance, is: “Take the exception, and ‘throw’ it up the call stack for the first handler that can handle it”.
  2. Second, when used alone, is: “Take this exception, and continue the initial throw up the call stack for the next handler that can handle it (rethrow)”.

The first meaning tells the runtime to start logging the stack for the exception from the line where the exception is thrown again – replacing the original call stack. The second meaning tells the runtime that the original exception has not been handled, and it should therefore continue to find a handler for that original exception.

If you’re still not convinced there is a real difference, let’s prove it one more time to ourselves.

When we replace the “throw ex” in Method1 with “throw”, the exception call stack looks as follows:

  StackTrace:[System.Exception:Yeah, it's horrible!]
    at Program.Method2(): line 34
    at Program.Method1(): line 30
    at Program.Main(): line 18

Now we really see where our exception occurred, and the call route we took to get there. Many programs, more complex than this 3-method example, have multiple variations on the invocation route (call stack) to get to a particular method. This is important information to have since the program may work for all routes, save one. That means that somewhere in the call stack is a method that may not have performed a required action.

Basically, using the wrong meaning of “throw” makes root-cause analysis harder than it needs to be.

2. IDisposable

IDisposable defines a single method:void Dispose() So what is it that we think we know about this, but don’t? It’s simple – just implement Dispose, right? Wrong. There’s a little more to it. To understand what that might be, you need to know how objects are garbage-collected. Without diving into too much detail on the complex garbage-collection process, what we need to know about it for an instance point of view is: “When the garbage collector finally decides that an instance can be collected, what does it do?”

The first thing to know is that it does not call Dispose if the instance type implements IDisposable. The runtime has absolutely no concept of the meaning of IDisposable.

What about the using keyword? The using keyword is applicable on IDisposable, so the runtime must know something of it, right? Good question, but no. I’ll touch on that later.

The only thing the garbage collector will do on an instance once it’s decided to collect, is call the instance finalizer. If you haven’t seen a finalizer, it looks like this: ~MyClass() {/*code*/}. After that, say bye-bye to your instance.

So how is this applicable to IDisposable? It has an impact on how you have to go about implementing the Dispose method. Look at the following example:

public class MyClass : IDisposable
{
  private FileStream _fileStream = new FileStream(@"c:temptest.txt", FileMode.OpenOrCreate);
  public void Dispose()
  {
       _fileStream.Close();
        GC.SuppressFinalize(this);
  }
}

That looks quite reasonable, but there is a catch! What if a consumer of MyClass never calls Dispose()? Remember, the runtime does not know that IDisposable is special, and honestly, it isn’t. All IDisposable does is inform consumers that the instance will have a method “Dispose()” on it. Whether the consumer calls it or not, is nobody else’s business.

If Dispose is not called, the garbage-collector could quite rightly decide that this object is not yet ready for collection, as there are still references to the instance that has not been resolved satisfactorily. Instances of MyClass could, and will, hang around longer than they should.

I mentioned that the finalizer is the last thing the garbage collector calls, so all we do to solve this problem is to call the Dispose method from the finalizer, right?

public class MyClass : IDisposable
{
  private FileStream _fileStream = new FileStream(@"c:temptest.txt", FileMode.OpenOrCreate);

  ~MyClass()
  {
    Dispose();
  }
  public void Dispose()
  {
    _fileStream.Close();
    GC.SuppressFinalize(this);
  }
}

Well, no, that will not quite work as expected. One characteristic of the garbage collector is that it is non-deterministic. You do not know how, or in what order, it will start collecting objects. So in our revised example, if the finalizer is called we could actually get a nasty error if “_fileStream” has already been collected. This would not be a good thing.

So how do you get around it? Simply tell the Dispose method from where you are calling it.

public class MyClass : IDisposable
{
  private FileStream _fileStream  = new FileStream(@"c:temptest.txt", FileMode.OpenOrCreate);

  ~MyClass()
  {
    Dispose(false);
  }
  public void Dispose()
  {
    Dispose(true);
  }
  public void Dispose(bool isManagedDispose)
  {
    //Clean up unmanaged objects
    if(isManagedDispose)
    {
      //Clean up managed objects
      _fileStream.Close();
      GC.SuppressFinalize(this);
    }
  }
}

n this version, when Dispose is called by a consumer, the managed objects (which would still be there) can be cleaned up. If the finalizer is called, we don’t try and clean up managed resources, as we cannot rely on them still being available for cleanup. However, we can (and should) still clean up any unmanaged resources.

Where does “using” fit into all this?

All “using” is, is syntactic sugar that the C# compiler caters for. The following code has a “using” statement, and a try-finally statement:

public static void Main()
{
  using(var fileStream1  = new FileStream(@"c:temptest.txt", FileMode.OpenOrCreate))
  {}

  var fileStream2  = new FileStream(@"c:temptest.txt", FileMode.OpenOrCreate);
  try
  {}
  finally
  {
    fileStream2.Dispose();
  }
}

When you compile that, and the look at the compiled intermediary language, this is what you’d see. (Notice that the “using” statement is converted to a try-finally statement by the C# compiler.)

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       62 (0x3e)
  .maxstack  2
  .locals init (class [mscorlib]System.IO.FileStream V_0,
           class [mscorlib]System.IO.FileStream V_1,
           bool V_2)
  IL_0000:  nop
  IL_0001:  ldstr      "c:temptest.txt"
  IL_0006:  ldc.i4.4
  IL_0007:  newobj     instance void [mscorlib]System.IO.FileStream::.ctor(string,
                                                                           valuetype [mscorlib]System.IO.FileMode)
  IL_000c:  stloc.0
  .try
  {
    IL_000d:  nop
    IL_000e:  nop
    IL_000f:  leave.s    IL_0021
  }  // end .try
  finally
  {
    IL_0011:  ldloc.0
    IL_0012:  ldnull
    IL_0013:  ceq
    IL_0015:  stloc.2
    IL_0016:  ldloc.2
    IL_0017:  brtrue.s   IL_0020
    IL_0019:  ldloc.0
    IL_001a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_001f:  nop
    IL_0020:  endfinally
  }  // end handler
  IL_0021:  nop
  IL_0022:  ldstr      "c:temptest.txt"
  IL_0027:  ldc.i4.4
  IL_0028:  newobj     instance void [mscorlib]System.IO.FileStream::.ctor(string,
                                                                           valuetype [mscorlib]System.IO.FileMode)
  IL_002d:  stloc.1
  .try
  {
    IL_002e:  nop
    IL_002f:  nop
    IL_0030:  leave.s    IL_003c
  }  // end .try
  finally
  {
    IL_0032:  nop
    IL_0033:  ldloc.1
    IL_0034:  callvirt   instance void [mscorlib]System.IO.Stream::Dispose()
    IL_0039:  nop
    IL_003a:  nop
    IL_003b:  endfinally
  }  // end handler
  IL_003c:  nop
  IL_003d:  ret
} // end of method Program::Main

That’s it: two kinda-simple, kinda-important things in .Net and C# that I’ve discovered us developers thought we knew.