Preparación entrevista C# backend

Preguntas y respuestas de entrevista C# 2026

Una selección enfocada de preguntas C# para entrevistas backend, con ejemplos de código breves para practicar respuestas orales.

Empezar mock interview C# en español Práctica de entrevista técnica en inglésDiseñado para hablantes no nativos que desean practicar entrevistas técnicas en inglés.

Tipos y fundamentos del lenguaje

1¿En qué se diferencian los value types y los reference types?

Las variables de value type normalmente contienen el valor, por lo que una asignación copia ese valor. Las variables de reference type contienen una referencia a un objeto, por lo que una asignación copia la referencia y ambas variables pueden observar mutaciones del mismo objeto. El matiz importante es que el almacenamiento depende del lifetime y del contexto, no solo del tipo: un valor local de un método puede vivir en la stack, mientras que un value type guardado como campo de clase, elemento de array, captura de closure, valor boxed o campo de una async state machine vive junto con ese objeto.

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¿En qué se diferencian class, struct y record?

class es un reference type con identidad de objeto. struct es un value type y encaja mejor con valores pequeños e inmutables como coordenadas, rangos o identificadores. record class es un reference type con value-based equality; record struct es la versión value type. Los records son sintaxis del compilador sobre tipos CLR normales que genera equality, GetHashCode, ToString, deconstruction y soporte de copia, incluido with para non-destructive mutation. En backend, los records son útiles para modelos tipo DTO, mientras que structs grandes y mutables son arriesgados porque copiar y modificar copias puede sorprender.

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¿En qué se diferencian los parámetros ref, out e in?

ref pasa un argumento por referencia y exige que esté inicializado antes de la llamada; el método puede leerlo y cambiarlo. out también pasa por referencia, pero el método debe asignar un valor antes de devolver. in pasa una readonly byref, lo que puede evitar copias en structs grandes, pero no garantiza inmutabilidad profunda: structs mutables y miembros no-readonly pueden forzar defensive copies. A nivel de IL, ref, out e in son parámetros managed byref con reglas distintas en 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¿Qué son boxing y unboxing?

Boxing envuelve un value type dentro de un object u otro contenedor reference-typed: el CLR crea un objeto y copia ahí el valor. Unboxing extrae el valor de vuelta y requiere el tipo compatible exacto. Boxing frecuente añade allocations y presión sobre el garbage collector. Generics evita muchos casos de boxing porque List<int> guarda valores int sin convertir cada elemento a 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¿Qué son nullable value types y nullable reference types?

Los nullable value types usan Nullable<T> o T? y representan un wrapper real en runtime para value types. Los nullable reference types son sobre todo análisis del compilador: string? le dice al compilador que una referencia puede ser null, pero no crea un nuevo tipo en runtime. El operador null-forgiving ! solo suprime warnings; no protege contra NullReferenceException.

int? age = null;

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

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

string trusted = optionalName!;

6¿En qué se diferencian const, readonly y static readonly?

const es una compile-time constant limitada a primitive types, enum, string o null, y su valor se sustituye en los assemblies consumidores durante la compilación. readonly es un campo de instancia que puede asignarse en la declaración o en el constructor. static readonly es un campo por tipo, asignado en la declaración o en el static constructor. Para valores públicos que pueden cambiar entre versiones de una librería, static readonly suele ser más seguro que public const, porque los clientes pueden conservar un const antiguo inlined hasta que se recompilen.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);

7¿Cómo trabajan juntos Equals, == y GetHashCode?

En clases ordinarias, Equals y == comparan referencias por defecto. Muchos built-in types y records comparan por valor. Si se sobrescribe Equals, GetHashCode debe ser coherente: si a.Equals(b) es true, ambos objetos deben tener el mismo hash code. Lo contrario no está garantizado. Nunca cambies campos usados por equality o hashing mientras el objeto sea una clave de Dictionary o un elemento de 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¿En qué se diferencia override de new?

override cambia el comportamiento virtual, así que el método llamado depende del runtime type del objeto. new oculta un miembro de la clase base, y el miembro seleccionado depende del static type de la variable. Para polimorfismo normalmente quieres virtual y override; ocultar con new es menos común y puede volver confuso el comportamiento.

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¿En qué se diferencia una abstract class de un interface?

Una abstract class puede contener estado, constructores, campos, métodos implementados, métodos abstractos y miembros protected. Un interface describe un contrato; los interfaces modernos pueden incluir default implementations y static abstract members, pero siguen sin ser el lugar habitual para estado de instancia. Una clase puede heredar de una sola base class, pero implementar múltiples interfaces. Usa abstract class para una base compartida con implementación común; usa interface para capacidades y límites.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}

Generics, colecciones y LINQ

10¿Qué son los generics y por qué son útiles?

Generics permite escribir código reutilizable y type-safe. Mueve muchas comprobaciones a compile time, elimina casts manuales y puede evitar boxing para value types. Generic constraints describen qué debe soportar un parámetro de tipo: clase, struct, implementación de interface, base class, unmanaged type o constructor público sin parámetros.

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¿Qué son covariance y contravariance?

