Forberedelse til C# backend-interview

C#-interviewspørgsmål og svar 2026

Et målrettet sæt af C#-interviewspørgsmål til backendudviklere. Hvert svar inkluderer den argumentation, som interviewere forventer, og mange indeholder små kodeeksempler, du kan diskutere højt.

Start C#-prøvesamtaleIntet kreditkort kræves. 1 gratis session er tilgængelig.
Øv tekniske jobsamtaler på engelskEn tilstand, hvor ikke-modersmålstalende kan øve sig i at bestå tekniske interviews.

Typer og grundlæggende sprogkoncepter

1Hvordan adskiller værdityper sig fra referencetyper?

Variabler af en værditype indeholder normalt selve værdien, så en tildeling kopierer værdien. Variabler af en referencetype indeholder en reference til et objekt, så en tildeling kopierer referencen, og begge variabler kan observere forandringer af det samme objekt. Den vigtige nuance er, at lagring afhænger af levetid og kontekst, og ikke kun af typen: en lokal værdi i en metode kan leve på stakken, mens en værditype, der er gemt som et klassefelt, et array-element, indfanget i en closure, en boxed værdi eller et felt i en async-tilstandsmaskine, lever sammen med det pågældende objekt.

int a = 10;
int b = a;
b = 20;

// a is still 10

var first = new User { Name = "Ann" };
var second = first;
second.Name = "Kate";

// first.Name is also "Kate"
Prøv at besvare dette spørgsmål med en AI-coach

2Hvordan adskiller `class`, `struct` og `record` sig fra hinanden?

En `class` er en referencetype med objektidentitet. En `struct` er en værditype og er bedst til små uforanderlige værdier såsom koordinater, intervaller eller identifikatorer. En `record class` er en referencetype med værdibaseret lighed; `record struct` er værditype-versionen. `record`-typer er compilersyntaks over almindelige CLR-typer (Common Language Runtime), som genererer lighed, `GetHashCode`, `ToString`, dekonstruktion og understøttelse af kopiering, herunder `with`-udtryk til ikke-destruktiv mutation. I backend-kode er `record`-typer nyttige til DTO-lignende modeller (Data Transfer Object), mens store foranderlige `struct`-typer er risikable, fordi kopiering og ændring af kopier kan overraske læserne.

public record User(int Id, string Name);

var first = new User(1, "Alice");
var second = new User(1, "Alice");

Console.WriteLine(first == second); // true
Prøv at besvare dette spørgsmål med en AI-coach

3Hvordan adskiller parameterne `ref`, `out` og `in` sig fra hinanden?

`ref` overfører et argument som reference og kræver, at det er initialiseret før kaldet; metoden kan læse og ændre det. `out` overfører også som reference, men metoden skal tildele en værdi, før den returnerer. `in` overfører som en skrivebeskyttet reference (readonly byref), hvilket kan forhindre kopiering for store `struct`-typer, men det er ikke en garanti for dyb uforanderlighed: foranderlige `struct`-typer og medlemmer, der ikke er skrivebeskyttede, kan fremtvinge defensiv kopiering. På IL-niveau (Intermediate Language) håndteres `ref`, `out` og `in` som administrerede byref-parametre med forskellige C#-regler.

void Increment(ref int value) => value++;

bool TryRead(string text, out int value) =>
    int.TryParse(text, out value);

double Length(in Vector vector) =>
    Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);
Prøv at besvare dette spørgsmål med en AI-coach

4Hvad er boxing og unboxing?

Boxing indpakker en værditype i et objekt af en referencetype ved at allokere et objekt og kopiere værdien over i det. Unboxing udtrækker værdien igen og kræver den nøjagtige kompatible type. Hyppig boxing medfører ekstra allokeringer og øger belastningen på systemets GC (Garbage Collector). Generiske typer undgår mange tilfælde af boxing, fordi `List<int>` gemmer `int`-værdier uden at konvertere hver enkelt værdi til `object`.

int number = 42;
object boxed = number;      // boxing
int restored = (int)boxed;  // unboxing

object value = 42;
// long wrong = (long)value; // InvalidCastException
long ok = (long)(int)value;
Prøv at besvare dette spørgsmål med en AI-coach

5Hvad er nullable værdityper og nullable referencetyper?

