Skip to main content


Just some notes on basic C#, as I have recently been learning .NET

Microsoft - Tour of C#

preproccessor directives


You can download .NET as an SDK or a runtime. The SDK is for developing applications, while the runtime is for executing applications that have been developed with the .NET SDK. The SDK includes runtimes required to run .NET applications. The .NET Framework consists of the Common Language Runtime (CLR) and the .NET class library that provides basic classes and types for use with Common Intermediate Language (CIL) languages.

More information on contents of the SDK and .NET runtime, and Runtime Libraries

Compiling to Microsoft Intermediate Language (MSIL). MSIL is also known as Common Intermediate Language (CIL), or Intermediate Language (IL). These terms are all referring to the same platform independent instructions produced by .NET languages after compilation, which exist under the Common Language Infrastructure (CLI) specification. This IL comes in the form of .exe or .dll files.

These files that adhere to the CLI are then used by the JIT compiler to produce native code for the platform that runs the CLR, and the native code can then be executed on this platform. The following diagram helps to understand this process visually.

Credit to the Author on Wikipedia

Common Language Runtime (CLR) is a virtual machine that compiles IL into native instructions using a Just In Time (JIT) compiler and executes applications natively. The JIT compiler translates Intermediate Language (IL) from compiled C# into native machine code that the processor understands. C# is just used as an example here, and is not the only .NET language that compiles to IL. The JIT compiler has a feature called Tiered Compilation which enables the recompilation of individual methods at run time, which in turn supports quick compilation of large applications.

Mono provides a cross-platform implementation of the CLR, and also provides us with a C# compiler to produce IL. More information on C# compilers, and the benefits of using Mono

The CLR is responsible for Automatic Memory Management through the process of Garbage Collection

It is possible to use unmanaged resources within a .NET application. For example, a FileHandle attached to a FileStream must be explicitly released by the caller. The FileStream itself is however a managed object. Unamanged objects implement the IDisposable interface. IDisposable objects call the Dispose() method which releases any contained unmanaged resources.

More information on cleaning up unmanaged resources

More .NET Terminology

.NET uses NuGet for package management, which can be installed with sudo apt install nuget on Ubuntu 20.04. Using NuGet is easy to figure out via the nuget CLI command and it's help menus, but one should also read up on Managing Dependencies and Package Restoration

Garbage Collection

The GC manages memory by allocating a contiguous section of memory for a new process. Using a pointer to the base adress of this section in memory, the GC can allocate new blocks of data for objects within managed memory. As each new object is created and memory is allocated to it, the pointer moves from the base of the managed memory block to the end of the last object allocated. Because this pointer is managed in this way, allocating new objects on the managed heap is faster than allocating objects in unmanaged memory. Because the block of managed memory is contiguous and we know all objects within it are before our pointer, accessing these objects is also fast and efficient.

The GC determines when it should clean up unused objects, and automatically kicks off this process. To determine which objects can be freed from managed memory, the GC requests an application's roots, which includes all variables and fields in an application, and builds a graph of all reachable objects within the application. The GC then compares this graph to the objects in managed memory, and frees objects that are not reachable from any point in the application. To free memory of an unused object, the GC uses a memory-copying function to compact the reachable objects in memory over the unused objects. This process both frees the memory taken up by the objects and ensures the objects that are still in use remain at the top of the managed memory block. The GC then corrects pointers to the objects, updating the graph locations to the new locations in memory, and places the managed heap's pointer at the last memory address used by existing objects. This process of memory compaction is only triggered when the GC discovers a signifigant amount of unreachable objects - if all objects remain within the application there is no need to compact memory.

The GC allocates large objects in a seperate heap, and automatically releases these objects as needed. To avoid copying large objects in memory, there is no compacting applied to this block of memory. By default the large object heap stores objects greater than 85,000 bytes, but this threshold can be configured if required.

GC Algorithm Generations explains the 0, 1, and 2 generations applied to objects within the managed heap. Generation 0 contains the newest objects, 1 is short-lived objects, and 2 is long-lived objects. By separating objects into these categories, the GC can avoid compacting the entire heap each time it frees up unused objects. For example, a generation 0 collection only requires the compacting of the generation 0 block of memory, and the blocks for 1 and 2 remain the same, so the total work required is reduced. When an object survives a generation 0 collection it is promoted to generation 1. When an object survives a generation 1 collection it is promoted to generation 2. Objects that survive generation 2 collections remain in generation 2.

