In case of unmanageable error, should you return null or throw exceptions?
Table of Contents
Just a second! 🫷 If you are here, it means that you are a software developer.
So, you know that storage, networking, and domain management have a cost .
If you want to support this blog, please ensure that you have disabled the adblocker for this site. I configured Google AdSense to show as few ADS as possible – I don’t want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.
Thank you for your understanding. – Davide
When you don’t have any fallback operation to manage null values (eg: retry pattern), you should throw an exception instead of returning null.
You will clean up your code and make sure that, if something cannot be fixed, it gets caught as soon as possible.
Don’t return null or false
Returning nulls impacts the readability of your code. The same happens for boolean results for operations. And you still have to catch other exceptions.
Take this example:
bool SaveOnFileSystem(ApiItem item)
{
// save on file systemreturnfalse;
}
ApiItem GetItemFromAPI(string apiId)
{
var httpResponse = GetItem(apiId);
if (httpResponse.StatusCode == 200)
{
return httpResponse.Content;
}
else {
returnnull;
}
}
DbItem GetItemFromDB()
{
// returns the item or nullreturnnull;
}
If all those methods complete successfully, they return an object (DbItem, ApiItem, or true); if they fail, they return null or false.
How can you consume those methods?
void Main()
{
var itemFromDB = GetItemFromDB();
if (itemFromDB != null)
{
var itemFromAPI = GetItemFromAPI(itemFromDB.ApiId);
if (itemFromAPI != null)
{
bool successfullySaved = SaveOnFileSystem(itemFromAPI);.
if (successfullySaved)
Console.WriteLine("Saved");
}
}
Console.WriteLine("Cannot save the item");
}
Note that there is nothing we can do in case something fails. So, do we really need all that nesting? We can do better!
Throw Exceptions instead
Let’s throw exceptions instead:
void SaveOnFileSystem(ApiItem item)
{
// save on file systemthrownew FileSystemException("Cannot save item on file system");
}
ApiItem GetItemFromAPI(string apiId)
{
var httpResponse = GetItem(apiId);
if (httpResponse.StatusCode == 200)
{
return httpResponse.Content;
}
else {
thrownew ApiException("Cannot download item");
}
}
DbItem GetItemFromDB()
{
// returns the item or throws an exceptionthrownew DbException("item not found");
}
Here, each method can complete in two statuses: it either completes successfully or it throws an exception of a type that tells us about the operation that failed.
We can then consume the methods in this way:
void Main()
{
try {
var itemFromDB = GetItemFromDB();
var itemFromAPI = GetItemFromAPI(itemFromDB.ApiId);
SaveOnFileSystem(itemFromAPI);
Console.WriteLine("Saved");
}
catch(Exception ex)
{
Console.WriteLine("Cannot save the item");
}
}
Now the reader does not have to spend time reading the nested operations, it’s all more linear and immediate.
Conclusion
Remember, this way of writing code should be used only when you cannot do anything if an operation failed. You should use exceptions carefully!
Now, a question for you: if you need more statuses as a return type of those methods (so, not only “success” and “fail”, but also some other status like “partially succeeded”), how would you transform that code?
Just a second! 🫷 If you are here, it means that you are a software developer.
So, you know that storage, networking, and domain management have a cost .
If you want to support this blog, please ensure that you have disabled the adblocker for this site. I configured Google AdSense to show as few ADS as possible – I don’t want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.
Thank you for your understanding. – Davide
You already know it: using meaningful names for variables, methods, and classes allows you to write more readable and maintainable code.
It may happen that a good name for your business entity matches one of the reserved keywords in C#.
What to do, now?
There are tons of reserved keywords in C#. Some of these are
int
interface
else
null
short
event
params
Some of these names may be a good fit for describing your domain objects or your variables.
Talking about variables, have a look at this example:
var eventList = GetFootballEvents();
foreach(vareventin eventList)
{
// do something}
That snippet will not work, since event is a reserved keyword.
You can solve this issue in 3 ways.
You can use a synonym, such as action:
var eventList = GetFootballEvents();
foreach(var action in eventList)
{
// do something}
But, you know, it doesn’t fully match the original meaning.
You can use the my prefix, like this:
var eventList = GetFootballEvents();
foreach(var myEvent in eventList)
{
// do something}
But… does it make sense? Is it really your event?
The third way is by using the @ prefix:
var eventList = GetFootballEvents();
foreach(var @event in eventList)
{
// do something}
That way, the code is still readable (even though, I admit, that @ is a bit weird to see around the code).
Of course, the same works for every keyword, like @int, @class, @public, and so on
Further readings
If you are interested in a list of reserved keywords in C#, have a look at this article:
Small changes sometimes make a huge difference. Learn these 6 tips to improve the performance of your application just by handling strings correctly.
Table of Contents
Just a second! 🫷 If you are here, it means that you are a software developer.
So, you know that storage, networking, and domain management have a cost .
If you want to support this blog, please ensure that you have disabled the adblocker for this site. I configured Google AdSense to show as few ADS as possible – I don’t want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.
Thank you for your understanding. – Davide
Sometimes, just a minor change makes a huge difference. Maybe you won’t notice it when performing the same operation a few times. Still, the improvement is significant when repeating the operation thousands of times.
In this article, we will learn five simple tricks to improve the performance of your application when dealing with strings.
Note: this article is part of C# Advent Calendar 2023, organized by Matthew D. Groves: it’s maybe the only Christmas tradition I like (yes, I’m kind of a Grinch 😂).
Benchmark structure, with dependencies
Before jumping to the benchmarks, I want to spend a few words on the tools I used for this article.
The project is a .NET 8 class library running on a laptop with an i5 processor.
Running benchmarks with BenchmarkDotNet
I’m using BenchmarkDotNet to create benchmarks for my code. BenchmarkDotNet is a library that runs your methods several times, captures some metrics, and generates a report of the executions. If you follow my blog, you might know I’ve used it several times – for example, in my old article “Enum.HasFlag performance with BenchmarkDotNet”.
All the benchmarks I created follow the same structure:
the class is marked with the [MemoryDiagnoser] attribute: the benchmark will retrieve info for both time and memory usage;
there is a property named Size with the attribute [Params]: this attribute lists the possible values for the Size property;
there is a method marked as [IterationSetup]: this method runs before every single execution, takes the value from the Size property, and initializes the AllStrings array;
the methods that are parts of the benchmark are marked with the [Benchmark] attribute.
Generating strings with Bogus
I relied on Bogus to create dummy values. This NuGet library allows you to generate realistic values for your objects with a great level of customization.
The string array generation strategy is shared across all the benchmarks, so I moved it to a static method:
Here I have a default set of predefined values ([string.Empty, " ", "\n \t", null]), which can be expanded with the values coming from the additionalStrings array. These values are then placed in random positions of the array.
In most cases, though, the value of the string is defined by Bogus.
Generating plots with chartbenchmark.net
To generate the plots you will see in this article, I relied on chartbenchmark.net, a fantastic tool that transforms the output generated by BenchmarkDotNet on the console in a dynamic, customizable plot. This tool created by Carlos Villegas is available on GitHub, and it surely deserves a star!
Please note that all the plots in this article have a Log10 scale: this scale allows me to show you the performance values of all the executions in the same plot. If I used the Linear scale, you would be able to see only the biggest values.
We are ready. It’s time to run some benchmarks!
Tip #1: StringBuilder is (almost always) better than String Concatenation
Let’s start with a simple trick: if you need to concatenate strings, using a StringBuilder is generally more efficient than concatenating string.
Whenever you concatenate strings with the + sign, you create a new instance of a string. This operation takes some time and allocates memory for every operation.
On the contrary, using a StringBuilder object, you can add the strings in memory and generate the final string using a performance-wise method.
Here’s the result table:
Method
Size
Mean
Error
StdDev
Median
Ratio
RatioSD
Allocated
Alloc Ratio
WithStringBuilder
4
4.891 us
0.5568 us
1.607 us
4.750 us
1.00
0.00
1016 B
1.00
WithConcatenation
4
3.130 us
0.4517 us
1.318 us
2.800 us
0.72
0.39
776 B
0.76
WithStringBuilder
100
7.649 us
0.6596 us
1.924 us
7.650 us
1.00
0.00
4376 B
1.00
WithConcatenation
100
13.804 us
1.1970 us
3.473 us
13.800 us
1.96
0.82
51192 B
11.70
WithStringBuilder
10000
113.091 us
4.2106 us
12.081 us
111.000 us
1.00
0.00
217200 B
1.00
WithConcatenation
10000
74,512.259 us
2,111.4213 us
6,058.064 us
72,593.050 us
666.43
91.44
466990336 B
2,150.05
WithStringBuilder
100000
1,037.523 us
37.1009 us
108.225 us
1,012.350 us
1.00
0.00
2052376 B
1.00
WithConcatenation
100000
7,469,344.914 us
69,720.9843 us
61,805.837 us
7,465,779.900 us
7,335.08
787.44
46925872520 B
22,864.17
Let’s see it as a plot.
Beware of the scale in the diagram!: it’s a Log10 scale, so you’d better have a look at the value displayed on the Y-axis.
As you can see, there is a considerable performance improvement.
There are some remarkable points:
When there are just a few strings to concatenate, the + operator is more performant, both on timing and allocated memory;
When you need to concatenate 100000 strings, the concatenation is ~7000 times slower than the string builder.
In conclusion, use the StringBuilder to concatenate more than 5 or 6 strings. Use the string concatenation for smaller operations.
Edit 2024-01-08: turn out that string.Concat has an overload that accepts an array of strings. string.Concat(string[]) is actually faster than using the StringBuilder. Read more this article by Robin Choffardet.
Tip #2: EndsWith(string) vs EndsWith(char): pick the right overload
One simple improvement can be made if you use StartsWith or EndsWith, passing a single character.
There are two similar overloads: one that accepts a string, and one that accepts a char.
Again, let’s generate the plot using the Log10 scale:
They appear to be almost identical, but look closely: based on this benchmark, when we have 10000, using EndsWith(string) is 10x slower than EndsWith(char).
Also, here, the duration ratio on the 1.000.000-items array is ~3.5. At first, I thought there was an error on the benchmark, but when rerunning it on the benchmark, the ratio did not change.
It looks like you have the best improvement ratio when the array has ~10.000 items.
Tip #3: IsNullOrEmpty vs IsNullOrWhitespace vs IsNullOrEmpty + Trim
As you might know, string.IsNullOrWhiteSpace performs stricter checks than string.IsNullOrEmpty.
To demonstrate it, I have created three benchmarks: one for string.IsNullOrEmpty, one for string.IsNullOrWhiteSpace, and another one that lays in between: it first calls Trim() on the string, and then calls string.IsNullOrEmpty.
As you can see from the Log10 table, the results are pretty similar:
On average, StringIsNullOrWhitespace is ~2 times slower than StringIsNullOrEmpty.
So, what should we do? Here’s my two cents:
For all the data coming from the outside (passed as input to your system, received from an API call, read from the database), use string.IsNUllOrWhiteSpace: this way you can ensure that you are not receiving unexpected data;
If you read data from an external API, customize your JSON deserializer to convert whitespace strings as empty values;
Needless to say, choose the proper method depending on the use case. If a string like “\n \n \t” is a valid value for you, use string.IsNullOrEmpty.
Tip #4: ToUpper vs ToUpperInvariant vs ToLower vs ToLowerInvariant: they look similar, but they are not
Even though they look similar, there is a difference in terms of performance between these four methods.
[MemoryDiagnoser]publicclassToUpperVsToLower()
{
[Params(100, 1000, 10_000, 100_000, 1_000_000)]publicint Size;
publicstring[] AllStrings { get; set; }
[IterationSetup]publicvoid Setup()
{
AllStrings = StringArrayGenerator.Generate(Size);
}
[Benchmark]publicvoid WithToUpper()
{
foreach (string s in AllStrings)
{
_ = s?.ToUpper();
}
}
[Benchmark]publicvoid WithToUpperInvariant()
{
foreach (string s in AllStrings)
{
_ = s?.ToUpperInvariant();
}
}
[Benchmark]publicvoid WithToLower()
{
foreach (string s in AllStrings)
{
_ = s?.ToLower();
}
}
[Benchmark]publicvoid WithToLowerInvariant()
{
foreach (string s in AllStrings)
{
_ = s?.ToLowerInvariant();
}
}
}
What will this benchmark generate?
Method
Size
Mean
Error
StdDev
Median
P95
Ratio
WithToUpper
100
9.153 us
0.9720 us
2.789 us
8.200 us
14.980 us
1.57
WithToUpperInvariant
100
6.572 us
0.5650 us
1.639 us
6.200 us
9.400 us
1.14
WithToLower
100
6.881 us
0.5076 us
1.489 us
7.100 us
9.220 us
1.19
WithToLowerInvariant
100
6.143 us
0.5212 us
1.529 us
6.100 us
8.400 us
1.00
WithToUpper
1000
69.776 us
9.5416 us
27.833 us
68.650 us
108.815 us
2.60
WithToUpperInvariant
1000
51.284 us
7.7945 us
22.860 us
38.700 us
89.290 us
1.85
WithToLower
1000
49.520 us
5.6085 us
16.449 us
48.100 us
79.110 us
1.85
WithToLowerInvariant
1000
27.000 us
0.7370 us
2.103 us
26.850 us
30.375 us
1.00
WithToUpper
10000
241.221 us
4.0480 us
3.588 us
240.900 us
246.560 us
1.68
WithToUpperInvariant
10000
339.370 us
42.4036 us
125.028 us
381.950 us
594.760 us
1.48
WithToLower
10000
246.861 us
15.7924 us
45.565 us
257.250 us
302.875 us
1.12
WithToLowerInvariant
10000
143.529 us
2.1542 us
1.910 us
143.500 us
146.105 us
1.00
WithToUpper
100000
2,165.838 us
84.7013 us
223.137 us
2,118.900 us
2,875.800 us
1.66
WithToUpperInvariant
100000
1,885.329 us
36.8408 us
63.548 us
1,894.500 us
1,967.020 us
1.41
WithToLower
100000
1,478.696 us
23.7192 us
50.547 us
1,472.100 us
1,571.330 us
1.10
WithToLowerInvariant
100000
1,335.950 us
18.2716 us
35.203 us
1,330.100 us
1,404.175 us
1.00
WithToUpper
1000000
20,936.247 us
414.7538 us
1,163.014 us
20,905.150 us
22,928.350 us
1.64
WithToUpperInvariant
1000000
19,056.983 us
368.7473 us
287.894 us
19,085.400 us
19,422.880 us
1.41
WithToLower
1000000
14,266.714 us
204.2906 us
181.098 us
14,236.500 us
14,593.035 us
1.06
WithToLowerInvariant
1000000
13,464.127 us
266.7547 us
327.599 us
13,511.450 us
13,926.495 us
1.00
Let’s see it as the usual Log10 plot:
We can notice a few points:
The ToUpper family is generally slower than the ToLower family;
The Invariant family is faster than the non-Invariant one; we will see more below;
So, if you have to normalize strings using the same casing, ToLowerInvariant is the best choice.
Tip #5: OrdinalIgnoreCase vs InvariantCultureIgnoreCase: logically (almost) equivalent, but with different performance
Comparing strings is trivial: the string.Compare method is all you need.
There are several modes to compare strings: you can specify the comparison rules by setting the comparisonType parameter, which accepts a StringComparison value.
As you can see, there’s a HUGE difference between Ordinal and Invariant.
When dealing with 100.000 items, StringComparison.InvariantCultureIgnoreCase is 12 times slower than StringComparison.OrdinalIgnoreCase!
Why? Also, why should we use one instead of the other?
Have a look at this code snippet:
var s1 = "Aa";
var s2 = "A" + newstring('\u0000', 3) + "a";
string.Equals(s1, s2, StringComparison.InvariantCultureIgnoreCase); //Truestring.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); //False
As you can see, s1 and s2 represent equivalent, but not equal, strings. We can then deduce that OrdinalIgnoreCase checks for the exact values of the characters, while InvariantCultureIgnoreCase checks the string’s “meaning”.
So, in most cases, you might want to use OrdinalIgnoreCase (as always, it depends on your use case!)
Tip #6: Newtonsoft vs System.Text.Json: it’s a matter of memory allocation, not time
For the last benchmark, I created the exact same model used as an example in the official documentation.
This benchmark aims to see which JSON serialization library is faster: Newtonsoft or System.Text.Json?
As you might know, the .NET team has added lots of performance improvements to the JSON Serialization functionalities, and you can really see the difference!
Method
Size
Mean
Error
StdDev
Median
Ratio
RatioSD
Gen0
Gen1
Allocated
Alloc Ratio
WithJson
100
2.063 ms
0.1409 ms
0.3927 ms
1.924 ms
1.00
0.00
–
–
292.87 KB
1.00
WithNewtonsoft
100
4.452 ms
0.1185 ms
0.3243 ms
4.391 ms
2.21
0.39
–
–
882.71 KB
3.01
WithJson
10000
44.237 ms
0.8787 ms
1.3936 ms
43.873 ms
1.00
0.00
4000.0000
1000.0000
29374.98 KB
1.00
WithNewtonsoft
10000
78.661 ms
1.3542 ms
2.6090 ms
78.865 ms
1.77
0.08
14000.0000
1000.0000
88440.99 KB
3.01
WithJson
1000000
4,233.583 ms
82.5804 ms
113.0369 ms
4,202.359 ms
1.00
0.00
484000.0000
1000.0000
2965741.56 KB
1.00
WithNewtonsoft
1000000
5,260.680 ms
101.6941 ms
108.8116 ms
5,219.955 ms
1.24
0.04
1448000.0000
1000.0000
8872031.8 KB
2.99
As you can see, Newtonsoft is 2x slower than System.Text.Json, and it allocates 3x the memory compared with the other library.
So, well, if you don’t use library-specific functionalities, I suggest you replace Newtonsoft with System.Text.Json.
Wrapping up
In this article, we learned that even tiny changes can make a difference in the long run.
Let’s recap some:
Using StringBuilder is generally WAY faster than using string concatenation unless you need to concatenate 2 to 4 strings;
Sometimes, the difference is not about execution time but memory usage;
EndsWith and StartsWith perform better if you look for a char instead of a string. If you think of it, it totally makes sense!
More often than not, string.IsNullOrWhiteSpace performs better checks than string.IsNullOrEmpty; however, there is a huge difference in terms of performance, so you should pick the correct method depending on the usage;
ToUpper and ToLower look similar; however, ToLower is quite faster than ToUpper;
Ordinal and Invariant comparison return the same value for almost every input; but Ordinal is faster than Invariant;
Newtonsoft performs similarly to System.Text.Json, but it allocates way more memory.
My suggestion is always the same: take your time to explore the possibilities! Toy with your code, try to break it, benchmark it. You’ll find interesting takes!
I hope you enjoyed this article! Let’s keep in touch on Twitter or LinkedIn! 🤜🤛
There are many business lines in the world that are not easy to manage, and the trucking business is one of them. This industry is one of the booming industries in many countries.
Nowadays, many business owners are trying to take part in this industry. Over the past years, this business has shown constant growth, which has made it a popular business line. If you are planning to start a trucking business, then you have to understand the complex jargon of this field. Along with that, you need to get a DOT authority for operating a business in your State.
In this blog, you will find out how you can start and run your trucking business successfully.
Do your research
To hit the jackpot, the first thing you need to do is to crack the nuts. This means you will have to research the market and needs.
By doing in-depth research, you will be able to identify your business niche in the trucking industry. Are you interested in transporting goods or using a truck for mobile billboards? These are only two examples, but when you research it, you will definitely find more possibilities in it.
After that, it will be easy for you to develop a business plan.
Find your target market
Another one of the leading business strategies is finding and understanding the target audience. Once you understand for whom you will offer your services and what their needs are, it will become easy for you to offer the services and make more sales.
It will be a wise decision if you develop your business strategy according to the niche market. By following this approach, you can ensure that your operations are cohesive and on track. When you tailor your trucking services according to the needs of your clients, in results your business will be able to earn a reputation and revenue.
Finance your fleet
Businesses are all about heavy investment, no matter the size or scale of your startup. When it comes to the trucking business, you will be surprised to know the buying cost of trucks. When planning the finances for buying trucks, you will also have to prepare for the maintenance costs. You can find many financing options to start your business.
You can also start your own company with new vehicles or can consider investing in offers for used commercial vehicles and construction machinery.
Make it legal
It is crucial for business owners to meet all the legal requirements to operate their businesses in State. Without legal recognition or approval, the federal ministry can take charge of you, and you could end up losing your business.
Many people enter the trucking business without knowing that it is highly regulated. You will need to get a permit or authority to operate your business activities interstate. You will also need to file for a DOT MC Number in your State.
Ensure that your business complies with the applicable laws for maintaining legitimacy.
Invest on technology
Technology is the future, and especially for trucking business startups, you should realize its importance earlier. Technology is about to dominate services and different businesses. With technology, you will provide numerous benefits to your business.
When it comes to transporting business, you will have to track and manage the orders. For this, it is crucial for you to use mobile applications or websites to promote your business and make it visible. If you cannot afford oversized technological items in your business, you can still add basics like GPS systems, smart cameras, and more.
Learn your competition
When you research your market, you should also study your competitors. It will help you to understand the threats and weaknesses that already existing businesses are facing. This way, you will come up with innovative business strategies and fill the needs of the clients.
You can also offer the most competitive prices from other truckers and brokers with reasonable margins, so a good number of clients will attract your business.
Pro tip:
You should always connect directly with consigners so you will pass the benefits to your clients through a reduction in prices.
Final note:
There is no doubt in it that the trucking business has been booming over the years, and it has brought gold for owners. If you get the fundamentals right, being new in the market, you can also harvest the jackpot.