Nullable værdityper bruger `Nullable<T>` eller `T?` og repræsenterer en reel indpakning (wrapper) i kørselsmiljøet for værdityper. Nullable referencetyper handler primært om kompilatoranalyse: `string?` fortæller kompilatoren, at en reference kan være `null`, men det opretter ikke en ny type i kørselsmiljøet. Den null-tilgivende operator (null-forgiving operator) `!` undertrykker kun advarsler; den beskytter ikke mod en `NullReferenceException`.

int? age = null;

string name = "Alice";
string? optionalName = null;

var length = optionalName?.Length;
var actualName = optionalName ?? "Unknown";
optionalName ??= "Default";

string trusted = optionalName!;
Prøv at besvare dette spørgsmål med en AI-coach

6Hvad er forskellen på `const`, `readonly` og `static readonly`?

`const` er en konstant på kompileringstidspunktet, som er begrænset til primitive typer, enum-værdier, `string` eller `null`, og dens værdi indsættes i de forbrugende assemblies under kompileringen. `readonly` er et instansfelt, der kan tildeles en værdi i en deklaration eller en konstruktør. `static readonly` er ét felt pr. type, som tildeles i en deklaration eller statisk konstruktør. Offentlige (public) værdier, der kan ændre sig på tværs af biblioteksversioner, er mere sikre som `static readonly` end `public const`, fordi klienter kan beholde en gammel, indlejret (inlined) `const`, indtil de genkompileres.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);
Prøv at besvare dette spørgsmål med en AI-coach

7Hvordan fungerer `Equals`, `==` og `GetHashCode` sammen?

For almindelige klasser sammenligner `Equals` og `==` som standard referencer. Mange indbyggede typer og `record`-typer sammenligner efter værdi. Hvis `Equals` overskrives, skal `GetHashCode` være konsistent: Hvis `a.Equals(b)` er sand, skal begge objekter have den samme hash-værdi. Det omvendte er ikke garanteret. Man bør aldrig ændre på felter, der bruges til lighedssammenligning eller hashing, mens objektet fungerer som en nøgle i en `Dictionary` eller som et element i et `HashSet`.

public sealed class User : IEquatable<User>
{
    public required int Id { get; init; }

    public bool Equals(User? other) =>
        other is not null && Id == other.Id;

    public override bool Equals(object? obj) =>
        obj is User other && Equals(other);

    public override int GetHashCode() =>
        Id.GetHashCode();
}
Prøv at besvare dette spørgsmål med en AI-coach

8Hvordan adskiller override sig fra new?

override ændrer virtuel adfærd, så den metode, der kaldes, afhænger af objektets type ved kørselstid. new skjuler et medlem fra basisklassen, og det valgte medlem afhænger af variablens statiske type. Til polymorfi ønsker man normalt virtual og override; at skjule med new er mindre almindeligt og kan gøre adfærden forvirrende.

class Base
{
    public virtual void First() => Console.WriteLine("Base.First");
    public void Second() => Console.WriteLine("Base.Second");
}

class Derived : Base
{
    public override void First() => Console.WriteLine("Derived.First");
    public new void Second() => Console.WriteLine("Derived.Second");
}

Base value = new Derived();
value.First();  // Derived.First
value.Second(); // Base.Second
Prøv at besvare dette spørgsmål med en AI-coach

9Hvordan adskiller en abstrakt klasse sig fra et interface?

En abstrakt klasse kan indeholde tilstand, konstruktører, felter, implementerede metoder, abstrakte metoder og `protected`-medlemmer. Et interface beskriver en kontrakt; moderne interfaces kan indeholde standardimplementeringer og statiske abstrakte medlemmer, men de er stadig ikke det normale sted til instanstilstand. En klasse kan nedarve fra én basisklasse, men implementere flere interfaces. Brug en abstrakt klasse som en delt basis med delt implementering; brug et interface til kapabiliteter og grænseflader.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}
Prøv at besvare dette spørgsmål med en AI-coach

Generics, Collections og LINQ

10Hvad er generics, og hvorfor er de nyttige?

Generics giver dig mulighed for at skrive genanvendelig og typesikker kode. De flytter mange kontroller til kompileringstidspunktet, fjerner behovet for manuelle typekonverteringer (casts) og kan forhindre boxing af værdityper. Generiske begrænsninger (generic constraints) beskriver, hvad en typeparameter skal understøtte, som for eksempel at være en `class`, `struct`, en implementering af et interface, en basisklasse, en u-håndteret (unmanaged) type eller at have en offentlig konstruktør uden parametre.

