When writing programs for a multitasking operating system (i.e. Windows, Linux, OS X, any modern operating system), you have to realize that resources are shared, and that the machine’s state can change at any time without your knowledge. It seems obvious, but many programmers don’t fully understand the ramifications. For example, I see code like this distressingly often:
if (File.Exists(filename))
{
File.Delete(filename);
}
The programmer apparently wants to avoid trying to delete a non-existent file thinking that File.Delete
will throw an exception if the file isn’t there.
The problem with the code is that it doesn’t work as expected. It reduces the likelihood that the program will call File.Delete
with the name of a file that doesn’t exist, but it doesn’t eliminate the possibility. Why? Because some other thread (perhaps in a different process) could delete the file between the time File.Exists
returns true
, and when the program tries to delete the file. In addition, this code doesn’t take into account the possibility that some other thread could have the file open, or any number of rare but certainly not impossible things like a disk crash.
What I find amusing is that in the most common case the call to File.Exists
isn’t even necessary. The typical use here is to delete a file that’s in the application’s data or output directory. The programmer erroneously believes that File.Delete
will throw an exception if passed the name of a file that doesn’t exist. But File.Delete
fails silently in that case. It will throw an exception if the path is invalid, but in most cases the program couldn’t get this far if the path was invalid. The programmer is trying to avoid a problem that doesn’t exist, and ignoring a much bigger potential problem.
When you’re working with shared resources, any information you have about that resource is a snapshot. It tells you what the state was at the time you checked, but that state can change at any moment, potentially just milliseconds after you check. File.Exists
tells you that the file existed when you checked. It doesn’t guarantee that the file will still exist when you try to delete it.
The correct way to delete a file is:
try
{
File.Delete(filename);
}
catch (DirectoryNotFoundException)
{
}
catch (IOException)
{
}
// etc.
Of course, you should handle only the exceptions you’re expecting and know how to handle, and depending on your application you might want to do something other than just swallow them. File.Delete can throw a number of different exceptions, and it’s up to you to decide which ones to handle.
Another very common mistake is checking to see if a file is locked before trying to open it for reading. For example:
if (CanOpenFileForReading(filename))
{
using (FileStream f = File.OpenRead(filename))
{
// do stuff
}
}
The implementation of CanOpenFileForReading
is irrelevant. This can’t work because some other thread could lock the file or delete it before your thread gets around to opening it. I’ll grant that the window of vulnerability is pretty small, but it’s there. And I’ve seen programs written this way fail precisely because another thread managed to sneak in and lock the file during that time. The right way to open a file is:
try
{
using (FileStream f = File.OpenRead(filename))
{
try
{
// do stuff
}
catch (Exceptions that occur while doing stuff)
{
}
}
}
catch (Exceptions that occur when trying to open the file
{
}
There are those who have some quasi-religious resistance to using exceptions in this way. To them I say: Get over it! We can debate the proper use of exceptions ’til the cows come home, but the .NET runtime library in general and the File
class in particular use exceptions for error handling. If you try to code around that, you’re going to write buggy and fragile code that just won’t work in the face of errors. Use the API in the way that it’s intended.