Concurrent garbage collection is applied to workstation .NET 3.5 and earlier, as well as .NET server 4.0 and earlier.

After .NET 4.0, concurrent garbage collection was replaced with Background Garbage Collection. Both concurrent and background GC applies only to generation 2 collections.

Read more on what happens during garbage collection

We can use Induced Garbage Collection at points in our code where we have recently stopped using a large number of objects. This is useful in scenarios where the programmer may know that a certain path of the program results in several objects no longer being needed. Instead of depending on the GC to figure this out on its own, we can just call GC.Collect() to trigger collection at this time.

The GC class contains documentation on GC.Collect() and all other methods of the GC class.


MSBuild for .NET 6.0 and later uses Implicit Using Directives for different project types. You can optionally disable implicit using directives, but I will likely not disable these as I feel I should probably get used to default .NET settings for now.

To specify the version of C#, use the LangVersion Property within your .csproj file. You can check your current version by writing #error version in your program and running it to check the output of the produced error.

Type Categories

Microsoft - Reference Types

Microsoft - Value Types

Microsoft - Pointer Types


I'm coming to .NET from C++, so it will help me to go through the System.Collections.Generic documentation and find the containers that closely match those which I use in C++.

The obvious -

LinkedList<T> is equivalent to std::list as a doubly-linked list in C++

Queue<T> is equivalent to std::queue in C++

Stack<T> is equivalent to std::stack in C++

The not-so obvious -

List<T> is equivalent to std::vector in C++

SortedSet<T>is equivalent to std::set in C++

HashSet<T> is equivalent to std::unordered_set in C++

SortedDictionary<TKey, TValue> is equivalent to std::map, as it is sorted by keys with O(log N) insertion time and retrieval.

Dictionary<TKey, TValue> is equivalent to std::unordered_map and provides O(1) retrieval as it is implemented using a hash table.

Collections with no C++ equivalent, or not similar enough to be compared to C++ containers -

SortedList<TKey, TValue> is similar to SortedDictionary<TKey, TValue>, but uses less memory and has O(n) insertion time with O(log n) retrieval. If a SortedList is constructed from pre-sorted data, it is faster than SortedDictionary.

For collections that use <TKey, TValue>, we can anticipate the enumerator to provide each element as a KeyValuePair<TKey, TValue>. For example, we can iterate over each element in a Dictionary using the following foreach loop

foreach( KeyValuePair<string, string> kvp in myDictionary )
    Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);

PriorityQueue<TElement, TPriority>

Array is the type applied to arrays created using []. For example, the following variables a, b, and c are all of the same Array type, but each were created using a different approach

int[] a = { 1, 2, 3};
Array b = new int[3];
Array c = Array.CreateInstance(typeof(int), 3);
Console.Write("\na.GetType: {0}", a.GetType());
Console.Write("\nb.GetType: {0}", b.GetType());
Console.Write("\nc.GetType: {0}", c.GetType());

The output of this code is

a.GetType: System.Int32[]
b.GetType: System.Int32[]
c.GetType: System.Int32[]

These are all arrays of the Int32 type, but you could create arrays of custom class objects, or other builtin types.

For help on selecting the correct collection, see Collections and Data Structures, where collections and their operation complexity are compared.





ConcurrentDictionary<TKey, TValue>

Input / Output

void TestInput()
  string formattingString = "Captured {0} input: {1}\n";

  Console.Write("\nInput a character, then press enter: ");
  int ascii = Console.Read();
  char ch = Convert.ToChar(ascii);
  Console.Write(formattingString, "character", ch);
  Console.ReadLine(); // Discard any left over input
  Console.Write("\nPress a key: ");
  ConsoleKeyInfo key = Console.ReadKey();
  Console.Write("\n" + formattingString, "key", key.KeyChar);

  Console.Write("\nEnter a line: ");
  string? line = Console.ReadLine();
  Console.Write(formattingString, "line", line);

TODO: Read from file

String Composite Formatting

String Composite Formatting takes a list of objects that follow the initial formatting string. We use {0} to select the first object, {1} to select the second, and so on. We can reuse {0} or any other index as many times as we like within the formatting string. We can also use Format String Components such as {0:F6}. This example outputs a float to the 6th decimal place.

