dotnet / csharplang

The official repo for the design of the C# programming language
11.4k stars 1.02k forks source link

Discussion: `try` expression without `catch` for inline use #220

Closed lachbaer closed 7 years ago

lachbaer commented 7 years ago

Proposal: try expression without catch for inline use

Intent

Sometimes it is not necessary to catch an exception, because you can either check side conditions or proceed though the expression is failed. In these cases it would be nice to skip the exception checking.

Example

In current code there would be something like

try {
    var textStream = new StreamReader("C:\nonexistingfile.txt");
    ProcessFile(textStream);
}
catch { }
GoOnWithOtherThings();

or even wrap it additionally with

var fileName = @"C:\notexisting.txt";
if (File.Exists(fileName)) {
   // try block from above
}
GoOnWithOtherThings();

This could be abbrevated and streamlined drastically with

var textStream = try new StreamReader("C:\nonexistingfile.txt");
if (textStream != null) ProcessFile(textStream);
GoOnWithOtherThings();

The catching isn't necessary here, because a null-check (that should be done in ProcessFile() anyway) already cares about the failure. A complete try { } catch { } is just unnecessary boilerplate.

I guess that there are plenty of other useful scenarios where this would come in handy.

Other languages

PHP uses the @ operator before an expression to suppress errors and warnings

$fp = @fsockopen("www.example.com", 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";

In PHP @ is used quite often to shorten things.

Now, open for discussion... ๐Ÿ˜„

sharwell commented 7 years ago

Wouldn't this procedure be practical possible?

No, a change like this can force developers to make changes to otherwise stable code which they aren't familiar with (often because it's been years since they looked at it). Changes like have a higher risk of introducing incorrect behavior, and increase the barrier to adoption of new releases of the compiler.

For years the answer was simply "there's nothing [practical] we can do about it", but with Roslyn that changed by the added support for custom analysis which can be selectively applied.

lachbaer commented 7 years ago

Well, a change of

try {
    ...
}
catch { }

to

try {
    ...
}
catch (Exception e) { e.GetBaseException(); }

in those cases would be easy, without any further side effects. One side effect though is that the programmer's eye is "catched" to this. :wink:

JasonBock commented 7 years ago

@lachbaer I gave a link to a Rust page that shows what a match would look like with a Result type. I don't propose to give a C# version of that here as I think that's a much larger request ("Have great pattern matching in C#").

Thaina commented 7 years ago

I embrace your intention but wholeheartedly disagree to drop catch in anycase

I don't go against dropping bracket or empty catch but it must be there. To make it work like ??

But I would support these syntax

var x = try SomeThing() catch default(T);
// catch specific, throw everything else
var x = try SomeThing() catch(ArgumentException ae) => default(T);
//
var x = try SomeThing() catch(Exception e) => {
    return null; // multiline catch
};
lachbaer commented 7 years ago

Today I just came about the following: I have UDPClient and I want to just close the connection - no matter what. The code is:

try { _client.Close(); } catch (SocketException) { }

// Or if not in one line
try {
    _client.Close();
}
catch (SocketException) { }

The documentation tells me that a SocketException could be thrown, but not under what conditions. We should refer to the Winsockets API for that. Well, I think in this case it is really, really okay to do it like shown above and just ignore that exception.

Without any prejudice, here I know what I am doing and I would really like to have something like

try _client.Close();

without all the characters around the actual statement _client.Close().

You must admit, that try _client.Close() really looks much, much better and cleaner.

I do not (any more) want to ease the ignorance of exceptions, but I am eager to hear your comments on this. ๐Ÿ˜„

HaloFour commented 7 years ago

@lachbaer

For the exceptionally rare occasion where it might even make sense the language already lets you. It doesn't warrant any shorthand. Personally, I'd still catch and at least log that exception, if not bubbling it up to the consumer to handle.

lachbaer commented 7 years ago

@HaloFour

For the exceptionally rare occasion where it might even make sense the language already lets you.

Lets me what? catch { }? That's obvious.

DavidArno commented 7 years ago

@lachbaer,

I have UDPClient and I want to just close the connection - no matter what

Not sure if I'm missing something here. Surely you just call _client.Dispose() when you want to close it, no matter what?

yaakov-h commented 7 years ago

Dispose can also throw an exception. In fact, according to Reference Source, Close() just calls Dispose(true).

DavidArno commented 7 years ago

@yaakov-h,

Ah, OK. Thanks. Scrap that idea then ๐Ÿ˜€

