In this article, we are going to look at some common C# programming mistakes. We are pretty sure that there are a lot more mistakes that developers make while writing their projects, but here, we will try to summarize the ones we often noticed while working with other developers.
Let’s start with some beginner-level mistakes programmers make with the C# naming convention.
Using The Wrong Naming Convention
In programming, a convention refers to a set of guidelines that recommend the best practices to be followed for a particular language. Naming conventions are one of the most common mistakes known.
How To Properly Name a Class In C#
In C#, a class represents the blueprint of an object and should be named as nouns or noun phrases eg Car, Person, etc. Classes should be named with the pascal case convention. PascalCase is when every letter that starts a new word in your variable name is capitalized:
public class microwaveoven { } //wrong public class MicrowaveOven { } //right
How To Properly Name an Interface In C#
We should name interfaces as nouns, noun phrases, or adjectives. When we declare an interface, we should prefix them with the letter I:
public interface Machine { } //wrong public interface IMachine { } //right
How To Properly Name a Variable In C#
Variables should be named in such a way that they describe what they are holding. It is never okay to use abbreviations or single letters for variable names:
int lv = 3; //wrong int no_si = 20; //wrong
There are some exceptions to this rule. A typical example is when we declare a variable that holds an Id or URI:
int id = 0; //an exception to this rule
When declaring variable names we should not use underscores or hyphens as separators. They should also be named using the camel case notation:
int levels = 3; //right int numberOfStudents = 20; //right
How To Properly Name a NameSpace In C#
A namespace is used to declare a scope that contains a set of related objects. Organizing our namespaces to show hierarchy makes our general structure well-defined:
API.Services.Implementations.ProductService
How To Properly Name Constants In C#
Constants are immutable values known at compile time that remain the same throughout the duration of the program. They should be named using the Pascal case convention:
public const string FavoriteFood = "Egusi Soup"; //right public const string favoriteFood = "Egusi Soup"; //wrong public const string FAVORITEFOOD = "Egusi Soup"; //wrong
Misplacing Reference and Value Types
We categorize data types based on how their values are stored in memory. A data type is regarded as a value type if it holds a data value within its own memory space while a reference type contains a pointer to another memory location that holds the data:
public struct Person { public string Name {get; set;} public int Age {get; set;} } public class Car { public int Id {get; set;} public string Name {get; set;} } public static void MisplaceTypes() { Person personOne = new(); Person personTwo = new(); Console.WriteLine(personOne.Equals(personTwo)); //True Car carOne = new(); Car carTwo = new(); Console.WriteLine(carOne == carTwo); //False }
When we compare new instantiation of struct types, the result shows that they are equal. This is not the same for classes because classes are reference types while structs are value types. From this, we start to see the issues that could arise if we make use of a reference type as if it were a value type or vice versa.
Overlooking Extension Methods In C#
An extension method will allow an existing type to be extended without relying on inheritance or having to change the source code. In other words, we can add custom methods to predefined types without having to recompile, create a new derived type or even modify the type being extended.
Even though extension methods are very useful, they can get confusing. A lot of confusion arises when we go through or debug a codebase and find object-calling methods that are not defined on the types they’re invoked on. Let’s demystify extension methods.
For starters, they are static methods that have the “this” keyword attached to their first parameter:
public static class Extensions { public static void PersonExtension(this Person person) { person.Name ??= "A person"; Console.WriteLine($"{person.Name} is happy"); } }
After declaring an extension method, we can use it as though it is a native method of the extended object. To use it, we first need to call the namespace in which we declared:
using Extensions; public class Application { public static void OverLookingExtensionTypes() { Person personOne = new(); personOne.Name = "John"; personOne.PersonExtension(); //John is happy } }
As awesome as they sound, we should use them in moderation.
Extension methods cannot be used to override existing methods. If we try to declare an extension method with the same name and signature as an instance method of the type being extended; our extension method will not be called. Another important thing to note is that using an extension method is not a full-blown way to go about inheritance, this is because extension methods do not have access to private and protected members of the type they are extending.
Putting Too Much Load on Classes
Object-oriented programming (OOP) is a programming structure where every program is organized around an object. As stated earlier, classes are blueprints for the creation of objects. When we design classes, we need to make sure they contain only the members that are relevant to them:
public class Drink { public int Width { get; set; } public int Height { get; set; } public string Color { get; set; } public string Taste { get; set; } public void Break() { Console.WriteLine("Broken into pieces"); } public void Evaporating() { Console.WriteLine("Eveporating into thin air"); } }
This is a badly written class because it has the properties of a bottle and a liquid. The proper thing to do is to consider these objects as separate entities. Some properties of the bottle are its width, height, and color, and some abilities of the bottle are its ability to break or its ability to get filled up with liquid.
Some properties of the wine are its color, taste, and ability to evaporate. As we can see, the Bottle
object does not need a lot of information that is concerned with the Wine
object.
The better approach here is the proper separation of concern:
public class Bottle { public int Width { get; set; } public int Height { get; set; } public void Break() { Console.WriteLine("Broken into pieces"); } } public class Wine { public string Color { get; set; } public string Taste { get; set; } public void Evaporate() { Console.WriteLine("Eveporating into thin air"); } } public class WineBottle { public Wine Wine { get; set; } public Bottle Bottle { get; set; } }
Having a Jack-Of-All-Trade Method
Methods should have just one job. Having too many lines of code in a method is usually a red flag and most times it’s an indicator that our method has more than one responsibility:
public void JackOfAllTrade() { try { Console.WriteLine("Whats your name?"); string name = Console.ReadLine(); Console.WriteLine("How old are you?"); int age = Convert.ToInt32(Console.ReadLine()); string welcomeMessage = $"Welcome to the castle {name}"; Console.WriteLine(welcomeMessage); int count = 0; foreach (var letter in name) { char[] vowels = { 'A', 'E', 'I', 'O', 'U' }; if (vowels.Contains(letter)) count++; } welcomeMessage = $"Your name has {count} vowel(s)"; Console.WriteLine(welcomeMessage); Person person = new() { Name = name, Age = age }; if (count % 2 == 0) { person.Team = "Team-even"; } else { person.Team = "Team-Odd"; } people.Add(person); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
The JackOfAllTrade
method combines a range of functionalities within one method body which makes it difficult to ascertain what it does. When we write methods like this, it makes our code difficult to read. A better approach will be to break up the code into sizable and reusable chunks:
public void HandleUser() { (string name, int age) = CollectUserInfo(); RegisterUser(name, age); } private (string name, int age) CollectUserInfo() { try { Console.WriteLine("Whats your name?"); string name = Console.ReadLine(); Console.WriteLine("How old are you?"); int age = Convert.ToInt32(Console.ReadLine()); string welcomeMessage = $"Welcome to the castle {name}"; Console.WriteLine(welcomeMessage); return (name, age); } catch { Console.WriteLine("Age Should be a Number!"); } return (default!, default); } private int GetVowelCount(string word) { int count = 0; foreach (var letter in word) { char[] vowels = { 'A', 'E', 'I', 'O', 'U' }; if (vowels.Contains(letter)) count++; } return count; } private void RegisterUser(string name, int age) { int count = GetVowelCount(name); Person person = new() { Name = name, Age = age }; if (count % 2 == 0) { person.Team = "Team-even"; } else { person.Team = "Team-Odd"; } }
The beauty of this approach is that our methods can be reused in various parts of our codebase.
Using The “+” Operator For String Concatenation
The string
is an immutable type. This means that any operation performed on a string
creates a new instance in memory instead of modifying the previously defined one:
string[] words = { "doe", "is", "a", "developer" }; string introduction = default; foreach (string word in words) { introduction = introduction + word; }
Using this method will negatively impact the performance of our application.
The StringBuilder
class represents a string-like object whose value is a mutable sequence of characters:
StringBuilder stringBuilder = new(); foreach (string word in words) { stringBuilder.Append(word).Append(" "); } introduction = stringBuilder.ToString();
By utilizing StringBuilder
, we can optimize our code by addressing the memory management issues of the string
class.
Neglecting Exception Handling
An exception is a problem that appears unexpectedly from our codebase. Since we cannot avoid the occurrence of exceptions in the life cycle of an application, we should handle them. Exception handling is the process of building a system that can detect and manage exceptions. When they are not handled properly, exceptions will disrupt the application flow:
Console.WriteLine("Whats your name?"); string name = Console.ReadLine(); Console.WriteLine("How old are you?"); int age = Convert.ToInt32(Console.ReadLine()); string welcomeMessage = $"Welcome! {name}"; Console.WriteLine(welcomeMessage); return (name, age)
If we pass a string to the age
variable that can’t be parsed, an exception will be thrown.
A good approach is wrapping our code in try-catch blocks:
try { Console.WriteLine("Whats your name?"); string name = Console.ReadLine(); Console.WriteLine("How old are you?"); int age = Convert.ToInt32(Console.ReadLine()); string welcomeMessage = $"Welcome to the castle {name}{age}"; Console.WriteLine(welcomeMessage); } catch { Console.WriteLine("Age Should be a Number!"); }
This way, whenever exceptions pop up in our code the catch block will secure it. Check out our exception handling and global exception handling articles for more info.
Discarding The Stack Trace During Exception Handling
The stack trace is an execution stack that keeps track of all the methods in execution at a given instant:
public static void Run() { Console.WriteLine("Start Program"); Sum(6, 8); Console.WriteLine($"{Environment.StackTrace}"); // Result: at System.Environment.get_StackTrace() // at CommonMistakesInACsharpProgram.Application.DiscardStackTrace.Main() // at Program.<Main>$(String[] args) } public static int Sum(int numberOne, int numberTwo) => numberOne + numberTwo;
In case of an exception, the stack trace shows us the exact point where the exception happened. This is very helpful especially during debugging.
When we encounter exceptions we are either tasked with handling the exceptions or delegating the exception handling to another component of our code using throw ex
:
public static void TestMethod() { try { Run(); ThrowError(); Sum(6, 8); } catch (Exception ex) { throw ex; } } public static void ThrowError() => throw new Exception("Custom Error");
The problem with this delegating approach is we would lose the initial stack trace that gives us information about the origin of the exception.
A better approach would be to use the throw
keyword as this preserves the stack track from the origin of the exception up till the point where it is handled:
public static void TestMethod() { try { Run(); ThrowError(); Sum(6, 8); } catch (Exception ex) { throw; } }
Overlooking The Possibility of Memory Leaks
The C# garbage collector manages memory automatically by disposing of objects that are no longer in use. This however does not guarantee the absence of memory leaks. A memory leak occurs when an object that is not in use is referenced in an application. In this scenario, the Garbage collector will not dispose of such an object because it looks useful.
Let’s look at some typical ways memory leaks can come up.
A memory leak may result from running a long thread that never releases objects. When we run these kinds of implementations within our code, we must be careful not to lock up objects that have no immediate use.
Another example arises from excessive caching. Caching enables us to store data in memory to improve accessibility. While this is an impressive feature, excessive use of caching can lead to an OutOfMemory exception. To manage this we can limit the size of the objects that we cache and also we should make sure that we are not adding unnecessary objects to the cache.
Static objects are useful because we do not need to instantiate them. The only downside to this is that the Garbage collector does not dispose of static classes. We should endeavor to use static classes in moderation so we don’t shoot ourselves in the foot.
We should also consider making use of the using statement which will automatically dispose of resources after execution.
Building Tightly Coupled Systems
A tightly coupled system is one in which all the components rely so much on each other, that removing or changing any one component will destroy the entire system.
When we directly instantiate classes, we end up with a tightly coupled system. Instead, we should employ the use of dependency injection. With dependency injection, dependencies are injected into the client via a constructor, method, or property. To have a better grasp of this concept, check out our article on Dependency Injection.
Using The Equality Operator For Null Checks in C#
The equality operator is a comparison operator that checks if its operands are equal:
int value = 0; if (value == 1) Console.WriteLine("Hello World");
A common mistake programmers make is using ==
for checking runtime types of objects. The problem with this approach of type check is that the equality operator is overloadable:
public class Complex { private static int _id; Random random = new(); public Complex() => _id = random.Next(200); public static bool operator ==(Complex c1, Complex c2) => _id % 2 == 0; public static bool operator !=(Complex c1, Complex c2) => _id % 2 == 1; public int GetId => _id; }
We have overloaded ==
to check if the _id
property is even or odd.
This becomes a problem, especially in cases when we use the equality operator for null check validations. The compiler will use the overloaded code version instead of the default one we want:
Complex complexOne = new(); Console.WriteLine(complexOne.GetId); // A random valur from 0 - 200 Complex complexTwo = new(); Console.WriteLine(complexTwo.GetId); // A random value from 0 -200 if (complexOne == complexTwo) { Console.WriteLine("Equal"); }
This code will have anomalies because ==
has been overloaded to behave differently.
The is
operator checks if the run-time type of an object is compatible with a given type. It is a better alternative in terms of null checks because it cannot be overridden and will always give us what we want:
if (complexOne is complexTwo) { Console.WriteLine("Equal"); }
Returning Null Values For Empty Collections
It’s common for a method to return a collection in C#. A typical example is when we want to return all even numbers from a collection of numbers:
List<int> numbers = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; List<int> evenNumbers = numbers.Where(x => x % 2 == 0).ToList();
The result is an actual collection because we have members in numbers
that satisfy our condition.
What if we had to return an empty collection, how do we return this? A common mistake is to return the empty collection as null
:
public static IEnumerable<Person> GetUserByName(string name = null) { if (string.IsNullOrWhiteSpace(name)) { return null; } List<Person> users = new() { new Person { Age = 12, Name = "John" }, new Person { Age = 34, Name = "doe" }, }; return users.Where(x => x.Name == name); }
A better but inefficient method is to return a newly initialized collection. This is inefficient because every time we initialize an empty collection we consume memory space and assign unnecessary tasks to the garbage collector:
public static IEnumerable<Person> GetUserByName(string name = null) { if (string.IsNullOrWhiteSpace(name)) { return new List<Person>(); } List<Person> users = new() { new Person { Age = 12, Name = "John" }, new Person { Age = 3, Name = "doe" }, }; return users.Where(x => x.Name == name); }
The best approach is to return the default empty collection:
public static IEnumerable<Person> GetUserByName(string name = null) { if (string.IsNullOrWhiteSpace(name)) { return Enumerable.Empty<Person>(); } List<Person> users = new() { new Person { Age = 12, Name = "John" }, new Person { Age = 3, Name = "doe" }, }; return users.Where(x => x.Name == name); }
Conclusion
In this article, we have gone through some of the common C# programming mistakes and their prevention.
As we said, we are pretty sure that there are more of those. So, if you have some of those in mind, feel free to share them with us in the comment section.