Skip to content

6. Basics

C# is an object-oriented programming language that features garbage collection and single inheritance. It supports many modern programming paradigms and includes syntactic sugar for improved readability and convenience.

In the following sections, you will find coding snippets demonstrating how to program in C#. All of these examples can be executed in a simple console application. It is essential to learn C# as its own language rather than viewing it merely as C++ with a different syntax. This means you should not only focus on the new syntax but also adopt a C#-centric way of thinking.

The code inspection will provide you with hints on how to effectively utilize C# concepts, so be sure to review and understand these suggestions.

You may encounter examples that illustrate functional programming in C#. However, it's worth noting that F# is often a better choice for functional programming tasks.

6.1 StringBuilder and String-Templates

In C#, the string keyword is an alias for String; therefore, String and string are equivalent.

String objects in C# are immutable, meaning they cannot be changed once they have been created. All methods and operators that appear to modify a string actually return the results in a new string object. Therefore, for better performance, it is advisable to use StringBuilder when you need to concatenate multiple strings.

C# also supports string interpolation $, strings to span multiple lines """ and string literals to be interpreted verbatim @.

Below you will find some examples addressing strings, see also

// Initialize with a regular string literal.
string oldPath = "c:\\Program Files\\Microsoft Visual Studio 8.0";

// Initialize with a verbatim string literal.
string newPath = @"c:\Program Files\Microsoft Visual Studio 9.0";

string embeddedXML = """
       <element attr = "content">
           <body style="normal">
               Here is the main text
           </body>
           <footer>
               Excerpts from "An amazing story"
           </footer>
       </element >
       """;