string fmt = "This is pi: {0}\nThis is the date: {1}\nThis is also pi: {0:F6}";
Console.WriteLine(fmt, Math.PI, DateTime.Now);

The output of this code is

This is pi: 3.141592653589793
This is the date: 5/1/2022 6:04:20 PM
This is also pi: 3.141593

String Interpolation

String interpolation is similar to f-strings in Python. By leading a string with the $ character, we define an interpolated string in C#. If we want to include { or } in our output, we need to escape them by doubling the brackets with {{ and }} respectively.

string a = "This is my string!";
// Right-align using `, 30` or any positive integer to represent; Negative integers are for left-align
Console.WriteLine($"This is my rifle; {a, 30}");
Console.WriteLine($"This is {{my}} rifle; {a}");
var b = $"This {{is}} my rifle; {a}";

The output of this code is

This is my rifle;             This is my string!
This is {my} rifle; This is my string!            
This {is} my rifle; This is my string!

There is also conditional formatting and the Format String Component

var b = $"This {{is}} my rifle; {a}";
// Conditional formatting must be wrapped in ( and )
Console.WriteLine($"Conditional formatting result: {(b.Length == 0 ? "Empty" : "Not empty")}");
var pi = Math.PI;
// Formatting string components
Console.WriteLine($"{pi:F3}, {pi:F10}, {DateTime.Now:d}, {DateTime.Now:f}, {DateTime.Now.ToLocalTime():h:mm:ss tt zz}");

The output of this code is

Conditional formatting result: Not empty
3.142, 3.1415926536, 5/1/2022, Sunday, May 1, 2022 5:55 PM, 5:55:56 PM -04

It's worth mentioning you can do some neat stuff in C# 11 or later, using .NET 7.0; Unfortunately I don't have access to these things or rather I don't want to set them up at the moment - I'm just learning. See the bottom-half of this section for more info

String Verbatim

C# - Verbatim (@)

Not to be confused with a string literal. A string literal is simply a string that is defined literally within the code of an application. The following is an example of a string literal

string lit = "This is a literal\nWe are now on a new line.";

The output of this code is

This is a literal
We are now on a new line.

Verbatim string literals are useful when formatting long strings or strings with several \ characters within them. To declare a verbatim string literal, prepend a @ character to the opening double-quotes of your string literal

var lit = @"
""are"" you? \this\is\a\literal

The output of this code is exactly as we defined the lit string above, aside from the appearances of "" being replaced with a single double-quote "

"are" you? \this\is\a\literal

The verbatim @ symbol can also be used to allow us to define functions or variables with otherwise reserved names in C#.

// Without @ we wouldn't be able to declare a variable named `foreach`
string[] @foreach = {@"\this\is\new\a\test\n", "Not verbatim\nBut still literal"};
foreach (string s in @foreach)

The output of this code is

Not verbatim
But still literal


Lamdas in C# were a bit odd to read at first, but once I understood the types behind them and what these types meant, things started to make more sense.

// Both of these lambdas are of the same type; Func<string, int> where int is the value returned
var getLen = (string s) => s.Length;
Func<string, int> funcLen = (string s) => s.Length;
Console.WriteLine("Length: {0}", getLen("Hello").ToString());
Console.WriteLine("Length: {0}", funcLen("Hello").ToString());

var isEqual = (string a, string b) => a == b;
Console.WriteLine(isEqual("Test", "Test"));
Func<string, string, bool> funcIsEqual = (string a, string b) => a == b;
Console.WriteLine(funcIsEqual("Test", "Test"));

// These two lamdas are both of type Action<string>, as they do not return a result
var statement = (string s) =>
  var arr = s.ToCharArray();
  Console.WriteLine($"\"{s}\" reversed: {new string(arr)}");
Action<string> actionReverse = (string s) =>
  var arr = s.ToCharArray();
  Console.WriteLine($"\"{s}\" reversed: {new string(arr)}");

// This lamda is a Func<string, string> as it take a string parameter and returns a string as a result
Func<string, string> revString = (string s) =>
  var revArr = s.ToCharArray();
  return new string(revArr);
string testS = "Racecar";
Console.WriteLine($"{testS} reversed: {revString(testS)}");

The output of this code is

Length: 5
Length: 5
Racecar reversed: racecaR
"Test" reversed: tseT


