How heavy is .NET exception handling?

Sometimes I have a situation where it is quite simple to wrap the entire code fragment in a try-catch block, rather than doing a lot of checks that greatly reduce the readability of the code. For example, this

var result = string.Empty; if (rootObject != null) { if (rootObject.FirstProperty != null) { if (rootObject.FirstProperty.SecondProperty != null) { if (!string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString)) { result = rootObject.FirstProperty.SecondProperty.InterestingString; } } } } 

I really prefer to do it

 var result = string.Empty; try { result = rootObject.FirstProperty.SecondProperty.InterestingString; } catch { } 

But after reviewing the code, I often hear from my mentor that I should avoid try-catch blocks when it is possible to do a simple check. Is it really that important and every try-catch block consumes a lot of system resources (relatively)? Are these resources used only when an error occurs, or in each case (successful or not) is equally "heavy"?

+6
source share
9 answers

The exceptions in the .NET platform are relatively heavy - itโ€™s worth a little effort to avoid them. After all, they are called Exceptions - they should not be shared.

However, they are not as expensive as some people think.

When debugging in Visual Studio, handling the exception may take several seconds, because the IDE calls the line where the exception was detected. Because of this, some people think that each exception takes several seconds to process, and therefore should be avoided at all costs.

I have seen people blame poor system performance for throwing dozens of exceptions per hour.

It is a myth. The system is capable of throwing and catching many thousands of exceptions per second.

However, you can clean your source code with a few simple extension functions, such as:

 var result = rootObject.With( r => FirstProperty) .With( r => r.SecondProperty) .Return( r => r.InterestingString, string.Empty); 

Where With has this definition:

  public static TResult With<TInput, TResult>( this TInput o, Func<TInput, TResult> evaluator) where TResult : class where TInput : class { if (o == null) { return null; } return evaluator(o); } 

And Return has the following:

  public static TResult Return<TInput, TResult>( this TInput o, Func<TInput, TResult> evaluator, TResult defaultValue) where TInput : class { if (o == null) { return defaultValue; } return evaluator(o); } 
+8
source

Regardless of whether the exceptions are heavy or light, it is completely irrelevant. An abandoned exception that could be easily prevented is a mistake . Don't catch the exception: correct the error so you don't have to.

+16
source

Exception Handling

I would not worry about how "heavy" exceptions have such code. I will explain why later.

Exceptions for exceptional situations. The presence of an exception handler means that these properties will never be zero.

When to write protection conditions

Is null common to these properties?

If so, this is not an exceptional situation. You must write the appropriate null tests and change the exception handler code to the null condition code.

Is it unusual for these properties to be null , i.e. can you reasonably expect them to never be null ?

If so, you could just not write null checks, but just let the code drop out. However, you will not get much context as to which property the exception selected.

You can also perform a null check and throw an exception that is more specific to the context.

When to write an exception handler

If these properties are unusually null , then this is an exception. But this does not mean that you must have a handler.

Do you have an easy way to check for an exception before this happens?

If so, then you should test this before allowing the use of base code for exception. Since you just need to check for null , I would say this is pretty easy.

Do you have reasonable logic to handle this case at this level?

If you have a reasonable way to handle the exception at this level and still ensure that your method runs correctly, then go ahead and add the handler code. If you rely on a mechanism, for example, returning null , then be sure that from the point of view of the consumer it makes sense that they will not always get the result. For instance. name the FindInterestingString method, not GetInterestingString .

Unless you have a reasonable way to handle the situation, do not place an exception handler at this level. Let your exception bubble up and handle it at a higher place in the code.

If you donโ€™t have a reasonable way to handle any exception, just let the program crash. It is always better than swallowing an exception and continuing. This hides errors.

Exceptions to these rules

Sometimes you cannot easily check a condition without exception.

External dependencies, such as a file system, will change under your program. Even if you perform a preliminary test, and even if a preliminary test passes, an exception can be thrown as soon as you try to use this object. In such cases, you can do nothing about it and must rely on exception handling.

