Przygotowanie do rozmowy rekrutacyjnej C# backend

Pytania i odpowiedzi rekrutacyjne C# 2026

Wyselekcjonowany zestaw pytań rekrutacyjnych C# dla programistów backendowych. Każda odpowiedź zawiera uzasadnienie oczekiwane przez rekruterów, a wiele z nich zawiera krótkie przykłady kodu, które można omówić na głos.

Rozpocznij próbną rozmowę C#Karta płatnicza nie jest wymagana. Dostępna 1 darmowa sesja.
Ćwiczenie rozmów technicznych po angielskuTryb dla osób, dla których angielski nie jest językiem ojczystym, do ćwiczenia rozmów technicznych.

Typy i podstawy języka

1Czym różnią się typy wartościowe od typów referencyjnych?

Zmienne typów wartościowych zazwyczaj przechowują samą wartość, więc operacja przypisania powoduje jej skopiowanie. Zmienne typów referencyjnych przechowują referencję do obiektu, więc przypisanie kopiuje referencję, a obie zmienne mogą obserwować mutacje tego samego obiektu. Ważnym niuansem jest to, że miejsce przechowywania zależy od czasu życia (lifetime) i kontekstu, a nie tylko od samego typu: wartość lokalna metody może być przechowywana na stosie, podczas gdy typ wartościowy zapisany jako pole klasy, element tablicy, zmienna przechwycona w domknięciu (closure capture), wartość opakowana (boxed value) lub będąca polem maszyny stanu metody asynchronicznej (async), żyje wraz z powiązanym obiektem.

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"
Odpowiedz na to pytanie z trenerem AI

2Czym różnią się od siebie klasa (`class`), struktura (`struct`) i rekord (`record`) w C#?

Klasa to typ referencyjny z tożsamością obiektu. Struktura to typ wartościowy, który najlepiej sprawdza się dla małych, niezmiennych wartości, takich jak współrzędne, zakresy czy identyfikatory. Klasa typu rekordu (`record class`) to typ referencyjny z równością opartą na wartościach; rekord strukturalny (`record struct`) to jego odpowiednik jako typ wartościowy. Rekordy to składnia kompilatora nałożona na zwykłe typy CLR (Common Language Runtime), która generuje obsługę równości, metod `GetHashCode` i `ToString`, dekonstrukcji oraz wsparcie dla kopiowania, w tym dla wyrażeń `with` umożliwiających nieniszczącą mutację. W kodzie backendowym rekordy są użyteczne jako modele podobne do DTO (Data Transfer Object), podczas gdy duże, mutowalne struktury stanowią ryzyko, ponieważ kopiowanie i modyfikowanie kopii może być zaskoczeniem dla czytających kod.

public record User(int Id, string Name);

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

Console.WriteLine(first == second); // true
Odpowiedz na to pytanie z trenerem AI

3Czym różnią się parametry `ref`, `out` i `in` w C#?

`ref` przekazuje argument przez referencję i wymaga, aby był on zainicjowany przed wywołaniem; metoda może go odczytywać i modyfikować. `out` również przekazuje parametr przez referencję, ale metoda musi przypisać mu wartość przed powrotem z wywołania. `in` przekazuje parametr jako referencję tylko do odczytu (`readonly byref`), co może zapobiegać kopiowaniu dużych struktur, ale nie gwarantuje głębokiej niezmienności: mutowalne struktury i członkowie niebędący `readonly` mogą wymuszać tworzenie kopii defensywnych. Na poziomie języka pośredniego IL (Intermediate Language) w .NET, `ref`, `out` oraz `in` to zarządzane parametry referencyjne podlegające różnym regułom języka C#.

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);
Odpowiedz na to pytanie z trenerem AI

4Czym są pakowanie (boxing) i rozpakowywanie (unboxing)?

