5 min. read

Use interfaces and abstract classes

שימוש במחלקות בסיס וממשקים

Don’t:

1
2
3
4
5
6
7
8
public class Serializer
{
public byte[] Serialize(object obj)
{
throw new NotImplementedException();
}
}

Prefer:

1
2
3
4
public interface ISerializer
{
byte[] Serialize(object obj);
}

Instances are created from classes, if your class has no value being an instance - prefer demoting it to an abstract class or an interface.

Prefer using Generic T instead of object

Don’t:

1
2
3
4
public interface ISerializer
{
byte[] Serialize(object obj);
}

Prefer:

1
2
3
4
public interface ISerializer
{
byte[] Serialize<T>(T obj);
}

This guideline exist to avoid Boxing.
When a value type is used in the CLR as object the virtual machine needs to box the integer into a heap value which causes memory allocations.
This is one of the differences between Value type and Reference type.
Generic values don’t create boxing~

Use record instead of class

Don’t:

1
2
3
4
public class Person
{
public string Name {get;set;}
}

Prefer:

1
public record Person(string Name);

או:

1
2
3
4
public record Person
{
public string Name { get; set; } = default!;
};

C# 9.0 added record classes which make model classes easier to create.

Prefer IEnumerable instead of List or Array

Don’t:

1
2
3
4
public class Namer
{
public List<string> SuggestNames() { ... }
}

Prefer:

1
2
3
4
public class Namer
{
public IEnumerable<string> SuggestNames() { ... }
}

When you use a concrete type like List<string> you make the user to be bound to a specific container type.
You lose couple of advantages to this:

  • Multi purpose code.
  • The ability to use different containers such as thread-safe containers.
  • Make List and Array use immediate memory instead of Lazy algorithms.
  • Using Linq with various containers.

Prefer readonly

Don’t:

1
2
3
4
5
6
7
8
9
public class ServiceClass
{
private ICaller mCaller;

ServiceClass(ICaller caller)
{
mCaller= caller;
}
}

Prefer:

1
2
3
4
5
6
7
8
9
public class ServiceClass
{
private readonly ICaller mCaller;

ServiceClass(ICaller caller)
{
mCaller= caller;
}
}

Variables initialized once but not const should be readonly.

Prefer StringBuilder to appending strings.

Don’t:

1
2
3
string a = "Hello";
string b = " World";
Console.WriteLine(a + b + "!");

Prefer:

1
2
3
string a = "Hello";
string b = "World";
Console.WriteLine($"{a} {b}!"); // This uses string.Format()

Or:

1
2
3
4
5
6
StringBuilder builder = new StringBuilder();
builder.Append("Hello");
builder.Append(" World");
builder.Append("!");

Console.WriteLine(builder.ToString());

Avoid is or as

Don’t:

1
2
3
4
5
6
7
8
9
10
11
object MyObj = fromObject;

if(myObj is Developer)
{
Developer d = myObj as Developer;
if(d.YearsOfProgramming > 5)
{
Console.WriteLine($"{d.name} is a senior dev!");
}
// Do something with d....
}

Prefer:

1
2
3
4
5
6
7
8
9
10
11
object MyObj = fromObject;

var description = myObj switch
{
Developer d when d.YearsOfProgramming is >= 5 => $"{d.name} is a senior dev!",
Developer d when d.YearsOfProgramming is < 5 => $"{d.name} is a junior dev!",
Person p => $"{p.Name} is not a developer",
_ => "Not a person"
};

Console.WriteLine(description);

The new pattern matching switch case makes our code more readable.
Behind the scene it still uses the same mechanism as null checking and casting, however this will make your code more readable.

Another point is - use the strategy pattern instead of type cast-check.

Use Generic Attributes

Don’t:

1
2
[TypeAttribute(typeof(string))]
public string Method() => default;

Prefer:

1
2
[GenericAttribute<string>()]
public string Method() => default;

C# 11.0 allows the usage of generic Attribute instead of using the typeof operator.

Use ref struct to avoid Heap allocations

Don’t:

1
2
3
4
struct Data
{
public Span<byte> Image;
}

Prefer:

1
2
3
4
ref struct Data
{
public Span<byte> Image;
}

It has many limitations but if your code needs high-end optimization for performance you may use the ref struct to avoid referencing and boxing.

In performance optimization - prefer ValueTask instead of Task

Don’t:

1
public Task<byte[]> Read(Stream s);

Prefer:

1
2
public ValueTask<byte[]> Read(Stream s);

Task is a reference type - each time you return from an async function you reallocate a heap type.
Therefore ValueTask exist to avoid those allocations.

Moreover - C# may optimize types like ValueTask<bool> because only 2 values may exist - true or false.

Thanks for reading!


Is this helpful? You can show your appreciation here: Buy me a coffee