C# Programming — Data Structures, Classes, Coding Style & Language Mechanics

Last updated: December 13, 2025
Author: Paul Namalomba
- SESKA Computational Engineer
- Software Developer
- PhD Candidate (Civil Engineering Spec. Computational and Applied Mechanics)

Contact: kabwenzenamalomba@gmail.com
Website: paulnamalomba.github.io

Language: C# License: MIT

Overview

This guide is a comprehensive reference for C# developers covering language fundamentals, data structures (built-in and advanced), classes/objects/modules, coding style, and practical language mechanics. Designed to match the style of existing guides, it provides clear examples, code snippets, tables for comparison, tips, and pitfalls for each major topic.

Use this guide as a reference when designing APIs, choosing data structures, writing idiomatic C#, or preparing for production deployment across .NET platforms.

Contents


C# Fundamentals

Compilation & Runtime

Key steps: 1. Source (.cs) → Roslyn compiler → IL (assembly) 2. CLR loads assembly, verifies metadata 3. JIT compiles IL methods on first use to native code 4. Execution under CLR with GC, security, and interop

Notes: - Roslyn provides compiler-as-a-service APIs used by analyzers and IDE tooling. - Ahead-of-Time (AOT) compilation and ReadyToRun options exist for performance-sensitive scenarios.

CLR, JIT & Memory Management

Memory tips: - Avoid large allocations on LOH when possible. - Prefer pooling (ArrayPool<T>) for frequently allocated buffers. - Use Span<T>, Memory<T> to work with slices without allocations for high-performance workloads.

Type System (Value vs Reference)