var jh = (firstName: "Jupiter", lastName: "Hammon", born: 1711, published: 1761);
Console.WriteLine($"He'd be over {Math.Round((2018d - jh.born) / 100d) * 100d} years old today.");
Console.WriteLine($@"{jh.firstName} {jh.lastName}
    was an African American poet born in {jh.born}.");
Console.WriteLine(@$"He was first published in {jh.published}
at the age of {jh.published - jh.born}.");

var pw = (firstName: "Phillis", lastName: "Wheatley", born: 1753, published: 1773);
Console.WriteLine("{0} {1} was an African American poet born in {2}.", pw.firstName, pw.lastName, pw.born);

using System;
using System.Text;
int MyInt = 25;
StringBuilder myStringBuilder = new StringBuilder("Your total is ");
myStringBuilder.AppendFormat("{0:C} ", MyInt);

6.2 Generics

"Generic classes encapsulate operations that are not specific to a particular data type 1." In C++ generics are known as templates. However, there are some differences, see 2.

C# requires that the code within a generic class operates with any type that satisfies the specified constraints. These constraints may include the implementation of a particular interface or the requirement that the type derives from a specified base class.

Example for Generics1

List<int> list = new List<int>();

static void Swap<T>(ref T lhs, ref T rhs)
{

    T temp;
    temp = lhs;
    lhs = rhs;
    rhs = temp;
}

6.3 Lambdas - Arrow Operator - =>

Lambdas are anonymous functions and are best thought as a mathematical map to, i.e.

(a,b,c) → f(a,b,c)
Code example for lambda3

int[] numbers = { 4, 7, 10 };

int product = numbers.Aggregate(1, (int interim, int next) => interim * next);

6.4 Delegates

Delegates 4 are type-safe function pointers, meaning they are guaranteed to point to methods with a specific signature.

Delegates support multicast functionality, allowing a single delegate instance to reference multiple methods. You can use the += operator to add methods to the delegate's invocation list, enabling the execution of all methods in sequence when the delegate is invoked.

Example code for a delegate4

public delegate void Callback(string message); // declare

public static void DelegateMethod(string message) // method that fits the signature
{
    Console.WriteLine(message);
}
Callback handler = DelegateMethod; // instantiate
handler("Hello World"); // call and hence 'Hello World' will be on the console

There are many predefined delegates

  • Action for methods without return values.
  • Func for methods with return values.
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T,out TResult>(T arg);
    // and many more signatures of the same kind
    

Tip

You hardly ever need to declare your own delegate but should use these predefined types. These predefined delegates exist with many parameters.

6.5 Types

Although C# is a garbage-collected programming language, it is important to understand the distinction between reference types and value types. "There are two kinds of types in C#: reference types and value types. Variables of reference types store references to their data (objects), while variables of value types directly contain their data."5

In C#, reference types are more akin to C++ pointers than to C++ references. C# references can be null and exhibit assignment semantics similar to pointers, whereas C++ references function more like aliases or alternative names for the same object. In C#, reference type variables are stored on the stack, but they hold the address of an object located on the heap, much like pointers in C++. Therefore, there are significant differences between C# reference types and C++ reference types regarding nullability and assignment semantics. This revision improves the flow and clarity of the text while maintaining the original meaning2.

6.5.1 Value Types

A value type can be one of the two following kinds

  • a structure type, which encapsulates data and related functionality
  • an enumeration type, which is defined by a set of named constants and represents a choice or a combination of choices

C# provides the following built-in value types, also known as simple types

6.5.2 Reference Types

The following keywords are used to declare reference types

C# also provides the following built-in reference types

6.5.3 Boxing

Boxing and Unboxing
Boxing and Unboxing

"Boxing is the process of converting a value type to the type object or to any interface type implemented by this value type. Boxing is used to store value types in the garbage-collected heap. Boxing is an implicit conversion of a value type to the type object or to any interface type implemented by this value type."6

6.5.4 Null Safety

The default value for all reference types is null. Value type have a default value, e.g. the default value for an int is 0. To define a nullable value type you add a question mark.

Examples using nullable value types:

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null

To ensure null safety you should use ? for reference types too.

Warning

In C# the ? is just a hint for the compiler and causes warnings only.

6.6 Object Orientation

C# supports single inheritance, multiple constructors, a short notation for getters/setters and default values.

To get familiar with the syntax, here are some snippets how to declare classes and interfaces and how to use them.

namespace oo;  

public interface IStudent  
{  
    string Name { get; }  
    int CalculateLearningTime(DateTime startTime);  
}  

public class LazyStudent : IStudent  
{  
    public string Name { get; set; } = "???";  

    public int CalculateLearningTime(DateTime startTime)  
    {        return 1;  
    }}  

public class ElectiveStudent : IStudent  
{  
    private readonly Random _random = new();  
    private readonly List<string> _toDos = new();  
    public ElectiveStudent(string name)  
    {        Name = name;  
        _toDos.Add("first: understand the exercise");  
        _toDos.Add("second: make a plan");  
        CurrentGrade = 3.0f;  
    }    public ElectiveStudent():this("Studious")  
    {            }  

    public float CurrentGrade { get; }  

    public string Name { get; set; }  

    public int CalculateLearningTime(DateTime startTime)  
    {        return _random.Next(7, startTime.Hour); // makes no sense  
    }  

    public void PrintToDos()  
    {        Console.WriteLine("my todos: ");  
        foreach (var toDo in _toDos)  
        {            Console.WriteLine(toDo);  
        }    }}  

internal static class Program  
{  
    private static void Main()  
    {        var robert = new ElectiveStudent("Robert");  
        var now = DateTime.Now;  
        AnalyzeStudent(robert, now);  
        robert.PrintToDos();  
        Console.WriteLine($"my current grade is {robert.CurrentGrade}");  
        Console.WriteLine("------------");  
        var other = new ElectiveStudent();  
        AnalyzeStudent(other, now);  
        Console.WriteLine("------------");  
        var anonymous = new LazyStudent  
        {  
            Name = "Anonymous"  
        };  
        AnalyzeStudent(anonymous, now);  
    }  
    private static void AnalyzeStudent(IStudent student, DateTime now)  
    {        Console.WriteLine(  
            $"My name is {student.Name} and I am available for {student.CalculateLearningTime(now)} hours to learn starting from {now}.");  
    }}

6.6.1 Records -- C# 9

"You use the record modifier to define a reference type that provides built-in functionality for encapsulating data. C# 10 allows the record class syntax as a synonym to clarify a reference type, and record struct to define a value type with similar functionality."7

Basically, you use it for simple data classes and hence a lightweight, immutable way to encapsulate data.

public record Person(string FirstName, string LastName);
public record Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
};

6.6.2 Namespaces

Namespaces in C# are a fundamental organizational feature that allows developers to group related classes, interfaces, structures, and other types together.

namespace N2
{
    class A {}
    class B {}
}

Using directives facilitate the use of namespaces and types defined in other namespaces.

It is common practice for namespaces to match the folder structure of a project.

Warning

During the last semester, some students mixed up namespaces and folder structures after copying and pasting code, which led to difficulties in resolving these issues.

6.7 Events

With events, an object can inform other objects that something interesting has occurred, such as a button being clicked, a timeout being reached, a list being changed, or a new message arriving. This is typically solved with the publish-subscriber-pattern -- also known as observer, event-subscriber, Listener.

Subscriber-Publisher
Publish-Subscribe pattern [^9]

The standard is to define events based on the following delegate

EventHandler Delegate

public delegate void EventHandler(object? sender, EventArgs e);

"Events have the following properties:

  • The publisher determines when an event is raised; the subscribers determine what action is taken in response to the event.
  • An event can have multiple subscribers. A subscriber can handle multiple events from multiple publishers.
  • Events that have no subscribers are never raised.
  • Events are typically used to signal user actions such as button clicks or menu selections in graphical user interfaces.
  • When an event has multiple subscribers, the event handlers are invoked synchronously when an event is raised. To invoke events asynchronously, see Calling Synchronous Methods Asynchronously."9
  • Declaring an event as public allows other classes to subscribe to it. However, the event can only be raised within the class where it is defined, meaning that the subscribed methods will be invoked only from within that class.
// with regions you can collapse code sections
#region event-example
public class Stock
{
    private decimal _price;
    public event Action<decimal, string> PriceChanged;
    public string Symbol { get; }

    public Stock(string symbol, decimal initialPrice)
    {
        Symbol = symbol;
        _price = initialPrice;
    }

    // Method to update the stock price
    public void UpdatePrice(decimal newPrice)
    {
        if (_price != newPrice)
        {
            _price = newPrice;

            // Raise the event and invoke the delegate
            PriceChanged?.Invoke(newPrice, Symbol);
        }
    }
}

// Observer class
public class StockObserver
{
    private string _name;

    public StockObserver(string name)
    {
        this._name = name;
    }
    public void OnPriceChanged(decimal newPrice, string symbol)
    {
        Console.WriteLine($"{_name}: The new price of {symbol} is: {newPrice:C}");
    }
}

public class StockCleverObserver
{
    public void OnPriceChanged(decimal newPrice, string symbol)
    {
        if (newPrice < 100 && symbol == "AAPL")
        {
            Console.WriteLine($"The price of {symbol} is below 100. It's time to buy!");
        }
        else
        {
            Console.WriteLine("I am clever, I am waiting :)");
        }
    }
}
#endregion

internal static class Program
{
    private static void Main()
    {
        Stock appleStock = new Stock("AAPL", 150.00m);
        StockObserver donald = new StockObserver("Donald");
        StockObserver mickey = new StockObserver("Mickey");
        StockCleverObserver dagobert = new StockCleverObserver();
        // Subscribe to the PriceChanged event
        appleStock.PriceChanged += donald.OnPriceChanged;
        appleStock.PriceChanged += mickey.OnPriceChanged;
        appleStock.PriceChanged += dagobert.OnPriceChanged;

        // Update the stock price
        appleStock.UpdatePrice(155.00m); 
        appleStock.UpdatePrice(152.50m); 
        appleStock.UpdatePrice(99); 
    }
}   

6.8 Operators

Besides the typical arithmetic operators, there are some more

  • is, as, type of -- checks the type
  • ?: -- the ternary or conditional operator is a short form for if-else
  • ! (null-forgiving) -- use this operator for a nullable object, which should at this line of code not be null, it will suppress all nullable warnings and has no effect on runtime
  • ?? and ??= -- the null-coalescing operators, this operator shortens code with first null check and then assigning values or executing a statement Below you will find code snippets showing, how to use the above mentioned operators.
    string GetWeatherDisplay(double tempInCelsius) => tempInCelsius < 20.0 ? "Cold." : "Perfect!"; // if ..<20 then Cold else Perfect
    
    int b = a ?? -1; // if a is null b=-1, else b=a
    
    x ??= y; // if x is null assign y to x -- good to ensure non-null values
    

6.9 Extension Methods

"Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are static methods, but they're called as if they were instance methods on the extended type."10

They are a powerful way to add features to library classes and access them as if they were build in methods. See the following examples.

public static int WordCount(this string str)  
{  
    return str.Split(new char[] { ' ', '.', '?' },  
        StringSplitOptions.RemoveEmptyEntries).Length;  
}
string greeting = "Hello Extension Methods";
int numberWords = greeting.WordCount();

6.10 LINQ

With LINQ you can do almost anything you can do with SQL for databases on any IEnumerable, e.g. select, order, group values of a list of objects.

Warning

The execution of a LINQ query is always deferred until the results are actually needed. This is important to consider when using variables in your query, as the query will be executed at the time you access its results. Consequently, if a variable's value changes before the query is executed, the query will reflect the updated value. As long as you are working with an IEnumerable, the query remains unevaluated. However, calling methods like ToList(), First(), or similar will trigger the execution of the query.

Examples for Language Integrated Query (LINQ)

// Specify the data source.
int[] scores = [97, 92, 81, 60];

// Define the query expression.
IEnumerable<int> scoreQuery =
    from score in scores
    where score > 80
    select score;

// Execute the query.
foreach (var i in scoreQuery)
{
    Console.Write(i + " ");
}

// Output: 97 92 81

Video LINQ Query Expressions From, Where, Orderby, and Select

Watch and understand the video 📹 LINQ Query Expressions From, Where, Orderby, and Select, where they discuss some more features of LINQ, using VS Code.

6.11 Attributes and Reflection

Attributes are declarative information attached to a class or method or other structure that may be read/analyzed at runtime using reflection. This mechanism is widely used in C#, e.g. to mark a class as serializable (see next chapter) or to write less boilerplate code with MVVM.

In the following you find an example for the pre-defined attribute Obsolete.

[Obsolete("This class is obsolete; use class B instead")]
class A
{
    public void F() {}
}

6.12 Serialization

graph LR
 A[Object] --> B[String]
 A --> C[Other Object]
 A --serialize---> D[Linear Stream]
 D --> E[File/...]
 E --deserialize---> F[Object]
 F --> G[String]
 F --> H[Other Object]
Serialization is the process of transforming an object graph into a linear stream of bytes. These bytes may be saved to the file system and e.g. after restarting the app you may de-serialize and re-create the object.

The following example is from How to write .NET objects as JSON (serialize).

using System.Text.Json;

namespace SerializeToFile
{
    public class WeatherForecast
    {
        public DateTimeOffset Date { get; set; }
        public int TemperatureCelsius { get; set; }
        public string? Summary { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            var weatherForecast = new WeatherForecast
            {
                Date = DateTime.Parse("2019-08-01"),
                TemperatureCelsius = 25,
                Summary = "Hot"
            };

            string fileName = "WeatherForecast.json"; 
            string jsonString = JsonSerializer.Serialize(weatherForecast);
            File.WriteAllText(fileName, jsonString);

            Console.WriteLine(File.ReadAllText(fileName));
        }
    }
}
// output:
//{"Date":"2019-08-01T00:00:00-07:00","TemperatureCelsius":25,"Summary":"Hot"}

  1. Bill Wagner. Generic classes - c# programming guide - c#. 2022. URL: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-classes (visited on 19.01.2024). 

  2. Bill Wagner. Differences between c++ templates and c# generics - c# programming guide - c#. 2023. URL: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/differences-between-cpp-templates-and-csharp-generics (visited on 19.01.2024). 

  3. Bill Wagner. Lambda expressions - lambda expressions and anonymous functions - c#. 2023. URL: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions (visited on 19.01.2024). 

  4. Bill Wagner. Delegates - c# programming guide - c#. 2022. URL: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/ (visited on 19.01.2024). 

  5. Bill Wagner. Reference types - c# reference - c#. 2023. URL: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types (visited on 05.03.2024). 

  6. Bill Wagner. Boxing and unboxing. 2021. URL: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing (visited on 16.04.2024). 

  7. Bill Wagner. Records - c# reference - c#. 2024. URL: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record (visited on 07.03.2024). 

  8. Vincenzo Iannino, Claudio Mocci, Marco Vannocci, Valentina Colla, and Andrea Caputo. An event-driven agent-based simulation model for industrial processes. Applied Sciences, 10:4343, 06 2020. doi:10.3390/app10124343

  9. Bill Wagner. Events (c# programming guide). 2023. URL: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/ (visited on 25.07.2024). 

  10. Bill Wagner. Extension methods - c# programming guide - c#. 2024. URL: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods (visited on 29.02.2024).