Skip to main content

Basics

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

Microsoft - Tour of C#

preproccessor directives

Architecture

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.

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

Common Language Runtime (CLR) is a virtual machine that executes applications and generates / compiles code using a Just In Time (JIT) compiler. The JIT compiler translates Intermediate Language (IL) from compiled C# into 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.

Compiling to Microsoft Intermediate Language (MSIL)

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

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

Collections

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.

Concurrency

ConcurrentQueue<T>

ConcurrentStack<T>

ConcurrentBag<T>

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}";
Console.WriteLine(b);

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.";
Console.WriteLine(lit);

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 = @"
hi
    how
""are"" you? \this\is\a\literal
";
Console.WriteLine(lit);

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 "

hi
    how
"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)
{
  Console.WriteLine(@s);
}

The output of this code is

\this\is\new\a\test\n
Not verbatim
But still literal

Lamdas

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();
  Array.Reverse(arr);
  Console.WriteLine($"\"{s}\" reversed: {new string(arr)}");
};
Action<string> actionReverse = (string s) =>
{
  var arr = s.ToCharArray();
  Array.Reverse(arr);
  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();
  Array.Reverse(revArr);
  return new string(revArr);
};
string testS = "Racecar";
Console.WriteLine($"{testS} reversed: {revString(testS)}");

The output of this code is

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

Class

.NET Polymorphism has several examples of using override, virtual, sealed, and more.

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

Supports single inheritance, where 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 -

public abstract class Animal
{
  public Animal(string n, string p)
  {
    this.Name = n;
    this.Phrase = p;
  }

  public abstract void Speak();

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

public class Human : Animal
{
  public Human(string n, string p) : base(n, p) { }

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

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();
  }
}

Interfaces

Microsoft - Interface

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

Struct

Supports multiple inheritance, where N interfaces can inherit from each other to create a single interface

Generics

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

IDisposable

Nullable

.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.

C# Nullable provides examples of nullable types in C#

Boxing

async / await

LINQ

Language-Integrated Query (LINQ) Overview

RPC / WCF

Microsoft - RPC Types