public T? Find<T>(
    IEnumerable<T> items,
    Predicate<T> predicate)
{
    return items.FirstOrDefault(item => predicate(item));
}

public T Create<T>() where T : class, new()
{
    return new T();
}
Prøv at besvare dette spørgsmål med en AI-coach

11Hvad er kovarians og kontravarians?

Kovarians giver kode mulighed for at bruge en mere specifik type, hvor der forventes en mere generel produceret type, såsom `IEnumerable<string>` i stedet for `IEnumerable<object>`. Det markeres med `out` på grænseflader (interfaces) og delegater. Kontravarians går i den modsatte retning for forbrugte (modtagne) værdier og markeres med `in`. Varians fungerer kun for grænseflader og delegater med referencetyper; `IEnumerable<int>` kan ikke blive til `IEnumerable<object>`, fordi det ville kræve "boxing", og `List<string>` kan ikke tildeles til `List<object>`.

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariance

Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // contravariance

interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T value); }
Prøv at besvare dette spørgsmål med en AI-coach

12Hvordan adskiller IEnumerable<T> sig fra IQueryable<T>?

IEnumerable<T> repræsenterer enumerering i .NET, og operatorer i LINQ (Language Integrated Query) modtager normalt delegater såsom Func<T, bool>. IQueryable<T> gemmer udtrykstræer, for eksempel Expression<Func<T, bool>>, som en udbyder som Entity Framework Core kan oversætte til SQL (Structured Query Language). Hvis ToList kaldes for tidligt, materialiseres data, hvilket flytter efterfølgende filtrering over i hukommelsen. Desuden kan ikke alle C#-udtryk oversættes af en forespørgselsudbyder. Undgå at lade IQueryable lække gennem alle applikationens lag, medmindre det er et bevidst designvalg.

IEnumerable<User> loaded = loadedUsers;
var inMemoryAdults = loaded.Where(x => x.Age >= 18);

IQueryable<User> query = dbContext.Users;
var adults = await query
    .Where(x => x.Age >= 18)
    .ToListAsync();
Prøv at besvare dette spørgsmål med en AI-coach

13Hvad er udskudt udførelse (deferred execution) i LINQ (Language Integrated Query)?

Mange LINQ-operatorer bygger en forespørgsel uden at udføre den med det samme. Udførelsen starter, når forespørgslen gennemløbes, eller når en afsluttende operation som `ToList`, `Count` eller `First` kaldes. Dette betyder, at kildeændringer før gennemløbet kan påvirke resultaterne, at gentagne gennemløb kan gentage arbejde eller SQL-forespørgsler (Structured Query Language), og at undtagelser kan forekomme under gennemløbet frem for under opbygningen af forespørgslen.

var query = numbers.Where(x => x > 10);

numbers.Add(42);

foreach (var number in query)
{
    Console.WriteLine(number);
}

var materialized = query.ToList();
Prøv at besvare dette spørgsmål med en AI-coach

14Hvordan adskiller et array, `List<T>`, `Dictionary<TKey,TValue>` og `HashSet<T>` sig fra hinanden?

Et array har en fast størrelse og hurtig indekseret adgang. `List<T>` er et dynamisk array med O(1) indekseret adgang; at tilføje i slutningen er amortiseret O(1), fordi en forøgelse af kapaciteten allokerer et større array og kopierer de eksisterende elementer, mens indsættelse i midten er O(n). `Dictionary<TKey,TValue>` knytter nøgler til værdier og giver normalt O(1) opslag på nøgler. `HashSet<T>` gemmer unikke elementer og er ideel til kontrol af medlemskab. For hash-baserede samlinger er det essentielt at have korrekte implementeringer af `Equals` og `GetHashCode`.

var ids = new HashSet<int> { 1, 2, 3 };

if (ids.Contains(userId))
{
    // Fast membership check
}
Prøv at besvare dette spørgsmål med en AI-coach

15Hvordan adskiller IEnumerable<T>, ICollection<T>, IList<T> og IReadOnlyCollection<T> sig fra hinanden?

IEnumerable<T> lover kun sekventielt gennemløb (enumeration). ICollection<T> tilføjer Count og mutationsmetoder såsom Add og Remove. IList<T> tilføjer indekseret adgang. IReadOnlyCollection<T> eksponerer gennemløb og Count, men tillader ikke mutation gennem dette interface. Et godt design af et API bør returnere den snævreste nyttige kontrakt, men skrivebeskyttede (read-only) interfaces garanterer ikke dyb uforanderlighed (deep immutability) af det underliggende objekt.