Complex validation, such as email addresses and URIs, may require the use of a construct that throws an exception. Again, this is not so. You should always look for the most suitable error handling method that suits your intentions. Just bend to use exception handling when you need to.

Performance

Performance is very unlikely to be a problem in error detection code.

Performance issues in high-performance code (when you write the framework), in the bottlenecks of your application and in algorithms that are known to work intensively in CPU / memory. You should learn when to worry about performance, but this should always be a secondary concern for the readability, maintainability, and correctness of your code.

You will find that it is impossible to accurately predict what will become a performance issue in your entire application. The only way to get an accurate picture is to run your code with realistic scripts in real conditions and profile it. Until you get to the development of the application, you should not worry about performance, unless you know that this will be a problem.

Using exceptions is not priced as high as many people would have believed. In .Net, they are designed to work very well when an exception is not thrown. This is because exceptions are for exceptions.


Code Samples

There were several additional issues with the code samples that you provided. I hope I can point out some of them before you trap them. If not, I hope you can look back at them for guidance in case of problems.

Writing Exception Handlers

Code written for your exception handler is generally not acceptable. Here are some guidelines for writing better exception handler code:

Poorly:

 try { } catch // Note: Doesn't catch `Exception e` { // ... eats the exeption } 

This is a bad form and should never be used. There is absolutely no way to properly handle all types of exceptions. The most commonly used example is an OutOfMemoryException .