C# Polymorphism has several examples of using override, virtual, sealed, and Inheritance outlines some basic rules and limitations. See C# Properties for more information on properties, and their getters / setters.

See C# Specification - Classes for a detailed outline of different usecases for classes with examples.

C# - Class (reference types)

Unless otherwise specified, the Access Modifier for classes is internal, and all class members are private. This means a class can by default only be accessed within the code assembly where it was declared (AKA the same compilation), and the class members can by default only be accessed from within the class itself.

A class is a Reference Type, which means they are allocated on the heap and assignment actually creates a reference to this data on the heap, rather than a copy. For example

var shape = new Square(); // Heap allocation
var shapeRef = shape; // Reference to heap data
shapeRef.Height = 20;

This code produces the following output, showing that the shapeRef variable is actually modifying the same data within shape itself.

Printing Square info...
Square WxH is 10x10 at position (0,0)
Printing Square info...
Square WxH is 20x20 at position (0,0)

Classes only support single inheritance, which means a class may inherit from a single base class and extend or define functionality. Classes may inherit from multiple interfaces, but may only inherit from a single base class. These are not mutually exclusive, so the following class is valid. Here, we declare an abstract base class Animal and inherit from it to create the Dog and Human classes. More information on the abstract modifier

// By declaring Animal as abstract, we cannot construct an Animal; We must inherit from it and define Speak()
public abstract class Animal
  public Animal(string n, string p)
    this.Name = n;
    this.Phrase = p;

  // All abstract methods or members will need to be defined to inherit from an abstract base class
  public abstract void Speak();

  private string name;
  public string Name { get; set; }
  private string phrase;
  public string Phrase { get; set; }

// Inherit from the Animal abstract base class
public class Human : Animal
  public Human(string n, string p) : base(n, p) { }

  public override void Speak()
    Console.WriteLine("{0} (Human): {1}", Name, Phrase);

// Inherit from Human, and N Interfaces
public class Teacher : Human, IComparable, ICloneable
  public Teacher(string n, string p) : base(n, p) { }

  public int CompareTo(object? obj)
    throw new NotImplementedException();

  public object Clone()
    throw new NotImplementedException();

We can use another example of a Square class to show more features of C# classes and polymorphism. Here, we define a few virtual members that we will later use to show some interesting uses of class polymorphism. More information on virtual keyword

class Shape
  public Shape(int y, int x)
    Y = y;
    X = x;
    Name = this.ToString();

  // Default ctor will be used for inheriting classes
  public Shape()
    X = 0;
    Y = 0;
    Name = this.ToString();

  // Shape position; Private setter, public getter
  public int X { get; private set; }
  public int Y { get; private set; }
  public string Name { get; private set; }
  // Auto-implemented properties may use a default value initializer
  public virtual int Width { get; set; } = 1;
  public virtual int Height { get; set; } = 2;
  public virtual void Print()
    Console.WriteLine($"{Name} WxH is {Width}x{Height} at position ({X},{Y})");

What if we want to make a Square shape class? We can inherit from Shape and override the Width and Height properties to enforce the requirements of a square; Width must always be equal to height. To do this, we create two new encapsulated values and override the Height and Width properties. More information on override modifier

class Square : Shape
  // Set default value on encapsulated value for non auto-implemented properties
  private int width = 10;
  private int height = 10;

  // We can override properties just as we can functions
  // + Height and Width properties can no longer set default values
  public override int Height
    // Can use expressions for getters / setters
    get => height;
    set => width = height = value;

  public override int Width
    get => width;
      // Same setter as Height, just within a block of statements
      width = value;
      height = value;

  public override void Print()
    Console.WriteLine("Printing Square info...");
    base.Print(); // Will now use Square's getter / setter to print private int width, height

Now what if we want to create a Cube class? We will need to handle Print() differently, and the call to base.Print() will no longer be useful to us because we don't need that output. Instead, we want to print WxHxD where we have added the Depth to represent the third-dimension of our cube. Notice the new keyword specified for Print(), which indicates it is an entirely new implementation and not an override. More information on the new modifier