Pakowanie (boxing) opakowuje typ wartościowy w obiekt typu referencyjnego poprzez zaalokowanie obiektu i skopiowanie do niego wartości. Rozpakowywanie (unboxing) wyodrębnia wartość z powrotem i wymaga dokładnie takiego samego, kompatybilnego typu. Częste pakowanie powoduje dodatkowe alokacje i zwiększa presję na GC (Garbage Collector). Typy generyczne (generics) pozwalają uniknąć wielu przypadków pakowania, ponieważ `List<int>` przechowuje wartości `int` bez konwertowania każdej z nich na `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;
Odpowiedz na to pytanie z trenerem AI

5Czym są typy wartościowe dopuszczające wartość null (nullable value types) i typy referencyjne dopuszczające wartość null (nullable reference types)?

Typy wartościowe dopuszczające wartość null używają struktury `Nullable<T>` lub zapisu `T?` i stanowią rzeczywistą nakładkę (wrapper) na typy wartościowe w czasie działania programu. Typy referencyjne dopuszczające wartość null to w głównej mierze wynik analizy na poziomie kompilatora: zapis `string?` informuje kompilator, że referencja może mieć wartość null, ale nie tworzy nowego typu w czasie wykonania. Operator wybaczający null (null-forgiving operator) `!` służy jedynie do tłumienia ostrzeżeń; nie chroni natomiast przed wyjątkiem `NullReferenceException`.

int? age = null;

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

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

string trusted = optionalName!;
Odpowiedz na to pytanie z trenerem AI

6Czym różnią się modyfikatory const, readonly i static readonly?

Modyfikator const oznacza stałą czasu kompilacji (compile-time constant), ograniczoną do typów prostych (primitives), wartości enum, typu string lub wartości null, a jej wartość jest bezpośrednio wstawiana do korzystających z niej podzespołów (assemblies) podczas kompilacji. Modyfikator readonly to pole instancji, któremu można przypisać wartość podczas deklaracji lub w konstruktorze. Z kolei static readonly to jedno pole dla całego typu, do którego przypisanie następuje w deklaracji lub w konstruktorze statycznym. Publiczne wartości, które mogą ulec zmianie w nowych wersjach biblioteki, są bezpieczniejsze jako static readonly niż jako publiczne const, ponieważ kod klienta korzystający z biblioteki zatrzyma starą, wstawioną bezpośrednio (inlined) wartość const aż do czasu własnej rekompilacji.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);
Odpowiedz na to pytanie z trenerem AI

7Jak współpracują ze sobą metody `Equals`, `==` oraz `GetHashCode`?

W przypadku zwykłych klas, `Equals` oraz `==` domyślnie porównują referencje. Wiele wbudowanych typów oraz typów `record` porównuje wartości. Jeśli metoda `Equals` jest przesłonięta, `GetHashCode` musi być z nią spójna: jeśli `a.Equals(b)` jest prawdą, oba obiekty muszą mieć ten sam kod mieszający (hash code). Sytuacja odwrotna nie jest gwarantowana. Nigdy nie należy modyfikować pól używanych do porównywania lub obliczania kodu mieszającego, gdy obiekt jest kluczem w `Dictionary` lub elementem `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();
}
Odpowiedz na to pytanie z trenerem AI

8Czym różni się słowo kluczowe override od new?

override zmienia wirtualne zachowanie, co oznacza, że wywoływana metoda zależy od typu obiektu w czasie działania programu (runtime). new ukrywa składową z klasy bazowej, a wybrana składowa zależy od statycznego typu zmiennej. Aby uzyskać polimorfizm, zazwyczaj stosuje się virtual i override; ukrywanie za pomocą new jest rzadsze i może prowadzić do niejasnego zachowania programu.

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
Odpowiedz na to pytanie z trenerem AI

9Czym różni się klasa abstrakcyjna od interfejsu?

Klasa abstrakcyjna może zawierać stan, konstruktory, pola, zaimplementowane metody, metody abstrakcyjne oraz chronione składowe. Interfejs opisuje kontrakt; nowoczesne interfejsy mogą zawierać domyślne implementacje i statyczne składowe abstrakcyjne, ale nadal nie są one standardowym miejscem dla stanu instancji. Klasa może dziedziczyć po jednej klasie bazowej, ale implementować wiele interfejsów. Klasy abstrakcyjnej używaj jako wspólnej bazy i do współdzielonej implementacji; interfejsu używaj do definiowania możliwości i granic systemowych.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}
Odpowiedz na to pytanie z trenerem AI

Typy generyczne, kolekcje i LINQ

10Czym są typy generyczne (generics) i dlaczego są przydatne?

Typy generyczne pozwalają na pisanie kodu wielokrotnego użytku, który jest bezpieczny typologicznie. Przenoszą one wiele weryfikacji na etap kompilacji, eliminują konieczność ręcznego rzutowania typów i pozwalają uniknąć pakowania (boxing) w przypadku typów wartościowych. Ograniczenia typów generycznych określają, co dany parametr typu musi obsługiwać, np. czy musi być klasą, strukturą, implementacją interfejsu, klasą bazową, typem niezarządzanym (unmanaged type), czy posiadać publiczny konstruktor bezparametrowy.

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();
}
Odpowiedz na to pytanie z trenerem AI

