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

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.

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 Interpolation

String Literals

Class

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.

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

Microsoft - Nullable

Boxing

async / await

LINQ

Language-Integrated Query (LINQ) Overview

RPC / WCF

Microsoft - RPC Types