Perhaps acceptable:

 try { } catch(Exception e) { logger.Log(e.ToString()); // ... eats the exeption } 

If you catch an exception and register it or display it, the exception may be thrown. This is normal if you actively monitor / report these exceptions and have the ability to ensure that these exceptions are diagnosed.

So:

 try { } catch(Exception e) { logger.Log(e.ToString()); // Make sure your logger never throws... throw; // Note: *not* `throw e;` } // Or: try { } catch { // Todo: Do something here, but be very careful... throw; } 

You can do whatever you want in the exception handler if you are very careful not to create new exceptions, and if you re-select the exception . This ensures that an error is noticed. If you re- throw; exception, be sure to use throw; , not throw e; otherwise, your original stack trace will be destroyed.

Good:

 try { } catch(NullReferenceException e) { // ... Do whatever you want here ... } 

This is safe because you only catch certain types of exceptions that are known to be generated by code in the try block. It is easy to understand the intent of the code and it is easy to verify the code. It's easy to see if the exception handler code is good or not.

Avoid code duplication

Never move on to properties when they can be avoided. Instead of writing code that accesses your properties as follows:

 rootObject ... rootObject.FirstProperty ... rootObject.FirstProperty.SecondProperty ... rootObject.FirstProperty.SecondProperty.InterestingString ... 

... call getters only once:

 var firstProperty = rootObject.FirstProperty; var secondProperty = firstProperty.SecondProperty; var interestingString = secondProperty.InterestingString; 

An example of your code will look like this:

 if (rootObject != null) { var firstProperty = rootObject.FirstProperty; if (firstProperty != null) { var secondProperty = firstProperty.SecondProperty; if (secondProperty != null) { var interestingString = secondProperty.InterestingString; if (!string.IsNullOrEmpty(interestingString)) { result = interestingString; } } } } 

One reason for this is that getters can have complex logic and repeatedly call it several times, which can affect performance.

Another reason is that you avoid repetition . Code is always read when it does not have many repetitions.

When you repeat, it is also affected by maintainability. If you change the name of one of these properties, you will have to change each line of code in which it occurs, making it difficult to reason about the impact of the change.

Avoid deepening the dependency hierarchy too deeply.

You should avoid accessing the chain within the same method. I.e:.

 rootObject.FirstProperty.SecondProperty.InterestingString 

Even if you split it so as not to repeat yourself (as I said above), you probably didnโ€™t correctly consider your code. Your code is still closely related to the hierarchy of this data structure. Every time you change this hierarchy, any code that crosses this hierarchy will need to be changed. If this is your code, then you are in poor condition.

To avoid this, separate the code that knows about each level from the levels below it.

The code that processes the root object must invoke code that processes the objects directly below the root. The code that processes FirstProperty should only know properties at the SecondProperty level (under FirstProperty ). The only code that should know anything about InterestingString is the handler code for the type of object returned by SecondProperty .

An easy way to do this is to break the workaround code and move it into the objects themselves.

Cm:

Example code that separates logic:

 public class SomeClassUsingRoot { public string FindInterestingString() { return root != null ? root.FindInterestingString() : null; } private RootSomething root; } public class RootSomething { public string FindInterestingString() { return FirstProperty != null ? FirstProperty.FindInterestingString() : null; } public SomethingTopLevel FirstProperty { get; set; } } public class SomethingTopLevel { public string FindInterestingString() { return SecondProperty != null ? SecondProperty.InterestingString : null; } public SomethingLowerLevel SecondProperty { get; set; } } public class SomethingLowerLevel { public string InterestingString { get; set; } } 

This is not the only way to solve the problem. The key is to split the logic that processes each level into separate methods or (even better) separate objects. Thus, you reduce the influence of the hierarchy.

+5
source

Exceptions for .. well, exceptional situations. They are designed for when something happens, which otherwise cannot be planned. Exceptions have a certain amount of overhead, and using them to fix common problems like this is considered bad practice, especially if you just ignore the result of the exception (with your empty catch block).

They can make your LOOK code cleaner, but they don't make your EXECUTE code clean.

+2
source

It depends.

If rootObject etc. probable , equal to zero, then its coding will be better, since this is not an exceptional circumstance. However, this will lead to the fact that the execution of the method will be somewhat slower. Although there are ways to transcode nested if to avoid deep nesting and allow quick exits from the method.

On the other hand, if normal execution speed is the problem, and rootObject , etc. it is unlikely to be zero, then its coding will be better, since this is ** and exceptional circumstances.

You need to analyze your system to find out which one is best for your application.

+1
source

One way to ensure readability is to change your conditions:

 var result = string.Empty; if (rootObject == null) return result; if (rootObject.FirstProperty == null) return result; if (rootObject.FirstProperty.SecondProperty == null) return result; if (!string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString)) { result = rootObject.FirstProperty.SecondProperty.InterestingString; } 

The next step is to use the conditional reduction, which the compiler will do for you:

 var result = string.Empty; if (rootObject == null || rootObject.FirstProperty == null || rootObject.FirstProperty.SecondProperty == null) return result; if (!string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString)) { result = rootObject.FirstProperty.SecondProperty.InterestingString; } 
+1
source

One option is to use code contracts . They are a very clean way to do the type of validation you are doing, and if you configure your debug build correctly, the compiler can really find code that breaks your contracts. An empty catch block is really not a good idea (and not because it will use resources ... it's just not very good coding for many reasons).

+1
source

If you can prevent your code from throwing an exception in the first place, than you should do it.

Try / Catch blocks are good to use when they are used correctly (for example, when you access something that is outside of your code, for example, opening network connections).

You can also use them to keep your code running if you think the error in a certain part of your code is non-fatal. Just make sure you handle the error correctly in your catch block.

One thing you should think about is how the compiler executes your predicates.

This will take the leftmost sentence of your predicate, rootObject != null , and if it is false, and it AND'ed to your other sentences, then this predicate will be guaranteed to evaluate to false. The compiler then ignores the rest of the predicate, so you can do the following:

 if (rootObject != null && rootObject.FirstProperty != null && rootObject.FirstProperty.SecondProperty != null && !string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString)) { result = rootObject.FirstProperty.SecondProperty.InterestingString; } 
0
source

In most cases, you should not check that references are not null, since null should not even be in the range of possible values. If a function cannot return something in the range of possible values, this function has no choice but an exception. This should happen without an explicit zero check. This should happen when the return value is built, for example.

But that means you need types with a null value, something should ideally support the language, but C # does not.

You can use something like NonNullable <> from Jon Skeet.

0
source

Source: https://habr.com/ru/post/906155/


All Articles