11Czym są kowariancja i kontrawariancja?

Kowariancja pozwala kodowi użyć bardziej szczegółowego typu tam, gdzie oczekiwany jest bardziej ogólny typ produkowany (zwracany), na przykład IEnumerable<string> jako IEnumerable<object>. Jest oznaczana słowem kluczowym out w interfejsach i delegatach. Kontrawariancja działa w odwrotnym kierunku dla wartości konsumowanych (przyjmowanych jako argumenty) i jest oznaczana słowem kluczowym in. Wariancja działa tylko dla interfejsów i delegatów z typami referencyjnymi; IEnumerable<int> nie może stać się IEnumerable<object>, ponieważ wymagałoby to pakowania (boxing), a List<string> nie jest przypisywalna do 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); }
Odpowiedz na to pytanie z trenerem AI

12Czym różni się IEnumerable<T> od IQueryable<T>?

IEnumerable<T> reprezentuje iterację w środowisku .NET, a operatory LINQ (Language Integrated Query) zazwyczaj przyjmują delegaty, takie jak Func<T, bool>. Z kolei IQueryable<T> przechowuje drzewa wyrażeń (expression trees), na przykład Expression<Func<T, bool>>, które dostawca, taki jak Entity Framework Core, może przetłumaczyć na zapytanie SQL (Structured Query Language). Zbyt wczesne wywołanie metody ToList materializuje dane i przenosi późniejsze filtrowanie do pamięci. Ponadto, nie każde wyrażenie w C# może zostać przetłumaczone przez dostawcę zapytań. Należy unikać przekazywania IQueryable przez wszystkie warstwy aplikacji, chyba że jest to celowa decyzja projektowa.

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();
Odpowiedz na to pytanie z trenerem AI

13Czym jest odroczone wykonanie (deferred execution) w LINQ?

Wiele operatorów LINQ (Language Integrated Query) buduje zapytanie bez jego natychmiastowego wykonania. Wykonanie rozpoczyna się, gdy zapytanie jest iterowane (enumerowane) lub gdy zostanie wywołana operacja końcowa, taka jak ToList, Count lub First. Oznacza to, że zmiany w źródle przed iteracją mogą wpłynąć na wyniki, wielokrotna iteracja może powodować powtarzanie pracy lub zapytań SQL (Structured Query Language), a wyjątki mogą występować podczas iteracji, a nie podczas budowania zapytania.

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

numbers.Add(42);

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

var materialized = query.ToList();
Odpowiedz na to pytanie z trenerem AI

14Czym różnią się tablica (array), `List<T>`, `Dictionary<TKey,TValue>` i `HashSet<T>`?

Tablica (array) ma stały rozmiar i zapewnia szybki dostęp z użyciem indeksu. `List<T>` to tablica dynamiczna z dostępem indeksowanym o złożoności O(1); metoda `Add` dodająca element na końcu działa w zamortyzowanym czasie O(1), ponieważ zwiększanie pojemności powoduje alokację większej tablicy i kopiowanie istniejących elementów, podczas gdy wstawienie w środku zajmuje O(n). `Dictionary<TKey,TValue>` mapuje klucze na wartości i zazwyczaj zapewnia wyszukiwanie klucza w czasie O(1). `HashSet<T>` przechowuje unikalne elementy i jest idealny do sprawdzania przynależności. Dla kolekcji opartych na haszowaniu kluczowe są poprawne implementacje metod `Equals` i `GetHashCode`.

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

if (ids.Contains(userId))
{
    // Fast membership check
}
Odpowiedz na to pytanie z trenerem AI

15Czym różnią się interfejsy `IEnumerable<T>`, `ICollection<T>`, `IList<T>` oraz `IReadOnlyCollection<T>`?

Interfejs `IEnumerable<T>` gwarantuje jedynie możliwość sekwencyjnej iteracji. `ICollection<T>` dodaje właściwość `Count` oraz metody modyfikujące (np. dodawanie - `Add`, usuwanie - `Remove`). `IList<T>` udostępnia dodatkowo dostęp za pomocą indeksu. `IReadOnlyCollection<T>` umożliwia z kolei iterację i odczyt właściwości `Count`, ale nie pozwala na operacje modyfikujące z poziomu tego interfejsu. W przypadku projektowania API (Application Programming Interface), powinno się zwracać najwęższy użyteczny kontrakt. Warto jednak pamiętać, że samo stosowanie interfejsów tylko do odczytu (read-only) nie zapewnia głębokiej niemutowalności leżącego pod nimi obiektu.

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

