Introduction
Few weeks ago, me and my friend were discussing about functions and we came up with one question about scope and reachable code in .NET and we started understanding reachable code and found very interesting results.
Background
We will evaluate the scope of return
statement in function along with try
-catch
. Before that, it is important to understand what the compiler does for a function having signature and returning any value before ending function. When a function has a return
statement in other blocks, the compiler adds a new variable and returns that value before returning from that function.
Using the Code
Everyone knows that in try
-catch
block, finally
block must execute before returning from the method. Then what happened with variables declared in Finally
block, when are they assigned and what is the use of them.
Guess what will be the return value when calling MyFunction
method in the below code?
private int MyFunction()
{
int i = 0;
try
{
i = 1;
throw new Exception("Int Error");
}
catch
{
i = 2;
return i;
}
finally
{
i = 3;
Console.Write(i.ToString());
}
}
If you execute the above code just after setting value of i = 2
, when executing a return
statement, finally
block should be called. And in finally
block, the value of I
has been set to 3
, even Debug.Print
should return 3
but when you get return value of function(return i
), you will get answer 2.
Guess what happened.
Let me change the existing code, let me pass a reference parameter and we will see what happens to the reference variable.
private int MyFunction(ref int passInt)
{
int i = 0;
try
{
i = 1;
passInt = 1;
throw new Exception("Int Error");
}
catch
{
i = 2;
passInt = 2;
return i;
}
finally
{
i = 3;
passInt = 3;
Console.Write(i.ToString());
}
}
In the above code, the passed parameter value becomes
3
but the return value becomes
2
.
Now try to understand what happened in the previous code.
"Finally
statement is always calling before returning from function", that means return
statement is executed but value is yet not returned. Then in that case, even after changing the value of I
in finally
block, why does the return value persist the previous value.
Now go back to C++ era, and think that every function is called by a pointer and address of that function is set to callee. Now you will understand what happened.
Below is code generated by the compiler before creating IL code.
private int MyFunction()
{
int CS$1$0000;
int i = 0;
try{
i = 1;
throw new Exception("Int Error");
}
catch{
i = 2;
CS$1$0000 = i;
}
finally{
Console.Write(i.ToString());
}
return CS$1$0000;
}
See that return
statement has been reset and return
statement has been placed at the end of function. Also one variable CS$1$0000
is added to set return value.
You will see that in Catch
block, the value of i
is set to CS$1$0000
variable and in turn CS$1$0000
gets return from method.
The IL version of the above code will give a better explanation with stack trace; you will also get clear when you will execute IL code with IL compiler.
.method private hidebysig instance int32 MyFunction() cil managed
{
.maxstack 2
.locals init (
[0] int32 i,
[1] int32 CS$1$0000)
L_0000: nop
L_0001: ldc.i4.0
L_0002: stloc.0
L_0003: nop
L_0004: ldc.i4.1
L_0005: stloc.0
L_0006: ldstr "Int Error"
L_000b: newobj instance void [mscorlib]System.Exception::.ctor(string)
L_0010: throw
L_0011: pop
L_0012: nop
L_0013: ldc.i4.2
L_0014: stloc.0
L_0015: ldloc.0
L_0016: stloc.1
L_0017: leave.s L_002b
L_0019: nop
L_001a: ldc.i4.3
L_001b: stloc.0
L_001c: ldloca.s i
L_001e: call instance string [mscorlib]System.Int32::ToString()
L_0023: call void [System]System.Diagnostics.Debug::Print(string)
L_0028: nop
L_0029: nop
L_002a: endfinally
L_002b: nop
L_002c: ldloc.1
L_002d: ret
.try L_0003 to L_0011 catch object handler L_0011 to L_0019
.try L_0003 to L_0019 finally handler L_0019 to L_002b
}
Summary
The compiler setting the return value before returning a method and changing into the method level variable is not affecting return value.
I may extend this tip to understand the scope of other reachable code.