class Cube : Square
  // Add new properties or encapsulated values as needed
  private int depth = 10;
  public int Depth
    get => depth;
    set => depth = value;
  // Implement a `new` Print() function which acts as a new stand-alone implementaton 
  public new void Print()
    Console.WriteLine("Printing Cube info...");
    Console.WriteLine($"{Name} WxHxD is {Width}x{Height}x{Depth} at position ({X},{Y})");

As a final example, we look at the sealed keyword, which marks itself as the final implementation for a previously virtual member. This means future classes that inherit will no longer be able to override this member, which could be useful in some cases. More information on the sealed modifier

class Rectangle : Shape
  // Classes that inherit from Rectangle can not override Print
  public sealed override void Print()
    Console.WriteLine("Printing sealed Rectangle info...");

class Rect : Rectangle
  // Rect can't override Print(), since its base class declared it as `sealed`

With all that said, we can use our above shape classes with the following code

// Test our custom ctor for Shape
var shape = new Shape(5, 5);

// Test default ctor for Shape
shape = new Shape();

// Test square, which no longer has access to a custom ctor but can still be default constructed
shape = new Square();
shape.Height = 8; // Modify the height of the square, which also sets width to 8

// Create a cube to test our new Print implementation
// + We can't reuse shape here; We need to declare a new object of type Cube
Cube cube = new Cube();

// Create a rectangle to show sealed overrides
shape = new Rectangle();

// Create a rect to show the use of our inherited sealed memebr
shape = new Rect();

Which gives us this output

Shape WxH is 1x2 at position (5,5)
Shape WxH is 1x2 at position (0,0)
Printing Square info...
Square WxH is 10x10 at position (0,0)
Printing Square info...
Square WxH is 8x8 at position (0,0)
Printing Cube info...
Cube WxHxD is 10x10x10 at position (0,0)
Printing sealed Rectangle info...
Rectangle WxH is 1x2 at position (0,0)
Printing sealed Rectangle info...
Rect WxH is 1x2 at position (0,0)

Note that because of polymorphism, we can produce the same output as above, using the following code

var box = new List<Shape>();
box.Add(new Shape(5, 5));
box.Add(new Square());
box.Last().Width = 8; // Access the last element we added to the List, set its width to 8
box.Add(new Cube());
box.Add(new Rectangle());
box.Add(new Rect());
// Use a lambda to find a Cube, get a reference to it; If we found a Cube, set its depth to 5
if (box.Find((Shape s) => s.GetType() == typeof(Cube)) is Cube cubeRef) cubeRef.Depth = 5;
foreach (var s in box) s.Print(); // Print all the Shapes

The above example shows use of the keyword is, which tests whether casting is successful, and then allows us to create the cubeRef local variable for use as a Cube.

We could also use as to perform a similar test, where instead of throwing an exception the as expression would return null. An example of this is below.

var cub = new Cube();
var sqr = cub as Square;
if (sqr != null) sqr.Print();

Which has the following output - note that the reason Cube appears in the last line is because the cube constructor initializes this member to be the name of the initial type constructed. Since we declared cub as a Cube, cub.Name is Cube! Upcasting to a Square does not change this value. Square appears on the first line of the output because this line is a literal string within the Square class, and does not reference the Name property.

Printing Square info...
Cube WxH is 10x10 at position (0,0)

Inherited classes are initialized in a bottom-up fashion, where the initialization of class fields occurs first, then the constructor is called, and the process repeats for the classes we have inheritied from. To test this, I wrote the following example code

class A
  public A()
    Console.WriteLine("A default constructor was called");
    Val = 0;
  public A(int v)
    Console.WriteLine("A parameterized constructor was called");
    Val = v;
  private int val;
  public int Val
    // No setter; We can only initialize on construction
      Console.WriteLine($"A.val was initialized: {val}");
      val = value;

class B : A
  public B()
    Console.WriteLine("B default constructor was called");
    BVal = 0;
  public B(int bv)
    Console.WriteLine("B parameterized constructor was called");
    BVal = bv;

  private int bVal;

  public int BVal
      bVal = value;
      Console.WriteLine($"B.bVal was initialized: {bVal}");

class C : B
  public C()
    Console.WriteLine("C default constructor was called");
    CVal = 0;
  public C(int cv)
    Console.WriteLine("C parameterized constructor was called");
    CVal = cv;

  private int cVal;

  public int CVal
      cVal = value;
      Console.WriteLine($"C.cVal was initialized: {cVal}");

And then constructed a C object

