Misc¶
A collection of different topics which didn’t fit in the other categories.
new Random
in a loop¶
💡 Info: This part is only valid for the old .NET Framework and not the newer .NET Core / .NET 5 or 6 versions.
In the .NET Framework creating the Random
class without any parameter in very short time spans will create identical outputs.
Especially in for loops this can result in the same number over and over again.
If no seed is provided for the Random
class, it will create a seed which is time dependent. The resolution of the timer is depending on the implementation of the underlying OS. On most windows system the resolution is 15ms.
❌ Bad Will produce the same number 3 times.
for(var i = 0; i < 3; i++)
{
var random = new Random();
var number = random.Next(6);
Console.WriteLine(number);
}
Prints:
4
4
4
✅ Good Extract the creation of the Random
class outside the loop.
var random = new Random();
for(var i = 0; i < 3; i++)
{
var number = random.Next(6);
Console.WriteLine(number);
}
Prints:
5
3
5
Implementing GetHashCode
¶
GetHashCode
is mainly used for Dictionary
. Mainly two things are important to say an object (at least from an dictionary point of view) is equal:
* GetHashCode
for two objects are the same and:
* Equals
is the same.
Dotnet does the first because its cheaper to calculate hashes and only if they match to check if the object is really the same.
❌ Bad GetHashCode which can cause collision
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public override int GetHashCode() => X + Y; // Can lead to massive collisions
}
✅ Good Since .netcore there is a helper class called: HashCode.Combine
.
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public override int GetHashCode() => HashCode.Combine(X, Y);
}
✅ Good Also ValueTuple
can be used for that purpose (since C# 7).
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public override int GetHashCode() => (X, Y).GetHashCode();
}
Virtual member calls in constructor¶
Calling a virtual member in a constructor can lead to unexpected behavior. The reason is that initializers run from most derived to base type but constructors run from base constructor to most derived.
❌ Bad This example will lead to a NullReferenceException
public class Parent
{
public Parent() { Console.WriteLine(GetType().Name); PrintHello(); }
public virtual void PrintHello() {}
}
public class Child : Parent
{
private string foo;
public Child()
{
foo = "Something";
}
public override void PrintHello()
{
// foo will be null as it gets called in the base constructor before "our"
// Child constructor is called and initializes the state
Console.WriteLine(foo.ToUpperInvariant());
}
}
Child
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
✅ Good Avoid virtual member calls via sealing the class or don’t make the method virtual.
public class Parent
{
public Parent() { Console.WriteLine(GetType().Name); PrintHello(); }
public void PrintHello() {}
}
public class Child : Parent
{
private string foo;
public Child()
{
foo = "Something";
}
}
💡 Info: It is perfectly fine to call a virtual member if the caller is the most derived type in the chain and there can’t be another derived type (therefore the class is
sealed
).
Don’t have empty finalizers¶
An empty finalizer does not offer any functionality and also decreases performance. Objects have to live at least one more generation before they can be removed.
❌ Bad Empty finalizer defined.
public class MyClass
{
~MyClass() {}
}
✅ Good Don’t define an empty finalizer.
public class MyClass
{
}
💡 Info: More information can be found here.
Benchmark¶
Results for the given example above:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated |
|------------------- |-----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|
| WithoutFinalizer | 2.205 ns | 0.1230 ns | 0.1416 ns | 1.00 | 0.00 | 0.0057 | - | 24 B |
| WithEmptyFinalizer | 144.038 ns | 2.9594 ns | 6.1773 ns | 65.32 | 4.19 | 0.0057 | 0.0029 | 24 B |
Enum.TryParse
unexpected behavior¶
Enum.TryParse
parses a string and tries to convert it into a valid representation of a given enum.
If the operation is successful it returns true
, otherwise false
. Have a look at the following code. What will be the output of that code?
var couldParse = Enum.TryParse("3", out WeekendDay weekendDay);
Console.WriteLine($"Could parse: {couldParse}");
Console.WriteLine($"Value: {weekendDay}");
public enum WeekendDay
{
Saturday = 0,
Sunday = 1,
}
3
is not a valid WeekendDay
therefore false
sure thing. But unfortunately no. The output is
Could parse: true
Value: 3
You can try this on out sharplab.io if you want.
✅ Good Use Enum.IsDefined
to check if a given value is in the correct range.
With Enum.IsDefined
we can check if the value is correct for a given enum.
var couldParse = Enum.TryParse("3", out WeekendDay weekendDay);
if (couldParse && Enum.IsDefined(typeof(WeekendDay), weekendDay))
{
// Only here we have a valid value for WeekendDay
}
Be careful of closures¶
Closures are “attached” to their parent to close over a certain variable. Basically the anonymous function / lambda is taking an argument outside of his own scope. The following code demonstrates such behavior:
var list = new List<Action>();
for(var i = 0; i < 5; i++)
list.Add(() => Console.WriteLine(i));
list.ForEach(action => action());
And here is the pitfall. The snippet might print something unexpecting:
5
5
5
5
5
i
(instead if a copy). That will lead to that all Action
s point to the same reference of i
, which after the for loop is 5
. The way to fix this is the following:
var list = new List<Action>();
for (var i = 0; i < 5; i++)
{
var t = i;
list.Add(() => Console.WriteLine(t));
}
list.ForEach(action => action());
This works because copying an interger to a temporary variable will result in a new reference behind the scenes. You can fiddle around with that example on sharplab.io.
Additionally adding the static
keyword to an anonymous function will prohibit any closures. So list.Add(() => Console.WriteLine(i));
will result in an compiler error.