jnm2 commented 7 years ago

@lachbaer Echoing @HaloFour, if you don't log that exception, you're throwing away valuable diagnostic information. There should not be an exception and it means you should figure out why it's happening and stop it.

lachbaer commented 7 years ago

Meanwhile I really wonder if I am so misled about exceptions?! ๐Ÿ˜ž

As nearly every method, every constructor, etc. may throw exceptions, do really wrap every single call in a try-catch and catch every possible exception...?!?!?!?!?!?! ๐Ÿ˜•

DavidArno commented 7 years ago

@lachbaer,

No, that path leads to insanity.

What you do, is you write tests: tests for ideal situations and tests for situations that lead to exceptions and recovery. You write code that makes those tests pass. Finally, you add logging at the top level that captures unhandled exceptions. Then you release...

You examine the logs, you find exceptions. You work out what caused them, add new tests that fail under those conditions. You make the tests pass by changing the code. Then you release...

You examine the logs, you find exceptions...

Until the app is retired.

lachbaer commented 7 years ago

Well, guys (and galls). I just had a deeper peek into the Roslyn source code (not latest). What shall I say...?

There are quite a lot of catch-alls and even more one-liners! Just one example:

            try
            {
                _symWriter.GetDebugInfo(ref debugDir, 0, out dataLength, IntPtr.Zero);
            }
            catch (Exception ex)
            {
                throw new PdbWritingException(ex);
            }

src\Compilers\Core\Portable\NativePdbWriter\PdbWriter.cs(871) Very often it is either a re-throwing, Logging, returning or setting something to null. (I've also seen some catch { } es!)

So, at last, it doesn't seem to be all that different in the real world than I thought. And therefore this discussion makes absolute sense!!!! Maybe not about ommiting the catch, but to "refresh" the try-catch-syntax a bit to make it more streamlined in the code.

And if even professional code looks like this, then please remember all us leisure programmers - who certainly don't do "red-green-refactor" in our freetime, but nevertheless want running code that's not going to be stopped by exceptions now and then.

yaakov-h commented 7 years ago

There's a very big difference between wrapping an exception to maintain a consistent API surface and ignoring exceptions.

You still don't seem to have a solid case for any change other than that you don't seem to like catch clauses very much.

lachbaer commented 7 years ago

By the way, it would already drastical reduce the number of lines just to omit the braces in try-catch-clauses, like it can be done with other constructs.

            try
                _symWriter.GetDebugInfo(ref debugDir, 0, out dataLength, IntPtr.Zero);
            catch (Exception ex) throw new PdbWritingException(ex);
yaakov-h commented 7 years ago

If you're trying to propose expression-bodied try/catch/finally clauses, then propose that. Ideally separately.

lachbaer commented 7 years ago

@yaakov-h

There's a very big difference between wrapping an exception to maintain a consistent API surface and ignoring exceptions.

The above is only a sample. As I said I also fould catch-and-ignore-patterns.

You still don't seem to have a solid case for any change other than that you don't seem to like catch clauses very much.

I don't like the syntactical overhead that comes with it! This overhead is one of the reasons for me to avoid exceptions and catches in the first place. (The second reason being that there are so many places throwing possible exceptions that I'm just too confused when to catch and when not - I cannot examine every single call, can I?)

If you're trying to propose expression-bodied try/catch/finally clauses, then propose that. Ideally separately.

I'm thinking about it. But not expression-bodied, until it is not an assignment (or alike).

HaloFour commented 7 years ago

@lachbaer

Omitting said braces make try/catch blocks completely ambiguous when nested. This is why it's one of the few cases where C# absolutely requires a block and not an embedded statement.

lachbaer commented 7 years ago

@HaloFour I don't see any ambiguity when try as first statement after try, catch and finally is prohibited. Otherwise you're probably going to have more than one statement anyway and you'll use a block.

Am 9. Mรคrz 2017 00:45:56 MEZ schrieb HaloFour notifications@github.com:

@lachbaer

Omitting said braces make try/catch blocks completely ambiguous when nested. This is why it's one of the few cases where C# absolutely requires a block and not an embedded statement.

-- You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub: https://github.com/dotnet/csharplang/issues/220#issuecomment-285207586

yaakov-h commented 7 years ago
try
    if (true)
         Foo()
    else
        try
            DoTheThing();
        catch (Exception1 e) // This one has to belong to the inner try
             throw WrappedException(e);
       catch (Exception2 e) // Ambiguous - which try does this belong to?
            throw WrappedException(e);