Console.WriteLine("\nTesting construction order...");
// What will be the value of cVal? Our constructor parameter, or the initialization value, CVal = 10?
var aClass = new C(5) {CVal = 10};

Which produced the following output, which was not what I had expected before writing this example code.

Testing construction order...
A default constructor was called
A.val was initialized: 0
B default constructor was called
B.bVal was initialized: 0
C parameterized constructor was called
C.cVal was initialized: 5
C.cVal was initialized: 10

Operator Overloading


The object base class can be used for boxing and unboxing of any type, which makes the following code completely valid. An object is not to be confused with the concept of Objects, which are intantiations of classes.

var container = new List<object>();
container.Add(new object());
container.Add(new Shape());
container.Add(new int());
container.Add(new C());
container.Add(new string(""));

What's really happening here is each time we call Add to our container, we are implicitly boxing the values with the object class. So we place a Shape within the container of objects, but before the shape is added to the container a new object is constructed, which holds our Shape. This is expensive, but it can be applied to all types in C#. To program for different types more efficiently, we should take advantage of covariance in polymorphism, or we could use Generics. With generics, we must provide type parameters just as we do within List<T>, but once we provide this type we must only use this type or its subclasses within the container.

We can box and unbox objects using the following approach. All of these examples are valid.

int x = 9;
object obj = x; // Box the int
int y = (int)obj; // Unbox the int
object obj = 9;
long x = (int) obj;
object obj = 3.5; // 3.5 inferred to be type double
int x = (int) (double) obj; // x is now 3


The following example shows the use of as for testing if a cast succeeds. The as operator is comparable to dynamic_cast in C++, and should be checked for null if there is any chance of the cast failing. If the cast will never fail, consider using the is operator to cast one variable to a new variable within an if statement, which limits the scope to that block of code.

To use the as or is keywords, we must be referring to a base type of an inherited class. For example, we can check asset as House, but we cannot check stock as House.

public class Asset
  protected int _value = 10;
  public int Value { get => _value; }

public class Stock : Asset
  protected int _count = 5;
  public int Count { get => _count; }
  public void PrintStock()
    Console.WriteLine($"We own {Count} stocks valued at {Value};" +
                      $" Total value: {Count*Value}");

public class House : Asset
  private int _sqFt = 100;

  public int SquareFt
    get => _sqFt;

  public void PrintHouse()
    Console.WriteLine($"We own a house valued at {Value * SquareFt}");

var stock = new Stock();
// Upcast Stock to an Asset
var asset = stock as Asset;
// asset variable does not have access to PrintStock; We can only see Value
Console.WriteLine($"Asset value is {asset.Value}");

// Downcast an Asset to a Stock; If we succeed, call PrintStock()
// + assetStock is declared as the downcasted variable result
if (asset is Stock assetStock)

// We use `as` here, and check for null
var notHouse = asset as House;
if (notHouse != null)
  Console.WriteLine("Asset is a house! This case will not happen.");
  // notHouse is not available within this scope
  Console.WriteLine($"Variable `asset` is not an asset; Type: {asset.GetType().Name}");

The output of this code is seen blow. Note that the reason asset.GetType().Name returns Stock as our type is because we instantiated the asset variable using an upcast from a Stock to an Asset

Asset value is 10
We own 5 stocks valued at 10; Total value: 50
Variable `asset` is not an asset; Type: Stock

We can also perform explicit casts with the following format

var stock = new Stock();
var asset = (Asset) stock; 	 // Upcast
var downCast = (Stock)asset; // Downcast


Microsoft - Interface

Some useful interfaces to consider implementing -

void PrintEnum(IEnumerable<int> obj)
  foreach (var i in obj)
    Console.Write("{0}, ", i);


Supports multiple inheritance, where N interfaces can inherit from each other to create a single interface. Structs do not support inheriting from other structs or classes. Structs can not be declared as abstract or sealed, because stuct types are never abstract and implicitly sealed. Member variables (fields) of a struct may not include initializers unless declared static. Fields of a struct that are of reference type are automatically initialized to null when constructed. Value Semantics is important to understand and is a key difference between a class and a struct. A struct can not have a parameterless constructor, field initializers, a finalizer, or vir‐ tual / protected members.

