There are two types of FileStream
constructors—those for interop scenarios, and the “normal”
ones. The “normal” ones take a string for the file path, while the interop
ones require either an IntPtr
or a
SafeFileHandle
. These wrap a Win32 file
handle that you have retrieved from somewhere. (If you’re not already
using such a thing in your code, you don’t need to use these versions.)
We’re not going to cover the interop scenarios here.
If you look at the list of constructors, the first thing you’ll
notice is that quite a few of them duplicate the various permutations of
FileShare
, FileAccess
, and FileMode
overloads we had on File.Open
.
You’ll also notice equivalents with one extra int
parameter. This allows you to provide a hint
for the system about the size of the internal buffer you’d like the stream
to use. Let’s look at buffering in more detail.
Many streams provide buffering. This means that when you read and write, they actually use an intermediate in-memory buffer. When writing, they may store your data in an internal buffer, before periodically flushing the data to the actual output device. Similarly, when you read, they might read ahead a whole buffer full of data, and then return to you only the particular bit you need. In both cases, buffering aims to reduce the number of I/O operations—it means you can read or write data in relatively small increments without incurring the full cost of an operating system API call every time.
There are many layers of buffering for a typical storage device. There might be some memory buffering on the actual device itself (many hard disks do this, for example), the filesystem might be buffered (NTFS always does read buffering, and on a client operating system it’s typically write-buffered, although this can be turned off, and is off by default for the server configurations of Windows). The .NET Framework provides stream buffering, and you can implement your own buffers (as we did in our example earlier).
These buffers are generally put in place for performance reasons.
Although the default buffer sizes are chosen for a reasonable trade-off
between performance and robustness, for an I/O-intensive application,
you may need to hand-tune this using the appropriate constructors on
FileStream
.
As usual, you can do more harm than good if you don’t measure the impact on performance carefully on a suitable range of your target systems. Most applications will not need to touch this value.
Even if you don’t need to tune performance, you still need to be
aware of buffering for robustness reasons. If either the process or the
OS crashes before the buffers are written out to the physical disk, you
run the risk of data loss (hence the reason write buffering is typically
disabled on the server). If you’re writing frequently to a Stream
or StreamWriter
, the .NET Framework will
flush the write buffers periodically. It also ensures that everything is
properly flushed when the stream is closed. However, if you just stop
writing data but you leave the stream open, there’s a good chance data
will hang around in memory for a long time without getting written out,
at which point data loss starts to become more likely.
In general, you should close files as early as possible, but
sometimes you’ll want to keep a file open for a long time, yet still
ensure that particular pieces of data get written out. If you need to
control that yourself, you can call Flush
. This is particularly useful if you have
multiple threads of execution accessing the same stream. You can
synchronize writes and ensure that they are flushed to disk before the
next worker gets in and messes things up! Later in this chapter, we’ll
see an example where explicit flushing is extremely important.
Another parameter we can set in the constructor is the
FileSystemRights
. We used this type
earlier in the chapter to set filesystem permissions. FileStream
lets us set these directly when we
create a file using the appropriate constructor. Similarly, we can also
specify an instance of a FileSecurity
object to further control the permissions on the underlying file.
Finally, we can optionally pass another enumeration to the
FileStream
constructor, FileOptions
, which contains some advanced
filesystem options. They are enumerated in Table 11-9. This is a flags-style enumeration, so you can combine these
values.
Table 11-9. FileOptions enumeration
FileOptions | Purpose |
---|---|
| No options at all. |
| Ignores any
filesystem-level buffers, and writes directly to the output
device. This affects only the O/S, and not any of the other
layers of buffering, so it’s still your responsibility to call
|
| Indicates that we’re going to be seeking about in the file in an unsystematic way. This acts as a hint to the OS for its caching strategy. We might be writing a video-editing tool, for example, where we expect the user to be leaping about through the file. |
| Indicates that we’re going to be sequentially reading from the file. This acts as a hint to the OS for its caching strategy. We might be writing a video player, for example, where we expect the user to play through the stream from beginning to end. |
| Indicates that we want the file to be encrypted so that it can be decrypted and read only by the user who created it. |
| Deletes the file when it is closed. This is very handy for temporary files. If you use this option, you never hit the problem where the file still seems to be locked for a short while even after you’ve closed it (because its buffers are still flushing asynchronously). |
| Allows the file to be accessed asynchronously. |
The last option, Asynchronous
,
deserves a section all to itself.