finally // Still ambiguous down here
     DoAnotherThing();
lachbaer commented 7 years ago

@yaakov-h

It is not ambigous if there are clear rules.

  1. Either forbid further try statements in non-blocked statements, or
  2. let compiler complain (error) about ambiguitiy and force using of blocks, or
  3. define the rules.

To me, number 3 is trouble-free possible. Just have a look at if-else statements, where many else statements can follow without blocks. Additionally, any finally will close the complete corresponding try statement, because it always comes last.

With these rules the block above would be bound like

try
    if (true)
         Foo()
    else
        try
            DoTheThing();
        catch (Exception1 e) // This one has to belong to the inner try
            throw WrappedException(e);
        catch (Exception2 e) // This one also belongs to the inner try - like a 2nd `else` 
           throw WrappedException(e);
        finally // This one belong to the inner try - like a 3rd `else`
            DoAnotherThing();
catch (Exception3 e) 
    // This one belongs to the outer try, because
    // previous `finally` has closed that `try` statement
    DoCompletelyOtherThing();

This way blocks and one-liners could also be mixed up. Of course I wouldn't recommend that style and use blocks everywhere instead in these cases. I wouldn't do it with if-else's also.

Also a one line assignment would be easily handled:

try var sr = new StreamReader("nonexistentfile.exe");
catch (ArgumentException ex) LogException(ex);

Because var sr is not block scoped it is hoisted and belongs to the surrounding block. In case of an ArgumentException sr is declared, but not initialized, leading to the same compiler complaints as

StreamReader sr;
try {
    sr = new StreamReader("nonexistentfile.exe");
}
catch (ArgumentException ex) { LogException(ex); }
// sr might still be uninitialized here
HaloFour commented 7 years ago

@lachbaer

finally clauses are completely optional. Unless you're implying that a non-block version of try require one? But to do what? And why? And how is that somehow better than requiring a block?

There is absolutely nothing about the syntax which disambiguates the following two:

try
    if (true)
         Foo()
    else
        try
            DoTheThing();
        catch (Exception1 e)
            throw WrappedException(e);
        catch (Exception2 e)
           throw WrappedException(e);
catch (Exception 3 e)
    throw WrappedException(e);

try
    if (true)
         Foo()
    else
        try
            DoTheThing();
        catch (Exception1 e)
            throw WrappedException(e);
catch (Exception2 e)
    throw WrappedException(e);
catch (Exception 3 e)
    throw WrappedException(e);

define the rules.

Simple rules were already defined back at C# 1.0 specifically to handle this. Require a block. It's quick and elegant and doesn't require modifying half of the grammar of the C# spec just to handle what happens to every single possible clause if it happens to be contained within a try statement.

lachbaer commented 7 years ago

@HaloFour To make this short, yes a finally; would break the inner nested try statement. finally would be the only way to escape the nesting.

try
    if (true)
         Foo()
    else
        try
            DoTheThing();
        catch (Exception1 e)
            throw WrappedException(e);
        catch (Exception2 e)
            throw WrappedException(e);
        finally;
catch (Exception3 e)
    throw WrappedException(e);

There can also be the rule that nested trys within blockless statements are simply forbidden!

try
    if (true)
         Foo()
    else
        try      // <---------- ERROR, nested try in blockless statement
            DoTheThing();
        catch (Exception1 e)
            throw WrappedException(e);
        catch (Exception2 e)
           throw WrappedException(e);
catch (Exception 3 e)
    throw WrappedException(e);

I have inspected the Roslyn code lately, because I think it is sure that the programmers who are working on it are professionals and their style is kind of representative. I found many, many, many places where blockless try statements would shorten the code and make it a bit more readable - besides they enclose every single-liner within an block. But there weren't really any nestings that could be represented without blocks. All nested trys were in many lines containing blocks. So I think it is for sure that the discussion of nesting is purely theoretical and it could be handled what is the easiest to implement.

But as there are others out there who love and do "blockless" coding, blockless try statements would be quite an impovement! C# has gone its way towards them already, e.g. with expression-bodies and auto-properties.

And with todays omni-present syntax highlighting, coding fonts and automatic indention I don't see any actual disadvantages! (Remember, you're always free to block-wrap everything nevertheless, like the Roslyn team does)

DavidArno commented 7 years ago

@lachbaer,

I have inspected the Roslyn code lately, because I think it is sure that the programmers who are working on it are professionals and their style is kind of representative

