Подготовка за бекенд интервю по C#

Въпроси и отговори за интервю по C# 2026

Фокусиран набор от въпроси за интервю по C# за бекенд разработчици. Всеки отговор включва обосновката, която интервюиращите очакват, и много от тях съдържат кратки примери с код, които можете да обсъждате на глас.

Започни пробно интервю по C#Не се изисква кредитна карта. Налична е 1 безплатна сесия.
Практика за техническо интервю на английски езикРежим, в който хора, за които английският не е роден език, могат да се упражняват за преминаване на технически интервюта.

Типове и основи на езика

1Как се различават стойностните типове от референтните типове?

Променливите от стойностен тип обикновено съдържат самата стойност, така че при присвояване стойността се копира. Променливите от референтен тип съдържат референция към обект, така че при присвояване се копира референцията и двете променливи могат да наблюдават промените в същия обект. Важният нюанс е, че съхранението зависи от времето на живот и контекста, а не само от типа: стойност, локална за метод, може да живее в стека, докато стойностен тип, съхранен като поле на клас, елемент на масив, прихваната от closure променлива, опакована стойност или поле в машина на състоянията на асинхронния метод, живее заедно с този обект.

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"
Опитайте да отговорите на този въпрос с треньор с ИИ

2Каква е разликата между `class`, `struct` и `record`?

Един `class` е референтен тип с обектна идентичност. Един `struct` е тип по стойност и е най-подходящ за малки неизменяеми стойности като координати, диапазони или идентификатори. Един `record class` е референтен тип с равенство, базирано на стойността; `record struct` е версията като тип по стойност. Типовете `record` представляват синтаксис на компилатора над обикновени CLR (Common Language Runtime) типове, който генерира логика за равенство, `GetHashCode`, `ToString`, деструктуриране и поддръжка за копиране, включително `with` изрази за недеструктивна промяна. В бекенд кода, типовете `record` са полезни за модели, подобни на DTO (Data Transfer Object), докато големите изменяеми структури са рисковани, тъй като копирането и модифицирането на копията може да изненада четящите кода.

public record User(int Id, string Name);

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

Console.WriteLine(first == second); // true
Опитайте да отговорите на този въпрос с треньор с ИИ

3По какво се различават параметрите `ref`, `out` и `in`?

`ref` подава аргумент по референция и изисква той да бъде инициализиран преди извикването; методът може да го чете и променя. `out` също подава аргумент по референция, но методът трябва да присвои стойност, преди да приключи изпълнението си. `in` подава параметър по референция само за четене, което може да избегне копирането при големи структури (`struct`), но не предоставя гаранция за дълбока неизменяемост: изменяемите структури и членовете, които не са `readonly`, могат да наложат създаването на защитни копия. На ниво междинен език (IL - Intermediate Language), `ref`, `out` и `in` са управлявани параметри по референция с различни 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);
Опитайте да отговорите на този въпрос с треньор с ИИ

4Какво представляват опаковането (boxing) и разопаковането (unboxing)?

Опаковането преобразува стойностен тип в обект от референтен тип, като заделя памет за обект и копира стойността в него. Разопаковането извлича стойността обратно и изисква точно съвпадение на типа. Честото опаковане води до заделяне на допълнителна памет и повишено натоварване на системата за почистване на паметта – GC (Garbage Collector). Генеричните типове избягват много случаи на опаковане, защото `List<int>` съхранява `int` стойности, без да преобразува всяка стойност в `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;
Опитайте да отговорите на този въпрос с треньор с ИИ

5Какво представляват стойностните типове, допускащи null, и референтните типове, допускащи null?

Стойностните типове, допускащи null, използват `Nullable<T>` или `T?` и представляват реална обвивка за стойностните типове по време на изпълнение. Референтните типове, допускащи null, са предимно резултат от анализ на компилатора: `string?` указва на компилатора, че дадена референция може да бъде null, но не създава нов тип по време на изпълнение. Операторът за пренебрегване на null (null-forgiving operator) `!` само потиска предупрежденията; той не предпазва от `NullReferenceException`.

int? age = null;

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

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

string trusted = optionalName!;
Опитайте да отговорите на този въпрос с треньор с ИИ

6Каква е разликата между `const`, `readonly` и `static readonly`?

`const` е константа по време на компилация, ограничена до примитивни типове, `enum` стойности, `string` или `null`, и нейната стойност се замества директно в използващите я асемблита по време на компилация. `readonly` е поле на инстанция, на което може да бъде присвоена стойност при деклариране или в конструктор. `static readonly` е едно поле за целия тип, на което се присвоява стойност при деклариране или в статичен конструктор. Публичните стойности, които могат да се променят в различните версии на дадена библиотека, са по-безопасни като `static readonly`, отколкото като публични `const`, тъй като клиентите могат да запазят старата заместена стойност на `const`, докато не бъдат прекомпилирани.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);
Опитайте да отговорите на този въпрос с треньор с ИИ

7Как работят заедно Equals, == и GetHashCode?

За обикновените класове по подразбиране `Equals` и `==` сравняват референциите. Много вградени типове и `record` типове се сравняват по стойност. Ако `Equals` бъде презаписан, `GetHashCode` трябва да бъде съгласуван: ако `a.Equals(b)` е `true`, двата обекта трябва да имат еднакъв хеш код. Обратното не е гарантирано. Никога не променяйте полета, използвани за проверка за равенство или хеширане, докато обектът е ключ в `Dictionary` или елемент в `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();
}
Опитайте да отговорите на този въпрос с треньор с ИИ