Since a struct is a value type, we can save ourselves many heap allocations by using a struct in place of a class when we want to instantiate a large number of them. For example, a List<T> where T is a struct only requires the single heap allocation for the List<struct> itself - Each item in our list will be a value type that is placed on the stack, and thus we save ourselves from allocating each list item on the heap. Usually a struct makes more sense when we are using integer types or other values where assignment should produce a copy, instead of a reference.

C# Specification - Structs

C# Specification - Differences Between Class and Struct

C# - Structs (value types)

Below we create a class Bag that holds Item structs

// Inherit from IEquatable interface so we can check our Bag for a certain Item :D
public struct Item : IEquatable<Item>
  public Item(double v, int q, string name)
    Value = v;
    Qty = q;
    Name = name;

  public Item(double v, string name)
    Value = v;
    Qty = 1;
    Name = name;

  public string Name { get; set; }
  public double Value { get; set; }
  public int Qty { get; set; }
  public static bool operator ==(Item a, Item b)
    if (((object)a) == null || ((object)b) == null) return Object.Equals(a, b);
    return a.Equals(b);

  public static bool operator !=(Item a, Item b)
    return !(a == b);

  public bool Equals(Item other)
    return Name == other.Name && Value == other.Value;

  public override bool Equals(object? obj)
    return obj is Item other && Equals(other);
  public override int GetHashCode()
    return HashCode.Combine(Name, Value);

class Bag
  public Bag()
    contents = new List<Item>();
    maxCarry = 10;
  private List<Item> contents;
  private int maxCarry;

  public bool AddItem(Item i)
    if (contents.Count >= maxCarry) return false;
    return true;

  public Item? TakeItem(Item i)
    var found = contents.Find((Item inBag) => inBag == i);
    if (found == default(Item)) return null;
    return found;

And we can use our Bag class and Item struct, where a bag is a reference type and an item is a value type.

var bag = new Bag();
Item wrench = new Item(1.5, 1, "wrench"); // Create a wrench using Item ctor
var spanner = wrench; // Copy wrench to a new item
spanner.Name = "spanner";
spanner.Value = 5.0;
spanner.Qty = 2;
Item socket = new Item(2.5, 5, "socket"); // Create a new item using ctor
var bagRef = bag; // Reference to the same Bag
Item? bagSpanner = bagRef.TakeItem(spanner); // Take an item from bag / bagRef
Console.WriteLine(Object.ReferenceEquals(bagSpanner, spanner)); // Proof that spanner and bagSpanner are each their own instance of item
Item? noSpanner = bag.TakeItem(spanner); // Take the same item from bag / bagRef; Returns null because we no longer have a spanner :(
Console.WriteLine(noSpanner == null);

The output of this code is



C# - Records (reference types)


Generics are supported by class, struct, interface, and delegate types. For basic examples see C# Type System - Generics. Generics are used to implement System.Collections.Generic much like templates are used to implement the Standard Template Library in C++.

This does not imply that Generics and Templates are the same, as there are a few key differences between the two.

TODO: Differences from C++ templates

Generics can be applied to a class, or a single method of a non-generic class. The appearance of type parameters (<T>) indicates the method or class is generic

public class Generic<T>
    public T Field;

// Non-generic class A with generic method G<T>
class A
    T G<T>(T arg)
        T temp = arg;
        return temp;

Covariance, Contravariance, and Invariance

Unmanaged Memory

Cleaning up unmanaged resources


Marshall - For allocating unmanaged memory

The following code shows a quick example of how destructors are used in C#. Destructors are also referred to as Finalizers

// Test class to show destructor use by the garbage collector
public class Garbage
  private static int count = 1;
    Console.WriteLine($"Dtor called: {count++}");

// Produce 9 pieces of garbage
foreach (int i in Enumerable.Range(1, 10))
  var trash = new Garbage();

The output of this program is seen below. Note that if we had not called GC.Collect(), we would have left it up to the garbage collector to perform collection on the trash we created in the loop. We explicitly call this to

Dtor called: 1
Dtor called: 2
Dtor called: 3
Dtor called: 4
Dtor called: 5
Dtor called: 6
Dtor called: 7
Dtor called: 8
Dtor called: 9