Whilst the compiler team are obviously professionals, bear in mind that we all want a super-fast compiler, that doesn't throw exceptions at us. So I'd be cautious in viewing their style as representative of the best ways of writing code for situations other than those of similar need.

lachbaer commented 7 years ago

@DavidArno ๐Ÿคฃ

Whilst the compiler team are obviously professionals [...] I'd be cautious in viewing their style as representative of the best ways of writing code [...]

Don't let that hear @MadsTorgersen, @gafter et co. ๐Ÿ˜‰ ๐Ÿ˜†

tannergooding commented 7 years ago

@lachbaer, @DavidArno. None of the three primary DotNet repositories (roslyn, corefx, or coreclr) follow a consistent coding style 100%.

However, that being said, we do have a coding style document that is probably the closest thing to the 'recommended' coding style guidelines: https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md.

But, it's your code and you're free to write it how you want. Just don't expect others to "enjoy" your code if it differs to far from the "common" practice.

DavidArno commented 7 years ago

@tannergooding,

Hey, that's not a bad set of guidelines. I agree with, and follow, > 90% of that. Only one I completely disagree with is number 10. I'm a strong proponent of "var always, unless specifying the explicit type is absolutely needed". But life would be boring if I didn't disagree on something ๐Ÿ˜€

tannergooding commented 7 years ago

Personally, I prefer var everywhere as well, but I understand why they want it only where the type is obvious (since IntelliSense is not always available, such as in GitHub) ๐Ÿ˜„

iam3yal commented 7 years ago

@DavidArno var is used in Roslyn all over the place, well, at least the parts that I've touched. ๐Ÿ˜„

DavidArno commented 7 years ago

Well that's disappointing. Where's the fun in us all agreeing? ๐Ÿ˜‰

lachbaer commented 7 years ago

According to rule 1, there is absolutely nothing that stands again blockless try-catch-finally. This comment states that indention won't be a problem either. If that is so, let's go and allow blockless try-statemens! ๐Ÿ˜„

BTW, my dislike for blocks also comes from using a German keyboard most of the time (I have a US version, too, but that is often hidden under lots of papers or somewhere else)

German keyboard layout

It is not fun to AltGr-7/8/9/0 (or Ctrl-Alt-7...) while writing! And language switching with Alt-Shift is exhausting... (@ and | are no problem though when using two hands).

orthoxerox commented 7 years ago

@lachbaer you should rebind the < > key to be the { } key instead.

lachbaer commented 7 years ago

@orthoxerox Do you have a link how to do so? Scancode remapping seems to work with whole keys only whereas { and } are 2 different keys on DE and EN keyboards.

Am 9. Mรคrz 2017 23:31:20 MEZ schrieb orthoxerox notifications@github.com:

@lachbaer you should rebind the < > key to be the { } key instead.

-- You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub: https://github.com/dotnet/csharplang/issues/220#issuecomment-285504824

orthoxerox commented 7 years ago

https://msdn.microsoft.com/en-us/globalization/keyboardlayouts.aspx

CyrusNajmabadi commented 7 years ago

Things we make simple and codify into the language should be things we recommend as 'best practices'. This proposal concerns me because it makes patterns that are traditionally deeply frowned upon elevated to a first-class construct.

Sometimes, if someone is doing something bad, it should be a lot to write. That's a good way to help hammer home hte idea of "you should not do this". If we make this a first-class construct, then the messaging becomes super-unclear. We would not have a super simple way for people to do something which then every piece of advice/tooling would then tell you to stop doing. That doesn't seem like a sensible or appropriate thing for us to be doing.

CyrusNajmabadi commented 7 years ago

Note: what i would far prefer to see would be an adoption of the (value, error) pattern that you see in languages like Rust. try could then be something akin to:

var v = try OperationThatReturnsTupleOfResultAndError(...);

// this rewrites to:

var __t = OperationThatReturnsTupleOfResultAndError(...);
if (__t.error != null) return t;
var v = __t.value;

The explicit value/error tuple pattern (or 'Option' pattern) or bool/result pattern can be quite nice in many APIs. But you def need certain operators to make common cases and control flow easy to do instead of forcing everyone to use much more verbose and unweildy pattern matching and whatnot for the common code constructs.

Thaina commented 7 years ago

@tannergooding var would still be obvious on one line try catch. In the same manner as ?: or ??

var x = try CalculateInt() catch(Exception e) 0;

Similar to

var x = TryCalculateInt() ?? 0;

int? TryCalculateInt()
{
   try { return CalculateInt(); }
   catch(Exception e) { return null; }
}
iam3yal commented 7 years ago

@Thaina I think that it's nicer to compile what you suggested into something like this:

var x = try CalculateInt() ?? 0;

Just because we're not catching anything.

When we actually want to catch something it might make sense to have this:

var x = try CalculateInt() catch(Exception e) throw e; // Do something with the exception
lachbaer commented 7 years ago

@CyrusNajmabadi

This proposal concerns me

We have already discarded the initial proposal (besides, the topic is renamed to "Discussion"). There seems to be broad consent on simply not allowing catch-all in any further ways than it is now. And I stick with my proposal to depracate catch and catch (...) { } (empty block, trivia only), e.g. with a default warning (or error) under "Options/Text Editor/C#/Code Style"

About the (value, error)-pattern, it reminds me of On Error Resume Next error checking in VB:

var __t = OperationThatReturnsTupleOfResultAndError(...);
if (__t.error != null) return t;
var v = __t.value;
Dim m_t as Variant

On Error Resume Next

m_t = OperationThatReturnsValueAndPossiblyFailsWithError(...);
If Err.Number <> 0 then
   Return t
End If

On Error Goto 0

Dim v as Variant
v = m_t

I don't like that ! And possibly most other participants here, too.

An even if that pattern would make it, the result should have its own type "ValueErrorTuple<T1, T2, ..., Exception>"

Thaina commented 7 years ago

@eyalsk That's seem so wrong

try is pair with catch and should not be anything else. We should not expect try to emit null As I always said that I go against dropping catch in any case. I prefer empty catch than other operator

But I have new idea now. What if we allow one line catch to write in the same symbol as pattern matching match ?

var x = try CalculateInt() catch (
    case WebException we: we.Code == 404 ? 0 : throw we
    case NullReferenceException nre: 0
    case Exception e: throw e
);
lachbaer commented 7 years ago

@eyalsk

var x = try CalculateInt() ?? 0;

That would actually be like the inital proposal here, with an implied catch-all and "do nothing".

lachbaer commented 7 years ago

@Thaina

var x = try CalculateInt() catch (
    case WebException we: we.Code == 404 ? 0 : throw we; break;
    case NullReferenceException nre: 0; break;
    case Exception e: throw e; break;
);

That actually makes sense! ๐Ÿ˜ƒ

Thaina commented 7 years ago

@CyrusNajmabadi

Things we make simple and codify into the language should be things we recommend as 'best practices'. This proposal concerns me because it makes patterns that are traditionally deeply frowned upon elevated to a first-class construct.

But

try
{
}
catch
{
}

Is valid syntax from the start. So I never think empty catch is bad by itself if we need only just that

lachbaer commented 7 years ago

@Thaina

So I never think empty catch is bad

Not in every case it is bad, but as a primary assumption it should be considered bad practice and still exists for historical reasons (and because sometimes, but rarely makes sense).

CyrusNajmabadi commented 7 years ago

Is valid syntax from the start.

Yes. And it's very verbose and very easy to spot. It's not meant to be terse and easy to use. It's supposed to be something you fill out and 'do the right thing' with.

Thaina commented 7 years ago

@lachbaer @CyrusNajmabadi

That's why I was proposing multiline catch after one line try. When we just really want default value for any case like HttpGet then it should be just

var res = try HttpGet(url) catch "";

And if we are in other place that should follow best practice then

var res = try HttpGet(url) catch(AllowedException e) {
    //DoAnything
} "";

//or
var res = try HttpGet(url) catch (
    case AllowedException isOK : ""
    //for other case
    default:throw e
);
CyrusNajmabadi commented 7 years ago

I'd rather see us incorporate this into whatever 'match(expr)' work we're doing. i.e.:

var v = DoSomething() match (
    case SomeType t: Something(),
    case OtherType x: Whatever(),
    catch (Exception e): AndSoOn());
DavidArno commented 7 years ago

@Thaina,

[try .. catch] is valid syntax. So I never think empty catch is bad

The following is valid syntax:

var x = (a:1 << 4 << 1 is var y ? y * 2 - 22 : 0, b:0).a;

But I suspect you'd consider it a bad way of assigning 42 to x...

Thaina commented 7 years ago

@DavidArno Well, I think your example is bad in general. It has no purpose to write like that at all. While empty catch still has benefit where it should