8По какво се различава override от new?

`override` променя виртуалното поведение, така че извиканият метод зависи от типа на обекта по време на изпълнение. `new` скрива член от базовия клас и избраният член зависи от статичния тип на променливата. За полиморфизъм обикновено се използват `virtual` и `override`; скриването с `new` е по-рядко срещано и може да направи поведението объркващо.

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
Опитайте да отговорите на този въпрос с треньор с ИИ

9С какво абстрактният клас се различава от интерфейса?

Абстрактният клас може да съдържа състояние, конструктори, полета, имплементирани методи, абстрактни методи и защитени членове. Интерфейсът описва договор; съвременните интерфейси могат да включват имплементации по подразбиране и статични абстрактни членове, но те все още не са обичайното място за състояние на инстанцията. Един клас може да наследи един базов клас, но може да имплементира множество интерфейси. Използвайте абстрактен клас за споделена база и споделена имплементация; използвайте интерфейс за способности и граници.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}
Опитайте да отговорите на този въпрос с треньор с ИИ

Генерични типове, колекции и LINQ

10Какво представляват генеричните типове (generics) и защо са полезни?

Генеричните типове ви позволяват да пишете преизползваем и типово безопасен код. Те преместват много проверки по време на компилация, премахват ръчното преобразуване на типове и могат да избегнат опаковането (boxing) за стойностни типове. Ограниченията на генеричните типове описват какво трябва да поддържа параметърът на типа, например да бъде клас, struct, реализация на интерфейс, базов клас, неуправляем тип или да има публичен конструктор без параметри.

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();
}
Опитайте да отговорите на този въпрос с треньор с ИИ

11Какво представляват ковариантността и контравариантността?