public IReadOnlyCollection<Order> Orders => _orders;
Odpowiedz na to pytanie z trenerem AI

16Jak działa yield return?

Instrukcja yield return tworzy iterator z odroczonym wykonaniem (deferred execution). Kompilator przekształca metodę w maszynę stanów, a wartości są produkowane pojedynczo po wywołaniu metody MoveNext. Pozwala to uniknąć tworzenia kolekcji pośrednich i dobrze sprawdza się w przypadku dużych sekwencji. Pułapki związane są z czasem życia i wielokrotną enumeracją: nie należy zwracać leniwie ewaluowanej sekwencji (lazy sequence), która zależy od zlikwidowanego (disposed) obiektu DbContext lub strumienia. Należy również pamiętać, że wywołanie metody Count, a po niej pętli foreach, może wykonać iterator dwukrotnie, powtarzając efekty uboczne lub operacje I/O (Input/Output).

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}
Odpowiedz na to pytanie z trenerem AI

Delegaty, zdarzenia i wyjątki

17Czym są delegaty i wyrażenia lambda?

Delegat (delegate) to bezpieczna typologicznie referencja do metody. Wyrażenia lambda dostarczają zwięzłe, wbudowane (inline) funkcje i są powszechnie przypisywane do delegatów typu Func, Action lub Predicate. Wyrażenia lambda przechwytują zmienne, a nie migawki (snapshots) ich wartości, więc lambda może zaobserwować późniejszą wartość pętli lub zmiennej. Jeśli lambda niczego nie przechwytuje, kompilator może zbuforować statyczny delegat i uniknąć alokacji domknięcia (closure); w kodzie wrażliwym na wydajność, domknięcia i wydłużony czas życia obiektów mogą mieć istotne znaczenie.

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;
Odpowiedz na to pytanie z trenerem AI

18Czym różni się `event` od `delegate`?

Zdarzenie (`event`) zazwyczaj opiera się na delegacie (`delegate`), ale ogranicza to, co może z nim zrobić kod z zewnątrz. Zewnętrzny kod może subskrybować i anulować subskrypcję, ale tylko klasa będąca właścicielem może wywołać to zdarzenie. Publiczne pole typu `delegate` pozwoliłoby kodowi wywołującemu na zastąpienie listy wywołań lub bezpośrednie jej wywołanie. Zdarzenia tworzą również referencje od publikującego do subskrybenta, więc długo żyjące obiekty publikujące mogą powodować wycieki pamięci, jeśli subskrybenci nie anulują subskrypcji.

public event EventHandler<OrderEventArgs>? OrderCreated;

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

OrderCreated?.Invoke(this, args);
Odpowiedz na to pytanie z trenerem AI

19Jak powinno się obsługiwać wyjątki w C#?

Używaj wyjątków dla sytuacji wyjątkowych, a nie jako elementu normalnego przepływu sterowania. Przechwytuj tylko te wyjątki, z którymi potrafisz sobie poradzić, unikaj pustych bloków `catch`, dodawaj kontekst podczas ich opakowywania i nie ujawniaj użytkownikom wewnętrznych szczegółów. Używaj `throw;`, aby rzucić wyjątek ponownie przy zachowaniu oryginalnego śladu stosu. Jeśli opakowujesz wyjątek, przekaż oryginalny jako `InnerException`, na przykład `throw new DomainException("...", ex)`. Preferuj API w stylu `TryParse` dla oczekiwanych niepowodzeń.

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

catch
{
    throw; // preserves stack trace
}
Odpowiedz na to pytanie z trenerem AI

20Czym są filtry wyjątków (exception filters)?

Filtry wyjątków pozwalają na wykonanie bloku `catch` tylko wtedy, gdy dodatkowy warunek jest prawdziwy. Filtr jest oceniany przed rozwinięciem stosu (stack unwinding) i przed wejściem do ciała bloku `catch`, dzięki czemu w przypadku zwrócenia wartości false, oryginalny stos pozostaje nienaruszony, co ułatwia diagnostykę i analizę zrzutów awaryjnych (crash dumps). Filtry są przydatne przy obsłudze kodów statusu HTTP (Hypertext Transfer Protocol), kodów błędów bazy danych, rozróżnianiu błędów przejściowych i warunkowym logowaniu. Należy dbać o to, aby filtry były proste i unikać w nich złożonych efektów ubocznych.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}
Odpowiedz na to pytanie z trenerem AI