Aspect Value Types Reference Types
Examples int, float, struct, bool class, string, object, arrays, delegates
Storage Stack (or inline in object) Heap (object referenced by pointer)
Copy semantics Copy the value (deep copy of fields) Copy the reference (shallow copy)
Nullability Non-nullable (unless nullable T?) Can be null (nullable reference types in C#8+ enabled)
Default Zero-initialized null reference

Pitfalls: - Boxing/unboxing: avoid unnecessary boxing of value types into object (performance overhead). - Structs should be small and immutable; large structs cause copies and performance overhead.

Example:

int a = 5;
int b = a; // copy of value
b = 7; // a is still 5

class Node { public int Value; }
var n1 = new Node { Value = 5 };
var n2 = n1; // reference copy
n2.Value = 7; // n1.Value is now 7

Primitive Types & Built-in Collections

Primitive Types (short summary)

Use decimal for money, double for scientific, float for memory-constrained scenarios.

Arrays

Example:

int[] arr = new int[5];
arr[0] = 42;

// Multidimensional
int[,] matrix = new int[3,4];

// Jagged array
int[][] jagged = new int[3][];
jagged[0] = new int[] {1,2};

Notes/tips: - Arrays have Length, not Count. - When you need dynamic sizing use List<T>.

List and Generic Collections

Example:

var list = new List<string>();
list.Add("hello");
list.RemoveAt(0);
foreach(var s in list) Console.WriteLine(s);

Performance: - List<T> has amortized O(1) append; use Capacity property to pre-allocate when size known.

Dictionary, HashSet, Queue, Stack

Example:

var dict = new Dictionary<string,int>();
dict["apples"] = 3;
if (dict.TryGetValue("apples", out var val)) Console.WriteLine(val);

var set = new HashSet<int> {1,2,3};
set.Add(2); // ignored, already present

var q = new Queue<string>();
q.Enqueue("a"); var head = q.Dequeue();

var s = new Stack<int>();
s.Push(1); var top = s.Pop();

Pitfalls: - Dictionary throws if key not present with indexer; use TryGetValue. - Choose good Equals/GetHashCode implementations for keys.

Span and Memory (brief)

Use in performance-sensitive code to avoid allocations and copying.

Example:

Span<byte> buffer = stackalloc byte[256]; // stack memory
// operate on buffer without heap allocation

Advanced Data Structures

This section focuses on building blocks beyond the standard collections: when to implement them and how to use them in C#.

Linked Lists

Example using LinkedList<T>:

var ll = new LinkedList<int>();
var node = ll.AddLast(1);
ll.AddAfter(node, 2);
ll.Remove(node);

Singly-linked list implementation (simple):

public class SinglyNode<T> { public T Value; public SinglyNode<T>? Next; }

public class SinglyLinkedList<T>
{
    private SinglyNode<T>? head;
    public void AddFirst(T value) { head = new SinglyNode<T>{ Value = value, Next = head }; }
    public T? RemoveFirst() { if (head == null) return default; var val = head.Value; head = head.Next; return val; }
}

Tips: - Avoid using linked lists for cache-friendly workloads; arrays/Lists are often faster due to contiguous memory.

Trees and Binary Trees

Binary search tree (BST) basic example:

public class TreeNode<T> where T : IComparable<T>
{
    public T Value; public TreeNode<T>? Left; public TreeNode<T>? Right;
}

public class BinarySearchTree<T> where T : IComparable<T>
{
    private TreeNode<T>? root;
    public void Insert(T value) { root = InsertRec(root, value); }
    private TreeNode<T> InsertRec(TreeNode<T>? node, T value)
    {
        if (node == null) return new TreeNode<T>{ Value = value };
        if (value.CompareTo(node.Value) < 0) node.Left = InsertRec(node.Left, value);
        else node.Right = InsertRec(node.Right, value);
        return node;
    }
}

Traversal: In-order (sorted), pre-order, post-order.

Notes: - Self-balancing trees (AVL, Red-Black) are used in production for predictable performance. - SortedSet<T> and SortedDictionary<TKey,TValue> implement tree-based collections in BCL.

Graphs (Adjacency lists/matrices)

Simple graph representation (adjacency list):

public class Graph
{
    private readonly Dictionary<int, List<int>> _adj = new();
    public void AddEdge(int u, int v) { if (!_adj.ContainsKey(u)) _adj[u] = new List<int>(); _adj[u].Add(v); }
    public IEnumerable<int> Neighbors(int v) => _adj.TryGetValue(v, out var list) ? list : Enumerable.Empty<int>();
}

// BFS
public IEnumerable<int> BFS(int start)
{
    var visited = new HashSet<int>();
    var q = new Queue<int>();
    q.Enqueue(start); visited.Add(start);
    while (q.Count > 0) {
        var v = q.Dequeue(); yield return v;
        foreach(var n in Neighbors(v)) if (visited.Add(n)) q.Enqueue(n);
    }
}

Pitfalls: - For weighted graphs use Dijkstra/A* algorithms; for negative weights use Bellman-Ford.

Hash Tables & Collision Strategies

Example custom key:

public struct Point : IEquatable<Point>
{
    public int X { get; }
    public int Y { get; }
    public override int GetHashCode() => HashCode.Combine(X, Y);
    public bool Equals(Point other) => X == other.X && Y == other.Y;
}

Note: HashCode.Combine is available to make good composite hashes.


Classes, Objects & OOP

Classes, Structs, Records

Examples:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public struct Point { public int X; public int Y; }

public record User(string Username, string Email);

When to use what: - Use struct for small (<16 bytes commonly) and immutable types. - Use record for DTOs/immutable data carriers where value equality is desired.

Constructors, Properties, Auto-properties

public class Config
{
    // Auto-property
    public string Name { get; set; }

    // Read-only property with init-only setter (C#9+)
    public string Id { get; init; }

    // Constructor
    public Config(string name) => Name = name;
}

Use private set or init to control mutability.

Inheritance, Interfaces & Polymorphism

Example:

public interface IRepository<T> { void Add(T item); }

public abstract class RepositoryBase<T> : IRepository<T>
{
    public abstract void Add(T item);
}

public class MemoryRepository<T> : RepositoryBase<T>
{
    private readonly List<T> _items = new();
    public override void Add(T item) => _items.Add(item);
}

Polymorphism example:

public class Animal { public virtual string Speak() => "..."; }
public class Dog : Animal { public override string Speak() => "woof"; }

Animal a = new Dog(); Console.WriteLine(a.Speak()); // "woof"

Pitfalls: - Avoid deep inheritance hierarchies; prefer composition and explicit interfaces. - Virtual methods in constructors are dangerous (override called before derived constructor runs).

Encapsulation & Access Modifiers

Modifiers: public, internal, protected, private, protected internal, private protected.

Rules: - Expose behaviour (methods) not internal state (fields). - Use properties with validation rather than public fields.

Example:

public class BankAccount
{
    private decimal _balance;
    public decimal Balance => _balance;
    public void Deposit(decimal amt) { if (amt <= 0) throw new ArgumentException(); _balance += amt; }
}

Design: SRP, DI, SOLID Summary

Example DI registration (ASP.NET Core):

builder.Services.AddScoped<IUserService, UserService>();

Modules, Namespaces & Assemblies

Namespaces & using

namespace MyApp.Features;

using System.Collections.Generic;

Notes: - Avoid wildcard imports; prefer explicit naming to reduce ambiguity in large projects.

Assemblies & csproj

Minimal csproj example:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>

NuGet and Versioning


Coding Style & Conventions

This section aligns with .NET/C# community and Microsoft conventions and adds practical rules used in production.

Naming, Indentation, Braces, XML docs

public class Foo
{
    public void Bar()
    {
        // code
    }
}
/// <summary>Gets the user by id.</summary>
/// <param name="id">User identifier.</param>
/// <returns>User object or null.</returns>
public User? GetUser(int id) { ... }

Best practices for readability, maintainability & performance

Performance-specific: - Use Span<T>/Memory<T> for zero-copy slices. - Prefer struct for small value types; prefer class for entities and polymorphic behavior. - Use ArrayPool<T> to reduce allocations in hot paths.


Testing, Debugging & Tooling

Example test (xUnit):

public class CalculatorTests
{
    [Fact]
    public void Add_ReturnsSum()
    {
        var calc = new Calculator();
        Assert.Equal(3, calc.Add(1,2));
    }
}

Debugging tips: - Use conditional breakpoints and tracepoints to collect runtime info without stopping. - Use dotnet test --filter to run subsets of tests.


C# in the Modern .NET Ecosystem

Interoperability: - Use P/Invoke and DllImport to call native libraries. - Use System.Text.Json for high-performance JSON; fallback to Newtonsoft.Json for advanced scenarios.

Deployment patterns: - Containerize with mcr.microsoft.com/dotnet/aspnet or SDK images. - Use CI/CD to build and publish NuGet artifacts and container images.


Appendices — Cheatsheets & Common Patterns

Common language idioms

var name = user?.Profile?.Name ?? "(unknown)";
if (o is Person p) Console.WriteLine(p.Name);
switch (shape) { case Circle c: ...; break; }
using var conn = new SqlConnection(connStr);

Common pitfalls checklist


References & Further Reading


This guide follows the tone, structure, and style of other guides in this repository: header metadata, badges, an overview, contents list, thorough sections, code blocks, pitfalls, and practical recommendations.