Ковариантността позволява на кода да използва по-специфичен тип там, където се очаква по-общ създаден тип, като например `IEnumerable<string>` вместо `IEnumerable<object>`. Тя се отбелязва с `out` при интерфейси и делегати. Контравариантността работи в обратната посока за консумирани стойности и се отбелязва с `in`. Вариативността работи само за интерфейси и делегати с референтни типове; `IEnumerable<int>` не може да стане `IEnumerable<object>`, защото това би изисквало опаковане, а `List<string>` не може да бъде присвоен на `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); }
Опитайте да отговорите на този въпрос с треньор с ИИ

12Каква е разликата между `IEnumerable<T>` и `IQueryable<T>`?

`IEnumerable<T>` представлява изброяване в .NET, а LINQ (Language Integrated Query) операторите обикновено приемат делегати като `Func<T, bool>`. `IQueryable<T>` съхранява дървета на изрази, например `Expression<Func<T, bool>>`, които даден доставчик, като Entity Framework Core, може да преведе в SQL (Structured Query Language). Извикването на `ToList` твърде рано материализира данните и премества последващото филтриране в паметта. Освен това, не всеки C# израз може да бъде преведен от доставчика на заявки. Избягвайте изтичането на `IQueryable` през всички слоеве на приложението, освен ако това не е съзнателно архитектурно решение.

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();
Опитайте да отговорите на този въпрос с треньор с ИИ

13Какво представлява отложеното изпълнение в LINQ (Language Integrated Query)?

Много LINQ оператори изграждат заявка, без да я изпълняват веднага. Изпълнението започва, когато заявката се обходи или когато се извика терминираща операция като ToList, Count или First. Това означава, че промените в източника преди обхождането могат да повлияят на резултатите, многократното обхождане може да повтори работата или SQL (Structured Query Language) заявките, а изключенията могат да възникнат по време на обхождането, вместо при конструирането на заявката.

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

numbers.Add(42);

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

var materialized = query.ToList();
Опитайте да отговорите на този въпрос с треньор с ИИ

14Как се различават масивът, `List<T>`, `Dictionary<TKey,TValue>` и `HashSet<T>`?

Масивът има фиксиран размер и бърз достъп по индекс. `List<T>` е динамичен масив с достъп по индекс за O(1); добавянето в края е с амортизирано O(1), тъй като увеличаването на капацитета заделя по-голям масив и копира съществуващите елементи, докато вмъкването в средата е O(n). `Dictionary<TKey,TValue>` съпоставя ключове към стойности и обикновено осигурява търсене по ключ за O(1). `HashSet<T>` съхранява уникални елементи и е идеален за проверки за принадлежност. За базираните на хеширане колекции, правилните имплементации на `Equals` и `GetHashCode` са от съществено значение.

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

if (ids.Contains(userId))
{
    // Fast membership check
}
Опитайте да отговорите на този въпрос с треньор с ИИ

15По какво се различават интерфейсите `IEnumerable<T>`, `ICollection<T>`, `IList<T>` и `IReadOnlyCollection<T>`?

Интерфейсът `IEnumerable<T>` гарантира единствено последователно изброяване. `ICollection<T>` добавя свойството `Count` и методи за промяна като `Add` и `Remove`. `IList<T>` добавя достъп по индекс. `IReadOnlyCollection<T>` предоставя изброяване и `Count`, но не и възможност за промяна през този интерфейс. При дизайна на API интерфейси трябва да се връща най-тесният полезен договор, но интерфейсите само за четене не гарантират дълбока неизменяемост на лежащия отдолу обект.

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

public IReadOnlyCollection<Order> Orders => _orders;
Опитайте да отговорите на този въпрос с треньор с ИИ

16Как работи `yield return`?

`yield return` създава итератор с отложено изпълнение. Компилаторът трансформира метода в машина на състоянията и стойностите се произвеждат една по една, когато се извика `MoveNext`. Това избягва създаването на междинни колекции и работи добре за големи последователности. Капаните са свързани с времето на живот на обектите и многократното изброяване: не връщайте отложена последователност, която зависи от вече освободен `DbContext` или поток, и помнете, че извикването на `Count`, последвано от `foreach`, може да изпълни итератора два пъти, което да повтори страничните ефекти или входно-изходните операции.

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}
Опитайте да отговорите на този въпрос с треньор с ИИ

Делегати, събития и изключения

17Какво представляват делегатите и ламбда изразите?

Делегатът е безопасна спрямо типовете референция към метод. Ламбда изразите предоставят кратки вмъкнати функции и често се присвояват на делегатите Func, Action или Predicate. Ламбдите прихващат променливи, а не моментни техни стойности, така че даден ламбда израз може да наблюдава по-късна стойност на променлива или цикъл. Ако ламбдата не прихваща нищо, компилаторът може да кешира статичен делегат и да избегне заделянето на памет за замикание; в критични за производителността части от кода, замиканията и удълженото време на живот на обектите могат да бъдат от голямо значение.

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;
Опитайте да отговорите на този въпрос с треньор с ИИ

18По какво се различава събитието (`event`) от делегата (`delegate`)?

Събитието (`event`) обикновено се основава на делегат (`delegate`), но ограничава действията на външния код. Външният код може да се абонира и да прекратява абонамента си, но само класът собственик може да генерира събитието. Едно публично поле от тип делегат би позволило на извикващия код да замени списъка с извиквания или да го извика директно. Събитията също така създават референции от издателя към абоната, така че дългоживеещите издатели могат да причинят изтичане на памет, ако абонатите не прекратят абонамента си.

public event EventHandler<OrderEventArgs>? OrderCreated;

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

OrderCreated?.Invoke(this, args);
Опитайте да отговорите на този въпрос с треньор с ИИ

19Как трябва да се обработват изключенията в C#?

Използвайте изключения за изключителни ситуации, а не за нормално управление на потока на изпълнение. Прихващайте само изключения, които можете да обработите, избягвайте празни `catch` блокове, добавяйте контекст при обвиване и не разкривайте вътрешни детайли на потребителите. Използвайте `throw;` за повторно хвърляне, запазвайки оригиналната стекова следа. Ако обвивате изключение, подайте оригиналното като `InnerException`, например `throw new DomainException("...", ex)`. Предпочитайте API интерфейси в стил `TryParse` за очаквани неуспехи.

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

catch
{
    throw; // preserves stack trace
}
Опитайте да отговорите на този въпрос с треньор с ИИ

20Какво представляват филтрите за изключения?

Филтрите за изключения позволяват изпълнението на `catch` блок само когато допълнително условие е истина. Филтърът се оценява преди развиването на стека и преди влизането в тялото на `catch`, така че когато върне `false`, оригиналният стек остава непокътнат за диагностика и дъмпове при срив. Филтрите са полезни за HTTP статус кодове, кодове за грешки от база данни, различаване на временни грешки и условно логиране. Поддържайте филтрите прости и избягвайте сложни странични ефекти.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}
Опитайте да отговорите на този въпрос с треньор с ИИ