private readonly List<Order> _orders = new();

public IReadOnlyCollection<Order> Orders => _orders;
Prøv at besvare dette spørgsmål med en AI-coach

16Hvordan fungerer yield return?

yield return opretter en iterator med udskudt udførelse. Kompileren omdanner metoden til en tilstandsmaskine, og værdierne produceres én efter én, når der kaldes MoveNext. Dette undgår midlertidige samlinger og fungerer godt for store sekvenser. Faldgruberne er levetid og flere gennemløb: Returnér ikke en lazy-evalueret sekvens, der afhænger af en frigivet DbContext eller strøm, og husk at Count efterfulgt af foreach kan udføre iteratoren to gange, hvilket gentager bivirkninger eller I/O.

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}
Prøv at besvare dette spørgsmål med en AI-coach

Delegates, Events og Exceptions

17Hvad er delegater (delegates) og lambda-udtryk?

En delegat (delegate) er en typesikker reference til en metode. Lambda-udtryk tilbyder præcise inline-funktioner og tildeles ofte til `Func`, `Action` eller `Predicate`-delegater. Lambdaer indfanger (captures) variabler, ikke øjebliksbilleder af deres værdier, så en lambda kan observere en senere løkke- eller variabelværdi. Hvis en lambda ikke indfanger noget, kan compileren cache en statisk delegat og undgå allokering af en closure; i kode, der er kritisk for ydeevnen, kan closures og forlængede objekters levetid have betydning.

public delegate int Operation(int x, int y);

Operation operation = (x, y) => x + y;
Console.WriteLine(operation(2, 3));

int factor = 10;
Func<int, int> multiply = x => x * factor;
Prøv at besvare dette spørgsmål med en AI-coach

18Hvordan adskiller event sig fra delegate?

En event er normalt understøttet af en delegate, men den begrænser, hvad ekstern kode kan gøre. Ekstern kode kan abonnere og afmelde sig, men kun ejerklassen kan udløse hændelsen. Et offentligt delegate-felt ville tillade kaldende kode at erstatte aktiveringslisten eller aktivere den direkte. Events skaber også referencer fra udgiver til abonnent, så langlivede udgivere kan forårsage hukommelseslækager, hvis abonnenter ikke afmelder sig.

public event EventHandler<OrderEventArgs>? OrderCreated;

service.OrderCreated += Handler;
service.OrderCreated -= Handler;

OrderCreated?.Invoke(this, args);
Prøv at besvare dette spørgsmål med en AI-coach

19Hvordan bør undtagelser (exceptions) håndteres i C#?

Brug undtagelser til exceptionelle situationer, ikke til normalt kontrolflow. Grib kun undtagelser, du kan håndtere, undgå tomme `catch`-blokke, tilføj kontekst ved indpakning (wrapping), og læk ikke interne detaljer til brugere. Brug `throw;` for at kaste undtagelsen igen, mens det oprindelige stakspor (stack trace) bevares. Hvis du indpakker en undtagelse, skal du videregive den oprindelige som `InnerException`, for eksempel `throw new DomainException("...", ex)`. Foretræk API'er (Application Programming Interfaces) i `TryParse`-stil til forventede fejl.

try
{
    await service.ProcessAsync();
}
catch (ValidationException exception)
{
    // Expected domain error
}
finally
{
    // Cleanup
}

catch
{
    throw; // preserves stack trace
}
Prøv at besvare dette spørgsmål med en AI-coach

20Hvad er exception-filtre (undtagelsesfiltre)?

Exception-filtre gør det muligt kun at køre en `catch`-blok, når en ekstra betingelse er opfyldt. Filteret evalueres før afvikling af stakken (stack unwinding) og før `catch`-blokken udføres. Det betyder, at hvis filteret returnerer `false`, forbliver den oprindelige stak intakt, hvilket er nyttigt til diagnosticering og crash dumps. Filtre er velegnede til at tjekke HTTP-statuskoder, databasefejlkoder, skelne mellem forbigående fejl og til betinget logning. Hold filtre enkle og undgå komplekse sideeffekter.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}
Prøv at besvare dette spørgsmål med en AI-coach