try/catch and scoping
I received an interesting customer question last week, and thought it would be valuable to share the answer here.
Here's the question:
<question>
On the subject of try/catch scope in C#, it would be useful if the scope of variables created in try{} could be extended to it’s corresponding catch{} block. I know that I could put the declaration outside of the try block, but this introduces side effects such as having the object around for longer than intended. Is there a problem with extending the rule of {} scoping in this particular instance? Could it also be extended to the finally{} block?
Thanks,
Gary
</question>
One of our language principles is to adhere to C++ heritage. This isn’t a particularly strong principle – we use it as a general rule and deviate from it when there is a good reason to do so. For instance, we thought that the number of bugs and amount of confusion caused by C++’s switch fall-through problem was worth fixing. I’m not sure we’ve ever discussed having the scopes for try..catch..finally be as you describe. I can see why this would be valuable, but I doubt that we would think it would be so valuable that we would want to depart from C++ heritage.
Changing this scoping rule in retrospect would be difficult since there could be existing code like
try {
object o = new object();
}
catch {
object o = …; // different o
}
that would pose a problem. Dealing with this would be complex. Perhaps the try block would act as if it was an enclosing block for the catch, there would be shadowing rules for accessing the try’s o despite the presence of another o in the catch, etc. Possible, but very very messy – you only get one chance to make a decision like this, and that’s for 1.0.
Peter Hallam walked into my office as I was finishing off this mail, and he pointed out another reason why this would be hard. Or not actually why it would be hard, but why it might not have the desired effect that you might think that it would. What you’d really like to do is define the variable in the try and use it subsequently in the catch:
try {
...
object o = new object();
}
catch {
Console.WriteLine(o);
}
Unfortunately, the assignment to ‘o’ or code before it could throw an exception. Since this is possible, o would not be definitely assigned. I suggested that it might be possible to get around this by doing a seemingly failsafe assignment first, like
try {
object o = null;
...
object o = new object();
}
catch {
Console.WriteLine(o);
}
but there are circumstances when even the null assignment can throw an exception. The solution to this is to declare and initialize o outside the try, which defeats the purpose of the suggested feature:
object o = null;
try {
object o = new object();
}
catch {
Console.WriteLine(o);
}
And this seemed like such a simple suggestion at first glance!
I asked Anders for comments on my draft blog entry, and here's what he said:
<anders>
The real reason is what Peter Hallam says: You can't assume that variables declared in the try block are definitely assigned on entry to the catch block (e.g. in your example, the "new object()" might throw an out of memory exception). Thus, we'd have to require separate initialization in the catch block, which would defeat the purpose of the suggestion.
Anders</anders>
--Scott
Comments
- Anonymous
March 21, 2005
> but this introduces side effects such as having the object around for longer than intended.
This is a non-issue because the GC can detect when a object is not being used, so as long as you don't touch it after the catch, the GC could collect it anytime.
For example:
object o = null;
try
{
o = new object();
}
catch
(
Console.WriteLine(o);
}
// GC could collect 'o' here
LongRunningOperation(); - Anonymous
March 21, 2005
<quote>
The real reason is what Peter Hallam says: You can't assume that variables declared in the try block are definitely assigned on entry to the catch block (e.g. in your example, the "new object()" might throw an out of memory exception). Thus, we'd have to require separate initialization in the catch block, which would defeat the purpose of the suggestion.
</quote>
Seems to be missing the issue. If you have to declare the object outside the Try block, you end up with no handlers for this situation - Anonymous
March 21, 2005
David: The GC cannot detect an object not being used, it can only detect objects that aren't being referenced anymore. The reference must either go out of scope (which is what the proposal was about) or you must manually set it to a null reference. Your example wouldn't work.
That said, I still think the proposed idea is a bad one because it breaks a fundamental rule for lifetime scoping in C-based languages: dynamic variables live from "{" to the matching "}", and no further.
If you have two consecutive (non-nested) {} blocks, dynamic variables declared in the first block shouldn't survive to see the other block. Exceptions to this rule are too confusing to the reader. - Anonymous
March 21, 2005
Chris,
You are incorrect. The GC does not rely on scopes to determine if an object is referenced anymore.
See this post by Chris Brumme:
http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx. - Anonymous
March 21, 2005
I stand corrected. Thanks for the link, that's quite surprising. In that case I agree with your example and argument. - Anonymous
March 21, 2005
By the way, a variation on the theme of what GC will collect, in debug mode, GC will not collect an object until it is out of scope, allowing you to debug and watch all variables that are actually in scope, even if code will not touch it. - Anonymous
March 22, 2005
Instituting that change would cause try / catch blocks to use fundamentally differnt scoping rules than the rest of the language.
I think thats dangerous. One of the nice things about C# and the .NET framework is that they are relative simple, consistent, and have few suprises.
Violating that, just for the benefit of not having to declare varaibles outside of a try block, would be a big mistake. - Anonymous
March 22, 2005
If it's so important to narrow down the accessibility to a variable, why don't you just extract the code which need it to its own method? - Anonymous
March 22, 2005
Wouldn't an easy workaround be:
try
{
object o = null;
try
{
o = new object();
}
catch
{
Console.WriteLine(o);
}
}
// object o is now out of scope
catch
{
Console.WriteLine(ErrorMessage)
} - Anonymous
March 23, 2005
My solution to the problem is:
{ //extra dummy scope for the anomaly
object o = null; //decare in dummy scope
try {
object o = new object();
}
catch {
Console.WriteLine(o);
}
}