Basics
Just some notes on basic C#, as I have recently been learning .NET
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
.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
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
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
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
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
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