Covariance permite usar un tipo más específico donde se espera un tipo producido más general, por ejemplo IEnumerable<string> como IEnumerable<object>. Se marca con out en interfaces y delegates. Contravariance va en la dirección opuesta para valores consumidos y se marca con in. Variance funciona solo en interfaces y delegates con reference types: IEnumerable<int> no puede convertirse en IEnumerable<object> porque requeriría boxing, y List<string> no se puede asignar a 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¿En qué se diferencia IEnumerable<T> de IQueryable<T>?

IEnumerable<T> representa enumeración .NET y los operadores LINQ suelen tomar delegates como Func<T, bool>. IQueryable<T> guarda expression trees, por ejemplo Expression<Func<T, bool>>, que un provider como Entity Framework Core puede traducir a SQL. Llamar ToList demasiado pronto materializa datos y mueve filtros posteriores a memoria. Además, no toda expresión C# puede traducirse por un query provider. Evita filtrar IQueryable por todas las capas salvo que sea una decisión deliberada.

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¿Qué es deferred execution en LINQ?

Muchos operadores LINQ construyen una query sin ejecutarla inmediatamente. La ejecución empieza cuando la query se enumera o cuando se llama una operación terminal como ToList, Count o First. Esto significa que cambios en la fuente antes de la iteración pueden afectar al resultado, la enumeración repetida puede repetir trabajo o consultas SQL, y las excepciones pueden aparecer durante la enumeración en vez de durante la construcción de la query.

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

numbers.Add(42);

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

var materialized = query.ToList();

14¿En qué se diferencian array, List<T>, Dictionary<TKey,TValue> y HashSet<T>?

Un array tiene tamaño fijo y acceso rápido por índice. List<T> es un dynamic array con acceso por índice O(1); Add al final es amortized O(1) porque al crecer la capacity se asigna un array mayor y se copian los elementos, mientras que insertar en medio es O(n). Dictionary<TKey,TValue> asigna claves a valores y normalmente ofrece lookup por clave O(1). HashSet<T> guarda elementos únicos y es ideal para membership checks. En colecciones basadas en hash, Equals y GetHashCode correctos son esenciales.

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

if (ids.Contains(userId))
{
    // Fast membership check
}

15¿En qué se diferencian IEnumerable<T>, ICollection<T>, IList<T> e IReadOnlyCollection<T>?

IEnumerable<T> solo promete enumeración secuencial. ICollection<T> añade Count y métodos de mutación como Add y Remove. IList<T> añade acceso por índice. IReadOnlyCollection<T> expone enumeración y Count, pero no mutación a través de esa interfaz. En diseño de APIs conviene devolver el contrato útil más estrecho, aunque las interfaces read-only no garantizan inmutabilidad profunda del objeto interno.

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

public IReadOnlyCollection<Order> Orders => _orders;

16¿Cómo funciona yield return?

yield return crea un iterator con deferred execution. El compilador transforma el método en una state machine, y los valores se producen uno a uno cuando se llama MoveNext. Esto evita colecciones intermedias y funciona bien con secuencias grandes. Las trampas son lifetime y multiple enumeration: no devuelvas una lazy sequence que depende de un DbContext o stream ya disposed, y recuerda que Count seguido de foreach puede ejecutar el iterator dos veces, repitiendo side effects o I/O.

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}

Delegates, events y exceptions

17¿Qué son delegates y lambda expressions?

Un delegate es una referencia type-safe a un método. Las lambdas proporcionan funciones inline concisas y suelen asignarse a delegates Func, Action o Predicate. Las lambdas capturan variables, no snapshots de sus valores, así que una lambda puede observar un valor posterior de un loop o de una variable. Si una lambda no captura nada, el compilador puede cachear un static delegate y evitar closure allocation; en código sensible al rendimiento, closures y lifetimes extendidos pueden importar.

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¿En qué se diferencia un event de un delegate?

Un event normalmente está respaldado por un delegate, pero restringe lo que puede hacer el código externo. El código externo puede subscribirse y desuscribirse, pero solo la clase propietaria puede lanzar el event. Un campo delegate público permitiría a callers reemplazar la invocation list o invocarlo directamente. Los events también crean referencias del publisher al subscriber, así que publishers de larga vida pueden causar memory leaks si los subscribers no se desuscriben.

public event EventHandler<OrderEventArgs>? OrderCreated;

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

OrderCreated?.Invoke(this, args);

19¿Cómo se deben manejar exceptions en C#?

Usa exceptions para situaciones excepcionales, no para control flow normal. Captura solo exceptions que puedas manejar, evita catch vacíos, añade contexto al envolver y no filtres detalles internos a los usuarios. Usa throw; para relanzar conservando el stack trace original. Si envuelves una excepción, pasa la original como InnerException, por ejemplo throw new DomainException("...", ex). Para fallos esperados, prefiere APIs estilo TryParse.

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

catch
{
    throw; // preserves stack trace
}

20¿Qué son exception filters?

Exception filters permiten que un catch se ejecute solo cuando una condición adicional es verdadera. El filter se evalúa antes del stack unwinding y antes de entrar al catch body, así que si when devuelve false el stack original queda intacto para diagnósticos y crash dumps. Los filters son útiles para HTTP status codes, códigos de base de datos, distinguir transient errors y logging condicional. Mantén los filters simples y sin side effects complejos.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}