To take this example further, we create a new Reader class that has some managed and unamanged resources. The destructor (finalizer) is used to ensure unmanaged resources are never left undisposed, while the Dispose methods are used to dispose of managed resources.

  public Reader()
    _client = new HttpClient();
    _stream = File.OpenRead("/home/kapper/test.txt");
    _unmanagedString = (IntPtr) Marshal.StringToHGlobalAnsi("This is our unmanaged string");
  public void PrintString()
  public void Dispose()
  public void Dispose(bool disposing)
    // Prevent from disposing multiple times
    if (!_isDisposed)
      if (disposing)
        // Cleaning up managed resources
        Console.WriteLine($"Disposing: {_count++}");
      // Clean up unmanaged resources here
      _isDisposed = true;

    Console.WriteLine($"Calling destructor {_count}");
    // Ensure we clean up unmanaged resources; Do not dispose managed resources
    // + This is for the case where we forget to call dispose

foreach (int i in Enumerable.Range(1, 5))
  // Create `trash` with using; Automatically calls Dispose when we leave scope
  using var trash = new Reader();
// Nothing to collect; We disposed of our `trash` when we left the scope of using
// GC.Collect();

The output of this code is seen below -

This is our unmanaged string
Disposing: 0
This is our unmanaged string
Disposing: 1
This is our unmanaged string
Disposing: 2
This is our unmanaged string
Disposing: 3
This is our unmanaged string
Disposing: 4

More on finalization / disposal


.NET Nullable<T> supports nullable types for languages within .NET, but the use of Nullable<T> isn't needed for C# and Visual Basic as these languages both have syntax for nullable types built-in.

dynamic? a = null;
var b = a ?? "A is null";
a ??= "A is null; ??= will apply this assignment";
b ??= "This value will not be changed; B is not null";

char? c = null;
// Null conditional operator returns null if lhs of ?. is null
string d = c?.ToString();
// ? operator can be applied to [] operators to prevent out of bounds exceptions
Console.WriteLine(d?[2] == null ? "Is null" : "Not null");

The output of this code is

A is null; ??= will apply this assignment
A is null
Is null

C# Nullable provides more examples of nullable types in C#.

async / await


Language-Integrated Query (LINQ) Overview

C# LINQ API Documentation provides information on various LINQ classes and methods, but a notable mention is Enumerable, which represents any collection of objects that can be enumerated on.

For example, we can use Where and Aggregate to conditionally select values using Where within a collection, and then perform some custom function to transform the selected elements via Aggregate.

The following function selects all values within the nums array and multiplies them together - notice the use of i in the for loop to prevent us from including the current index in the final product. The _ symbol is used to discard the TSource (int, in this case, since our array type source is Int32) value of the predicate Func<TSource,Int32,Boolean>. In this example, we pass a lambda to Where that is defined as (_, index) => index != i - this expression simply returns an enumerable from our nums array, where we skip the value at index i.

public int[] ProductExceptSelf(int[] nums) {
  int[] result = new int[nums.Length];
  for (int i = 0; i < nums.Length; i++) {
    result[i] = nums.Where((_, index) => index != i).Aggregate((a, b) => a * b);
  return result;

We can also format LINQ expressions as shown below, where we extract all characters from a string and convert them to lowercase. Notice that we only select characters that are within the alphabet.

public class Solution {
// Input: s = "A man, a plan, a canal: Panama"
  public bool IsPalindrome(string s) {
    // Select each char from s that is a letter or digit; Convert to lower case
    IEnumerable<char> query =
      from c in s
      where Char.IsLetterOrDigit(c)
      select Char.ToLower(c);
    // arr: "amanaplanacanalpanama"
    var arr = query.ToArray();
    Array.Reverse(arr); // Copy query array; Reverse it
    return new string(query.ToArray()) == new string(arr); // Use string comparison to test palindrome

Which is the same as

public class Solution {
  public bool IsPalindrome(string s) {
    // Select each character that is a letter or digit; Convert to lower case
    var query = s.Where(c => Char.IsLetterOrDigit(c)).Select(c => Char.ToLower(c));
    var arr = query.ToArray(); // Copy array returned from query
    Array.Reverse(arr); // Reverse arr to test palindrome
	// Using string compare against query result and reversed result
    return new string(query.ToArray()) == new string(arr); 

Both of these functions produce the same results. This is just one example of using LINQ expressions vs instance method LINQ syntax.


Microsoft - RPC Types

Cool Code

nameof can be used to obtain the name of a variable, type, or method.