-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
File open should have non-throwing alternatives #27217
Comments
CC. @JeremyKuhne, @pjanotti |
I think this is a good idea. But what about combining this with Since a stream is disposable, I think a new pattern for opening files should work well with using statements. Do you have any ideas for that? Something like the below won't compile. (Using variables are immutable so they cannot be passed by ref). (Or am I overlooking something?)
|
This should work if (File.TryOpen("foo.bar", out Stream stream))
{
using (stream)
{
// do something with Stream
}
} |
I love the proposal, but would:
|
...how does this interact with the fact that |
Are you feeling (Status status, FileStream output) = await File.TryOpenAsync("foo.bar"); Cloudflare: How we scaled nginx and saved the world 54 years every day
|
Why not just return using (var stream = File.TryOpen("foo.txt"))
{
// ...
} I know it doesn't fit well with the When nullable types make it into the language, turn that into a |
Once you eliminate exceptions there is really no way to have it work as easily with // Option A: return null
using (var stream = File.TryOpen("bah.txt")) {
// Still have to account for stream being null before I can call Write, Close, etc ...
if (stream == null) {
return;
}
}
// Option B: return bool
if (!File.TryOpen("blah.txt", out var stream)) {
return;
}
using (stream) {
...
} |
That's true, although I'm worried the |
I realize it's probably a hair more "functional" than C# historically leans, but what about church encoding? Given something like: interface IFileOpenResult : IDisposable {
T Match(Func<Stream, T> onSuccess, Func<T> onAccessDenied, Func<T> onSomeOtherFailureReason …);
}
public static class File {
public static IFileOpenResult TryOpen(string path, …) { … }
} Consuming code could be written in the form of: using(var fileOpenResult = File.TryOpen(…)) {
var dataOrFailureReason = fileOpenResult.Match(
(s) => /* read & return data from stream */ ,
onAccessDenied: () => null,
onSomeOtherFailureReason: () => throw new Exception("I really wasn't expecting this!"));
} I've personally used this pattern often in line-of-business apps in order to avoid using either exceptions or Some might find, though, that this brings a burden for consumers to have a suitable common return type from the matcher parameters. Third party libraries can help such consumers, but standard library support for things like Of course, then we get into maybe wanting better language (C#, VB) support for some functional programming idioms (monoid comprehenions, |
If the developer wants to handle error conditions, she should use the normal non-Try API, right? This is the way error conditions are propagated in .NET: exceptions. Otherwise, I don't get it. |
@migueldeicaza i like the idea of public static class File {
public static bool TryOpen(
string path,
FileMode mode,
FileAccess access,
FileShare share,
out FileStream fileStream,
out Status status);
...
public enum Status
{
SUCCESS,
FILE_NOT_FOUND,
ERROR_SHARING_VIOLATION, <- or generic name
...
} |
Most code wants to stop on first error but continue unobstructed, so I think we should borrow from languages like Rust with Option/Result. |
What about |
Doesn't help when probing the binary file to open from a set of possible paths |
Functional languages usually have additional support for |
@benaadams I am not sure I follow the proposal, but if the idea was that we should additionally have an async version, I support the idea, but perhaps the name should be |
It can be made to work with
|
I like the idea at face value with the given history of the I think part of the problem is the ambiguity of |
Wouldn't that defeat the purpose of the |
Completely agree, which is kinda the same thing I said in AFAIK all Try methods in the BCL return a single success bool. |
@knocte yeah, I thought that what the pattern was about :) |
@jaredpar Do you think you'd consider introducing an error enum for this API instead of just returning bool so that it can be used for scenarios like the one outlined in #926?
where it's needed to treat very specific failures specially and completely ignore them because they are expected (as opposed to other errors). |
Per discussion in #926 this is not possible to do in a reasonable cross platform way, for many common cases. |
Right, it is not possible today. It should be possible once we introduce cross-platform IOError enum like what is suggested in #926. The basic non-throwing API can be something like:
|
Would IOError need to be nullable for the success case? Or will you add a IOError.None member? |
|
@Neme12 @jkotas IMO I like |
@Neme12 I've thought about an |
The cross platform issue isn't that we can't have cross platform error codes. It's that we don't have them. 😞 That's why a new enum would be useful. @jaredpar What if the API returned bool but there was an overload that had an error code as an out parameter? |
I think this is the best solution. In many cases you don't care about the error code just success/failure and in that case you would use the overload with the single out parameter that is consistent with all other Try API. |
That's good by me. I think the 99% case is succeeded / failed but agree that 1% where the care about how it failed is important. |
One possible consideration is to wait until C# gets discriminated unions, and then change our APIs to support the |
While I like discriminated unions, the problem I see with You know, exceptions. Essentially we'd be propagating a second, "softer" form of exceptions, ones declared as part of the API. Given we already have exceptions, I think I'd prefer going the Midori route: methods either explicitly throw a specific set of exceptions that can be recovered from, or none. Things that can't be recovered from - which is usually from programmer error - cause abandonment (essentially, exit the running task/process/application). The language is already starting to move in the direction where such a thing might be viable; we're getting non/nullable reference types, meaning |
So, what's the progress on this after 3 years? |
There should also be |
Opening a file today in. NET today requires developers to introduce exception handling into their code. That is often unwanted and adds unnecessary complexity to otherwise straight forward code. .NET should offer non-throwing alternatives to facilitate these cases.
Rationale and Usage
File operations in .NET today require developers to introduce exception handling for even the simplest of operations. This is due to a combination of the operations being inherently unpredictable in nature and the BCL only provides exceptions as a way to indicate failure.
This is problematic because it forces exception handling into every .NET app which use the file system. In many cases developers would prefer to use simple
if
checks as it fits into the flow of their code. This is particularly true of lower level code which tends to be written with local control flow vs. exceptions. Using .NET though there is no way to avoid exceptions when dealing with the file system.Additionally this is particularly annoying when debugging code in Visual Studio. Even when code has proper exception handling VS will still break
File.Open
calls when first chance exceptions are enabled (quite common). Disabling notifications for these exceptions is often not an option because in addition to hiding benign case it could hide the real error that you're trying to debug.Many developers attempt to work around this by using the following anti-pattern:
This code reads great but is quite broken in at least the following ways:
File.Exists
is a side effect free operation. This is not always the case. Example is whenfilePath
points to a named pipe. CallingExists
for a named pipe which is waiting for a connection will actually complete the connection under the hood (and then immediately break the pipe).File.Open
call can still throw exceptions hence even with theFile.Exists
predicate. The file could be deleted between the call, it could be open as part of an NTFS transactional operation, permissions could change, etc ... Hence correct code must still use exception handling here.A more correct work around looks like the following. Even this sample I'm sure is subtle to cases that would have an observable difference from just calling
File.Open
):To support these scenarios .NET should offer a set of file system helpers that use return values to indicate failure via the
Try
pattern:File.TryOpen
.Proposed API
Details
TryOpenExisting
instead ofTryOpen
as there is prior art here inMutex.TryOpenExisting. That seems like a good fit for
Mutex
as it had anOpenExisting
method. Not a good fit forFile
which hasOpen
and a series of other modes via
FileMode
that don't like up with the terminology Existing.Directory
. Similar rationale exists for that but theFile
examples are much more predominant. Can expand based on feedback.File.Exists
is inherently broken. When many developers are using it for common patterns it generally indicates there is a more correct API that has not been provided.The text was updated successfully, but these errors were encountered: