2

Before everyone espouses code correctness, I realize that the generally correct way to do things is to not use try/catch for control flow. However, this is a bit of an edge case, and I'd like some input on what others would do in this situation.

This is example code, not actual code, but it should get the point across. If anyone wants me to define aDictionary or some mock classes for the types here just let me know and I will.

public bool IsSomethingTrue1(string aString)
{
    if (aDictionary.ContainsKey(aString)) //If the key exists..
    {
        var aStringField = aDictionary[aString].Field; //Get the field from the value.
        if (aStringField != null) //If the field isn't null..
        {
            return aStringField.SubField != "someValue"; //Return whether a subfield isn't equal to a specific value.
        }
    }
    return false; //If the key isn't found or the field is null, return false.
}

public bool IsSomethingTrue2(string aString)
{
    try
    {
        return aDictionary[aString].Field.SubField != "someValue"; //Return whether the subfield isn't equal to a specific value.
    }
    catch (KeyNotFoundException) //The key wasn't in the dictionary..
    {
        return false;
    }
    catch (NullReferenceException) //The field (or the dictionary, if it is dynamically assigned rather than hardcoded) was null..
    {
        return false;
    }
}

So, in the above example the first method checks to see if the dictionary contains the key, then if it does, it checks to see if a field is null, and returns true if a subvalue isn't equal to a specific value, otherwise it returns false. This avoids try/catch, but every single time the code is accessed it performs a check on the dictionary to see if the key is present (assuming no under-the-hood caching/etc. - let me know if there is any please), then a check to see if a field is null, and then the actual check we care about.

In the second method, we use try/catch and avoid multi-tier control logic. We immediately go "straight to the point" and try to access the value in question, and if it fails in either of the two known ways it will return false. If the code executes successfully, it may return true or false (the same as the above).

Both of these methods will get the job done, but my understanding is that the first method will do so more slowly on average if nothing goes wrong in most cases. The first method will have to check everything every time, where the second only has to handle cases where something goes wrong. There will be an extra space for an Exception on the stack, and some instructions for jumping to the different catch blocks/etc.. This should all not affect performance in the regular case where nothing is wrong other than that one stack variable, however, if I understand correctly what I've read here: Do try/catch blocks hurt performance when exceptions are not thrown?

Of course with this exact example, the difference will be negligible - however, imagine a complex scenario with a lot of checks in a complex if/then/else tree compared to a try/catch with a list of failure conditions. I realize that yes, this sort of code could always be broken down into smaller bits or otherwise refactored, but for the sake of argument let's say it can't be changed other than the control flow (there are cases where changing actual logic would require a re-validation of scientific algorithms, for example, which is costly/slow and needs to be minimized).

Textbook I realize that the answer is to use the first method, but in some cases the second method could be significantly faster.

Again, I know this is somewhat pedantic, but I really like to make sure I write the most efficient code possible in places where it counts, while keeping everything readable and maintainable (and well-commented). Thanks in advance for providing me with opinions on the matter!

Community
  • 1
  • 1
Yushatak
  • 741
  • 1
  • 5
  • 15
  • I don't understand why you're doing this " return aDictionary[aString].Field.SubField != "someValue";" instead of "return aStringField.SubField != "someValue""; That way you're not inspecting the dictionary twice – Koenyn Jan 23 '17 at 19:22
  • 2
    Exceptions are for "exceptional" cases. A key not existing in a dictionary is not exceptional, in my opinion. The first example is easier to understand. – Dan Wilson Jan 23 '17 at 19:22
  • This is a bit too broad and somewhat based on opinion. IBTL, but other than that, there's more reason than just performance to avoid using exceptions for flow control. What happens if the dictionary was supposed to exist, but is null? Your only clue is that you're returning "false", which is as likely to come from the try actually *passing* your conditions as it is to come from a legitimate exception, which you've hidden. – CDove Jan 23 '17 at 19:23
  • @Koenyn - that was a mistake, thanks for catching it. – Yushatak Jan 23 '17 at 19:23
  • @DanWilson True, but is there a point where the performance/convenience of the latter case outweighs convention? – Yushatak Jan 23 '17 at 19:24
  • I'd suggest taking this to [codereview.se]. – Filburt Jan 23 '17 at 19:25
  • http://softwareengineering.stackexchange.com/questions/107723/arguments-for-or-against-using-try-catch-as-logical-operators – Steve Coleman Jan 23 '17 at 19:25
  • @user1895086 In the real-world example I loosely based this on, the dictionary is defined in code and does not change, so that isn't an issue. However, your overall point is correct, and in the real world would need to be considered depending on the specific exceptions that could be thrown. For the sake of argument, perhaps assume that each exception is inspected to make sure it's what we think it is? – Yushatak Jan 23 '17 at 19:25
  • Why do you check `if (aString.Field != null)` in the first example, but not in the second? Did you mean `if(aStringField != null)`? – juharr Jan 23 '17 at 19:28
  • this question really is too broad and should be on software engineering – oziomajnr Jan 23 '17 at 19:28
  • @juharr Because catching NullReferenceException handles that task. – Yushatak Jan 23 '17 at 19:29
  • I don't think it's too broad, but if everyone else agrees then I suppose it will be closed for that reason. In the meantime, I appreciate the input! – Yushatak Jan 23 '17 at 19:29
  • @Yushatak You don't get it, I'm saying that `aString.Field` is wrong especially since `aString` is a `string` which doesn't have a `Field` member. – juharr Jan 23 '17 at 19:30
  • @juharr Ah, you're right - I screwed up my example code. Let me fix that.. – Yushatak Jan 23 '17 at 19:30
  • When it comes to what is "correct", it depends on what you intend. Is a missing key *actually* exceptional to you? If not then avoiding exceptions as control flow would be the clearest thing to do. From a performance perspective, you'd just have to measure it and see. I'd wager `TryGetValue` would be more performant than either method you've presented here. – Glorin Oakenfoot Jan 23 '17 at 19:30
  • 2
    Maybe you would be interested in [`Dictionary.TryGetValue`](https://msdn.microsoft.com/en-us/library/bb347013(v=vs.110).aspx) and the new null conditional operator in C# 6 that would allow you to do `return aDictionary[aString]?.Field?.SubField ?? string.Empty != "someValue"` – juharr Jan 23 '17 at 19:31
  • @juharr Good for this specific case, I probably should be using that in this example, but that sidesteps the point of the thought exercise too, lol. – Yushatak Jan 23 '17 at 19:32
  • The overall point is that the moment a `try/catch` makes more sense than conditional statements to manage flow control, you know there's a flaw in the code that led you to this point. `try/catch` flow control is usually a symptom of a larger logical misstep. – CDove Jan 23 '17 at 19:33
  • @user1895086 In my specific real-world example, I can't change certain parts of the code's logic (which I didn't write) because it would require a revalidation by a customer which would be costly and time-intensive. Part of the reason I asked this question is for cases like that, where your choices don't include refactoring/rewriting. I agree, though! If the code makes this seem like a good idea there's probably a problem with the code. – Yushatak Jan 23 '17 at 19:35
  • @Ogbe when referring other sites, it is often helpful to point that [cross-posting is frowned upon](http://meta.stackexchange.com/tags/cross-posting/info) – gnat Jan 23 '17 at 20:23

2 Answers2

3

Exceptions are supposed to be used in exceptional cases (e.g. when something's going wrong: file not found, connection broken etc.). They are very slow (stack trace consumes time). In your implementation key's abscence is a quite routine situation:

    public bool IsSomethingTrue2(string aString) {
      if (null == aString)
        return false; 

      MyType value;

      if (!aDictionary.TryGetValue(aString, out value))
        return false;

      return (null == value) 
        ? false 
        : null == value.Field 
           ? false
           : value.Field.SubField == "someValue"; 
    }

Another issue is that KeyNotFoundException and NullReferenceException are not supposed to be thrown unless the routine has a bug. Debugging can well appear to be a difficult process, please, do not turn it into nightmare.

Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • Does the stacktrace get built when an exception is thrown, or does that get built and maintained as the code executes even when nothing goes wrong? If it is maintained as code executes without issues then I completely agree. Again, I agree with your solution, but like another in the comments I think this sidesteps the point of this thought exercise by addressing this *exact* scenario, when the question is meant to be slightly more general. Perhaps that makes it too broad, though. – Yushatak Jan 23 '17 at 19:47
  • @Yushatak: whenever exception is thrown it does stack trace. When nothing is going wrong (no exception thrown) no stack tracing will be performed; so try `{...} finally {...}` is fast; `try {} catch {...}` is fast as well, providing that no exception is thrown. – Dmitry Bychenko Jan 23 '17 at 19:48
1

You seem to agree that not using boneheaded exceptions to control program flow is a proper thing to do.

Nonetheless you'd still prefer the try-catch solution because it is a concise and relatively clear way, and because it may be even faster due to absent null/presence checks and that exception throwing and handling may be not that bad performance-wise.

It is all a good reasoning, but there are few moments we should be more careful about:

Performance.

With anything related to performance there is only one thing you can be (relatively) sure - benchmarks. Measure everything.

We do not know exact patterns of data in dictionary. If nothing can throw exceptions during property drilling, then you may well be better with exceptions.

But cost of actual null comparisons/TryGetValue methods is minuscule compared to exception throwing.

Again - measure it against sample data, consider how often this code will get executed and only then make any decision on acceptable/unacceptable performance.

Readability

No one would argue that this code is shorter than the one with a lot of ifs. And it is much more difficult to make any mistakes with it.

But there is one fallacy hidden behind all this readability - such try-catch code is not fully equivalent to the original.

Correctness/equivalence.

Why isn't it fully equivalent?

  1. Because used dictionary may not be a Dictionary<String, T>, but rather some custom IDictionary that may have a defect, that causes NullReferenceException during internal calculations.
  2. Or that stored T object may be a proxy object with internal object, that under certain cicumstances starts initialized with null, causing NullReferenceException in T.SomeProp.
  3. Or that T can be a proxy to some object in another Dictionary, it is actually absent from, hence KeyNotFoundException.

In all those cases we will ignore a potential defect.

Isn't it a movie plot threat that won't happen in your particular case, you may say? Perhaps, or perhaps not. You may decide to accept such risks as rather tiny, but such cases are not impossible, because the system will change, while this code won't.


All in all, I'd rather stick to the first noexcept solution. It is a bit longer, but it does exactly what it says, in any case won't have significantly worse performance, and doesn't throw exception for no reason.

Community
  • 1
  • 1
Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
  • This is exactly the kind of answer I was hoping for - clear, concise facts and opinions on the topic without much bias against either viewpoint. Thank you! I will prefer if/then/else unless the performance cost of a particular case is significant enough to warrant the deviation from the norm when dealing with these kinds of situations. In this particular example those would be "movie plot threats", but it was an example and the real world is quite likely to have some such potential failures, I agree. Again, thank you. – Yushatak Jan 24 '17 at 15:36